From 40a03d987f1f82a2a2c7c9f48ecd2677a0b9a7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 24 Aug 2026 15:31:37 +0000 Subject: [PATCH] tell an existing commander how to reach its owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the voice-memos commander was asked for a notification and reached for its harness's own push tool — a different channel, which refuses to interrupt a live terminal. it did that because nothing had told it Officer has a route of its own. that rule is the harness's; the platform's route has none, and had already pushed to a phone twice with the owner sitting at the terminal. so the notify part is its own section now, appendable on its own, and the backfill lands it on a commander that already has a credential — which is every commander that predates this, i.e. the ones that need it. the backfill also stopped being all-or-nothing. it skipped everything after the credential check, so a commander with an officer.env could never receive anything added later. each section is now appended independently, keyed on a marker the section itself contains. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- scripts/backfill-commander-credentials.ts | 50 +++++++++++++++-------- src/servers/api/command-hq/directory.ts | 15 +++++++ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/scripts/backfill-commander-credentials.ts b/scripts/backfill-commander-credentials.ts index de51c464..c4e6b05d 100644 --- a/scripts/backfill-commander-credentials.ts +++ b/scripts/backfill-commander-credentials.ts @@ -21,7 +21,7 @@ import { readFileSync, existsSync, appendFileSync, chmodSync } from 'node:fs'; import { join } from 'node:path'; import { getUsers, listCommandHqSessions } from 'officerdb'; import { writeCommanderCredential, commanderEnvPath } from '../src/servers/api/command-hq/credential'; -import { credentialSection } from '../src/servers/api/command-hq/directory'; +import { credentialSection, notifySection } from '../src/servers/api/command-hq/directory'; import { getCommanderDir } from '../src/servers/data-path'; import { lookupOsUser } from '../src/servers/os-user'; @@ -64,26 +64,42 @@ for (const row of rows) { continue; } - const envPath = commanderEnvPath(row.email, row.dirName); - if (existsSync(envPath)) { - console.log(`ok ${label} — already has a credential`); - continue; - } - - const { path } = await writeCommanderCredential({ userId: row.userId, email: row.email, dirName: row.dirName }); - chmodSync(path, 0o600); - await giveToAccount(row.osUser, path); - - // Appended only when it is not already described. A commander whose CLAUDE.md was hand-edited to - // mention its credential must not end up with the section twice. const claudeMd = join(dir, 'CLAUDE.md'); - const existing = existsSync(claudeMd) ? readFileSync(claudeMd, 'utf8') : ''; - if (!existing.includes('officer.env')) { - appendFileSync(claudeMd, `\n${credentialSection(path).join('\n')}`); + + /** + * Append a section only when it is not already described. + * + * The marker is a string the section itself contains, so a commander whose CLAUDE.md was hand-edited to + * describe the same thing does not end up with it twice. + */ + const appendIfMissing = async (marker: string, lines: string[]): Promise => { + const existing = existsSync(claudeMd) ? readFileSync(claudeMd, 'utf8') : ''; + if (existing.includes(marker)) return false; + appendFileSync(claudeMd, `\n${lines.join('\n')}`); await giveToAccount(row.osUser, claudeMd); + return true; + }; + + const envPath = commanderEnvPath(row.email, row.dirName); + + if (!existsSync(envPath)) { + const { path } = await writeCommanderCredential({ userId: row.userId, email: row.email, dirName: row.dirName }); + chmodSync(path, 0o600); + await giveToAccount(row.osUser, path); + await appendIfMissing('officer.env', credentialSection(path)); + console.log(`MINT ${label} — ${path}`); + } else { + console.log(`ok ${label} — already has a credential`); } - console.log(`MINT ${label} — ${path}`); + /* + Separately, because it arrived later and the commanders that need it most already have a credential. + Without it a commander does not know Officer HAS a notification route, so it reaches for whatever its + harness offers — a different channel, with its own rules about when it may interrupt. + */ + if (await appendIfMissing('/api/notify/_officer/notify', notifySection())) { + console.log(`NOTIFY ${label} — told how to reach the owner`); + } } process.exit(0); diff --git a/src/servers/api/command-hq/directory.ts b/src/servers/api/command-hq/directory.ts index 91507a35..6091dcbb 100644 --- a/src/servers/api/command-hq/directory.ts +++ b/src/servers/api/command-hq/directory.ts @@ -74,6 +74,21 @@ export function credentialSection(credentialPath: string | null): string[] { 'A 403 is an answer, not a problem to route around. It means this commander was not given that, and', 'the owner is the one who decides otherwise. Say so rather than finding another way in.', '', + ...notifySection(), + ]; +} + +/** + * How a commander tells the owner something, and what it may say. + * + * Its own function because it lands in two places at different times: inside `credentialSection` for a + * new commander, and appended on its own by `scripts/backfill-commander-credentials.ts` to a commander + * that already has a credential but predates this. The Voice Memos commander is exactly that case — it + * reached for the harness's own push tool, which is a different channel and refuses to interrupt a live + * terminal, because nothing had told it Officer has a route of its own. + */ +export function notifySection(): string[] { + return [ '### Telling the owner something', '', 'A push to their devices, in your own words.',