refactor(claude-md): compose per-group CLAUDE.md from shared base + fragments
Replace the per-group "written once at init, owned by the group" CLAUDE.md
with a host-regenerated entry point that imports:
- a shared base (`container/CLAUDE.md` mounted RO at `/app/CLAUDE.md`)
- optional per-skill fragments (skills that ship `instructions.md`)
- optional per-MCP-server fragments (inline `instructions` field in
`container.json`)
- per-group agent memory (`CLAUDE.local.md`, auto-loaded by Claude Code)
Principle: RW = per-group memory, RO = shared content. Source/skills/base
are shared; personality, config, working files, and Claude state stay
per-group.
Key changes:
- New `src/claude-md-compose.ts` — per-spawn composition +
`migrateGroupsToClaudeLocal()` one-time cutover.
- New `container/CLAUDE.md` — shared base, seeded verbatim from the
former `groups/global/CLAUDE.md`.
- `src/container-runner.ts` — swap `/workspace/global` mount for RO
`/app/CLAUDE.md`; call `composeGroupClaudeMd()` after
`initGroupFilesystem()`.
- `src/group-init.ts` — drop `.claude-global.md` symlink + initial
`CLAUDE.md` write; seed `CLAUDE.local.md` from `opts.instructions`.
- `src/index.ts` — call `migrateGroupsToClaudeLocal()` at startup.
- `src/container-config.ts` — add optional `instructions` field to
`McpServerConfig` (inline per-MCP guidance fragment).
- `container/Dockerfile` — drop dead `/workspace/global` mkdir.
- Remove obsolete `scripts/migrate-group-claude-md.ts`.
Migration (runs once at host startup, idempotent):
- Delete `.claude-global.md` symlinks in each group.
- Rename each `groups/<folder>/CLAUDE.md` → `CLAUDE.local.md`
(preserves existing per-group content as memory).
- Delete `groups/global/` directory.
Design docs: `docs/claude-md-composition.md` and `docs/shared-source.md`
(the latter is the sibling design discussion this refactor builds on).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* One-shot migration: wire each existing group up to global memory via
|
||||
* an in-tree symlink + @-import.
|
||||
*
|
||||
* Claude Code's @-import only follows paths inside cwd, so a direct
|
||||
* `@/workspace/global/CLAUDE.md` or `@../global/CLAUDE.md` silently does
|
||||
* nothing (the import line is parsed but the target file is never
|
||||
* loaded into context). The working approach:
|
||||
*
|
||||
* 1. Symlink `groups/<folder>/.claude-global.md` →
|
||||
* `/workspace/global/CLAUDE.md` (container path; dangling on host,
|
||||
* valid inside the container via the /workspace/global mount).
|
||||
* 2. Have the group's CLAUDE.md import the symlink:
|
||||
* `@./.claude-global.md`.
|
||||
*
|
||||
* This script:
|
||||
* - Creates the symlink if missing.
|
||||
* - Replaces any existing broken `@/workspace/global/CLAUDE.md` or
|
||||
* `@../global/CLAUDE.md` import line with the symlink form.
|
||||
* - Prepends the symlink import if neither form is present.
|
||||
* - Skips entirely if `groups/global/CLAUDE.md` doesn't exist.
|
||||
*
|
||||
* Idempotent — safe to re-run.
|
||||
*
|
||||
* Usage: pnpm exec 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 GLOBAL_MEMORY_CONTAINER_PATH = '/workspace/global/CLAUDE.md';
|
||||
const GLOBAL_MEMORY_LINK_NAME = '.claude-global.md';
|
||||
const IMPORT_LINE = `@./${GLOBAL_MEMORY_LINK_NAME}`;
|
||||
|
||||
// Match any existing @-import that points at global/CLAUDE.md, whether
|
||||
// via absolute path, relative path, or the new symlink form.
|
||||
const EXISTING_IMPORT_REGEX =
|
||||
/^@(?:\/workspace\/global\/CLAUDE\.md|\.\.\/global\/CLAUDE\.md|\.\/\.claude-global\.md)\s*$/m;
|
||||
|
||||
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;
|
||||
let symlinksCreated = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name === 'global') continue;
|
||||
|
||||
const groupDir = path.join(GROUPS_DIR, entry.name);
|
||||
|
||||
// Symlink (idempotent — skip if already present)
|
||||
const linkPath = path.join(groupDir, GLOBAL_MEMORY_LINK_NAME);
|
||||
let linkExists = false;
|
||||
try {
|
||||
fs.lstatSync(linkPath);
|
||||
linkExists = true;
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
if (!linkExists) {
|
||||
fs.symlinkSync(GLOBAL_MEMORY_CONTAINER_PATH, linkPath);
|
||||
console.log(`[link] ${entry.name}: created ${GLOBAL_MEMORY_LINK_NAME}`);
|
||||
symlinksCreated++;
|
||||
}
|
||||
|
||||
// CLAUDE.md import wiring
|
||||
const claudeMd = path.join(groupDir, 'CLAUDE.md');
|
||||
if (!fs.existsSync(claudeMd)) {
|
||||
console.log(`[skip] ${entry.name}: no CLAUDE.md`);
|
||||
missingClaudeMd++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = fs.readFileSync(claudeMd, 'utf-8');
|
||||
const match = body.match(EXISTING_IMPORT_REGEX);
|
||||
|
||||
if (match && match[0] === IMPORT_LINE) {
|
||||
console.log(`[wired] ${entry.name}: already imports ${IMPORT_LINE}`);
|
||||
alreadyWired++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let newBody: string;
|
||||
if (match) {
|
||||
// Replace the broken import with the working form
|
||||
newBody = body.replace(EXISTING_IMPORT_REGEX, IMPORT_LINE);
|
||||
console.log(`[fix] ${entry.name}: rewrote ${match[0]} → ${IMPORT_LINE}`);
|
||||
} else {
|
||||
// Prepend fresh
|
||||
newBody = `${IMPORT_LINE}\n\n${body}`;
|
||||
console.log(`[ok] ${entry.name}: prepended ${IMPORT_LINE}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(claudeMd, newBody);
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nDone. updated=${updated} alreadyWired=${alreadyWired} missingClaudeMd=${missingClaudeMd} symlinksCreated=${symlinksCreated}`,
|
||||
);
|
||||
Reference in New Issue
Block a user