import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { writeAtomic } from './write-atomic'; /** * Writing a brief where BOTH harnesses will find it. * * ── The bug this exists to remove ── * * Claude Code discovers `CLAUDE.md` by walking up from its working directory. opencode discovers * `AGENTS.md` and nothing else — verified in the binary, whose only target is * `H.up({ targets: ["AGENTS.md"], start, stop })`. There is no overlap. * * Every brief the platform writes was named `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 survived this long. * * ── The shape, and why the pointer rather than two copies ── * * `AGENTS.md` holds the content; `CLAUDE.md` is six lines pointing at it. That is the same arrangement * the repository's own docs use, and it is chosen over writing the text twice for one reason: two copies * of a file that is REWRITTEN ON EVERY TURN will eventually disagree, and the one that is stale is the * one somebody reads. * * A harness that discovers neither name still gets nothing, and that is honest — it is a fact about the * harness rather than something this can paper over. */ const POINTER = `# CLAUDE.md **The instructions live in [\`AGENTS.md\`](AGENTS.md). Read it first — this file only points there.** Written by the platform so that agents looking for \`AGENTS.md\` by convention and agents looking for \`CLAUDE.md\` find the same guidance, without the two drifting apart. `; /** * Put `content` in `dir` under both names. * * Content-diffed, so a directory's mtimes do not churn on every turn — these are rewritten per message * in the bots' case. Returns false when nothing was written. * * Best effort by contract: every caller must still work if this throws. An agent that knows less is * where all of them were before any of this existed. */ export async function writeAgentBrief( dir: string, content: string, /** * Never overwrite an existing `AGENTS.md`. * * For a brief the AGENT extends rather than one the platform owns. A commander's file is generated * once at creation and then edited by the commander over its whole life — rewriting it because the * template changed would silently delete the job it had agreed. The platform's own briefs (bots, * plugin chats) are the opposite: rewritten every turn ON PURPOSE, so a change reaches them. */ opts: { onlyIfAbsent?: boolean } = {}, ): Promise { let wrote = false; const agents = join(dir, 'AGENTS.md'); if (opts.onlyIfAbsent && existsSync(agents)) return false; if (!existsSync(agents) || (await Bun.file(agents).text()) !== content) { await writeAtomic(agents, content); wrote = true; } const claude = join(dir, 'CLAUDE.md'); if (!existsSync(claude) || (await Bun.file(claude).text()) !== POINTER) { await writeAtomic(claude, POINTER); wrote = true; } return wrote; } /** The pointer's text, for a caller that generates its own file listing and has to describe both. */ export const AGENT_BRIEF_POINTER = POINTER;