refactor: unify skill sync, remove commands directory
- 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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -235,10 +235,7 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
||||
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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user