every brief the platform writes, where both harnesses will find it
opencode discovers AGENTS.md and nothing else — its only target is
H.up({targets:["AGENTS.md"]}), verified in the binary. claude code discovers
CLAUDE.md. no overlap.
every brief the platform wrote was CLAUDE.md, so under opencode a commander got
none of its standing brief, a bot none of its arming instructions, and a plugin
chat none of its plugin's guidance. nothing failed and nothing was logged: the
agent simply knew nothing, which is indistinguishable from the feature not
existing, and is why it lasted this long. commanders can already run under
opencode, so this was live.
writeAgentBrief puts the content in AGENTS.md and a POINTER in CLAUDE.md. a
pointer rather than two copies because these are rewritten every turn, two
copies of that will eventually disagree, and the stale one is the one somebody
reads.
three writers, and the third needed a flag the others must not have:
bots rewritten every turn ON PURPOSE, so a template change reaches them
plugin chats same
commanders onlyIfAbsent — a commander EXTENDS this file over its life, and
rewriting it when the template changed would delete the job it
had agreed with the owner
two things that would have quietly defeated it. the file listing handed to a
commander said "CLAUDE.md — this file", which would have had it editing the
pointer. and backfill-commander-credentials appended to CLAUDE.md, which on a
new commander means burying a credential section inside six lines nobody reads —
it targets AGENTS.md where one exists, CLAUDE.md where it does not, so old
commanders still work.
plugins ship these too: email and proxy-manager's chat/CLAUDE.md are now
chat/AGENTS.md. the platform accepts BOTH source names, because a plugin is its
own repository on its own release cadence and dropping the old name would
un-brief any installed copy that had not updated — silently, again.
tested: both files written, pointer is not a second copy, idempotent, rewrites
on change, and onlyIfAbsent leaves an extended brief alone. verified on disk
after a restart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { join } from 'node:path';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { getBotsDir } from '@@/data-path';
|
||||
import { writeAtomic } from '@@/write-atomic';
|
||||
import { writeAgentBrief } from '@@/agent-brief';
|
||||
|
||||
/**
|
||||
* The instructions every bot runs under.
|
||||
@@ -194,10 +194,8 @@ export async function ensureBotsBrief(email: string): Promise<string> {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
try {
|
||||
const target = join(dir, 'CLAUDE.md');
|
||||
// Skip the write when nothing changed, so the directory's mtimes do not churn on every message.
|
||||
const file = Bun.file(target);
|
||||
if (!(await file.exists()) || (await file.text()) !== BRIEF) await writeAtomic(target, BRIEF);
|
||||
// Both names — a bot run under opencode was getting none of this. See `writeAgentBrief`.
|
||||
await writeAgentBrief(dir, BRIEF);
|
||||
} catch (err) {
|
||||
console.error('[bots] could not write the brief:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { claudeLoginState } from '@@/os-user-claude';
|
||||
import { mkdirSync, existsSync } from 'node:fs';
|
||||
import { snapshotPlugins } from '@@/plugins/mount';
|
||||
import { writeAtomic } from '@@/write-atomic';
|
||||
import { writeAgentBrief } from '@@/agent-brief';
|
||||
import { ensureBotsBrief } from '../bots/brief';
|
||||
import { logger } from './logger';
|
||||
import { isSuperAdmin } from '@@/super-admin';
|
||||
@@ -246,14 +247,18 @@ async function installPluginChatBrief(appName: string, dir: string): Promise<voi
|
||||
const state = states.find((s) => s.plugin.appName === appName && s.install?.enabled);
|
||||
if (!state) return;
|
||||
|
||||
const source = join(state.plugin.dir, 'chat', 'CLAUDE.md');
|
||||
if (!existsSync(source)) return;
|
||||
// `chat/AGENTS.md`, with the old name still accepted: a plugin is its own repository on its own
|
||||
// release cadence, so the platform cannot rename a file inside one and assume every installed copy
|
||||
// followed. Dropping the fallback would silently un-brief a plugin that had not been updated —
|
||||
// silently, because a missing brief is indistinguishable from a plugin that ships none.
|
||||
const source = ['AGENTS.md', 'CLAUDE.md']
|
||||
.map((name) => join(state.plugin.dir, 'chat', name))
|
||||
.find((path) => existsSync(path));
|
||||
if (!source) return;
|
||||
|
||||
const target = join(dir, 'CLAUDE.md');
|
||||
const next = await Bun.file(source).text();
|
||||
// Skip the write when nothing changed, so a directory's mtimes do not churn on every chat start.
|
||||
if (existsSync(target) && (await Bun.file(target).text()) === next) return;
|
||||
await writeAtomic(target, next);
|
||||
// Both names, so opencode finds it too — see `writeAgentBrief`. Content-diffed inside, so a
|
||||
// directory's mtimes do not churn on every chat start.
|
||||
await writeAgentBrief(dir, await Bun.file(source).text());
|
||||
} catch (err) {
|
||||
console.error(`[chat] could not install ${appName}'s chat brief:`, err instanceof Error ? err.message : err);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { writeAgentBrief } from '@@/agent-brief';
|
||||
import { getUserByEmail } from 'officerdb';
|
||||
import { lookupOsUser } from '@@/os-user';
|
||||
import { join } from 'node:path';
|
||||
@@ -10,10 +11,11 @@ import { writeCommanderCredential } from './credential';
|
||||
A commander's own directory, created once, up front — `docs/commander-directory.md`.
|
||||
|
||||
$DATA_PATH/<email>/commanders/<dirName>/
|
||||
CLAUDE.md written here, extended by the commander as the job is agreed
|
||||
AGENTS.md written here, extended by the commander as the job is agreed
|
||||
CLAUDE.md a pointer at it, so both harnesses discover the same brief
|
||||
state/ INSTANCE data: what it spawned, its loop's pid. Empty at creation.
|
||||
|
||||
Both exist before the first turn spawns, which is the whole point: Claude Code reads a CLAUDE.md from the
|
||||
Both exist before the first turn spawns, which is the whole point: a coding CLI reads its brief from the
|
||||
working directory at session start, so a commander told about itself here needs no per-turn injection —
|
||||
see the deleted `commander-block.ts` for what this replaced.
|
||||
*/
|
||||
@@ -365,7 +367,8 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
|
||||
'something broken.',
|
||||
'',
|
||||
'```',
|
||||
"CLAUDE.md this file. the platform's. never edited.",
|
||||
"AGENTS.md this file. the platform's, extended by you as the job is agreed.",
|
||||
"CLAUDE.md a pointer at AGENTS.md. the platform's. never edited.",
|
||||
'JOB.md what was agreed: what you watch for and what you do. yours, written once',
|
||||
' the job is settled and kept true after that.',
|
||||
'CLASSIFICATION.md the category rules, if it classifies. yours, and expected to change.',
|
||||
@@ -671,8 +674,14 @@ export async function createCommanderDirectory(
|
||||
credentialError = err instanceof Error ? err.message : 'could not mint a credential';
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(dir, 'CLAUDE.md'),
|
||||
// AGENTS.md with the brief in it, CLAUDE.md pointing at it — both, because opencode discovers only
|
||||
// the first and Claude Code only the second, and a commander can run under either.
|
||||
//
|
||||
// `onlyIfAbsent`, and that flag is the whole reason this is not the same call the bots make: a
|
||||
// commander EXTENDS this file over its life. Rewriting it when the template changes would delete the
|
||||
// job it had agreed with the owner.
|
||||
await writeAgentBrief(
|
||||
dir,
|
||||
commanderClaudeMd({
|
||||
name: params.name,
|
||||
referencePath: params.referencePath,
|
||||
@@ -680,6 +689,7 @@ export async function createCommanderDirectory(
|
||||
spawnedPath: spawnedRecordPath(email, dirName),
|
||||
credentialPath,
|
||||
}),
|
||||
{ onlyIfAbsent: true },
|
||||
);
|
||||
|
||||
// Last, so it covers CLAUDE.md, officer.env and state/ as well as the directory itself.
|
||||
|
||||
Reference in New Issue
Block a user