Files
nanoclaw/src/group-init.ts
gavrielc c8fc1da719 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>
2026-04-22 12:58:43 +03:00

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,
});
}
}