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:
@@ -12,6 +12,7 @@ import { OneCLI } from '@onecli-sh/sdk';
|
||||
import { CONTAINER_IMAGE, DATA_DIR, GROUPS_DIR, ONECLI_URL, TIMEZONE } from './config.js';
|
||||
import { readContainerConfig, writeContainerConfig } from './container-config.js';
|
||||
import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js';
|
||||
import { composeGroupClaudeMd } from './claude-md-compose.js';
|
||||
import { getAgentGroup } from './db/agent-groups.js';
|
||||
import { getDb, hasTable } from './db/connection.js';
|
||||
import { initGroupFilesystem } from './group-init.js';
|
||||
@@ -195,6 +196,10 @@ function buildMounts(
|
||||
const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared');
|
||||
syncSkillSymlinks(claudeDir, containerConfig);
|
||||
|
||||
// Compose CLAUDE.md fresh every spawn from the shared base, enabled skill
|
||||
// fragments, and MCP server instructions. See `claude-md-compose.ts`.
|
||||
composeGroupClaudeMd(agentGroup);
|
||||
|
||||
const mounts: VolumeMount[] = [];
|
||||
const sessDir = sessionDir(agentGroup.id, session.id);
|
||||
const groupDir = path.resolve(GROUPS_DIR, agentGroup.folder);
|
||||
@@ -218,6 +223,13 @@ function buildMounts(
|
||||
mounts.push({ hostPath: globalDir, containerPath: '/workspace/global', readonly: true });
|
||||
}
|
||||
|
||||
// Shared CLAUDE.md — read-only, imported by the composed entry point via
|
||||
// the `.claude-shared.md` symlink inside the group dir.
|
||||
const sharedClaudeMd = path.join(process.cwd(), 'container', 'CLAUDE.md');
|
||||
if (fs.existsSync(sharedClaudeMd)) {
|
||||
mounts.push({ hostPath: sharedClaudeMd, containerPath: '/app/CLAUDE.md', readonly: true });
|
||||
}
|
||||
|
||||
// Per-group .claude-shared at /home/node/.claude (Claude state, settings,
|
||||
// skill symlinks)
|
||||
mounts.push({ hostPath: claudeDir, containerPath: '/home/node/.claude', readonly: false });
|
||||
|
||||
Reference in New Issue
Block a user