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