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>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
/**
|
|
* 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}`);
|