tell an existing commander how to reach its owner

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 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 15:31:37 +00:00
co-authored by Claude Opus 5
parent 2d5e9835f5
commit 40a03d987f
2 changed files with 48 additions and 17 deletions
+33 -17
View File
@@ -21,7 +21,7 @@ import { readFileSync, existsSync, appendFileSync, chmodSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { getUsers, listCommandHqSessions } from 'officerdb'; import { getUsers, listCommandHqSessions } from 'officerdb';
import { writeCommanderCredential, commanderEnvPath } from '../src/servers/api/command-hq/credential'; 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 { getCommanderDir } from '../src/servers/data-path';
import { lookupOsUser } from '../src/servers/os-user'; import { lookupOsUser } from '../src/servers/os-user';
@@ -64,26 +64,42 @@ for (const row of rows) {
continue; 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 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<boolean> => {
const existing = existsSync(claudeMd) ? readFileSync(claudeMd, 'utf8') : '';
if (existing.includes(marker)) return false;
appendFileSync(claudeMd, `\n${lines.join('\n')}`);
await giveToAccount(row.osUser, claudeMd); 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); process.exit(0);
+15
View File
@@ -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', '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.', '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', '### Telling the owner something',
'', '',
'A push to their devices, in your own words.', 'A push to their devices, in your own words.',