From e029f8253390e7ee417e6877256ed6ccb350965a Mon Sep 17 00:00:00 2001 From: hsien wei Date: Tue, 8 Sep 2026 04:06:08 +0800 Subject: [PATCH] fix(hive): avoid rewriting generated docs during runtime operations - make ensureHive() create PROTOCOL.md and COMMANDS.md only when missing - add an explicit refreshGeneratedDocs() path for generated hive docs - refresh generated docs only during bootstrap - add generated notices to PROTOCOL.md and COMMANDS.md - add regressions for writeTasks(), ensureAgent(), explicit refresh, and missing-doc recovery --- src/main/hive.ts | 40 ++++++---- src/main/index.ts | 1 + test/hive-generated-docs.test.cjs | 123 ++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 test/hive-generated-docs.test.cjs diff --git a/src/main/hive.ts b/src/main/hive.ts index 4537c2bf6..ce4af6033 100644 --- a/src/main/hive.ts +++ b/src/main/hive.ts @@ -585,13 +585,10 @@ export class HiveManager { if (!root) return; mkdirSync(join(root, 'agents'), { recursive: true }); - // Refreshed each bootstrap, like COMMANDS.md just below. It used to be - // written only when absent, which meant a hive created once never saw a - // protocol change again: this repo's own hive still carried the file from - // the day it was initialised, so every protocol addition since had reached - // new hives only. The file is generated, not user-authored, and agents are - // pointed at it as the authority, so a stale copy is worse than a rewrite. - writeFileSync(join(root, 'PROTOCOL.md'), PROTOCOL_MD, 'utf8'); + for (const { filename, contents } of GENERATED_HIVE_DOCS) { + const path = join(root, filename); + if (!existsSync(path)) writeFileSync(path, contents, 'utf8'); + } const registry = join(root, 'registry.json'); if (!existsSync(registry)) { @@ -613,10 +610,6 @@ export class HiveManager { const log = join(root, 'log.jsonl'); if (!existsSync(log)) writeFileSync(log, '', 'utf8'); - // The Claude Code command reference Michael consults (refreshed each bootstrap - // so it tracks the bundled list). - writeFileSync(join(root, 'COMMANDS.md'), COMMANDS_MD, 'utf8'); - // Keep the churny/ephemeral live files out of the hive git repo. const gitignore = join(root, '.gitignore'); // `crashes/` holds raw PTY output from abnormal agent exits. It is @@ -648,6 +641,15 @@ export class HiveManager { } } + /** Deliberately replace generated hive docs with the bundled versions. */ + refreshGeneratedDocs(): void { + const root = this.root(); + if (!root) return; + for (const { filename, contents } of GENERATED_HIVE_DOCS) { + writeFileSync(join(root, filename), contents, 'utf8'); + } + } + /** Validate an agent's cwd the way a spawn does — it must be an ABSOLUTE path * that exists as a directory. Surfaced as `cwdValid` on the registry entry so * the roster reliably exposes whether a worker's working directory is usable. @@ -2776,7 +2778,10 @@ export class HiveManager { } } -// ─── PROTOCOL.md (written into the hive, readable by every agent) ──────────── +// ─── Generated hive docs (written into the hive for every agent) ───────────── + +const GENERATED_DOC_NOTICE = + ''; /** The Claude Code command reference written to /COMMANDS.md, rendered from * the SAME source as the UI "commands" tab so they never drift. Leads with the @@ -2784,6 +2789,8 @@ export class HiveManager { * siblings via fleet.json (claude agents does NOT see them). */ function renderCommandsMd(): string { const lines: string[] = [ + GENERATED_DOC_NOTICE, + '', '# Claude Code commands', '', 'Reference of the Claude Code commands available to you. Two kinds:', @@ -2804,7 +2811,9 @@ function renderCommandsMd(): string { } const COMMANDS_MD = renderCommandsMd(); -const PROTOCOL_MD = `# Hive protocol +const PROTOCOL_MD = `${GENERATED_DOC_NOTICE} + +# Hive protocol You are one of several Claude agents sharing this hive. Coordination is entirely file-based; the harness (main process) is the only thing that runs git and the @@ -2949,6 +2958,11 @@ Your \`memory.md\` is mined into the palace automatically, so the durable facts write there become searchable by every agent. You don't run \`mine\` yourself. `; +const GENERATED_HIVE_DOCS = [ + { filename: 'PROTOCOL.md', contents: PROTOCOL_MD }, + { filename: 'COMMANDS.md', contents: COMMANDS_MD } +] as const; + // ─── cth-hook shim (written to /bin/cth-hook.cjs) ────────────────────── // A minimal pipe: read the hook payload on stdin, tag it with this agent's id, // forward it to the hive's UDS, and relay the response back to `claude`. All the diff --git a/src/main/index.ts b/src/main/index.ts index 40634ac6f..58875766e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5047,6 +5047,7 @@ ipcMain.handle('workers:stop', (_evt, workerId: string): { ok: boolean; error?: function bootstrapHiveServices(): void { if (!hive.enabled()) return; hive.ensureHive(); + hive.refreshGeneratedDocs(); // Tell the hive what it is running inside, BEFORE anything spawns: the prompt // builder reads this, so an agent spawned earlier would never learn it. hive.setRuntimeInfo({ version: app.getVersion(), packaged: app.isPackaged, appPath: app.getAppPath() }); diff --git a/test/hive-generated-docs.test.cjs b/test/hive-generated-docs.test.cjs new file mode 100644 index 000000000..2dae66dbe --- /dev/null +++ b/test/hive-generated-docs.test.cjs @@ -0,0 +1,123 @@ +'use strict'; + +/** + * Generated hive documents are refreshed during hive-service bootstrap, not as a + * side effect of ordinary task or agent mutations. These regressions exercise the + * public HiveManager paths and pin the main-process bootstrap wiring without + * starting Electron, a PTY, or a CLI. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const loadTs = require('./load-ts.cjs'); + +const { HiveManager } = loadTs('src/main/hive.ts'); + +const GENERATED_NOTICE_PREFIX = '