ac-framework 1.9.5 → 1.9.7
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 +16 -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 +49 -0
- package/src/agents/constants.js +1 -0
- package/src/agents/model-selection.js +38 -0
- package/src/agents/opencode-client.js +68 -9
- package/src/agents/orchestrator.js +10 -3
- package/src/agents/runtime.js +82 -0
- package/src/agents/state-store.js +3 -1
- package/src/commands/agents.js +319 -83
- package/src/mcp/collab-server.js +105 -4
- package/src/services/dependency-installer.js +20 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { COLLAB_ROLES } from './constants.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
8
|
+
|
|
9
|
+
export function roleLogPath(sessionDir, role) {
|
|
10
|
+
return resolve(sessionDir, `${role}.log`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function runTmux(command, args, options = {}) {
|
|
14
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
15
|
+
const child = spawn(command, args, {
|
|
16
|
+
cwd: options.cwd || process.cwd(),
|
|
17
|
+
stdio: options.stdio || 'pipe',
|
|
18
|
+
env: process.env,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let stderr = '';
|
|
22
|
+
let stdout = '';
|
|
23
|
+
if (child.stderr) {
|
|
24
|
+
child.stderr.on('data', (chunk) => {
|
|
25
|
+
stderr += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (child.stdout) {
|
|
29
|
+
child.stdout.on('data', (chunk) => {
|
|
30
|
+
stdout += chunk.toString();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
child.on('error', rejectPromise);
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
resolvePromise({ stdout, stderr });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
rejectPromise(new Error(stderr.trim() || `${command} exited with code ${code}`));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
46
|
+
const role0 = COLLAB_ROLES[0];
|
|
47
|
+
const role0Log = roleLogPath(sessionDir, role0);
|
|
48
|
+
await runTmux('tmux', [
|
|
49
|
+
'new-session',
|
|
50
|
+
'-d',
|
|
51
|
+
'-s',
|
|
52
|
+
sessionName,
|
|
53
|
+
'-n',
|
|
54
|
+
role0,
|
|
55
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0} 2>&1 | tee -a "${role0Log}"'`,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
59
|
+
const role = COLLAB_ROLES[idx];
|
|
60
|
+
const roleLog = roleLogPath(sessionDir, role);
|
|
61
|
+
await runTmux('tmux', [
|
|
62
|
+
'split-window',
|
|
63
|
+
'-t',
|
|
64
|
+
sessionName,
|
|
65
|
+
'-v',
|
|
66
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`,
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await runTmux('tmux', ['select-layout', '-t', sessionName, 'tiled']);
|
|
71
|
+
await runTmux('tmux', ['set-option', '-t', sessionName, 'pane-border-status', 'top']);
|
|
72
|
+
await runTmux('tmux', ['set-option', '-t', sessionName, 'pane-border-format', '#{pane_index}:#{pane_title}']);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function tmuxSessionExists(sessionName) {
|
|
76
|
+
try {
|
|
77
|
+
await runTmux('tmux', ['has-session', '-t', sessionName]);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SESSION_ROOT_DIR,
|
|
9
9
|
CURRENT_SESSION_FILE,
|
|
10
10
|
} from './constants.js';
|
|
11
|
+
import { sanitizeRoleModels } from './model-selection.js';
|
|
11
12
|
|
|
12
13
|
function sleep(ms) {
|
|
13
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -53,6 +54,8 @@ function initialState(task, options = {}) {
|
|
|
53
54
|
roles: options.roles?.length ? options.roles : COLLAB_ROLES,
|
|
54
55
|
workingDirectory: options.workingDirectory || process.cwd(),
|
|
55
56
|
model: options.model || null,
|
|
57
|
+
roleModels: sanitizeRoleModels(options.roleModels),
|
|
58
|
+
opencodeBin: options.opencodeBin || null,
|
|
56
59
|
tmuxSessionName: options.tmuxSessionName || null,
|
|
57
60
|
messages: [
|
|
58
61
|
{
|
|
@@ -106,7 +109,6 @@ export async function saveSessionState(state) {
|
|
|
106
109
|
updatedAt: new Date().toISOString(),
|
|
107
110
|
};
|
|
108
111
|
await writeFile(getSessionStatePath(updated.sessionId), JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
109
|
-
await writeCurrentSession(updated.sessionId, updated.updatedAt);
|
|
110
112
|
return updated;
|
|
111
113
|
}
|
|
112
114
|
|
package/src/commands/agents.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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,
|
|
12
10
|
CURRENT_SESSION_FILE,
|
|
13
11
|
DEFAULT_MAX_ROUNDS,
|
|
12
|
+
DEFAULT_SYNAPSE_MODEL,
|
|
14
13
|
SESSION_ROOT_DIR,
|
|
15
14
|
} from '../agents/constants.js';
|
|
15
|
+
import { runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
16
16
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
17
17
|
import {
|
|
18
18
|
addUserMessage,
|
|
@@ -26,7 +26,15 @@ import {
|
|
|
26
26
|
setCurrentSession,
|
|
27
27
|
stopSession,
|
|
28
28
|
} from '../agents/state-store.js';
|
|
29
|
-
import {
|
|
29
|
+
import { roleLogPath, runTmux, spawnTmuxSession, tmuxSessionExists } from '../agents/runtime.js';
|
|
30
|
+
import { getAgentsConfigPath, loadAgentsConfig, updateAgentsConfig } from '../agents/config-store.js';
|
|
31
|
+
import {
|
|
32
|
+
buildEffectiveRoleModels,
|
|
33
|
+
isValidModelId,
|
|
34
|
+
normalizeModelId,
|
|
35
|
+
sanitizeRoleModels,
|
|
36
|
+
} from '../agents/model-selection.js';
|
|
37
|
+
import { ensureCollabDependencies, hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
30
38
|
|
|
31
39
|
function output(data, json) {
|
|
32
40
|
if (json) {
|
|
@@ -34,79 +42,12 @@ function output(data, json) {
|
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
function roleLogPath(sessionDir, role) {
|
|
38
|
-
return resolve(sessionDir, `${role}.log`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
45
|
function tailLines(text, maxLines) {
|
|
42
46
|
const lines = text.split('\n');
|
|
43
47
|
const sliced = lines.slice(Math.max(lines.length - maxLines, 0));
|
|
44
48
|
return sliced.join('\n').trimEnd();
|
|
45
49
|
}
|
|
46
50
|
|
|
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
51
|
async function ensureSessionId(required = true) {
|
|
111
52
|
const sessionId = await loadCurrentSessionId();
|
|
112
53
|
if (!sessionId && required) {
|
|
@@ -124,18 +65,20 @@ function printStartSummary(state) {
|
|
|
124
65
|
console.log();
|
|
125
66
|
console.log(chalk.cyan('Attach with:'));
|
|
126
67
|
console.log(chalk.white(` tmux attach -t ${state.tmuxSessionName}`));
|
|
68
|
+
console.log(chalk.white(' acfm agents live'));
|
|
127
69
|
console.log();
|
|
128
70
|
console.log(chalk.cyan('Interact with:'));
|
|
129
71
|
console.log(chalk.white(' acfm agents send "your message"'));
|
|
130
72
|
}
|
|
131
73
|
|
|
132
74
|
function toMarkdownTranscript(state, transcript) {
|
|
75
|
+
const displayedRound = Math.min(state.round, state.maxRounds);
|
|
133
76
|
const lines = [
|
|
134
77
|
`# SynapseGrid Session ${state.sessionId}`,
|
|
135
78
|
'',
|
|
136
79
|
`- Task: ${state.task}`,
|
|
137
80
|
`- Status: ${state.status}`,
|
|
138
|
-
`- Rounds: ${
|
|
81
|
+
`- Rounds: ${displayedRound}/${state.maxRounds}`,
|
|
139
82
|
`- Roles: ${state.roles.join(', ')}`,
|
|
140
83
|
`- Created: ${state.createdAt}`,
|
|
141
84
|
`- Updated: ${state.updatedAt}`,
|
|
@@ -155,6 +98,51 @@ function toMarkdownTranscript(state, transcript) {
|
|
|
155
98
|
return lines.join('\n');
|
|
156
99
|
}
|
|
157
100
|
|
|
101
|
+
function parseRoleModelOptions(opts) {
|
|
102
|
+
return sanitizeRoleModels({
|
|
103
|
+
planner: opts.modelPlanner,
|
|
104
|
+
critic: opts.modelCritic,
|
|
105
|
+
coder: opts.modelCoder,
|
|
106
|
+
reviewer: opts.modelReviewer,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertValidModelIdOrNull(label, value) {
|
|
111
|
+
const normalized = normalizeModelId(value);
|
|
112
|
+
if (!normalized) return null;
|
|
113
|
+
if (!isValidModelId(normalized)) {
|
|
114
|
+
throw new Error(`${label} must be in provider/model format`);
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function printModelConfig(state) {
|
|
120
|
+
const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
|
|
121
|
+
console.log(chalk.bold('\nModel configuration'));
|
|
122
|
+
console.log(chalk.dim(` Global fallback: ${state.model || '(opencode default)'}`));
|
|
123
|
+
for (const role of COLLAB_ROLES) {
|
|
124
|
+
const configured = state.roleModels?.[role] || '-';
|
|
125
|
+
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
126
|
+
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function preflightModel({ opencodeBin, model, cwd }) {
|
|
131
|
+
const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
|
|
132
|
+
try {
|
|
133
|
+
await runOpenCodePrompt({
|
|
134
|
+
prompt: 'Reply with exactly: OK',
|
|
135
|
+
cwd,
|
|
136
|
+
model: selected,
|
|
137
|
+
binaryPath: opencodeBin,
|
|
138
|
+
timeoutMs: 45000,
|
|
139
|
+
});
|
|
140
|
+
return { ok: true, model: selected };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { ok: false, model: selected, error: error.message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
158
146
|
export function agentsCommand() {
|
|
159
147
|
const agents = new Command('agents')
|
|
160
148
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
@@ -295,6 +283,31 @@ export function agentsCommand() {
|
|
|
295
283
|
}
|
|
296
284
|
});
|
|
297
285
|
|
|
286
|
+
agents
|
|
287
|
+
.command('live')
|
|
288
|
+
.description('Attach to live tmux collaboration view (all agent panes)')
|
|
289
|
+
.option('--readonly', 'Attach in read-only mode', false)
|
|
290
|
+
.action(async (opts) => {
|
|
291
|
+
try {
|
|
292
|
+
const sessionId = await ensureSessionId(true);
|
|
293
|
+
const state = await loadSessionState(sessionId);
|
|
294
|
+
if (!state.tmuxSessionName) {
|
|
295
|
+
throw new Error('No tmux session registered for active collaborative session');
|
|
296
|
+
}
|
|
297
|
+
const tmuxExists = await tmuxSessionExists(state.tmuxSessionName);
|
|
298
|
+
if (!tmuxExists) {
|
|
299
|
+
throw new Error(`tmux session ${state.tmuxSessionName} no longer exists. Run: acfm agents resume`);
|
|
300
|
+
}
|
|
301
|
+
const args = ['attach'];
|
|
302
|
+
if (opts.readonly) args.push('-r');
|
|
303
|
+
args.push('-t', state.tmuxSessionName);
|
|
304
|
+
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
298
311
|
agents
|
|
299
312
|
.command('resume')
|
|
300
313
|
.description('Resume a previous session and optionally recreate tmux workers')
|
|
@@ -308,15 +321,7 @@ export function agentsCommand() {
|
|
|
308
321
|
let state = await loadSessionState(sessionId);
|
|
309
322
|
|
|
310
323
|
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
|
-
}
|
|
324
|
+
const tmuxExists = hasCommand('tmux') ? await tmuxSessionExists(tmuxSessionName) : false;
|
|
320
325
|
|
|
321
326
|
if (!tmuxExists && opts.recreate) {
|
|
322
327
|
if (!hasCommand('tmux')) {
|
|
@@ -410,6 +415,136 @@ export function agentsCommand() {
|
|
|
410
415
|
}
|
|
411
416
|
});
|
|
412
417
|
|
|
418
|
+
const model = agents
|
|
419
|
+
.command('model')
|
|
420
|
+
.description('Manage default SynapseGrid model configuration');
|
|
421
|
+
|
|
422
|
+
model
|
|
423
|
+
.command('get')
|
|
424
|
+
.description('Show configured default global/per-role models')
|
|
425
|
+
.option('--json', 'Output as JSON')
|
|
426
|
+
.action(async (opts) => {
|
|
427
|
+
try {
|
|
428
|
+
const config = await loadAgentsConfig();
|
|
429
|
+
const payload = {
|
|
430
|
+
configPath: getAgentsConfigPath(),
|
|
431
|
+
defaultModel: config.agents.defaultModel,
|
|
432
|
+
defaultRoleModels: config.agents.defaultRoleModels,
|
|
433
|
+
};
|
|
434
|
+
output(payload, opts.json);
|
|
435
|
+
if (!opts.json) {
|
|
436
|
+
console.log(chalk.bold('SynapseGrid default models'));
|
|
437
|
+
console.log(chalk.dim(`Config: ${payload.configPath}`));
|
|
438
|
+
console.log(chalk.dim(`Global fallback: ${payload.defaultModel || '(none)'}`));
|
|
439
|
+
for (const role of COLLAB_ROLES) {
|
|
440
|
+
console.log(chalk.dim(`- ${role}: ${payload.defaultRoleModels[role] || '(none)'}`));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
output({ error: error.message }, opts.json);
|
|
445
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
model
|
|
451
|
+
.command('set <modelId>')
|
|
452
|
+
.description('Set default model globally or for a specific role')
|
|
453
|
+
.option('--role <role>', 'planner|critic|coder|reviewer|all', 'all')
|
|
454
|
+
.option('--json', 'Output as JSON')
|
|
455
|
+
.action(async (modelId, opts) => {
|
|
456
|
+
try {
|
|
457
|
+
const role = String(opts.role || 'all');
|
|
458
|
+
const normalized = assertValidModelIdOrNull('model', modelId);
|
|
459
|
+
if (!normalized) throw new Error('model must be provided');
|
|
460
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
461
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const updated = await updateAgentsConfig((current) => {
|
|
465
|
+
const next = {
|
|
466
|
+
agents: {
|
|
467
|
+
defaultModel: current.agents.defaultModel,
|
|
468
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
if (role === 'all') {
|
|
472
|
+
next.agents.defaultModel = normalized;
|
|
473
|
+
} else {
|
|
474
|
+
next.agents.defaultRoleModels = {
|
|
475
|
+
...next.agents.defaultRoleModels,
|
|
476
|
+
[role]: normalized,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return next;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const payload = {
|
|
483
|
+
success: true,
|
|
484
|
+
configPath: getAgentsConfigPath(),
|
|
485
|
+
defaultModel: updated.agents.defaultModel,
|
|
486
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
487
|
+
};
|
|
488
|
+
output(payload, opts.json);
|
|
489
|
+
if (!opts.json) {
|
|
490
|
+
console.log(chalk.green('✓ SynapseGrid model configuration updated'));
|
|
491
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
output({ error: error.message }, opts.json);
|
|
495
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
model
|
|
501
|
+
.command('clear')
|
|
502
|
+
.description('Clear default model globally or for a specific role')
|
|
503
|
+
.option('--role <role>', 'planner|critic|coder|reviewer|all', 'all')
|
|
504
|
+
.option('--json', 'Output as JSON')
|
|
505
|
+
.action(async (opts) => {
|
|
506
|
+
try {
|
|
507
|
+
const role = String(opts.role || 'all');
|
|
508
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
509
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const updated = await updateAgentsConfig((current) => {
|
|
513
|
+
const next = {
|
|
514
|
+
agents: {
|
|
515
|
+
defaultModel: current.agents.defaultModel,
|
|
516
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
if (role === 'all') {
|
|
520
|
+
next.agents.defaultModel = DEFAULT_SYNAPSE_MODEL;
|
|
521
|
+
next.agents.defaultRoleModels = {};
|
|
522
|
+
} else {
|
|
523
|
+
const currentRoles = { ...next.agents.defaultRoleModels };
|
|
524
|
+
delete currentRoles[role];
|
|
525
|
+
next.agents.defaultRoleModels = currentRoles;
|
|
526
|
+
}
|
|
527
|
+
return next;
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const payload = {
|
|
531
|
+
success: true,
|
|
532
|
+
configPath: getAgentsConfigPath(),
|
|
533
|
+
defaultModel: updated.agents.defaultModel,
|
|
534
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
535
|
+
};
|
|
536
|
+
output(payload, opts.json);
|
|
537
|
+
if (!opts.json) {
|
|
538
|
+
console.log(chalk.green('✓ SynapseGrid model configuration cleared'));
|
|
539
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
output({ error: error.message }, opts.json);
|
|
543
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
413
548
|
agents
|
|
414
549
|
.command('export')
|
|
415
550
|
.description('Export collaborative transcript')
|
|
@@ -459,6 +594,10 @@ export function agentsCommand() {
|
|
|
459
594
|
.requiredOption('--task <text>', 'Initial task from user')
|
|
460
595
|
.option('--rounds <n>', 'Maximum collaboration rounds', String(DEFAULT_MAX_ROUNDS))
|
|
461
596
|
.option('--model <id>', 'Model to use (provider/model)')
|
|
597
|
+
.option('--model-planner <id>', 'Model for planner role (provider/model)')
|
|
598
|
+
.option('--model-critic <id>', 'Model for critic role (provider/model)')
|
|
599
|
+
.option('--model-coder <id>', 'Model for coder role (provider/model)')
|
|
600
|
+
.option('--model-reviewer <id>', 'Model for reviewer role (provider/model)')
|
|
462
601
|
.option('--cwd <path>', 'Working directory for agents', process.cwd())
|
|
463
602
|
.option('--attach', 'Attach tmux immediately after start', false)
|
|
464
603
|
.option('--json', 'Output as JSON')
|
|
@@ -470,6 +609,10 @@ export function agentsCommand() {
|
|
|
470
609
|
if (!hasCommand('tmux')) {
|
|
471
610
|
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
472
611
|
}
|
|
612
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
613
|
+
if (!opencodeBin) {
|
|
614
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
615
|
+
}
|
|
473
616
|
|
|
474
617
|
await mkdir(SESSION_ROOT_DIR, { recursive: true });
|
|
475
618
|
const maxRounds = Number.parseInt(opts.rounds, 10);
|
|
@@ -477,11 +620,43 @@ export function agentsCommand() {
|
|
|
477
620
|
throw new Error('--rounds must be a positive integer');
|
|
478
621
|
}
|
|
479
622
|
|
|
623
|
+
const config = await loadAgentsConfig();
|
|
624
|
+
const cliModel = assertValidModelIdOrNull('--model', opts.model || null);
|
|
625
|
+
const cliRoleModels = parseRoleModelOptions(opts);
|
|
626
|
+
for (const [role, model] of Object.entries(cliRoleModels)) {
|
|
627
|
+
if (!isValidModelId(model)) {
|
|
628
|
+
throw new Error(`--model-${role} must be in provider/model format`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const defaultRoleModels = sanitizeRoleModels(config.agents.defaultRoleModels);
|
|
632
|
+
const roleModels = {
|
|
633
|
+
...defaultRoleModels,
|
|
634
|
+
...cliRoleModels,
|
|
635
|
+
};
|
|
636
|
+
const globalModel = cliModel || config.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
637
|
+
|
|
638
|
+
const modelsToCheck = new Set([globalModel, ...Object.values(roleModels)]);
|
|
639
|
+
for (const modelToCheck of modelsToCheck) {
|
|
640
|
+
const preflight = await preflightModel({
|
|
641
|
+
opencodeBin,
|
|
642
|
+
model: modelToCheck,
|
|
643
|
+
cwd: resolve(opts.cwd),
|
|
644
|
+
});
|
|
645
|
+
if (!preflight.ok) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Model preflight failed for ${preflight.model}: ${preflight.error}. ` +
|
|
648
|
+
'Check OpenCode auth/providers with: opencode auth list, opencode models'
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
480
653
|
const state = await createSession(opts.task, {
|
|
481
654
|
roles: COLLAB_ROLES,
|
|
482
655
|
maxRounds,
|
|
483
|
-
model:
|
|
656
|
+
model: globalModel,
|
|
657
|
+
roleModels,
|
|
484
658
|
workingDirectory: resolve(opts.cwd),
|
|
659
|
+
opencodeBin,
|
|
485
660
|
});
|
|
486
661
|
const tmuxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
487
662
|
const sessionDir = getSessionDir(state.sessionId);
|
|
@@ -499,6 +674,7 @@ export function agentsCommand() {
|
|
|
499
674
|
output({ sessionId: updated.sessionId, tmuxSessionName, status: updated.status }, opts.json);
|
|
500
675
|
if (!opts.json) {
|
|
501
676
|
printStartSummary(updated);
|
|
677
|
+
printModelConfig(updated);
|
|
502
678
|
}
|
|
503
679
|
|
|
504
680
|
if (opts.attach) {
|
|
@@ -538,14 +714,21 @@ export function agentsCommand() {
|
|
|
538
714
|
try {
|
|
539
715
|
const sessionId = await ensureSessionId(true);
|
|
540
716
|
const state = await loadSessionState(sessionId);
|
|
541
|
-
|
|
717
|
+
const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
|
|
718
|
+
output({ ...state, effectiveRoleModels }, opts.json);
|
|
542
719
|
if (!opts.json) {
|
|
543
720
|
console.log(chalk.bold(`${COLLAB_SYSTEM_NAME} Status`));
|
|
544
721
|
console.log(chalk.dim(`Session: ${state.sessionId}`));
|
|
545
722
|
console.log(chalk.dim(`Status: ${state.status}`));
|
|
546
|
-
console.log(chalk.dim(`Round: ${state.round}/${state.maxRounds}`));
|
|
723
|
+
console.log(chalk.dim(`Round: ${Math.min(state.round, state.maxRounds)}/${state.maxRounds}`));
|
|
547
724
|
console.log(chalk.dim(`Active agent: ${state.activeAgent || 'none'}`));
|
|
548
725
|
console.log(chalk.dim(`Messages: ${state.messages.length}`));
|
|
726
|
+
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
727
|
+
for (const role of COLLAB_ROLES) {
|
|
728
|
+
const configured = state.roleModels?.[role] || '-';
|
|
729
|
+
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
730
|
+
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
731
|
+
}
|
|
549
732
|
if (state.tmuxSessionName) {
|
|
550
733
|
console.log(chalk.dim(`tmux: ${state.tmuxSessionName}`));
|
|
551
734
|
}
|
|
@@ -603,6 +786,7 @@ export function agentsCommand() {
|
|
|
603
786
|
|
|
604
787
|
while (true) {
|
|
605
788
|
try {
|
|
789
|
+
console.log(`[${role}] polling session ${opts.session}`);
|
|
606
790
|
const state = await loadSessionState(opts.session);
|
|
607
791
|
if (!state.roles.includes(role)) {
|
|
608
792
|
console.log(`[${role}] role not configured in session. exiting.`);
|
|
@@ -613,13 +797,20 @@ export function agentsCommand() {
|
|
|
613
797
|
process.exit(0);
|
|
614
798
|
}
|
|
615
799
|
|
|
800
|
+
if (state.activeAgent === role) {
|
|
801
|
+
console.log(`[${role}] executing turn with model=${state.roleModels?.[role] || state.model || DEFAULT_SYNAPSE_MODEL}`);
|
|
802
|
+
}
|
|
616
803
|
const nextState = await runWorkerIteration(opts.session, role, {
|
|
617
804
|
cwd: state.workingDirectory || process.cwd(),
|
|
618
805
|
model: state.model || null,
|
|
806
|
+
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
807
|
+
timeoutMs: 180000,
|
|
619
808
|
});
|
|
620
809
|
const latest = nextState.messages[nextState.messages.length - 1];
|
|
621
810
|
if (latest?.from === role) {
|
|
622
811
|
console.log(`[${role}] message emitted (${latest.content.length} chars)`);
|
|
812
|
+
} else if (nextState.activeAgent && nextState.activeAgent !== role) {
|
|
813
|
+
console.log(`[${role}] waiting for active role=${nextState.activeAgent}`);
|
|
623
814
|
}
|
|
624
815
|
} catch (error) {
|
|
625
816
|
console.error(`[${role}] loop error: ${error.message}`);
|
|
@@ -629,5 +820,50 @@ export function agentsCommand() {
|
|
|
629
820
|
}
|
|
630
821
|
});
|
|
631
822
|
|
|
823
|
+
agents
|
|
824
|
+
.command('doctor')
|
|
825
|
+
.description('Run diagnostics for SynapseGrid/OpenCode runtime')
|
|
826
|
+
.option('--json', 'Output as JSON')
|
|
827
|
+
.action(async (opts) => {
|
|
828
|
+
try {
|
|
829
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
830
|
+
const tmuxInstalled = hasCommand('tmux');
|
|
831
|
+
const cfg = await loadAgentsConfig();
|
|
832
|
+
const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
833
|
+
const result = {
|
|
834
|
+
opencodeBin,
|
|
835
|
+
tmuxInstalled,
|
|
836
|
+
defaultModel,
|
|
837
|
+
defaultRoleModels: cfg.agents.defaultRoleModels,
|
|
838
|
+
preflight: null,
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
if (opencodeBin) {
|
|
842
|
+
result.preflight = await preflightModel({
|
|
843
|
+
opencodeBin,
|
|
844
|
+
model: defaultModel,
|
|
845
|
+
cwd: process.cwd(),
|
|
846
|
+
});
|
|
847
|
+
} else {
|
|
848
|
+
result.preflight = { ok: false, error: 'OpenCode binary not found' };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
output(result, opts.json);
|
|
852
|
+
if (!opts.json) {
|
|
853
|
+
console.log(chalk.bold('SynapseGrid doctor'));
|
|
854
|
+
console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
|
|
855
|
+
console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
|
|
856
|
+
console.log(chalk.dim(`default model: ${defaultModel}`));
|
|
857
|
+
console.log(chalk.dim(`preflight: ${result.preflight?.ok ? 'ok' : `failed - ${result.preflight?.error || 'unknown error'}`}`));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (!result.preflight?.ok) process.exit(1);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
output({ error: error.message }, opts.json);
|
|
863
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
|
|
632
868
|
return agents;
|
|
633
869
|
}
|