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>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { DATA_DIR, GROUPS_DIR } from './config.js';
|
|
import { initContainerConfig } from './container-config.js';
|
|
import { log } from './log.js';
|
|
import type { AgentGroup } from './types.js';
|
|
|
|
const DEFAULT_SETTINGS_JSON =
|
|
JSON.stringify(
|
|
{
|
|
env: {
|
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
|
|
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n';
|
|
|
|
/**
|
|
* Initialize the on-disk filesystem state for an agent group. Idempotent —
|
|
* every step is gated on the target not already existing, so re-running on
|
|
* an already-initialized group is a no-op.
|
|
*
|
|
* Called once per group lifetime at creation, or defensively from
|
|
* `buildMounts()` for groups that pre-date this code path.
|
|
*
|
|
* Source code and skills are shared RO mounts — not copied per-group.
|
|
* Skill symlinks are synced at spawn time by container-runner.ts.
|
|
*
|
|
* The composed `CLAUDE.md` is NOT written here — it's regenerated on every
|
|
* spawn by `composeGroupClaudeMd()` (see `claude-md-compose.ts`). Initial
|
|
* per-group instructions (if provided) seed `CLAUDE.local.md`.
|
|
*/
|
|
export function initGroupFilesystem(group: AgentGroup, opts?: { instructions?: string }): void {
|
|
const initialized: string[] = [];
|
|
|
|
// 1. groups/<folder>/ — group memory + working dir
|
|
const groupDir = path.resolve(GROUPS_DIR, group.folder);
|
|
if (!fs.existsSync(groupDir)) {
|
|
fs.mkdirSync(groupDir, { recursive: true });
|
|
initialized.push('groupDir');
|
|
}
|
|
|
|
// groups/<folder>/CLAUDE.local.md — per-group agent memory, auto-loaded by
|
|
// Claude Code. Seeded with caller-provided instructions on first creation.
|
|
const claudeLocalFile = path.join(groupDir, 'CLAUDE.local.md');
|
|
if (!fs.existsSync(claudeLocalFile)) {
|
|
const body = opts?.instructions ? opts.instructions + '\n' : '';
|
|
fs.writeFileSync(claudeLocalFile, body);
|
|
initialized.push('CLAUDE.local.md');
|
|
}
|
|
|
|
// groups/<folder>/container.json — empty container config, replaces the
|
|
// former agent_groups.container_config DB column. Self-modification flows
|
|
// read and write this file directly.
|
|
if (initContainerConfig(group.folder)) {
|
|
initialized.push('container.json');
|
|
}
|
|
|
|
// 2. data/v2-sessions/<id>/.claude-shared/ — Claude state + per-group skills
|
|
const claudeDir = path.join(DATA_DIR, 'v2-sessions', group.id, '.claude-shared');
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
initialized.push('.claude-shared');
|
|
}
|
|
|
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
if (!fs.existsSync(settingsFile)) {
|
|
fs.writeFileSync(settingsFile, DEFAULT_SETTINGS_JSON);
|
|
initialized.push('settings.json');
|
|
}
|
|
|
|
// Skills directory — created empty here; symlinks are synced at spawn
|
|
// time by container-runner.ts based on container.json skills selection.
|
|
const skillsDst = path.join(claudeDir, 'skills');
|
|
if (!fs.existsSync(skillsDst)) {
|
|
fs.mkdirSync(skillsDst, { recursive: true });
|
|
initialized.push('skills/');
|
|
}
|
|
|
|
if (initialized.length > 0) {
|
|
log.info('Initialized group filesystem', {
|
|
group: group.name,
|
|
folder: group.folder,
|
|
id: group.id,
|
|
steps: initialized,
|
|
});
|
|
}
|
|
}
|