From 09a089ccaf82aa750876a156a98d71266535b614 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Fri, 13 Mar 2026 17:03:28 +0900 Subject: [PATCH] refactor: unify skill sync, remove commands directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove commands/ sync from agent-runner (skills/ is SSOT for both agents) - Remove ~/.codex/skills/ as source (use ~/.claude/skills/ only) - Codex uses $skill prefix, Claude Code uses /skill — both read from skills/ - Minor formatting fixes in config.ts, group-queue.test.ts, index.ts --- src/agent-runner.ts | 93 +++++++++++++++-------------------------- src/config.ts | 11 ++++- src/group-queue.test.ts | 35 +++------------- src/index.ts | 5 +-- 4 files changed, 48 insertions(+), 96 deletions(-) diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 7a10b10..d8a0b0e 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -88,40 +88,27 @@ function prepareGroupEnvironment( ); } - // Sync skills and commands into each group's .claude/ session dir - // Sources: 1) user's global ~/.claude/ 2) project workDir/.claude/ 3) container/skills/ + // Sync skills into each group's .claude/ session dir + // Sources: 1) user's global ~/.claude/skills 2) project workDir/.claude/skills 3) container/skills/ const workDirClaude = group.workDir ? path.join(group.workDir, '.claude') : null; - const syncDirs = [ - { - dst: path.join(groupSessionsDir, 'skills'), - sources: [ - path.join(os.homedir(), '.claude', 'skills'), - ...(workDirClaude ? [path.join(workDirClaude, 'skills')] : []), - path.join(projectRoot, 'container', 'skills'), - ], - }, - { - dst: path.join(groupSessionsDir, 'commands'), - sources: [ - path.join(os.homedir(), '.claude', 'commands'), - ...(workDirClaude ? [path.join(workDirClaude, 'commands')] : []), - ], - }, + const skillSources = [ + path.join(os.homedir(), '.claude', 'skills'), + ...(workDirClaude ? [path.join(workDirClaude, 'skills')] : []), + path.join(projectRoot, 'container', 'skills'), ]; - for (const { dst, sources } of syncDirs) { - for (const src of sources) { - if (!fs.existsSync(src)) continue; - for (const entry of fs.readdirSync(src)) { - const srcPath = path.join(src, entry); - const dstPath = path.join(dst, entry); - if (fs.statSync(srcPath).isDirectory()) { - fs.cpSync(srcPath, dstPath, { recursive: true }); - } else { - fs.mkdirSync(dst, { recursive: true }); - fs.copyFileSync(srcPath, dstPath); - } + const skillsDst = path.join(groupSessionsDir, 'skills'); + for (const src of skillSources) { + if (!fs.existsSync(src)) continue; + for (const entry of fs.readdirSync(src)) { + const srcPath = path.join(src, entry); + const dstPath = path.join(skillsDst, entry); + if (fs.statSync(srcPath).isDirectory()) { + fs.cpSync(srcPath, dstPath, { recursive: true }); + } else { + fs.mkdirSync(skillsDst, { recursive: true }); + fs.copyFileSync(srcPath, dstPath); } } } @@ -241,37 +228,23 @@ function prepareGroupEnvironment( fs.copyFileSync(src, dst); } } - // Sync skills and commands into Codex session dir - // Sources: ~/.codex/, ~/.claude/ (shared), container/skills/ - const codexSyncDirs = [ - { - dst: path.join(sessionCodexDir, 'skills'), - sources: [ - path.join(hostCodexDir, 'skills'), - path.join(os.homedir(), '.claude', 'skills'), - path.join(projectRoot, 'container', 'skills'), - ], - }, - { - dst: path.join(sessionCodexDir, 'commands'), - sources: [ - path.join(hostCodexDir, 'commands'), - path.join(os.homedir(), '.claude', 'commands'), - ], - }, + // Sync skills into Codex session dir + // SSOT: ~/.claude/skills/ (shared with Claude Code) + container/skills/ + const codexSkillSources = [ + path.join(os.homedir(), '.claude', 'skills'), + path.join(projectRoot, 'container', 'skills'), ]; - for (const { dst, sources } of codexSyncDirs) { - for (const src of sources) { - if (!fs.existsSync(src)) continue; - for (const entry of fs.readdirSync(src)) { - const srcPath = path.join(src, entry); - const dstPath = path.join(dst, entry); - if (fs.statSync(srcPath).isDirectory()) { - fs.cpSync(srcPath, dstPath, { recursive: true }); - } else { - fs.mkdirSync(dst, { recursive: true }); - fs.copyFileSync(srcPath, dstPath); - } + const codexSkillsDst = path.join(sessionCodexDir, 'skills'); + for (const src of codexSkillSources) { + if (!fs.existsSync(src)) continue; + for (const entry of fs.readdirSync(src)) { + const srcPath = path.join(src, entry); + const dstPath = path.join(codexSkillsDst, entry); + if (fs.statSync(srcPath).isDirectory()) { + fs.cpSync(srcPath, dstPath, { recursive: true }); + } else { + fs.mkdirSync(codexSkillsDst, { recursive: true }); + fs.copyFileSync(srcPath, dstPath); } } } diff --git a/src/config.ts b/src/config.ts index a086e10..2852fb9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -37,14 +37,21 @@ export const AGENT_TIMEOUT = parseInt( 10, ); export const AGENT_MAX_OUTPUT_SIZE = parseInt( - process.env.AGENT_MAX_OUTPUT_SIZE || process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', + process.env.AGENT_MAX_OUTPUT_SIZE || + process.env.CONTAINER_MAX_OUTPUT_SIZE || + '10485760', 10, ); // 10MB default export const IPC_POLL_INTERVAL = 1000; export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep agent alive after last result export const MAX_CONCURRENT_AGENTS = Math.max( 1, - parseInt(process.env.MAX_CONCURRENT_AGENTS || process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5, + parseInt( + process.env.MAX_CONCURRENT_AGENTS || + process.env.MAX_CONCURRENT_CONTAINERS || + '5', + 10, + ) || 5, ); function escapeRegex(str: string): string { diff --git a/src/group-queue.test.ts b/src/group-queue.test.ts index f7ff600..da14480 100644 --- a/src/group-queue.test.ts +++ b/src/group-queue.test.ts @@ -298,12 +298,7 @@ describe('GroupQueue', () => { await vi.advanceTimersByTimeAsync(10); // Register a process so closeStdin has a groupFolder - queue.registerProcess( - 'group1@g.us', - {} as any, - 'agent-1', - 'test-group', - ); + queue.registerProcess('group1@g.us', {} as any, 'agent-1', 'test-group'); // Enqueue a task while agent is active but NOT idle const taskFn = vi.fn(async () => {}); @@ -338,12 +333,7 @@ describe('GroupQueue', () => { await vi.advanceTimersByTimeAsync(10); // Register process and mark idle - queue.registerProcess( - 'group1@g.us', - {} as any, - 'agent-1', - 'test-group', - ); + queue.registerProcess('group1@g.us', {} as any, 'agent-1', 'test-group'); queue.notifyIdle('group1@g.us'); // Clear previous writes, then enqueue a task @@ -377,12 +367,7 @@ describe('GroupQueue', () => { queue.setProcessMessagesFn(processMessages); queue.enqueueMessageCheck('group1@g.us'); await vi.advanceTimersByTimeAsync(10); - queue.registerProcess( - 'group1@g.us', - {} as any, - 'agent-1', - 'test-group', - ); + queue.registerProcess('group1@g.us', {} as any, 'agent-1', 'test-group'); // Agent becomes idle queue.notifyIdle('group1@g.us'); @@ -418,12 +403,7 @@ describe('GroupQueue', () => { // Start a task (sets isTaskProcess = true) queue.enqueueTask('group1@g.us', 'task-1', taskFn); await vi.advanceTimersByTimeAsync(10); - queue.registerProcess( - 'group1@g.us', - {} as any, - 'agent-1', - 'test-group', - ); + queue.registerProcess('group1@g.us', {} as any, 'agent-1', 'test-group'); // sendMessage should return false — user messages must not go to task agents const result = queue.sendMessage('group1@g.us', 'hello'); @@ -451,12 +431,7 @@ describe('GroupQueue', () => { await vi.advanceTimersByTimeAsync(10); // Register process and enqueue a task (no idle yet — no preemption) - queue.registerProcess( - 'group1@g.us', - {} as any, - 'agent-1', - 'test-group', - ); + queue.registerProcess('group1@g.us', {} as any, 'agent-1', 'test-group'); const writeFileSync = vi.mocked(fs.default.writeFileSync); writeFileSync.mockClear(); diff --git a/src/index.ts b/src/index.ts index c6b9116..cf4c38b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -235,10 +235,7 @@ async function processGroupMessages(chatJid: string): Promise { const resetIdleTimer = () => { if (idleTimer) clearTimeout(idleTimer); idleTimer = setTimeout(() => { - logger.debug( - { group: group.name }, - 'Idle timeout, closing agent stdin', - ); + logger.debug({ group: group.name }, 'Idle timeout, closing agent stdin'); queue.closeStdin(chatJid); }, IDLE_TIMEOUT); };