ac-framework 1.9.7 → 1.9.9
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 +37 -11
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +3 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- package/src/agents/orchestrator.js +225 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/runtime.js +75 -3
- package/src/agents/state-store.js +73 -0
- package/src/commands/agents.js +616 -60
- package/src/commands/init.js +9 -5
- package/src/mcp/collab-server.js +378 -24
- package/src/mcp/test-harness.mjs +410 -0
- package/src/services/dependency-installer.js +72 -4
package/src/commands/agents.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
3
4
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
6
|
import { readFileSync } from 'node:fs';
|
|
6
7
|
import { dirname, resolve } from 'node:path';
|
|
7
8
|
import {
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
DEFAULT_SYNAPSE_MODEL,
|
|
13
14
|
SESSION_ROOT_DIR,
|
|
14
15
|
} from '../agents/constants.js';
|
|
15
|
-
import { runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
16
|
+
import { listOpenCodeModels, runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
16
17
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
17
18
|
import {
|
|
18
19
|
addUserMessage,
|
|
@@ -25,8 +26,18 @@ import {
|
|
|
25
26
|
saveSessionState,
|
|
26
27
|
setCurrentSession,
|
|
27
28
|
stopSession,
|
|
29
|
+
writeMeetingSummary,
|
|
28
30
|
} from '../agents/state-store.js';
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
roleLogPath,
|
|
33
|
+
runTmux,
|
|
34
|
+
runZellij,
|
|
35
|
+
spawnTmuxSession,
|
|
36
|
+
spawnZellijSession,
|
|
37
|
+
tmuxSessionExists,
|
|
38
|
+
zellijSessionExists,
|
|
39
|
+
resolveMultiplexer,
|
|
40
|
+
} from '../agents/runtime.js';
|
|
30
41
|
import { getAgentsConfigPath, loadAgentsConfig, updateAgentsConfig } from '../agents/config-store.js';
|
|
31
42
|
import {
|
|
32
43
|
buildEffectiveRoleModels,
|
|
@@ -59,18 +70,53 @@ async function ensureSessionId(required = true) {
|
|
|
59
70
|
function printStartSummary(state) {
|
|
60
71
|
console.log(chalk.green(`✓ ${COLLAB_SYSTEM_NAME} session started`));
|
|
61
72
|
console.log(chalk.dim(` Session: ${state.sessionId}`));
|
|
62
|
-
|
|
73
|
+
const multiplexer = state.multiplexer || 'auto';
|
|
74
|
+
const muxSessionName = state.multiplexerSessionName || state.tmuxSessionName || '-';
|
|
75
|
+
console.log(chalk.dim(` Multiplexer: ${multiplexer}`));
|
|
76
|
+
console.log(chalk.dim(` Session name: ${muxSessionName}`));
|
|
63
77
|
console.log(chalk.dim(` Task: ${state.task}`));
|
|
64
78
|
console.log(chalk.dim(` Roles: ${state.roles.join(', ')}`));
|
|
65
79
|
console.log();
|
|
66
80
|
console.log(chalk.cyan('Attach with:'));
|
|
67
|
-
|
|
81
|
+
if (multiplexer === 'zellij') {
|
|
82
|
+
console.log(chalk.white(` zellij attach ${muxSessionName}`));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.white(` tmux attach -t ${muxSessionName}`));
|
|
85
|
+
}
|
|
68
86
|
console.log(chalk.white(' acfm agents live'));
|
|
69
87
|
console.log();
|
|
70
88
|
console.log(chalk.cyan('Interact with:'));
|
|
71
89
|
console.log(chalk.white(' acfm agents send "your message"'));
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
function validateMultiplexer(value) {
|
|
93
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
94
|
+
if (!['auto', 'zellij', 'tmux'].includes(normalized)) {
|
|
95
|
+
throw new Error('--mux must be one of: auto|zellij|tmux');
|
|
96
|
+
}
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sessionMuxName(state) {
|
|
101
|
+
return state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function sessionExistsForMux(multiplexer, sessionName) {
|
|
105
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName);
|
|
106
|
+
return tmuxSessionExists(sessionName);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function attachToMux(multiplexer, sessionName, readonly = false) {
|
|
110
|
+
if (multiplexer === 'zellij') {
|
|
111
|
+
await runZellij(['attach', sessionName], { stdio: 'inherit' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const args = ['attach'];
|
|
115
|
+
if (readonly) args.push('-r');
|
|
116
|
+
args.push('-t', sessionName);
|
|
117
|
+
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
118
|
+
}
|
|
119
|
+
|
|
74
120
|
function toMarkdownTranscript(state, transcript) {
|
|
75
121
|
const displayedRound = Math.min(state.round, state.maxRounds);
|
|
76
122
|
const lines = [
|
|
@@ -127,6 +173,39 @@ function printModelConfig(state) {
|
|
|
127
173
|
}
|
|
128
174
|
}
|
|
129
175
|
|
|
176
|
+
function groupModelsByProvider(models) {
|
|
177
|
+
const grouped = new Map();
|
|
178
|
+
for (const model of models) {
|
|
179
|
+
const [provider, ...rest] = model.split('/');
|
|
180
|
+
if (!provider || rest.length === 0) continue;
|
|
181
|
+
const modelName = rest.join('/');
|
|
182
|
+
if (!grouped.has(provider)) grouped.set(provider, []);
|
|
183
|
+
grouped.get(provider).push(modelName);
|
|
184
|
+
}
|
|
185
|
+
for (const [provider, modelNames] of grouped.entries()) {
|
|
186
|
+
grouped.set(provider, [...new Set(modelNames)].sort((a, b) => a.localeCompare(b)));
|
|
187
|
+
}
|
|
188
|
+
return grouped;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function runSummary(state) {
|
|
192
|
+
const run = state.run || {};
|
|
193
|
+
const events = Array.isArray(run.events) ? run.events.length : 0;
|
|
194
|
+
return {
|
|
195
|
+
status: run.status || 'idle',
|
|
196
|
+
runId: run.runId || null,
|
|
197
|
+
currentRole: run.currentRole || null,
|
|
198
|
+
lastError: run.lastError || null,
|
|
199
|
+
events,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function readSessionArtifact(sessionId, filename) {
|
|
204
|
+
const path = resolve(getSessionDir(sessionId), filename);
|
|
205
|
+
if (!existsSync(path)) return null;
|
|
206
|
+
return readFile(path, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
|
|
130
209
|
async function preflightModel({ opencodeBin, model, cwd }) {
|
|
131
210
|
const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
|
|
132
211
|
try {
|
|
@@ -147,12 +226,51 @@ export function agentsCommand() {
|
|
|
147
226
|
const agents = new Command('agents')
|
|
148
227
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
149
228
|
|
|
229
|
+
agents.addHelpText('after', `
|
|
230
|
+
Examples:
|
|
231
|
+
acfm agents start --task "Implement auth flow" --mux auto
|
|
232
|
+
acfm agents setup
|
|
233
|
+
acfm agents runtime get
|
|
234
|
+
acfm agents runtime set zellij
|
|
235
|
+
acfm agents model choose
|
|
236
|
+
acfm agents model list
|
|
237
|
+
acfm agents transcript --role all --limit 40
|
|
238
|
+
acfm agents summary
|
|
239
|
+
acfm agents export --format md --out ./session.md
|
|
240
|
+
`);
|
|
241
|
+
|
|
150
242
|
agents
|
|
151
243
|
.command('setup')
|
|
152
|
-
.description('Install optional collaboration dependencies (OpenCode + tmux)')
|
|
244
|
+
.description('Install optional collaboration dependencies (OpenCode + zellij/tmux)')
|
|
245
|
+
.option('--yes', 'Install dependencies without interactive confirmation', false)
|
|
153
246
|
.option('--json', 'Output as JSON')
|
|
154
247
|
.action(async (opts) => {
|
|
155
|
-
|
|
248
|
+
let installZellij = true;
|
|
249
|
+
let installTmux = true;
|
|
250
|
+
|
|
251
|
+
if (!opts.yes && !opts.json) {
|
|
252
|
+
const answers = await inquirer.prompt([
|
|
253
|
+
{
|
|
254
|
+
type: 'confirm',
|
|
255
|
+
name: 'installZellij',
|
|
256
|
+
message: 'Install zellij (recommended, multiplatform backend)?',
|
|
257
|
+
default: true,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
type: 'confirm',
|
|
261
|
+
name: 'installTmux',
|
|
262
|
+
message: 'Install tmux as fallback backend?',
|
|
263
|
+
default: true,
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
installZellij = Boolean(answers.installZellij);
|
|
267
|
+
installTmux = Boolean(answers.installTmux);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const result = ensureCollabDependencies({
|
|
271
|
+
installZellij,
|
|
272
|
+
installTmux,
|
|
273
|
+
});
|
|
156
274
|
let collabMcp = null;
|
|
157
275
|
|
|
158
276
|
if (result.success) {
|
|
@@ -165,7 +283,9 @@ export function agentsCommand() {
|
|
|
165
283
|
if (!opts.json) {
|
|
166
284
|
const oLabel = result.opencode.success ? chalk.green('ok') : chalk.red('failed');
|
|
167
285
|
const tLabel = result.tmux.success ? chalk.green('ok') : chalk.red('failed');
|
|
286
|
+
const zLabel = result.zellij.success ? chalk.green('ok') : chalk.red('failed');
|
|
168
287
|
console.log(`OpenCode: ${oLabel} - ${result.opencode.message}`);
|
|
288
|
+
console.log(`zellij: ${zLabel} - ${result.zellij.message}`);
|
|
169
289
|
console.log(`tmux: ${tLabel} - ${result.tmux.message}`);
|
|
170
290
|
if (collabMcp) {
|
|
171
291
|
console.log(`Collab MCP: ${chalk.green('ok')} - installed ${collabMcp.success}/${collabMcp.installed} on detected assistants`);
|
|
@@ -252,10 +372,10 @@ export function agentsCommand() {
|
|
|
252
372
|
}
|
|
253
373
|
console.log(chalk.bold('SynapseGrid Sessions'));
|
|
254
374
|
for (const item of sessions) {
|
|
255
|
-
|
|
375
|
+
console.log(
|
|
256
376
|
`${chalk.cyan(item.sessionId.slice(0, 8))} ${item.status.padEnd(10)} ` +
|
|
257
377
|
`round ${String(item.round).padStart(2)}/${String(item.maxRounds).padEnd(2)} ` +
|
|
258
|
-
`${item.tmuxSessionName || '-'} ${item.task}`
|
|
378
|
+
`${item.multiplexer || 'auto'}:${item.multiplexerSessionName || item.tmuxSessionName || '-'} ${item.task}`
|
|
259
379
|
);
|
|
260
380
|
}
|
|
261
381
|
}
|
|
@@ -268,15 +388,17 @@ export function agentsCommand() {
|
|
|
268
388
|
|
|
269
389
|
agents
|
|
270
390
|
.command('attach')
|
|
271
|
-
.description('Attach terminal to active SynapseGrid
|
|
391
|
+
.description('Attach terminal to active SynapseGrid multiplexer session')
|
|
272
392
|
.action(async () => {
|
|
273
393
|
try {
|
|
274
394
|
const sessionId = await ensureSessionId(true);
|
|
275
395
|
const state = await loadSessionState(sessionId);
|
|
276
|
-
|
|
277
|
-
|
|
396
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
397
|
+
const muxSessionName = sessionMuxName(state);
|
|
398
|
+
if (!muxSessionName) {
|
|
399
|
+
throw new Error('No multiplexer session registered for active collaborative session');
|
|
278
400
|
}
|
|
279
|
-
await
|
|
401
|
+
await attachToMux(multiplexer, muxSessionName, false);
|
|
280
402
|
} catch (error) {
|
|
281
403
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
282
404
|
process.exit(1);
|
|
@@ -285,23 +407,22 @@ export function agentsCommand() {
|
|
|
285
407
|
|
|
286
408
|
agents
|
|
287
409
|
.command('live')
|
|
288
|
-
.description('Attach to live
|
|
410
|
+
.description('Attach to live collaboration view (all agent panes)')
|
|
289
411
|
.option('--readonly', 'Attach in read-only mode', false)
|
|
290
412
|
.action(async (opts) => {
|
|
291
413
|
try {
|
|
292
414
|
const sessionId = await ensureSessionId(true);
|
|
293
415
|
const state = await loadSessionState(sessionId);
|
|
294
|
-
|
|
295
|
-
|
|
416
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
417
|
+
const muxSessionName = sessionMuxName(state);
|
|
418
|
+
if (!muxSessionName) {
|
|
419
|
+
throw new Error('No multiplexer session registered for active collaborative session');
|
|
296
420
|
}
|
|
297
|
-
const
|
|
298
|
-
if (!
|
|
299
|
-
throw new Error(
|
|
421
|
+
const sessionExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
422
|
+
if (!sessionExists) {
|
|
423
|
+
throw new Error(`${multiplexer} session ${muxSessionName} no longer exists. Run: acfm agents resume`);
|
|
300
424
|
}
|
|
301
|
-
|
|
302
|
-
if (opts.readonly) args.push('-r');
|
|
303
|
-
args.push('-t', state.tmuxSessionName);
|
|
304
|
-
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
425
|
+
await attachToMux(multiplexer, muxSessionName, Boolean(opts.readonly));
|
|
305
426
|
} catch (error) {
|
|
306
427
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
307
428
|
process.exit(1);
|
|
@@ -310,48 +431,60 @@ export function agentsCommand() {
|
|
|
310
431
|
|
|
311
432
|
agents
|
|
312
433
|
.command('resume')
|
|
313
|
-
.description('Resume a previous session and optionally recreate
|
|
434
|
+
.description('Resume a previous session and optionally recreate multiplexer workers')
|
|
314
435
|
.option('--session <id>', 'Session ID to resume (defaults to current)')
|
|
315
|
-
.option('--no-recreate', 'Do not recreate
|
|
316
|
-
.option('--no-attach', 'Do not attach
|
|
436
|
+
.option('--no-recreate', 'Do not recreate multiplexer session/workers when missing')
|
|
437
|
+
.option('--no-attach', 'Do not attach multiplexer after resume')
|
|
317
438
|
.option('--json', 'Output as JSON')
|
|
318
439
|
.action(async (opts) => {
|
|
319
440
|
try {
|
|
320
441
|
const sessionId = opts.session || await ensureSessionId(true);
|
|
321
442
|
let state = await loadSessionState(sessionId);
|
|
443
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
322
444
|
|
|
323
|
-
|
|
324
|
-
|
|
445
|
+
if (multiplexer === 'zellij' && !hasCommand('zellij')) {
|
|
446
|
+
throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
447
|
+
}
|
|
448
|
+
if (multiplexer === 'tmux' && !hasCommand('tmux')) {
|
|
449
|
+
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
450
|
+
}
|
|
325
451
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
452
|
+
const muxSessionName = sessionMuxName(state);
|
|
453
|
+
const muxExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
454
|
+
|
|
455
|
+
if (!muxExists && opts.recreate) {
|
|
330
456
|
const sessionDir = getSessionDir(state.sessionId);
|
|
331
|
-
|
|
457
|
+
if (multiplexer === 'zellij') {
|
|
458
|
+
await spawnZellijSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
|
|
459
|
+
} else {
|
|
460
|
+
await spawnTmuxSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
|
|
461
|
+
}
|
|
332
462
|
}
|
|
333
463
|
|
|
334
464
|
state = await saveSessionState({
|
|
335
465
|
...state,
|
|
336
466
|
status: 'running',
|
|
337
|
-
|
|
467
|
+
multiplexer,
|
|
468
|
+
multiplexerSessionName: muxSessionName,
|
|
469
|
+
tmuxSessionName: multiplexer === 'tmux' ? muxSessionName : (state.tmuxSessionName || null),
|
|
338
470
|
});
|
|
339
471
|
await setCurrentSession(state.sessionId);
|
|
340
472
|
|
|
341
473
|
output({
|
|
342
474
|
sessionId: state.sessionId,
|
|
343
475
|
status: state.status,
|
|
344
|
-
|
|
345
|
-
|
|
476
|
+
multiplexer,
|
|
477
|
+
multiplexerSessionName: muxSessionName,
|
|
478
|
+
recreatedSession: !muxExists && Boolean(opts.recreate),
|
|
346
479
|
}, opts.json);
|
|
347
480
|
|
|
348
481
|
if (!opts.json) {
|
|
349
482
|
console.log(chalk.green(`✓ Resumed session ${state.sessionId}`));
|
|
350
|
-
console.log(chalk.dim(`
|
|
483
|
+
console.log(chalk.dim(` ${multiplexer}: ${muxSessionName}`));
|
|
351
484
|
}
|
|
352
485
|
|
|
353
486
|
if (opts.attach) {
|
|
354
|
-
await
|
|
487
|
+
await attachToMux(multiplexer, muxSessionName, false);
|
|
355
488
|
}
|
|
356
489
|
} catch (error) {
|
|
357
490
|
output({ error: error.message }, opts.json);
|
|
@@ -419,6 +552,123 @@ export function agentsCommand() {
|
|
|
419
552
|
.command('model')
|
|
420
553
|
.description('Manage default SynapseGrid model configuration');
|
|
421
554
|
|
|
555
|
+
const runtime = agents
|
|
556
|
+
.command('runtime')
|
|
557
|
+
.description('Manage SynapseGrid runtime backend settings');
|
|
558
|
+
|
|
559
|
+
runtime
|
|
560
|
+
.command('get')
|
|
561
|
+
.description('Show configured multiplexer backend')
|
|
562
|
+
.option('--json', 'Output as JSON')
|
|
563
|
+
.action(async (opts) => {
|
|
564
|
+
try {
|
|
565
|
+
const cfg = await loadAgentsConfig();
|
|
566
|
+
const configured = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
567
|
+
const resolved = resolveMultiplexer(configured, hasCommand('tmux'), hasCommand('zellij'));
|
|
568
|
+
const payload = {
|
|
569
|
+
configPath: getAgentsConfigPath(),
|
|
570
|
+
multiplexer: configured,
|
|
571
|
+
resolved,
|
|
572
|
+
available: {
|
|
573
|
+
zellij: hasCommand('zellij'),
|
|
574
|
+
tmux: hasCommand('tmux'),
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
output(payload, opts.json);
|
|
578
|
+
if (!opts.json) {
|
|
579
|
+
console.log(chalk.bold('SynapseGrid runtime backend'));
|
|
580
|
+
console.log(chalk.dim(`Config: ${payload.configPath}`));
|
|
581
|
+
console.log(chalk.dim(`Configured: ${configured}`));
|
|
582
|
+
console.log(chalk.dim(`Resolved: ${resolved || 'none'}`));
|
|
583
|
+
console.log(chalk.dim(`zellij=${payload.available.zellij} tmux=${payload.available.tmux}`));
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
output({ error: error.message }, opts.json);
|
|
587
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
runtime
|
|
593
|
+
.command('set <mux>')
|
|
594
|
+
.description('Set multiplexer backend: auto|zellij|tmux')
|
|
595
|
+
.option('--json', 'Output as JSON')
|
|
596
|
+
.action(async (mux, opts) => {
|
|
597
|
+
try {
|
|
598
|
+
const selected = validateMultiplexer(mux);
|
|
599
|
+
const updated = await updateAgentsConfig((current) => ({
|
|
600
|
+
agents: {
|
|
601
|
+
defaultModel: current.agents.defaultModel,
|
|
602
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
603
|
+
multiplexer: selected,
|
|
604
|
+
},
|
|
605
|
+
}));
|
|
606
|
+
const resolved = resolveMultiplexer(updated.agents.multiplexer, hasCommand('tmux'), hasCommand('zellij'));
|
|
607
|
+
const payload = {
|
|
608
|
+
success: true,
|
|
609
|
+
configPath: getAgentsConfigPath(),
|
|
610
|
+
multiplexer: updated.agents.multiplexer,
|
|
611
|
+
resolved,
|
|
612
|
+
};
|
|
613
|
+
output(payload, opts.json);
|
|
614
|
+
if (!opts.json) {
|
|
615
|
+
console.log(chalk.green('✓ SynapseGrid runtime backend updated'));
|
|
616
|
+
console.log(chalk.dim(` Configured: ${payload.multiplexer}`));
|
|
617
|
+
console.log(chalk.dim(` Resolved: ${payload.resolved || 'none'}`));
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
output({ error: error.message }, opts.json);
|
|
621
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
model
|
|
627
|
+
.command('list')
|
|
628
|
+
.description('List available OpenCode models grouped by provider')
|
|
629
|
+
.option('--refresh', 'Refresh model cache from providers', false)
|
|
630
|
+
.option('--json', 'Output as JSON')
|
|
631
|
+
.action(async (opts) => {
|
|
632
|
+
try {
|
|
633
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
634
|
+
if (!opencodeBin) {
|
|
635
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const models = await listOpenCodeModels({
|
|
639
|
+
binaryPath: opencodeBin,
|
|
640
|
+
refresh: Boolean(opts.refresh),
|
|
641
|
+
});
|
|
642
|
+
const grouped = groupModelsByProvider(models);
|
|
643
|
+
const providers = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
|
|
644
|
+
|
|
645
|
+
const payload = {
|
|
646
|
+
count: models.length,
|
|
647
|
+
providers: providers.map((provider) => ({
|
|
648
|
+
provider,
|
|
649
|
+
models: grouped.get(provider).map((name) => `${provider}/${name}`),
|
|
650
|
+
})),
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
output(payload, opts.json);
|
|
654
|
+
if (!opts.json) {
|
|
655
|
+
console.log(chalk.bold('Available OpenCode models'));
|
|
656
|
+
console.log(chalk.dim(`Total: ${models.length}`));
|
|
657
|
+
for (const provider of providers) {
|
|
658
|
+
const providerModels = grouped.get(provider) || [];
|
|
659
|
+
console.log(chalk.cyan(`\n${provider}`));
|
|
660
|
+
for (const modelName of providerModels) {
|
|
661
|
+
console.log(chalk.dim(`- ${provider}/${modelName}`));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} catch (error) {
|
|
666
|
+
output({ error: error.message }, opts.json);
|
|
667
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
422
672
|
model
|
|
423
673
|
.command('get')
|
|
424
674
|
.description('Show configured default global/per-role models')
|
|
@@ -430,6 +680,7 @@ export function agentsCommand() {
|
|
|
430
680
|
configPath: getAgentsConfigPath(),
|
|
431
681
|
defaultModel: config.agents.defaultModel,
|
|
432
682
|
defaultRoleModels: config.agents.defaultRoleModels,
|
|
683
|
+
multiplexer: config.agents.multiplexer,
|
|
433
684
|
};
|
|
434
685
|
output(payload, opts.json);
|
|
435
686
|
if (!opts.json) {
|
|
@@ -439,6 +690,116 @@ export function agentsCommand() {
|
|
|
439
690
|
for (const role of COLLAB_ROLES) {
|
|
440
691
|
console.log(chalk.dim(`- ${role}: ${payload.defaultRoleModels[role] || '(none)'}`));
|
|
441
692
|
}
|
|
693
|
+
console.log(chalk.dim(`Multiplexer: ${payload.multiplexer || 'auto'}`));
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
output({ error: error.message }, opts.json);
|
|
697
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
model
|
|
703
|
+
.command('choose')
|
|
704
|
+
.description('Interactively choose a default model by provider and role')
|
|
705
|
+
.option('--refresh', 'Refresh model cache from providers', false)
|
|
706
|
+
.option('--json', 'Output as JSON')
|
|
707
|
+
.action(async (opts) => {
|
|
708
|
+
try {
|
|
709
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
710
|
+
if (!opencodeBin) {
|
|
711
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const models = await listOpenCodeModels({
|
|
715
|
+
binaryPath: opencodeBin,
|
|
716
|
+
refresh: Boolean(opts.refresh),
|
|
717
|
+
});
|
|
718
|
+
if (models.length === 0) {
|
|
719
|
+
throw new Error('No models returned by OpenCode. Run: opencode auth list, opencode models --refresh');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const grouped = groupModelsByProvider(models);
|
|
723
|
+
const providerChoices = [...grouped.keys()]
|
|
724
|
+
.sort((a, b) => a.localeCompare(b))
|
|
725
|
+
.map((provider) => ({
|
|
726
|
+
name: `${provider} (${(grouped.get(provider) || []).length})`,
|
|
727
|
+
value: provider,
|
|
728
|
+
}));
|
|
729
|
+
|
|
730
|
+
const { provider } = await inquirer.prompt([
|
|
731
|
+
{
|
|
732
|
+
type: 'list',
|
|
733
|
+
name: 'provider',
|
|
734
|
+
message: 'Select model provider',
|
|
735
|
+
choices: providerChoices,
|
|
736
|
+
},
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
const selectedProviderModels = grouped.get(provider) || [];
|
|
740
|
+
const { modelName } = await inquirer.prompt([
|
|
741
|
+
{
|
|
742
|
+
type: 'list',
|
|
743
|
+
name: 'modelName',
|
|
744
|
+
message: `Select model from ${provider}`,
|
|
745
|
+
pageSize: 20,
|
|
746
|
+
choices: selectedProviderModels.map((name) => ({ name, value: name })),
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
const roleChoices = [
|
|
751
|
+
{ name: 'Global fallback (all roles)', value: 'all' },
|
|
752
|
+
...COLLAB_ROLES.map((role) => ({ name: `Role: ${role}`, value: role })),
|
|
753
|
+
];
|
|
754
|
+
const { role } = await inquirer.prompt([
|
|
755
|
+
{
|
|
756
|
+
type: 'list',
|
|
757
|
+
name: 'role',
|
|
758
|
+
message: 'Apply model to',
|
|
759
|
+
choices: roleChoices,
|
|
760
|
+
},
|
|
761
|
+
]);
|
|
762
|
+
|
|
763
|
+
const modelId = `${provider}/${modelName}`;
|
|
764
|
+
const updated = await updateAgentsConfig((current) => {
|
|
765
|
+
const next = {
|
|
766
|
+
agents: {
|
|
767
|
+
defaultModel: current.agents.defaultModel,
|
|
768
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
769
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
if (role === 'all') {
|
|
774
|
+
next.agents.defaultModel = modelId;
|
|
775
|
+
} else {
|
|
776
|
+
next.agents.defaultRoleModels = {
|
|
777
|
+
...next.agents.defaultRoleModels,
|
|
778
|
+
[role]: modelId,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return next;
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const payload = {
|
|
786
|
+
success: true,
|
|
787
|
+
selected: {
|
|
788
|
+
role,
|
|
789
|
+
provider,
|
|
790
|
+
model: modelId,
|
|
791
|
+
},
|
|
792
|
+
configPath: getAgentsConfigPath(),
|
|
793
|
+
defaultModel: updated.agents.defaultModel,
|
|
794
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
output(payload, opts.json);
|
|
798
|
+
if (!opts.json) {
|
|
799
|
+
console.log(chalk.green('✓ SynapseGrid model selected and saved'));
|
|
800
|
+
console.log(chalk.dim(` Target: ${role === 'all' ? 'global fallback' : `role ${role}`}`));
|
|
801
|
+
console.log(chalk.dim(` Model: ${modelId}`));
|
|
802
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
442
803
|
}
|
|
443
804
|
} catch (error) {
|
|
444
805
|
output({ error: error.message }, opts.json);
|
|
@@ -466,6 +827,7 @@ export function agentsCommand() {
|
|
|
466
827
|
agents: {
|
|
467
828
|
defaultModel: current.agents.defaultModel,
|
|
468
829
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
830
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
469
831
|
},
|
|
470
832
|
};
|
|
471
833
|
if (role === 'all') {
|
|
@@ -514,6 +876,7 @@ export function agentsCommand() {
|
|
|
514
876
|
agents: {
|
|
515
877
|
defaultModel: current.agents.defaultModel,
|
|
516
878
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
879
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
517
880
|
},
|
|
518
881
|
};
|
|
519
882
|
if (role === 'all') {
|
|
@@ -545,6 +908,83 @@ export function agentsCommand() {
|
|
|
545
908
|
}
|
|
546
909
|
});
|
|
547
910
|
|
|
911
|
+
agents
|
|
912
|
+
.command('transcript')
|
|
913
|
+
.description('Show collaborative transcript (optionally filtered by role)')
|
|
914
|
+
.option('--session <id>', 'Session ID (defaults to current)')
|
|
915
|
+
.option('--role <role>', 'Role filter (planner|critic|coder|reviewer|all)', 'all')
|
|
916
|
+
.option('--limit <n>', 'Max messages to display', '40')
|
|
917
|
+
.option('--json', 'Output as JSON')
|
|
918
|
+
.action(async (opts) => {
|
|
919
|
+
try {
|
|
920
|
+
const sessionId = opts.session || await ensureSessionId(true);
|
|
921
|
+
const role = String(opts.role || 'all');
|
|
922
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
923
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
924
|
+
throw new Error('--limit must be a positive integer');
|
|
925
|
+
}
|
|
926
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
927
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const transcript = await loadTranscript(sessionId);
|
|
931
|
+
const filtered = transcript
|
|
932
|
+
.filter((msg) => role === 'all' || msg.from === role)
|
|
933
|
+
.slice(-limit);
|
|
934
|
+
|
|
935
|
+
output({ sessionId, count: filtered.length, transcript: filtered }, opts.json);
|
|
936
|
+
if (!opts.json) {
|
|
937
|
+
console.log(chalk.bold(`SynapseGrid transcript (${filtered.length})`));
|
|
938
|
+
for (const msg of filtered) {
|
|
939
|
+
console.log(chalk.cyan(`\n[${msg.from}] ${msg.timestamp || ''}`));
|
|
940
|
+
console.log(String(msg.content || '').trim() || chalk.dim('(empty)'));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} catch (error) {
|
|
944
|
+
output({ error: error.message }, opts.json);
|
|
945
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
agents
|
|
951
|
+
.command('summary')
|
|
952
|
+
.description('Show meeting summary generated from collaborative run')
|
|
953
|
+
.option('--session <id>', 'Session ID (defaults to current)')
|
|
954
|
+
.option('--json', 'Output as JSON')
|
|
955
|
+
.action(async (opts) => {
|
|
956
|
+
try {
|
|
957
|
+
const sessionId = opts.session || await ensureSessionId(true);
|
|
958
|
+
const state = await loadSessionState(sessionId);
|
|
959
|
+
const summaryFile = await readSessionArtifact(sessionId, 'meeting-summary.md');
|
|
960
|
+
const meetingLogFile = await readSessionArtifact(sessionId, 'meeting-log.md');
|
|
961
|
+
const payload = {
|
|
962
|
+
sessionId,
|
|
963
|
+
status: state.status,
|
|
964
|
+
finalSummary: state.run?.finalSummary || null,
|
|
965
|
+
sharedContext: state.run?.sharedContext || null,
|
|
966
|
+
summaryFile,
|
|
967
|
+
meetingLogFile,
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
output(payload, opts.json);
|
|
971
|
+
if (!opts.json) {
|
|
972
|
+
console.log(chalk.bold('SynapseGrid meeting summary'));
|
|
973
|
+
if (summaryFile) {
|
|
974
|
+
process.stdout.write(summaryFile.endsWith('\n') ? summaryFile : `${summaryFile}\n`);
|
|
975
|
+
} else if (payload.finalSummary) {
|
|
976
|
+
process.stdout.write(`${payload.finalSummary}\n`);
|
|
977
|
+
} else {
|
|
978
|
+
console.log(chalk.dim('No summary generated yet.'));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
} catch (error) {
|
|
982
|
+
output({ error: error.message }, opts.json);
|
|
983
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
548
988
|
agents
|
|
549
989
|
.command('export')
|
|
550
990
|
.description('Export collaborative transcript')
|
|
@@ -562,9 +1002,11 @@ export function agentsCommand() {
|
|
|
562
1002
|
throw new Error('--format must be md or json');
|
|
563
1003
|
}
|
|
564
1004
|
|
|
1005
|
+
const meetingSummary = await readSessionArtifact(sessionId, 'meeting-summary.md');
|
|
1006
|
+
const meetingLog = await readSessionArtifact(sessionId, 'meeting-log.md');
|
|
565
1007
|
const payload = format === 'json'
|
|
566
|
-
? JSON.stringify({ state, transcript }, null, 2) + '\n'
|
|
567
|
-
: toMarkdownTranscript(state, transcript)
|
|
1008
|
+
? JSON.stringify({ state, transcript, meetingSummary, meetingLog }, null, 2) + '\n'
|
|
1009
|
+
: `${toMarkdownTranscript(state, transcript)}\n\n## Meeting Summary\n\n${meetingSummary || state.run?.finalSummary || 'No summary generated yet.'}\n\n## Meeting Log\n\n${meetingLog || 'No meeting log generated yet.'}\n`;
|
|
568
1010
|
|
|
569
1011
|
if (opts.out) {
|
|
570
1012
|
const outputPath = resolve(opts.out);
|
|
@@ -598,17 +1040,15 @@ export function agentsCommand() {
|
|
|
598
1040
|
.option('--model-critic <id>', 'Model for critic role (provider/model)')
|
|
599
1041
|
.option('--model-coder <id>', 'Model for coder role (provider/model)')
|
|
600
1042
|
.option('--model-reviewer <id>', 'Model for reviewer role (provider/model)')
|
|
1043
|
+
.option('--mux <name>', 'Multiplexer backend: auto|zellij|tmux')
|
|
601
1044
|
.option('--cwd <path>', 'Working directory for agents', process.cwd())
|
|
602
|
-
.option('--attach', 'Attach
|
|
1045
|
+
.option('--attach', 'Attach multiplexer immediately after start', false)
|
|
603
1046
|
.option('--json', 'Output as JSON')
|
|
604
1047
|
.action(async (opts) => {
|
|
605
1048
|
try {
|
|
606
1049
|
if (!hasCommand('opencode')) {
|
|
607
1050
|
throw new Error('OpenCode is not installed. Run: acfm agents setup');
|
|
608
1051
|
}
|
|
609
|
-
if (!hasCommand('tmux')) {
|
|
610
|
-
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
611
|
-
}
|
|
612
1052
|
const opencodeBin = resolveCommandPath('opencode');
|
|
613
1053
|
if (!opencodeBin) {
|
|
614
1054
|
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
@@ -621,6 +1061,11 @@ export function agentsCommand() {
|
|
|
621
1061
|
}
|
|
622
1062
|
|
|
623
1063
|
const config = await loadAgentsConfig();
|
|
1064
|
+
const configuredMux = validateMultiplexer(opts.mux || config.agents.multiplexer || 'auto');
|
|
1065
|
+
const selectedMux = resolveMultiplexer(configuredMux, hasCommand('tmux'), hasCommand('zellij'));
|
|
1066
|
+
if (!selectedMux) {
|
|
1067
|
+
throw new Error('No multiplexer found. Install zellij or tmux with: acfm agents setup');
|
|
1068
|
+
}
|
|
624
1069
|
const cliModel = assertValidModelIdOrNull('--model', opts.model || null);
|
|
625
1070
|
const cliRoleModels = parseRoleModelOptions(opts);
|
|
626
1071
|
for (const [role, model] of Object.entries(cliRoleModels)) {
|
|
@@ -655,30 +1100,47 @@ export function agentsCommand() {
|
|
|
655
1100
|
maxRounds,
|
|
656
1101
|
model: globalModel,
|
|
657
1102
|
roleModels,
|
|
1103
|
+
multiplexer: selectedMux,
|
|
658
1104
|
workingDirectory: resolve(opts.cwd),
|
|
659
1105
|
opencodeBin,
|
|
660
1106
|
});
|
|
661
|
-
const
|
|
1107
|
+
const muxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
662
1108
|
const sessionDir = getSessionDir(state.sessionId);
|
|
1109
|
+
|
|
1110
|
+
if (selectedMux === 'zellij') {
|
|
1111
|
+
await spawnZellijSession({
|
|
1112
|
+
sessionName: muxSessionName,
|
|
1113
|
+
sessionDir,
|
|
1114
|
+
sessionId: state.sessionId,
|
|
1115
|
+
});
|
|
1116
|
+
} else {
|
|
1117
|
+
await spawnTmuxSession({
|
|
1118
|
+
sessionName: muxSessionName,
|
|
1119
|
+
sessionDir,
|
|
1120
|
+
sessionId: state.sessionId,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
663
1124
|
const updated = await saveSessionState({
|
|
664
1125
|
...state,
|
|
665
|
-
|
|
1126
|
+
multiplexer: selectedMux,
|
|
1127
|
+
multiplexerSessionName: muxSessionName,
|
|
1128
|
+
tmuxSessionName: selectedMux === 'tmux' ? muxSessionName : null,
|
|
666
1129
|
});
|
|
667
1130
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
output({ sessionId: updated.sessionId, tmuxSessionName, status: updated.status }, opts.json);
|
|
1131
|
+
output({
|
|
1132
|
+
sessionId: updated.sessionId,
|
|
1133
|
+
multiplexer: selectedMux,
|
|
1134
|
+
multiplexerSessionName: muxSessionName,
|
|
1135
|
+
status: updated.status,
|
|
1136
|
+
}, opts.json);
|
|
675
1137
|
if (!opts.json) {
|
|
676
1138
|
printStartSummary(updated);
|
|
677
1139
|
printModelConfig(updated);
|
|
678
1140
|
}
|
|
679
1141
|
|
|
680
1142
|
if (opts.attach) {
|
|
681
|
-
await
|
|
1143
|
+
await attachToMux(selectedMux, muxSessionName, false);
|
|
682
1144
|
}
|
|
683
1145
|
} catch (error) {
|
|
684
1146
|
output({ error: error.message }, opts.json);
|
|
@@ -723,15 +1185,22 @@ export function agentsCommand() {
|
|
|
723
1185
|
console.log(chalk.dim(`Round: ${Math.min(state.round, state.maxRounds)}/${state.maxRounds}`));
|
|
724
1186
|
console.log(chalk.dim(`Active agent: ${state.activeAgent || 'none'}`));
|
|
725
1187
|
console.log(chalk.dim(`Messages: ${state.messages.length}`));
|
|
1188
|
+
const summary = runSummary(state);
|
|
1189
|
+
console.log(chalk.dim(`Run: ${summary.status}${summary.currentRole ? ` (role=${summary.currentRole})` : ''}, events=${summary.events}`));
|
|
1190
|
+
if (summary.lastError?.message) {
|
|
1191
|
+
console.log(chalk.dim(`Run error: ${summary.lastError.message}`));
|
|
1192
|
+
}
|
|
726
1193
|
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
1194
|
+
console.log(chalk.dim(`Multiplexer: ${state.multiplexer || 'auto'} (${sessionMuxName(state)})`));
|
|
727
1195
|
for (const role of COLLAB_ROLES) {
|
|
728
1196
|
const configured = state.roleModels?.[role] || '-';
|
|
729
1197
|
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
730
1198
|
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
731
1199
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
1200
|
+
const meetingLogPath = resolve(getSessionDir(state.sessionId), 'meeting-log.md');
|
|
1201
|
+
const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
|
|
1202
|
+
console.log(chalk.dim(`meeting-log: ${existsSync(meetingLogPath) ? meetingLogPath : 'not generated yet'}`));
|
|
1203
|
+
console.log(chalk.dim(`meeting-summary: ${existsSync(meetingSummaryPath) ? meetingSummaryPath : 'not generated yet'}`));
|
|
735
1204
|
}
|
|
736
1205
|
} catch (error) {
|
|
737
1206
|
output({ error: error.message }, opts.json);
|
|
@@ -748,16 +1217,68 @@ export function agentsCommand() {
|
|
|
748
1217
|
try {
|
|
749
1218
|
const sessionId = await ensureSessionId(true);
|
|
750
1219
|
let state = await loadSessionState(sessionId);
|
|
1220
|
+
const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
|
|
751
1221
|
state = await stopSession(state, 'stopped');
|
|
752
|
-
if (state.
|
|
1222
|
+
if (state.run && state.run.status === 'running') {
|
|
1223
|
+
state = await saveSessionState({
|
|
1224
|
+
...state,
|
|
1225
|
+
run: {
|
|
1226
|
+
...state.run,
|
|
1227
|
+
status: 'cancelled',
|
|
1228
|
+
finishedAt: new Date().toISOString(),
|
|
1229
|
+
currentRole: null,
|
|
1230
|
+
lastError: {
|
|
1231
|
+
code: 'RUN_CANCELLED',
|
|
1232
|
+
message: 'Run cancelled by user',
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (!existsSync(meetingSummaryPath)) {
|
|
1239
|
+
const fallbackSummary = [
|
|
1240
|
+
'# SynapseGrid Meeting Summary',
|
|
1241
|
+
'',
|
|
1242
|
+
`Session: ${state.sessionId}`,
|
|
1243
|
+
`Status: ${state.status}`,
|
|
1244
|
+
'',
|
|
1245
|
+
'This summary was auto-generated at stop time because the run did not complete normally.',
|
|
1246
|
+
'',
|
|
1247
|
+
'## Last message',
|
|
1248
|
+
state.messages?.[state.messages.length - 1]?.content || '(none)',
|
|
1249
|
+
'',
|
|
1250
|
+
].join('\n');
|
|
1251
|
+
await writeMeetingSummary(state.sessionId, fallbackSummary);
|
|
1252
|
+
if (state.run && !state.run.finalSummary) {
|
|
1253
|
+
state = await saveSessionState({
|
|
1254
|
+
...state,
|
|
1255
|
+
run: {
|
|
1256
|
+
...state.run,
|
|
1257
|
+
finalSummary: fallbackSummary,
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
1264
|
+
const muxSessionName = sessionMuxName(state);
|
|
1265
|
+
if (multiplexer === 'zellij' && muxSessionName && hasCommand('zellij')) {
|
|
1266
|
+
try {
|
|
1267
|
+
await runZellij(['delete-session', muxSessionName]);
|
|
1268
|
+
} catch {
|
|
1269
|
+
// ignore if already closed
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (multiplexer === 'tmux' && muxSessionName && hasCommand('tmux')) {
|
|
753
1273
|
try {
|
|
754
|
-
await runTmux('tmux', ['kill-session', '-t',
|
|
1274
|
+
await runTmux('tmux', ['kill-session', '-t', muxSessionName]);
|
|
755
1275
|
} catch {
|
|
756
1276
|
// ignore if already closed
|
|
757
1277
|
}
|
|
758
1278
|
}
|
|
759
1279
|
output({ sessionId: state.sessionId, status: state.status }, opts.json);
|
|
760
1280
|
if (!opts.json) console.log(chalk.green('✓ Collaborative session stopped'));
|
|
1281
|
+
|
|
761
1282
|
} catch (error) {
|
|
762
1283
|
output({ error: error.message }, opts.json);
|
|
763
1284
|
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
@@ -765,6 +1286,33 @@ export function agentsCommand() {
|
|
|
765
1286
|
}
|
|
766
1287
|
});
|
|
767
1288
|
|
|
1289
|
+
agents
|
|
1290
|
+
.command('autopilot')
|
|
1291
|
+
.description('Internal headless collaborative driver (non-tmux)')
|
|
1292
|
+
.requiredOption('--session <id>', 'Session id')
|
|
1293
|
+
.option('--poll-ms <n>', 'Polling interval in ms', '900')
|
|
1294
|
+
.action(async (opts) => {
|
|
1295
|
+
const pollMs = Number.parseInt(opts.pollMs, 10);
|
|
1296
|
+
while (true) {
|
|
1297
|
+
try {
|
|
1298
|
+
const state = await loadSessionState(opts.session);
|
|
1299
|
+
if (state.status !== 'running') process.exit(0);
|
|
1300
|
+
|
|
1301
|
+
for (const role of state.roles || COLLAB_ROLES) {
|
|
1302
|
+
await runWorkerIteration(opts.session, role, {
|
|
1303
|
+
cwd: state.workingDirectory || process.cwd(),
|
|
1304
|
+
model: state.model || null,
|
|
1305
|
+
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
1306
|
+
timeoutMs: state.run?.policy?.timeoutPerRoleMs || 180000,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
console.error(`[autopilot] ${error.message}`);
|
|
1311
|
+
}
|
|
1312
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, Number.isInteger(pollMs) ? pollMs : 900));
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
|
|
768
1316
|
agents
|
|
769
1317
|
.command('worker')
|
|
770
1318
|
.description('Internal worker loop for a single collaborative role')
|
|
@@ -828,11 +1376,17 @@ export function agentsCommand() {
|
|
|
828
1376
|
try {
|
|
829
1377
|
const opencodeBin = resolveCommandPath('opencode');
|
|
830
1378
|
const tmuxInstalled = hasCommand('tmux');
|
|
1379
|
+
const zellijInstalled = hasCommand('zellij');
|
|
831
1380
|
const cfg = await loadAgentsConfig();
|
|
832
1381
|
const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
1382
|
+
const configuredMux = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
1383
|
+
const resolvedMux = resolveMultiplexer(configuredMux, tmuxInstalled, zellijInstalled);
|
|
833
1384
|
const result = {
|
|
834
1385
|
opencodeBin,
|
|
835
1386
|
tmuxInstalled,
|
|
1387
|
+
zellijInstalled,
|
|
1388
|
+
configuredMultiplexer: configuredMux,
|
|
1389
|
+
resolvedMultiplexer: resolvedMux,
|
|
836
1390
|
defaultModel,
|
|
837
1391
|
defaultRoleModels: cfg.agents.defaultRoleModels,
|
|
838
1392
|
preflight: null,
|
|
@@ -852,12 +1406,14 @@ export function agentsCommand() {
|
|
|
852
1406
|
if (!opts.json) {
|
|
853
1407
|
console.log(chalk.bold('SynapseGrid doctor'));
|
|
854
1408
|
console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
|
|
1409
|
+
console.log(chalk.dim(`zellij: ${zellijInstalled ? 'installed' : 'not installed'}`));
|
|
855
1410
|
console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
|
|
1411
|
+
console.log(chalk.dim(`multiplexer: configured=${configuredMux} resolved=${resolvedMux || 'none'}`));
|
|
856
1412
|
console.log(chalk.dim(`default model: ${defaultModel}`));
|
|
857
1413
|
console.log(chalk.dim(`preflight: ${result.preflight?.ok ? 'ok' : `failed - ${result.preflight?.error || 'unknown error'}`}`));
|
|
858
1414
|
}
|
|
859
1415
|
|
|
860
|
-
if (!result.preflight?.ok) process.exit(1);
|
|
1416
|
+
if (!result.preflight?.ok || !result.resolvedMultiplexer) process.exit(1);
|
|
861
1417
|
} catch (error) {
|
|
862
1418
|
output({ error: error.message }, opts.json);
|
|
863
1419
|
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|