ac-framework 1.9.5 → 1.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +13 -1
- package/framework/mobile_development/.agent/workflows/ac.md +57 -4
- package/framework/mobile_development/.amazonq/prompts/ac.md +57 -4
- package/framework/mobile_development/.antigravity/workflows/ac.md +57 -4
- package/framework/mobile_development/.augment/commands/ac.md +57 -4
- package/framework/mobile_development/.claude/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.cline/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.clinerules/workflows/ac.md +57 -4
- package/framework/mobile_development/.codebuddy/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.continue/prompts/ac.md +57 -4
- package/framework/mobile_development/.cospec/openspec/commands/ac.md +57 -4
- package/framework/mobile_development/.crush/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.cursor/commands/ac.md +57 -4
- package/framework/mobile_development/.factory/commands/ac.md +57 -4
- package/framework/mobile_development/.gemini/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.github/prompts/ac.md +57 -4
- package/framework/mobile_development/.iflow/commands/ac.md +57 -4
- package/framework/mobile_development/.kilocode/workflows/ac.md +57 -4
- package/framework/mobile_development/.kimi/workflows/ac.md +57 -4
- package/framework/mobile_development/.opencode/command/ac.md +57 -4
- package/framework/mobile_development/.qoder/commands/opsx/ac.md +57 -4
- package/framework/mobile_development/.qwen/commands/ac.md +57 -4
- package/framework/mobile_development/.roo/commands/ac.md +57 -4
- package/framework/mobile_development/.windsurf/workflows/ac.md +57 -4
- package/framework/new_project/.agent/workflows/ac.md +39 -0
- package/framework/new_project/.amazonq/prompts/ac.md +39 -0
- package/framework/new_project/.antigravity/workflows/ac.md +39 -0
- package/framework/new_project/.augment/commands/ac.md +39 -0
- package/framework/new_project/.claude/commands/opsx/ac.md +39 -0
- package/framework/new_project/.cline/commands/opsx/ac.md +39 -0
- package/framework/new_project/.clinerules/workflows/ac.md +39 -0
- package/framework/new_project/.codebuddy/commands/opsx/ac.md +39 -0
- package/framework/new_project/.continue/prompts/ac.md +39 -0
- package/framework/new_project/.cospec/openspec/commands/ac.md +39 -0
- package/framework/new_project/.crush/commands/opsx/ac.md +39 -0
- package/framework/new_project/.cursor/commands/ac.md +39 -0
- package/framework/new_project/.factory/commands/ac.md +39 -0
- package/framework/new_project/.gemini/commands/opsx/ac.md +39 -0
- package/framework/new_project/.github/prompts/ac.md +39 -0
- package/framework/new_project/.iflow/commands/ac.md +39 -0
- package/framework/new_project/.kilocode/workflows/ac.md +39 -0
- package/framework/new_project/.kimi/workflows/ac.md +39 -0
- package/framework/new_project/.opencode/command/ac.md +16 -4
- package/framework/new_project/.qoder/commands/opsx/ac.md +39 -0
- package/framework/new_project/.qwen/commands/ac.md +39 -0
- package/framework/new_project/.roo/commands/ac.md +39 -0
- package/framework/new_project/.windsurf/workflows/ac.md +39 -0
- package/framework/web_development/.agent/workflows/ac.md +39 -0
- package/framework/web_development/.amazonq/prompts/ac.md +39 -0
- package/framework/web_development/.antigravity/workflows/ac.md +39 -0
- package/framework/web_development/.augment/commands/ac.md +39 -0
- package/framework/web_development/.claude/commands/opsx/ac.md +39 -0
- package/framework/web_development/.cline/commands/opsx/ac.md +39 -0
- package/framework/web_development/.clinerules/workflows/ac.md +39 -0
- package/framework/web_development/.codebuddy/commands/opsx/ac.md +39 -0
- package/framework/web_development/.continue/prompts/ac.md +39 -0
- package/framework/web_development/.cospec/openspec/commands/ac.md +39 -0
- package/framework/web_development/.crush/commands/opsx/ac.md +39 -0
- package/framework/web_development/.cursor/commands/ac.md +39 -0
- package/framework/web_development/.factory/commands/ac.md +39 -0
- package/framework/web_development/.gemini/commands/opsx/ac.md +39 -0
- package/framework/web_development/.github/prompts/ac.md +39 -0
- package/framework/web_development/.iflow/commands/ac.md +39 -0
- package/framework/web_development/.kilocode/workflows/ac.md +39 -0
- package/framework/web_development/.kimi/workflows/ac.md +39 -0
- package/framework/web_development/.opencode/command/ac.md +16 -4
- package/framework/web_development/.qoder/commands/opsx/ac.md +39 -0
- package/framework/web_development/.qwen/commands/ac.md +39 -0
- package/framework/web_development/.roo/commands/ac.md +39 -0
- package/framework/web_development/.windsurf/workflows/ac.md +39 -0
- package/package.json +1 -1
- package/src/agents/config-store.js +48 -0
- package/src/agents/model-selection.js +38 -0
- package/src/agents/opencode-client.js +3 -2
- package/src/agents/orchestrator.js +10 -3
- package/src/agents/runtime.js +80 -0
- package/src/agents/state-store.js +3 -0
- package/src/commands/agents.js +230 -83
- package/src/mcp/collab-server.js +105 -4
- package/src/services/dependency-installer.js +20 -1
package/src/commands/agents.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
3
|
import { existsSync } from 'node:fs';
|
|
5
4
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
6
5
|
import { readFileSync } from 'node:fs';
|
|
7
6
|
import { dirname, resolve } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
7
|
import {
|
|
10
8
|
COLLAB_ROLES,
|
|
11
9
|
COLLAB_SYSTEM_NAME,
|
|
@@ -26,7 +24,15 @@ import {
|
|
|
26
24
|
setCurrentSession,
|
|
27
25
|
stopSession,
|
|
28
26
|
} from '../agents/state-store.js';
|
|
29
|
-
import {
|
|
27
|
+
import { roleLogPath, runTmux, spawnTmuxSession, tmuxSessionExists } from '../agents/runtime.js';
|
|
28
|
+
import { getAgentsConfigPath, loadAgentsConfig, updateAgentsConfig } from '../agents/config-store.js';
|
|
29
|
+
import {
|
|
30
|
+
buildEffectiveRoleModels,
|
|
31
|
+
isValidModelId,
|
|
32
|
+
normalizeModelId,
|
|
33
|
+
sanitizeRoleModels,
|
|
34
|
+
} from '../agents/model-selection.js';
|
|
35
|
+
import { ensureCollabDependencies, hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
30
36
|
|
|
31
37
|
function output(data, json) {
|
|
32
38
|
if (json) {
|
|
@@ -34,79 +40,12 @@ function output(data, json) {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
function roleLogPath(sessionDir, role) {
|
|
38
|
-
return resolve(sessionDir, `${role}.log`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
43
|
function tailLines(text, maxLines) {
|
|
42
44
|
const lines = text.split('\n');
|
|
43
45
|
const sliced = lines.slice(Math.max(lines.length - maxLines, 0));
|
|
44
46
|
return sliced.join('\n').trimEnd();
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
-
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
49
|
-
|
|
50
|
-
function runTmux(command, args, options = {}) {
|
|
51
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
52
|
-
const child = spawn(command, args, {
|
|
53
|
-
cwd: options.cwd || process.cwd(),
|
|
54
|
-
stdio: options.stdio || 'pipe',
|
|
55
|
-
env: process.env,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
let stderr = '';
|
|
59
|
-
let stdout = '';
|
|
60
|
-
if (child.stderr) {
|
|
61
|
-
child.stderr.on('data', (chunk) => {
|
|
62
|
-
stderr += chunk.toString();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
if (child.stdout) {
|
|
66
|
-
child.stdout.on('data', (chunk) => {
|
|
67
|
-
stdout += chunk.toString();
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
child.on('error', rejectPromise);
|
|
72
|
-
child.on('close', (code) => {
|
|
73
|
-
if (code === 0) {
|
|
74
|
-
resolvePromise({ stdout, stderr });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
rejectPromise(new Error(stderr.trim() || `${command} exited with code ${code}`));
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
83
|
-
const role0 = COLLAB_ROLES[0];
|
|
84
|
-
await runTmux('tmux', [
|
|
85
|
-
'new-session',
|
|
86
|
-
'-d',
|
|
87
|
-
'-s',
|
|
88
|
-
sessionName,
|
|
89
|
-
'-n',
|
|
90
|
-
role0,
|
|
91
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0} >> "${roleLogPath(sessionDir, role0)}" 2>&1'`,
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
|
-
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
95
|
-
const role = COLLAB_ROLES[idx];
|
|
96
|
-
await runTmux('tmux', [
|
|
97
|
-
'split-window',
|
|
98
|
-
'-t',
|
|
99
|
-
sessionName,
|
|
100
|
-
'-v',
|
|
101
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} >> "${roleLogPath(sessionDir, role)}" 2>&1'`,
|
|
102
|
-
]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
await runTmux('tmux', ['select-layout', '-t', sessionName, 'tiled']);
|
|
106
|
-
await runTmux('tmux', ['set-option', '-t', sessionName, 'pane-border-status', 'top']);
|
|
107
|
-
await runTmux('tmux', ['set-option', '-t', sessionName, 'pane-border-format', '#{pane_index}:#{pane_title}']);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
49
|
async function ensureSessionId(required = true) {
|
|
111
50
|
const sessionId = await loadCurrentSessionId();
|
|
112
51
|
if (!sessionId && required) {
|
|
@@ -124,18 +63,20 @@ function printStartSummary(state) {
|
|
|
124
63
|
console.log();
|
|
125
64
|
console.log(chalk.cyan('Attach with:'));
|
|
126
65
|
console.log(chalk.white(` tmux attach -t ${state.tmuxSessionName}`));
|
|
66
|
+
console.log(chalk.white(' acfm agents live'));
|
|
127
67
|
console.log();
|
|
128
68
|
console.log(chalk.cyan('Interact with:'));
|
|
129
69
|
console.log(chalk.white(' acfm agents send "your message"'));
|
|
130
70
|
}
|
|
131
71
|
|
|
132
72
|
function toMarkdownTranscript(state, transcript) {
|
|
73
|
+
const displayedRound = Math.min(state.round, state.maxRounds);
|
|
133
74
|
const lines = [
|
|
134
75
|
`# SynapseGrid Session ${state.sessionId}`,
|
|
135
76
|
'',
|
|
136
77
|
`- Task: ${state.task}`,
|
|
137
78
|
`- Status: ${state.status}`,
|
|
138
|
-
`- Rounds: ${
|
|
79
|
+
`- Rounds: ${displayedRound}/${state.maxRounds}`,
|
|
139
80
|
`- Roles: ${state.roles.join(', ')}`,
|
|
140
81
|
`- Created: ${state.createdAt}`,
|
|
141
82
|
`- Updated: ${state.updatedAt}`,
|
|
@@ -155,6 +96,35 @@ function toMarkdownTranscript(state, transcript) {
|
|
|
155
96
|
return lines.join('\n');
|
|
156
97
|
}
|
|
157
98
|
|
|
99
|
+
function parseRoleModelOptions(opts) {
|
|
100
|
+
return sanitizeRoleModels({
|
|
101
|
+
planner: opts.modelPlanner,
|
|
102
|
+
critic: opts.modelCritic,
|
|
103
|
+
coder: opts.modelCoder,
|
|
104
|
+
reviewer: opts.modelReviewer,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assertValidModelIdOrNull(label, value) {
|
|
109
|
+
const normalized = normalizeModelId(value);
|
|
110
|
+
if (!normalized) return null;
|
|
111
|
+
if (!isValidModelId(normalized)) {
|
|
112
|
+
throw new Error(`${label} must be in provider/model format`);
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printModelConfig(state) {
|
|
118
|
+
const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
|
|
119
|
+
console.log(chalk.bold('\nModel configuration'));
|
|
120
|
+
console.log(chalk.dim(` Global fallback: ${state.model || '(opencode default)'}`));
|
|
121
|
+
for (const role of COLLAB_ROLES) {
|
|
122
|
+
const configured = state.roleModels?.[role] || '-';
|
|
123
|
+
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
124
|
+
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
158
128
|
export function agentsCommand() {
|
|
159
129
|
const agents = new Command('agents')
|
|
160
130
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
@@ -295,6 +265,27 @@ export function agentsCommand() {
|
|
|
295
265
|
}
|
|
296
266
|
});
|
|
297
267
|
|
|
268
|
+
agents
|
|
269
|
+
.command('live')
|
|
270
|
+
.description('Attach to live tmux collaboration view (all agent panes)')
|
|
271
|
+
.option('--readonly', 'Attach in read-only mode', false)
|
|
272
|
+
.action(async (opts) => {
|
|
273
|
+
try {
|
|
274
|
+
const sessionId = await ensureSessionId(true);
|
|
275
|
+
const state = await loadSessionState(sessionId);
|
|
276
|
+
if (!state.tmuxSessionName) {
|
|
277
|
+
throw new Error('No tmux session registered for active collaborative session');
|
|
278
|
+
}
|
|
279
|
+
const args = ['attach'];
|
|
280
|
+
if (opts.readonly) args.push('-r');
|
|
281
|
+
args.push('-t', state.tmuxSessionName);
|
|
282
|
+
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
298
289
|
agents
|
|
299
290
|
.command('resume')
|
|
300
291
|
.description('Resume a previous session and optionally recreate tmux workers')
|
|
@@ -308,15 +299,7 @@ export function agentsCommand() {
|
|
|
308
299
|
let state = await loadSessionState(sessionId);
|
|
309
300
|
|
|
310
301
|
const tmuxSessionName = state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
311
|
-
|
|
312
|
-
if (hasCommand('tmux')) {
|
|
313
|
-
try {
|
|
314
|
-
await runTmux('tmux', ['has-session', '-t', tmuxSessionName]);
|
|
315
|
-
tmuxExists = true;
|
|
316
|
-
} catch {
|
|
317
|
-
tmuxExists = false;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
302
|
+
const tmuxExists = hasCommand('tmux') ? await tmuxSessionExists(tmuxSessionName) : false;
|
|
320
303
|
|
|
321
304
|
if (!tmuxExists && opts.recreate) {
|
|
322
305
|
if (!hasCommand('tmux')) {
|
|
@@ -410,6 +393,136 @@ export function agentsCommand() {
|
|
|
410
393
|
}
|
|
411
394
|
});
|
|
412
395
|
|
|
396
|
+
const model = agents
|
|
397
|
+
.command('model')
|
|
398
|
+
.description('Manage default SynapseGrid model configuration');
|
|
399
|
+
|
|
400
|
+
model
|
|
401
|
+
.command('get')
|
|
402
|
+
.description('Show configured default global/per-role models')
|
|
403
|
+
.option('--json', 'Output as JSON')
|
|
404
|
+
.action(async (opts) => {
|
|
405
|
+
try {
|
|
406
|
+
const config = await loadAgentsConfig();
|
|
407
|
+
const payload = {
|
|
408
|
+
configPath: getAgentsConfigPath(),
|
|
409
|
+
defaultModel: config.agents.defaultModel,
|
|
410
|
+
defaultRoleModels: config.agents.defaultRoleModels,
|
|
411
|
+
};
|
|
412
|
+
output(payload, opts.json);
|
|
413
|
+
if (!opts.json) {
|
|
414
|
+
console.log(chalk.bold('SynapseGrid default models'));
|
|
415
|
+
console.log(chalk.dim(`Config: ${payload.configPath}`));
|
|
416
|
+
console.log(chalk.dim(`Global fallback: ${payload.defaultModel || '(none)'}`));
|
|
417
|
+
for (const role of COLLAB_ROLES) {
|
|
418
|
+
console.log(chalk.dim(`- ${role}: ${payload.defaultRoleModels[role] || '(none)'}`));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
output({ error: error.message }, opts.json);
|
|
423
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
model
|
|
429
|
+
.command('set <modelId>')
|
|
430
|
+
.description('Set default model globally or for a specific role')
|
|
431
|
+
.option('--role <role>', 'planner|critic|coder|reviewer|all', 'all')
|
|
432
|
+
.option('--json', 'Output as JSON')
|
|
433
|
+
.action(async (modelId, opts) => {
|
|
434
|
+
try {
|
|
435
|
+
const role = String(opts.role || 'all');
|
|
436
|
+
const normalized = assertValidModelIdOrNull('model', modelId);
|
|
437
|
+
if (!normalized) throw new Error('model must be provided');
|
|
438
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
439
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const updated = await updateAgentsConfig((current) => {
|
|
443
|
+
const next = {
|
|
444
|
+
agents: {
|
|
445
|
+
defaultModel: current.agents.defaultModel,
|
|
446
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
if (role === 'all') {
|
|
450
|
+
next.agents.defaultModel = normalized;
|
|
451
|
+
} else {
|
|
452
|
+
next.agents.defaultRoleModels = {
|
|
453
|
+
...next.agents.defaultRoleModels,
|
|
454
|
+
[role]: normalized,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return next;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const payload = {
|
|
461
|
+
success: true,
|
|
462
|
+
configPath: getAgentsConfigPath(),
|
|
463
|
+
defaultModel: updated.agents.defaultModel,
|
|
464
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
465
|
+
};
|
|
466
|
+
output(payload, opts.json);
|
|
467
|
+
if (!opts.json) {
|
|
468
|
+
console.log(chalk.green('✓ SynapseGrid model configuration updated'));
|
|
469
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
472
|
+
output({ error: error.message }, opts.json);
|
|
473
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
model
|
|
479
|
+
.command('clear')
|
|
480
|
+
.description('Clear default model globally or for a specific role')
|
|
481
|
+
.option('--role <role>', 'planner|critic|coder|reviewer|all', 'all')
|
|
482
|
+
.option('--json', 'Output as JSON')
|
|
483
|
+
.action(async (opts) => {
|
|
484
|
+
try {
|
|
485
|
+
const role = String(opts.role || 'all');
|
|
486
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
487
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const updated = await updateAgentsConfig((current) => {
|
|
491
|
+
const next = {
|
|
492
|
+
agents: {
|
|
493
|
+
defaultModel: current.agents.defaultModel,
|
|
494
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
if (role === 'all') {
|
|
498
|
+
next.agents.defaultModel = null;
|
|
499
|
+
next.agents.defaultRoleModels = {};
|
|
500
|
+
} else {
|
|
501
|
+
const currentRoles = { ...next.agents.defaultRoleModels };
|
|
502
|
+
delete currentRoles[role];
|
|
503
|
+
next.agents.defaultRoleModels = currentRoles;
|
|
504
|
+
}
|
|
505
|
+
return next;
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const payload = {
|
|
509
|
+
success: true,
|
|
510
|
+
configPath: getAgentsConfigPath(),
|
|
511
|
+
defaultModel: updated.agents.defaultModel,
|
|
512
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
513
|
+
};
|
|
514
|
+
output(payload, opts.json);
|
|
515
|
+
if (!opts.json) {
|
|
516
|
+
console.log(chalk.green('✓ SynapseGrid model configuration cleared'));
|
|
517
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
518
|
+
}
|
|
519
|
+
} catch (error) {
|
|
520
|
+
output({ error: error.message }, opts.json);
|
|
521
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
413
526
|
agents
|
|
414
527
|
.command('export')
|
|
415
528
|
.description('Export collaborative transcript')
|
|
@@ -459,6 +572,10 @@ export function agentsCommand() {
|
|
|
459
572
|
.requiredOption('--task <text>', 'Initial task from user')
|
|
460
573
|
.option('--rounds <n>', 'Maximum collaboration rounds', String(DEFAULT_MAX_ROUNDS))
|
|
461
574
|
.option('--model <id>', 'Model to use (provider/model)')
|
|
575
|
+
.option('--model-planner <id>', 'Model for planner role (provider/model)')
|
|
576
|
+
.option('--model-critic <id>', 'Model for critic role (provider/model)')
|
|
577
|
+
.option('--model-coder <id>', 'Model for coder role (provider/model)')
|
|
578
|
+
.option('--model-reviewer <id>', 'Model for reviewer role (provider/model)')
|
|
462
579
|
.option('--cwd <path>', 'Working directory for agents', process.cwd())
|
|
463
580
|
.option('--attach', 'Attach tmux immediately after start', false)
|
|
464
581
|
.option('--json', 'Output as JSON')
|
|
@@ -470,6 +587,10 @@ export function agentsCommand() {
|
|
|
470
587
|
if (!hasCommand('tmux')) {
|
|
471
588
|
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
472
589
|
}
|
|
590
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
591
|
+
if (!opencodeBin) {
|
|
592
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
593
|
+
}
|
|
473
594
|
|
|
474
595
|
await mkdir(SESSION_ROOT_DIR, { recursive: true });
|
|
475
596
|
const maxRounds = Number.parseInt(opts.rounds, 10);
|
|
@@ -477,11 +598,28 @@ export function agentsCommand() {
|
|
|
477
598
|
throw new Error('--rounds must be a positive integer');
|
|
478
599
|
}
|
|
479
600
|
|
|
601
|
+
const config = await loadAgentsConfig();
|
|
602
|
+
const cliModel = assertValidModelIdOrNull('--model', opts.model || null);
|
|
603
|
+
const cliRoleModels = parseRoleModelOptions(opts);
|
|
604
|
+
for (const [role, model] of Object.entries(cliRoleModels)) {
|
|
605
|
+
if (!isValidModelId(model)) {
|
|
606
|
+
throw new Error(`--model-${role} must be in provider/model format`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const defaultRoleModels = sanitizeRoleModels(config.agents.defaultRoleModels);
|
|
610
|
+
const roleModels = {
|
|
611
|
+
...defaultRoleModels,
|
|
612
|
+
...cliRoleModels,
|
|
613
|
+
};
|
|
614
|
+
const globalModel = cliModel || config.agents.defaultModel || null;
|
|
615
|
+
|
|
480
616
|
const state = await createSession(opts.task, {
|
|
481
617
|
roles: COLLAB_ROLES,
|
|
482
618
|
maxRounds,
|
|
483
|
-
model:
|
|
619
|
+
model: globalModel,
|
|
620
|
+
roleModels,
|
|
484
621
|
workingDirectory: resolve(opts.cwd),
|
|
622
|
+
opencodeBin,
|
|
485
623
|
});
|
|
486
624
|
const tmuxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
487
625
|
const sessionDir = getSessionDir(state.sessionId);
|
|
@@ -499,6 +637,7 @@ export function agentsCommand() {
|
|
|
499
637
|
output({ sessionId: updated.sessionId, tmuxSessionName, status: updated.status }, opts.json);
|
|
500
638
|
if (!opts.json) {
|
|
501
639
|
printStartSummary(updated);
|
|
640
|
+
printModelConfig(updated);
|
|
502
641
|
}
|
|
503
642
|
|
|
504
643
|
if (opts.attach) {
|
|
@@ -538,14 +677,21 @@ export function agentsCommand() {
|
|
|
538
677
|
try {
|
|
539
678
|
const sessionId = await ensureSessionId(true);
|
|
540
679
|
const state = await loadSessionState(sessionId);
|
|
541
|
-
|
|
680
|
+
const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
|
|
681
|
+
output({ ...state, effectiveRoleModels }, opts.json);
|
|
542
682
|
if (!opts.json) {
|
|
543
683
|
console.log(chalk.bold(`${COLLAB_SYSTEM_NAME} Status`));
|
|
544
684
|
console.log(chalk.dim(`Session: ${state.sessionId}`));
|
|
545
685
|
console.log(chalk.dim(`Status: ${state.status}`));
|
|
546
|
-
console.log(chalk.dim(`Round: ${state.round}/${state.maxRounds}`));
|
|
686
|
+
console.log(chalk.dim(`Round: ${Math.min(state.round, state.maxRounds)}/${state.maxRounds}`));
|
|
547
687
|
console.log(chalk.dim(`Active agent: ${state.activeAgent || 'none'}`));
|
|
548
688
|
console.log(chalk.dim(`Messages: ${state.messages.length}`));
|
|
689
|
+
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
690
|
+
for (const role of COLLAB_ROLES) {
|
|
691
|
+
const configured = state.roleModels?.[role] || '-';
|
|
692
|
+
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
693
|
+
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
694
|
+
}
|
|
549
695
|
if (state.tmuxSessionName) {
|
|
550
696
|
console.log(chalk.dim(`tmux: ${state.tmuxSessionName}`));
|
|
551
697
|
}
|
|
@@ -616,6 +762,7 @@ export function agentsCommand() {
|
|
|
616
762
|
const nextState = await runWorkerIteration(opts.session, role, {
|
|
617
763
|
cwd: state.workingDirectory || process.cwd(),
|
|
618
764
|
model: state.model || null,
|
|
765
|
+
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
619
766
|
});
|
|
620
767
|
const latest = nextState.messages[nextState.messages.length - 1];
|
|
621
768
|
if (latest?.from === role) {
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -9,7 +9,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { COLLAB_ROLES } from '../agents/constants.js';
|
|
12
|
+
import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-selection.js';
|
|
12
13
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
14
|
+
import { getSessionDir } from '../agents/state-store.js';
|
|
15
|
+
import { spawnTmuxSession, tmuxSessionExists } from '../agents/runtime.js';
|
|
13
16
|
import {
|
|
14
17
|
addUserMessage,
|
|
15
18
|
createSession,
|
|
@@ -20,6 +23,7 @@ import {
|
|
|
20
23
|
setCurrentSession,
|
|
21
24
|
stopSession,
|
|
22
25
|
} from '../agents/state-store.js';
|
|
26
|
+
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
23
27
|
|
|
24
28
|
class MCPCollabServer {
|
|
25
29
|
constructor() {
|
|
@@ -39,20 +43,59 @@ class MCPCollabServer {
|
|
|
39
43
|
task: z.string().describe('Initial collaborative task'),
|
|
40
44
|
maxRounds: z.number().int().positive().default(3).describe('Maximum collaboration rounds'),
|
|
41
45
|
model: z.string().optional().describe('Model id (provider/model) for opencode run'),
|
|
46
|
+
roleModels: z.object({
|
|
47
|
+
planner: z.string().optional(),
|
|
48
|
+
critic: z.string().optional(),
|
|
49
|
+
coder: z.string().optional(),
|
|
50
|
+
reviewer: z.string().optional(),
|
|
51
|
+
}).partial().optional().describe('Optional per-role models (provider/model)'),
|
|
52
|
+
cwd: z.string().optional().describe('Working directory for agents'),
|
|
53
|
+
spawnWorkers: z.boolean().default(true).describe('Create tmux workers and panes'),
|
|
42
54
|
},
|
|
43
|
-
async ({ task, maxRounds, model }) => {
|
|
55
|
+
async ({ task, maxRounds, model, roleModels, cwd, spawnWorkers }) => {
|
|
44
56
|
try {
|
|
57
|
+
const workingDirectory = cwd || process.cwd();
|
|
58
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
59
|
+
if (!opencodeBin) {
|
|
60
|
+
throw new Error('OpenCode binary not found in PATH. Run: acfm agents setup');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (spawnWorkers && !hasCommand('tmux')) {
|
|
64
|
+
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
const state = await createSession(task, {
|
|
46
68
|
roles: COLLAB_ROLES,
|
|
47
69
|
maxRounds,
|
|
48
70
|
model: model || null,
|
|
49
|
-
|
|
71
|
+
roleModels: sanitizeRoleModels(roleModels),
|
|
72
|
+
workingDirectory,
|
|
73
|
+
opencodeBin,
|
|
50
74
|
});
|
|
75
|
+
let updated = state;
|
|
76
|
+
if (spawnWorkers) {
|
|
77
|
+
const tmuxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
78
|
+
const sessionDir = getSessionDir(state.sessionId);
|
|
79
|
+
await spawnTmuxSession({ sessionName: tmuxSessionName, sessionDir, sessionId: state.sessionId });
|
|
80
|
+
updated = await saveSessionState({ ...state, tmuxSessionName });
|
|
81
|
+
}
|
|
51
82
|
await setCurrentSession(state.sessionId);
|
|
83
|
+
|
|
84
|
+
const tmuxSessionName = updated.tmuxSessionName || null;
|
|
85
|
+
const attachCommand = tmuxSessionName ? `tmux attach -t ${tmuxSessionName}` : null;
|
|
52
86
|
return {
|
|
53
87
|
content: [{
|
|
54
88
|
type: 'text',
|
|
55
|
-
text: JSON.stringify({
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
success: true,
|
|
91
|
+
sessionId: updated.sessionId,
|
|
92
|
+
status: updated.status,
|
|
93
|
+
model: updated.model || null,
|
|
94
|
+
roleModels: updated.roleModels || {},
|
|
95
|
+
effectiveRoleModels: buildEffectiveRoleModels(updated, updated.model || null),
|
|
96
|
+
tmuxSessionName,
|
|
97
|
+
attachCommand,
|
|
98
|
+
}, null, 2),
|
|
56
99
|
}],
|
|
57
100
|
};
|
|
58
101
|
} catch (error) {
|
|
@@ -102,9 +145,11 @@ class MCPCollabServer {
|
|
|
102
145
|
if (!id) {
|
|
103
146
|
throw new Error('No active session found');
|
|
104
147
|
}
|
|
148
|
+
const loaded = await loadSessionState(id);
|
|
105
149
|
|
|
106
150
|
const state = await runWorkerIteration(id, role, {
|
|
107
151
|
cwd: process.cwd(),
|
|
152
|
+
opencodeBin: loaded.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
108
153
|
});
|
|
109
154
|
|
|
110
155
|
return {
|
|
@@ -116,6 +161,9 @@ class MCPCollabServer {
|
|
|
116
161
|
status: state.status,
|
|
117
162
|
round: state.round,
|
|
118
163
|
activeAgent: state.activeAgent,
|
|
164
|
+
model: state.model || null,
|
|
165
|
+
roleModels: state.roleModels || {},
|
|
166
|
+
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
119
167
|
messageCount: state.messages.length,
|
|
120
168
|
}, null, 2),
|
|
121
169
|
}],
|
|
@@ -126,6 +174,55 @@ class MCPCollabServer {
|
|
|
126
174
|
}
|
|
127
175
|
);
|
|
128
176
|
|
|
177
|
+
this.server.tool(
|
|
178
|
+
'collab_resume_session',
|
|
179
|
+
'Resume session and recreate tmux workers if needed',
|
|
180
|
+
{
|
|
181
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
182
|
+
recreateWorkers: z.boolean().default(true).describe('Recreate tmux session when missing'),
|
|
183
|
+
},
|
|
184
|
+
async ({ sessionId, recreateWorkers }) => {
|
|
185
|
+
try {
|
|
186
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
187
|
+
if (!id) throw new Error('No active session found');
|
|
188
|
+
let state = await loadSessionState(id);
|
|
189
|
+
|
|
190
|
+
const tmuxSessionName = state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
191
|
+
const tmuxExists = hasCommand('tmux') ? await tmuxSessionExists(tmuxSessionName) : false;
|
|
192
|
+
|
|
193
|
+
if (!tmuxExists && recreateWorkers) {
|
|
194
|
+
if (!hasCommand('tmux')) {
|
|
195
|
+
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
196
|
+
}
|
|
197
|
+
const sessionDir = getSessionDir(state.sessionId);
|
|
198
|
+
await spawnTmuxSession({ sessionName: tmuxSessionName, sessionDir, sessionId: state.sessionId });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
state = await saveSessionState({
|
|
202
|
+
...state,
|
|
203
|
+
status: 'running',
|
|
204
|
+
tmuxSessionName,
|
|
205
|
+
});
|
|
206
|
+
await setCurrentSession(state.sessionId);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: JSON.stringify({
|
|
212
|
+
success: true,
|
|
213
|
+
sessionId: state.sessionId,
|
|
214
|
+
status: state.status,
|
|
215
|
+
tmuxSessionName,
|
|
216
|
+
recreatedWorkers: !tmuxExists && recreateWorkers,
|
|
217
|
+
}, null, 2),
|
|
218
|
+
}],
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
129
226
|
this.server.tool(
|
|
130
227
|
'collab_status',
|
|
131
228
|
'Get current collaborative session state',
|
|
@@ -144,7 +241,11 @@ class MCPCollabServer {
|
|
|
144
241
|
return {
|
|
145
242
|
content: [{
|
|
146
243
|
type: 'text',
|
|
147
|
-
text: JSON.stringify({
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
state,
|
|
246
|
+
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
247
|
+
transcript,
|
|
248
|
+
}, null, 2),
|
|
148
249
|
}],
|
|
149
250
|
};
|
|
150
251
|
} catch (error) {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { platform } from 'node:os';
|
|
3
5
|
|
|
6
|
+
function preferredOpenCodePath() {
|
|
7
|
+
const home = process.env.HOME;
|
|
8
|
+
if (!home) return null;
|
|
9
|
+
return join(home, '.opencode', 'bin', 'opencode');
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
function run(command, args, options = {}) {
|
|
5
13
|
return spawnSync(command, args, {
|
|
6
14
|
stdio: options.stdio || 'pipe',
|
|
@@ -10,9 +18,20 @@ function run(command, args, options = {}) {
|
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export function hasCommand(command) {
|
|
21
|
+
return Boolean(resolveCommandPath(command));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCommandPath(command) {
|
|
25
|
+
const preferredPath = command === 'opencode' ? preferredOpenCodePath() : null;
|
|
26
|
+
if (preferredPath && existsSync(preferredPath)) {
|
|
27
|
+
return preferredPath;
|
|
28
|
+
}
|
|
13
29
|
const locator = platform() === 'win32' ? 'where' : 'which';
|
|
14
30
|
const result = run(locator, [command]);
|
|
15
|
-
|
|
31
|
+
if (result.status !== 0) return null;
|
|
32
|
+
const out = String(result.stdout || '').trim();
|
|
33
|
+
if (!out) return null;
|
|
34
|
+
return out.split('\n').map((line) => line.trim()).filter(Boolean)[0] || null;
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
export function installOpenCode() {
|