ac-framework 2.2.0 → 2.3.0
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 +2 -0
- package/package.json +1 -1
- package/src/agents/runtime.js +159 -8
- package/src/commands/agents.js +53 -12
- package/src/mcp/collab-server.js +25 -2
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
142
142
|
|---|---|
|
|
143
143
|
| `acfm agents setup` | Install optional dependencies (`opencode` and `zellij`/`tmux`) |
|
|
144
144
|
| `acfm agents doctor` | Validate OpenCode/multiplexer/model preflight before start |
|
|
145
|
+
| `acfm agents doctor --verbose` | Include zellij capability probe details for strategy diagnostics |
|
|
145
146
|
| `acfm agents install-mcps` | Install SynapseGrid MCP server for detected assistants |
|
|
146
147
|
| `acfm agents uninstall-mcps` | Remove SynapseGrid MCP server from assistants |
|
|
147
148
|
| `acfm agents start --task "..." --model-coder provider/model` | Start session with optional per-role models |
|
|
@@ -190,6 +191,7 @@ When driving SynapseGrid from another agent via MCP, prefer asynchronous run too
|
|
|
190
191
|
- Default SynapseGrid model fallback is `opencode/mimo-v2-pro-free`.
|
|
191
192
|
- Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
|
|
192
193
|
- When zellij is managed by AC Framework, its binary path is saved in `~/.acfm/config.json` and executed directly by SynapseGrid.
|
|
194
|
+
- `acfm agents start --json` now includes startup strategy diagnostics for zellij (`attach_with_layout`, fallbacks, and per-strategy errors).
|
|
193
195
|
|
|
194
196
|
Each collaborative session now keeps human-readable artifacts under `~/.acfm/synapsegrid/<sessionId>/`:
|
|
195
197
|
- `transcript.jsonl`: full chronological message stream
|
package/package.json
CHANGED
package/src/agents/runtime.js
CHANGED
|
@@ -51,8 +51,76 @@ function stripAnsi(text) {
|
|
|
51
51
|
return String(text || '').replace(/\x1B\[[0-9;]*m/g, '');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
async function commandSupports(command, args, pattern, runner) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await runner(command, args);
|
|
57
|
+
return pattern.test(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function probeZellijCapabilities(binaryPath, options = {}) {
|
|
64
|
+
const runner = options.runCommandImpl || runCommand;
|
|
65
|
+
const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
|
|
66
|
+
const capabilities = {
|
|
67
|
+
binary: command,
|
|
68
|
+
version: null,
|
|
69
|
+
attachCreateBackground: false,
|
|
70
|
+
actionNewTabLayout: false,
|
|
71
|
+
actionNewPane: false,
|
|
72
|
+
listPanesJson: false,
|
|
73
|
+
setupCheck: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const versionResult = await runner(command, ['--version']);
|
|
78
|
+
capabilities.version = stripAnsi(versionResult.stdout || versionResult.stderr || '').trim() || null;
|
|
79
|
+
} catch {
|
|
80
|
+
capabilities.version = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
capabilities.attachCreateBackground = await commandSupports(
|
|
84
|
+
command,
|
|
85
|
+
['attach', '--help'],
|
|
86
|
+
/--create-background/,
|
|
87
|
+
runner,
|
|
88
|
+
);
|
|
89
|
+
capabilities.actionNewTabLayout = await commandSupports(
|
|
90
|
+
command,
|
|
91
|
+
['action', 'new-tab', '--help'],
|
|
92
|
+
/--layout/,
|
|
93
|
+
runner,
|
|
94
|
+
);
|
|
95
|
+
capabilities.actionNewPane = await commandSupports(
|
|
96
|
+
command,
|
|
97
|
+
['action', 'new-pane', '--help'],
|
|
98
|
+
/USAGE:/,
|
|
99
|
+
runner,
|
|
100
|
+
);
|
|
101
|
+
capabilities.listPanesJson = await commandSupports(
|
|
102
|
+
command,
|
|
103
|
+
['action', 'list-panes', '--help'],
|
|
104
|
+
/--json/,
|
|
105
|
+
runner,
|
|
106
|
+
);
|
|
107
|
+
capabilities.setupCheck = await commandSupports(
|
|
108
|
+
command,
|
|
109
|
+
['setup', '--help'],
|
|
110
|
+
/--check/,
|
|
111
|
+
runner,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return capabilities;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function workerShellCommand(sessionId, role, roleLog) {
|
|
118
|
+
return `node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"`;
|
|
119
|
+
}
|
|
120
|
+
|
|
54
121
|
function workerCommand(sessionId, role, roleLog) {
|
|
55
|
-
|
|
122
|
+
const shell = workerShellCommand(sessionId, role, roleLog);
|
|
123
|
+
return `bash -lc '${shell}'`;
|
|
56
124
|
}
|
|
57
125
|
|
|
58
126
|
export async function runTmux(command, args, options = {}) {
|
|
@@ -136,34 +204,114 @@ export async function spawnZellijSession({
|
|
|
136
204
|
waitForSessionMs = 10000,
|
|
137
205
|
pollIntervalMs = 250,
|
|
138
206
|
runCommandImpl,
|
|
207
|
+
capabilities = null,
|
|
139
208
|
}) {
|
|
140
209
|
const layoutPath = resolve(sessionDir, 'synapsegrid-layout.kdl');
|
|
141
210
|
await writeZellijLayout({ layoutPath, sessionId, sessionDir });
|
|
142
211
|
const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
|
|
143
212
|
const runner = runCommandImpl || runCommand;
|
|
213
|
+
const caps = capabilities || await probeZellijCapabilities(binaryPath, { runCommandImpl: runner });
|
|
214
|
+
const strategyErrors = [];
|
|
144
215
|
|
|
145
216
|
const existing = await zellijSessionExists(sessionName, binaryPath, { runCommandImpl: runner });
|
|
146
217
|
if (existing) {
|
|
147
|
-
return { layoutPath };
|
|
218
|
+
return { layoutPath, strategy: 'already_exists', capabilities: caps, strategyErrors };
|
|
148
219
|
}
|
|
149
220
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
221
|
+
const strategies = [];
|
|
222
|
+
if (caps.attachCreateBackground) {
|
|
223
|
+
strategies.push({
|
|
224
|
+
name: 'attach_with_layout',
|
|
225
|
+
run: async () => {
|
|
226
|
+
await runner(command, ['--layout', layoutPath, 'attach', '--create-background', sessionName], {
|
|
227
|
+
cwd: sessionDir,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (caps.attachCreateBackground && caps.actionNewTabLayout) {
|
|
234
|
+
strategies.push({
|
|
235
|
+
name: 'attach_then_newtab_layout',
|
|
236
|
+
run: async () => {
|
|
237
|
+
await runner(command, ['attach', '--create-background', sessionName], { cwd: sessionDir });
|
|
238
|
+
await runner(command, ['--session', sessionName, 'action', 'new-tab', '--name', 'SynapseGrid', '--layout', layoutPath], {
|
|
239
|
+
cwd: sessionDir,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (caps.attachCreateBackground && caps.actionNewPane) {
|
|
246
|
+
strategies.push({
|
|
247
|
+
name: 'attach_then_action_panes',
|
|
248
|
+
run: async () => {
|
|
249
|
+
await runner(command, ['attach', '--create-background', sessionName], { cwd: sessionDir });
|
|
250
|
+
const role0 = COLLAB_ROLES[0];
|
|
251
|
+
const role0Log = roleLogPath(sessionDir, role0);
|
|
252
|
+
await runner(
|
|
253
|
+
command,
|
|
254
|
+
['--session', sessionName, 'action', 'new-tab', '--name', 'SynapseGrid', '--', 'bash', '-lc', workerShellCommand(sessionId, role0, role0Log)],
|
|
255
|
+
{ cwd: sessionDir },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const role1 = COLLAB_ROLES[1];
|
|
259
|
+
const role1Log = roleLogPath(sessionDir, role1);
|
|
260
|
+
await runner(
|
|
261
|
+
command,
|
|
262
|
+
['--session', sessionName, 'action', 'new-pane', '--direction', 'right', '--', 'bash', '-lc', workerShellCommand(sessionId, role1, role1Log)],
|
|
263
|
+
{ cwd: sessionDir },
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const role2 = COLLAB_ROLES[2];
|
|
267
|
+
const role2Log = roleLogPath(sessionDir, role2);
|
|
268
|
+
await runner(
|
|
269
|
+
command,
|
|
270
|
+
['--session', sessionName, 'action', 'new-pane', '--direction', 'down', '--', 'bash', '-lc', workerShellCommand(sessionId, role2, role2Log)],
|
|
271
|
+
{ cwd: sessionDir },
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const role3 = COLLAB_ROLES[3];
|
|
275
|
+
const role3Log = roleLogPath(sessionDir, role3);
|
|
276
|
+
await runner(
|
|
277
|
+
command,
|
|
278
|
+
['--session', sessionName, 'action', 'new-pane', '--direction', 'right', '--', 'bash', '-lc', workerShellCommand(sessionId, role3, role3Log)],
|
|
279
|
+
{ cwd: sessionDir },
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let strategyUsed = null;
|
|
286
|
+
for (const strategy of strategies) {
|
|
287
|
+
try {
|
|
288
|
+
// eslint-disable-next-line no-await-in-loop
|
|
289
|
+
await strategy.run();
|
|
290
|
+
strategyUsed = strategy.name;
|
|
291
|
+
break;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
strategyErrors.push({ strategy: strategy.name, error: error.message });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!strategyUsed) {
|
|
298
|
+
const details = strategyErrors.map((item) => `${item.strategy}: ${item.error}`).join(' | ');
|
|
299
|
+
throw new Error(`Unable to initialize zellij session using supported strategies. ${details || 'No compatible strategy available.'}`);
|
|
300
|
+
}
|
|
153
301
|
|
|
154
302
|
const startedAt = Date.now();
|
|
155
303
|
while ((Date.now() - startedAt) < waitForSessionMs) {
|
|
156
304
|
// eslint-disable-next-line no-await-in-loop
|
|
157
305
|
const exists = await zellijSessionExists(sessionName, binaryPath, { runCommandImpl: runner });
|
|
158
306
|
if (exists) {
|
|
159
|
-
return { layoutPath };
|
|
307
|
+
return { layoutPath, strategy: strategyUsed, capabilities: caps, strategyErrors };
|
|
160
308
|
}
|
|
161
309
|
// eslint-disable-next-line no-await-in-loop
|
|
162
310
|
await sleep(pollIntervalMs);
|
|
163
311
|
}
|
|
164
312
|
|
|
165
313
|
throw new Error(
|
|
166
|
-
`Timed out waiting for zellij session '${sessionName}' to start (binary: ${command}). ` +
|
|
314
|
+
`Timed out waiting for zellij session '${sessionName}' to start (binary: ${command}, strategy: ${strategyUsed || 'none'}). ` +
|
|
167
315
|
'Try `acfm agents doctor` or fallback with `acfm agents start --mux tmux ...`'
|
|
168
316
|
);
|
|
169
317
|
}
|
|
@@ -177,7 +325,10 @@ export async function zellijSessionExists(sessionName, binaryPath, options = {})
|
|
|
177
325
|
.split('\n')
|
|
178
326
|
.map((line) => line.trim())
|
|
179
327
|
.filter(Boolean);
|
|
180
|
-
return lines.some((line) =>
|
|
328
|
+
return lines.some((line) => {
|
|
329
|
+
if (line === sessionName || line.startsWith(`${sessionName} `)) return true;
|
|
330
|
+
return line.includes(sessionName);
|
|
331
|
+
});
|
|
181
332
|
} catch {
|
|
182
333
|
return false;
|
|
183
334
|
}
|
package/src/commands/agents.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
writeMeetingSummary,
|
|
32
32
|
} from '../agents/state-store.js';
|
|
33
33
|
import {
|
|
34
|
+
probeZellijCapabilities,
|
|
34
35
|
roleLogPath,
|
|
35
36
|
runTmux,
|
|
36
37
|
runZellij,
|
|
@@ -153,6 +154,16 @@ async function attachToMux(multiplexer, sessionName, readonly = false, zellijPat
|
|
|
153
154
|
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
async function readZellijCapabilities(config) {
|
|
158
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
159
|
+
if (!zellijPath) return null;
|
|
160
|
+
try {
|
|
161
|
+
return await probeZellijCapabilities(zellijPath);
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
function toMarkdownTranscript(state, transcript) {
|
|
157
168
|
const displayedRound = Math.min(state.round, state.maxRounds);
|
|
158
169
|
const lines = [
|
|
@@ -1270,6 +1281,7 @@ Examples:
|
|
|
1270
1281
|
const muxResolution = resolveMultiplexerWithPaths(config, configuredMux);
|
|
1271
1282
|
let selectedMux = muxResolution.selected;
|
|
1272
1283
|
let zellijPath = muxResolution.zellijPath;
|
|
1284
|
+
const tmuxPath = muxResolution.tmuxPath;
|
|
1273
1285
|
if (!selectedMux) {
|
|
1274
1286
|
if (configuredMux !== 'tmux' && shouldUseManagedZellij(config)) {
|
|
1275
1287
|
const installResult = await installManagedZellijLatest();
|
|
@@ -1336,41 +1348,64 @@ Examples:
|
|
|
1336
1348
|
const muxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
1337
1349
|
const sessionDir = getSessionDir(state.sessionId);
|
|
1338
1350
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1351
|
+
let activeMux = selectedMux;
|
|
1352
|
+
let startupDiagnostics = null;
|
|
1353
|
+
try {
|
|
1354
|
+
if (selectedMux === 'zellij') {
|
|
1355
|
+
startupDiagnostics = await spawnZellijSession({
|
|
1356
|
+
sessionName: muxSessionName,
|
|
1357
|
+
sessionDir,
|
|
1358
|
+
sessionId: state.sessionId,
|
|
1359
|
+
binaryPath: zellijPath,
|
|
1360
|
+
});
|
|
1361
|
+
} else {
|
|
1362
|
+
await spawnTmuxSession({
|
|
1363
|
+
sessionName: muxSessionName,
|
|
1364
|
+
sessionDir,
|
|
1365
|
+
sessionId: state.sessionId,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
} catch (muxError) {
|
|
1369
|
+
const canFallbackToTmux = configuredMux === 'auto' && selectedMux === 'zellij' && Boolean(tmuxPath);
|
|
1370
|
+
if (!canFallbackToTmux) {
|
|
1371
|
+
throw muxError;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (!opts.json) {
|
|
1375
|
+
console.log(chalk.yellow(`zellij startup failed, falling back to tmux: ${muxError.message}`));
|
|
1376
|
+
}
|
|
1347
1377
|
await spawnTmuxSession({
|
|
1348
1378
|
sessionName: muxSessionName,
|
|
1349
1379
|
sessionDir,
|
|
1350
1380
|
sessionId: state.sessionId,
|
|
1351
1381
|
});
|
|
1382
|
+
activeMux = 'tmux';
|
|
1352
1383
|
}
|
|
1353
1384
|
|
|
1354
1385
|
const updated = await saveSessionState({
|
|
1355
1386
|
...state,
|
|
1356
|
-
multiplexer:
|
|
1387
|
+
multiplexer: activeMux,
|
|
1357
1388
|
multiplexerSessionName: muxSessionName,
|
|
1358
|
-
tmuxSessionName:
|
|
1389
|
+
tmuxSessionName: activeMux === 'tmux' ? muxSessionName : null,
|
|
1359
1390
|
});
|
|
1360
1391
|
|
|
1361
1392
|
output({
|
|
1362
1393
|
sessionId: updated.sessionId,
|
|
1363
|
-
multiplexer:
|
|
1394
|
+
multiplexer: activeMux,
|
|
1364
1395
|
multiplexerSessionName: muxSessionName,
|
|
1365
1396
|
status: updated.status,
|
|
1397
|
+
startupDiagnostics,
|
|
1366
1398
|
}, opts.json);
|
|
1367
1399
|
if (!opts.json) {
|
|
1368
1400
|
printStartSummary(updated);
|
|
1401
|
+
if (startupDiagnostics?.strategy) {
|
|
1402
|
+
console.log(chalk.dim(` zellij strategy: ${startupDiagnostics.strategy}`));
|
|
1403
|
+
}
|
|
1369
1404
|
printModelConfig(updated);
|
|
1370
1405
|
}
|
|
1371
1406
|
|
|
1372
1407
|
if (opts.attach) {
|
|
1373
|
-
await attachToMux(
|
|
1408
|
+
await attachToMux(activeMux, muxSessionName, false, zellijPath);
|
|
1374
1409
|
}
|
|
1375
1410
|
} catch (error) {
|
|
1376
1411
|
output({ error: error.message }, opts.json);
|
|
@@ -1616,6 +1651,7 @@ Examples:
|
|
|
1616
1651
|
agents
|
|
1617
1652
|
.command('doctor')
|
|
1618
1653
|
.description('Run diagnostics for SynapseGrid/OpenCode runtime')
|
|
1654
|
+
.option('--verbose', 'Include backend capability details', false)
|
|
1619
1655
|
.option('--json', 'Output as JSON')
|
|
1620
1656
|
.action(async (opts) => {
|
|
1621
1657
|
try {
|
|
@@ -1638,8 +1674,13 @@ Examples:
|
|
|
1638
1674
|
defaultModel,
|
|
1639
1675
|
defaultRoleModels: cfg.agents.defaultRoleModels,
|
|
1640
1676
|
preflight: null,
|
|
1677
|
+
zellijCapabilities: null,
|
|
1641
1678
|
};
|
|
1642
1679
|
|
|
1680
|
+
if (opts.verbose && zellijPath) {
|
|
1681
|
+
result.zellijCapabilities = await readZellijCapabilities(cfg);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1643
1684
|
if (opencodeBin) {
|
|
1644
1685
|
result.preflight = await preflightModel({
|
|
1645
1686
|
opencodeBin,
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-se
|
|
|
18
18
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
19
19
|
import { getSessionDir } from '../agents/state-store.js';
|
|
20
20
|
import {
|
|
21
|
+
probeZellijCapabilities,
|
|
21
22
|
spawnTmuxSession,
|
|
22
23
|
spawnZellijSession,
|
|
23
24
|
tmuxSessionExists,
|
|
@@ -84,6 +85,16 @@ function resolveConfiguredZellijPath(config) {
|
|
|
84
85
|
return resolveCommandPath('zellij');
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
async function readZellijCapabilities(config) {
|
|
89
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
90
|
+
if (!zellijPath) return null;
|
|
91
|
+
try {
|
|
92
|
+
return await probeZellijCapabilities(zellijPath);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
async function muxExists(multiplexer, sessionName, zellijPath = null) {
|
|
88
99
|
if (multiplexer === 'zellij') return zellijSessionExists(sessionName, zellijPath);
|
|
89
100
|
return tmuxSessionExists(sessionName);
|
|
@@ -152,7 +163,13 @@ class MCPCollabServer {
|
|
|
152
163
|
const sessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
153
164
|
const sessionDir = getSessionDir(state.sessionId);
|
|
154
165
|
if (multiplexer === 'zellij') {
|
|
155
|
-
await spawnZellijSession({
|
|
166
|
+
await spawnZellijSession({
|
|
167
|
+
sessionName,
|
|
168
|
+
sessionDir,
|
|
169
|
+
sessionId: state.sessionId,
|
|
170
|
+
binaryPath: zellijPath,
|
|
171
|
+
capabilities: await readZellijCapabilities(config),
|
|
172
|
+
});
|
|
156
173
|
} else {
|
|
157
174
|
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
158
175
|
}
|
|
@@ -477,7 +494,13 @@ class MCPCollabServer {
|
|
|
477
494
|
const sessionDir = getSessionDir(state.sessionId);
|
|
478
495
|
if (multiplexer === 'zellij') {
|
|
479
496
|
if (!zellijPath) throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
480
|
-
await spawnZellijSession({
|
|
497
|
+
await spawnZellijSession({
|
|
498
|
+
sessionName,
|
|
499
|
+
sessionDir,
|
|
500
|
+
sessionId: state.sessionId,
|
|
501
|
+
binaryPath: zellijPath,
|
|
502
|
+
capabilities: await readZellijCapabilities(config),
|
|
503
|
+
});
|
|
481
504
|
} else {
|
|
482
505
|
if (!hasCommand('tmux')) throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
483
506
|
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|