refactor(v2): per-group filesystem init, persistent across spawns

Each group's on-disk state (CLAUDE.md, .claude-shared/, agent-runner-src/)
is now initialized exactly once at group creation and owned by the group
forever after. Spawn does only mounts — no copies, no settings.json
overwrites, no skill clobbers, no source resyncs.

Global memory composition switches from "host reads /workspace/global/CLAUDE.md
at bootstrap and stuffs it into systemPrompt.append" to "group CLAUDE.md
imports it via @/workspace/global/CLAUDE.md at the top." Edits to global
propagate instantly through the existing read-only mount; no copy, no
restart.

- src/group-init.ts: new initGroupFilesystem(group, opts?) — idempotent,
  populates groups/<folder>/, .claude-shared/, agent-runner-src/ only when
  paths don't already exist.
- src/container-runner.ts: buildMounts() calls init defensively at the
  top (catches existing groups on first spawn after this change), drops
  the inline settings.json write, skills cpSync loop, and agent-runner-src
  rm-then-copy. Just mounts now.
- src/delivery.ts: create_agent flow uses initGroupFilesystem with
  optional instructions, replacing the inline mkdirSync + writeFileSync.
- container/agent-runner/src/index.ts: drops GLOBAL_CLAUDE_MD reading.
  systemContext.instructions is now only the runtime-generated
  destinations addendum.
- scripts/migrate-group-claude-md.ts: one-shot migration that prepends
  the @-import to existing groups' CLAUDE.md. Skips if global doesn't
  exist or if the @-import is already present (regex match on the @ form
  to avoid false positives from prose mentions of the path).
- groups/main/CLAUDE.md: prepended by the migration.

Existing groups need a one-time wipe of their agent-runner-src/ dir so
init re-populates from current host source — done locally before this
commit. Future host-side updates to container/skills/ or
container/agent-runner/src/ won't auto-propagate; that's the trade-off
for unconditional persistence and will be covered by host-mediated
refresh tools in a follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-13 14:17:07 +03:00
parent 8676c07448
commit 2e6dc21748
6 changed files with 188 additions and 59 deletions

View File

@@ -0,0 +1,65 @@
/**
* One-shot migration: prepend `@/workspace/global/CLAUDE.md` to each
* existing group's CLAUDE.md so it imports the global memory under the
* new model where the host no longer reads global CLAUDE.md at bootstrap.
*
* - Skips entirely if `groups/global/CLAUDE.md` doesn't exist (nothing
* to import; running the script would just add a broken @-import).
* - Skips any group whose CLAUDE.md already references
* `/workspace/global/CLAUDE.md` (idempotent).
* - Skips groups with no CLAUDE.md (nothing to prepend to).
*
* Usage: npx tsx scripts/migrate-group-claude-md.ts
*/
import fs from 'fs';
import path from 'path';
import { GROUPS_DIR } from '../src/config.js';
const GLOBAL_CLAUDE_MD = path.join(GROUPS_DIR, 'global', 'CLAUDE.md');
const IMPORT_LINE = '@/workspace/global/CLAUDE.md';
// Must match the @-import syntax exactly — a bare path reference inside
// instructional prose ("you can write to /workspace/global/CLAUDE.md")
// shouldn't count as "already wired."
const IMPORT_REGEX = /@\/workspace\/global\/CLAUDE\.md/;
if (!fs.existsSync(GLOBAL_CLAUDE_MD)) {
console.error(`No global CLAUDE.md at ${GLOBAL_CLAUDE_MD} — nothing to migrate.`);
process.exit(1);
}
if (!fs.existsSync(GROUPS_DIR)) {
console.error(`No groups dir at ${GROUPS_DIR} — nothing to migrate.`);
process.exit(1);
}
const entries = fs.readdirSync(GROUPS_DIR, { withFileTypes: true });
let updated = 0;
let alreadyWired = 0;
let missingClaudeMd = 0;
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name === 'global') continue; // not a group
const claudeMd = path.join(GROUPS_DIR, entry.name, 'CLAUDE.md');
if (!fs.existsSync(claudeMd)) {
console.log(`[skip] ${entry.name}: no CLAUDE.md`);
missingClaudeMd++;
continue;
}
const body = fs.readFileSync(claudeMd, 'utf-8');
if (IMPORT_REGEX.test(body)) {
console.log(`[wired] ${entry.name}: already imports ${IMPORT_LINE}`);
alreadyWired++;
continue;
}
const newBody = `${IMPORT_LINE}\n\n${body}`;
fs.writeFileSync(claudeMd, newBody);
console.log(`[ok] ${entry.name}: prepended import`);
updated++;
}
console.log(`\nDone. updated=${updated} alreadyWired=${alreadyWired} missingClaudeMd=${missingClaudeMd}`);