ac-framework 1.9.6 → 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/README.md
CHANGED
|
@@ -141,6 +141,7 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
141
141
|
| Command | Description |
|
|
142
142
|
|---|---|
|
|
143
143
|
| `acfm agents setup` | Install optional dependencies (`opencode` and `tmux`) |
|
|
144
|
+
| `acfm agents doctor` | Validate OpenCode/tmux/model preflight before start |
|
|
144
145
|
| `acfm agents install-mcps` | Install SynapseGrid MCP server for detected assistants |
|
|
145
146
|
| `acfm agents uninstall-mcps` | Remove SynapseGrid MCP server from assistants |
|
|
146
147
|
| `acfm agents start --task "..." --model-coder provider/model` | Start session with optional per-role models |
|
|
@@ -164,6 +165,8 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
164
165
|
- Inspect worker errors quickly with `acfm agents logs --role all --lines 120`.
|
|
165
166
|
- MCP starts can now create tmux workers directly; if your assistant used headless steps before, start a new session and ensure worker spawning is enabled.
|
|
166
167
|
- Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model set`.
|
|
168
|
+
- Default SynapseGrid model fallback is `opencode/minimax-m2.5-free`.
|
|
169
|
+
- Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
|
|
167
170
|
|
|
168
171
|
### Spec Workflow
|
|
169
172
|
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { DEFAULT_SYNAPSE_MODEL } from './constants.js';
|
|
5
6
|
import { sanitizeRoleModels, normalizeModelId } from './model-selection.js';
|
|
6
7
|
|
|
7
8
|
const ACFM_DIR = join(homedir(), '.acfm');
|
|
@@ -11,7 +12,7 @@ function normalizeConfig(raw) {
|
|
|
11
12
|
const agents = raw?.agents && typeof raw.agents === 'object' ? raw.agents : {};
|
|
12
13
|
return {
|
|
13
14
|
agents: {
|
|
14
|
-
defaultModel: normalizeModelId(agents.defaultModel) ||
|
|
15
|
+
defaultModel: normalizeModelId(agents.defaultModel) || DEFAULT_SYNAPSE_MODEL,
|
|
15
16
|
defaultRoleModels: sanitizeRoleModels(agents.defaultRoleModels),
|
|
16
17
|
},
|
|
17
18
|
};
|
package/src/agents/constants.js
CHANGED
|
@@ -4,5 +4,6 @@ import { join } from 'node:path';
|
|
|
4
4
|
export const COLLAB_SYSTEM_NAME = 'SynapseGrid';
|
|
5
5
|
export const COLLAB_ROLES = ['planner', 'critic', 'coder', 'reviewer'];
|
|
6
6
|
export const DEFAULT_MAX_ROUNDS = 3;
|
|
7
|
+
export const DEFAULT_SYNAPSE_MODEL = 'opencode/minimax-m2.5-free';
|
|
7
8
|
export const SESSION_ROOT_DIR = join(homedir(), '.acfm', 'synapsegrid');
|
|
8
9
|
export const CURRENT_SESSION_FILE = join(SESSION_ROOT_DIR, 'current-session.json');
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
5
2
|
|
|
6
3
|
function parseOpenCodeRunOutput(stdout) {
|
|
4
|
+
const ndjsonLines = stdout
|
|
5
|
+
.split('\n')
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
|
|
9
|
+
if (ndjsonLines.length > 0) {
|
|
10
|
+
const textChunks = [];
|
|
11
|
+
for (const line of ndjsonLines) {
|
|
12
|
+
try {
|
|
13
|
+
const event = JSON.parse(line);
|
|
14
|
+
const text = event?.part?.text;
|
|
15
|
+
if (typeof text === 'string' && text.trim()) {
|
|
16
|
+
textChunks.push(text.trim());
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// ignore malformed lines and continue
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (textChunks.length > 0) {
|
|
23
|
+
return textChunks.join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
try {
|
|
8
28
|
const parsed = JSON.parse(stdout);
|
|
9
29
|
if (Array.isArray(parsed)) {
|
|
@@ -33,10 +53,48 @@ export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs =
|
|
|
33
53
|
}
|
|
34
54
|
args.push('--', prompt);
|
|
35
55
|
|
|
36
|
-
const { stdout, stderr } = await
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
const { stdout, stderr } = await new Promise((resolvePromise, rejectPromise) => {
|
|
57
|
+
const child = spawn(binary, args, {
|
|
58
|
+
cwd,
|
|
59
|
+
env: process.env,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let out = '';
|
|
64
|
+
let err = '';
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
timedOut = true;
|
|
69
|
+
child.kill('SIGTERM');
|
|
70
|
+
setTimeout(() => child.kill('SIGKILL'), 1500).unref();
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
|
|
73
|
+
child.stdout.on('data', (chunk) => {
|
|
74
|
+
out += chunk.toString();
|
|
75
|
+
});
|
|
76
|
+
child.stderr.on('data', (chunk) => {
|
|
77
|
+
err += chunk.toString();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
child.on('error', (error) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
rejectPromise(error);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
child.on('close', (code, signal) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
if (timedOut) {
|
|
88
|
+
rejectPromise(new Error(`opencode timed out after ${timeoutMs}ms`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (code !== 0) {
|
|
92
|
+
const details = [err.trim(), out.trim()].filter(Boolean).join(' | ');
|
|
93
|
+
rejectPromise(new Error(details || `opencode exited with code ${code}${signal ? ` (${signal})` : ''}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resolvePromise({ stdout: out, stderr: err });
|
|
97
|
+
});
|
|
40
98
|
});
|
|
41
99
|
|
|
42
100
|
const parsed = parseOpenCodeRunOutput(stdout);
|
package/src/agents/runtime.js
CHANGED
|
@@ -44,6 +44,7 @@ export function runTmux(command, args, options = {}) {
|
|
|
44
44
|
|
|
45
45
|
export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
46
46
|
const role0 = COLLAB_ROLES[0];
|
|
47
|
+
const role0Log = roleLogPath(sessionDir, role0);
|
|
47
48
|
await runTmux('tmux', [
|
|
48
49
|
'new-session',
|
|
49
50
|
'-d',
|
|
@@ -51,17 +52,18 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
51
52
|
sessionName,
|
|
52
53
|
'-n',
|
|
53
54
|
role0,
|
|
54
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0}
|
|
55
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0} 2>&1 | tee -a "${role0Log}"'`,
|
|
55
56
|
]);
|
|
56
57
|
|
|
57
58
|
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
58
59
|
const role = COLLAB_ROLES[idx];
|
|
60
|
+
const roleLog = roleLogPath(sessionDir, role);
|
|
59
61
|
await runTmux('tmux', [
|
|
60
62
|
'split-window',
|
|
61
63
|
'-t',
|
|
62
64
|
sessionName,
|
|
63
65
|
'-v',
|
|
64
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role}
|
|
66
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`,
|
|
65
67
|
]);
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -109,7 +109,6 @@ export async function saveSessionState(state) {
|
|
|
109
109
|
updatedAt: new Date().toISOString(),
|
|
110
110
|
};
|
|
111
111
|
await writeFile(getSessionStatePath(updated.sessionId), JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
112
|
-
await writeCurrentSession(updated.sessionId, updated.updatedAt);
|
|
113
112
|
return updated;
|
|
114
113
|
}
|
|
115
114
|
|
package/src/commands/agents.js
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
COLLAB_SYSTEM_NAME,
|
|
10
10
|
CURRENT_SESSION_FILE,
|
|
11
11
|
DEFAULT_MAX_ROUNDS,
|
|
12
|
+
DEFAULT_SYNAPSE_MODEL,
|
|
12
13
|
SESSION_ROOT_DIR,
|
|
13
14
|
} from '../agents/constants.js';
|
|
15
|
+
import { runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
14
16
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
15
17
|
import {
|
|
16
18
|
addUserMessage,
|
|
@@ -125,6 +127,22 @@ function printModelConfig(state) {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
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
|
+
|
|
128
146
|
export function agentsCommand() {
|
|
129
147
|
const agents = new Command('agents')
|
|
130
148
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
@@ -276,6 +294,10 @@ export function agentsCommand() {
|
|
|
276
294
|
if (!state.tmuxSessionName) {
|
|
277
295
|
throw new Error('No tmux session registered for active collaborative session');
|
|
278
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
|
+
}
|
|
279
301
|
const args = ['attach'];
|
|
280
302
|
if (opts.readonly) args.push('-r');
|
|
281
303
|
args.push('-t', state.tmuxSessionName);
|
|
@@ -495,7 +517,7 @@ export function agentsCommand() {
|
|
|
495
517
|
},
|
|
496
518
|
};
|
|
497
519
|
if (role === 'all') {
|
|
498
|
-
next.agents.defaultModel =
|
|
520
|
+
next.agents.defaultModel = DEFAULT_SYNAPSE_MODEL;
|
|
499
521
|
next.agents.defaultRoleModels = {};
|
|
500
522
|
} else {
|
|
501
523
|
const currentRoles = { ...next.agents.defaultRoleModels };
|
|
@@ -611,7 +633,22 @@ export function agentsCommand() {
|
|
|
611
633
|
...defaultRoleModels,
|
|
612
634
|
...cliRoleModels,
|
|
613
635
|
};
|
|
614
|
-
const globalModel = cliModel || config.agents.defaultModel ||
|
|
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
|
+
}
|
|
615
652
|
|
|
616
653
|
const state = await createSession(opts.task, {
|
|
617
654
|
roles: COLLAB_ROLES,
|
|
@@ -749,6 +786,7 @@ export function agentsCommand() {
|
|
|
749
786
|
|
|
750
787
|
while (true) {
|
|
751
788
|
try {
|
|
789
|
+
console.log(`[${role}] polling session ${opts.session}`);
|
|
752
790
|
const state = await loadSessionState(opts.session);
|
|
753
791
|
if (!state.roles.includes(role)) {
|
|
754
792
|
console.log(`[${role}] role not configured in session. exiting.`);
|
|
@@ -759,14 +797,20 @@ export function agentsCommand() {
|
|
|
759
797
|
process.exit(0);
|
|
760
798
|
}
|
|
761
799
|
|
|
800
|
+
if (state.activeAgent === role) {
|
|
801
|
+
console.log(`[${role}] executing turn with model=${state.roleModels?.[role] || state.model || DEFAULT_SYNAPSE_MODEL}`);
|
|
802
|
+
}
|
|
762
803
|
const nextState = await runWorkerIteration(opts.session, role, {
|
|
763
804
|
cwd: state.workingDirectory || process.cwd(),
|
|
764
805
|
model: state.model || null,
|
|
765
806
|
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
807
|
+
timeoutMs: 180000,
|
|
766
808
|
});
|
|
767
809
|
const latest = nextState.messages[nextState.messages.length - 1];
|
|
768
810
|
if (latest?.from === role) {
|
|
769
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}`);
|
|
770
814
|
}
|
|
771
815
|
} catch (error) {
|
|
772
816
|
console.error(`[${role}] loop error: ${error.message}`);
|
|
@@ -776,5 +820,50 @@ export function agentsCommand() {
|
|
|
776
820
|
}
|
|
777
821
|
});
|
|
778
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
|
+
|
|
779
868
|
return agents;
|
|
780
869
|
}
|