ac-framework 1.9.8 → 2.0.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.
@@ -18,6 +18,7 @@ import { runWorkerIteration } from '../agents/orchestrator.js';
18
18
  import {
19
19
  addUserMessage,
20
20
  createSession,
21
+ ensureSessionArtifacts,
21
22
  getSessionDir,
22
23
  loadCurrentSessionId,
23
24
  loadSessionState,
@@ -26,8 +27,19 @@ import {
26
27
  saveSessionState,
27
28
  setCurrentSession,
28
29
  stopSession,
30
+ sessionArtifactPaths,
31
+ writeMeetingSummary,
29
32
  } from '../agents/state-store.js';
30
- import { roleLogPath, runTmux, spawnTmuxSession, tmuxSessionExists } from '../agents/runtime.js';
33
+ import {
34
+ roleLogPath,
35
+ runTmux,
36
+ runZellij,
37
+ spawnTmuxSession,
38
+ spawnZellijSession,
39
+ tmuxSessionExists,
40
+ zellijSessionExists,
41
+ resolveMultiplexer,
42
+ } from '../agents/runtime.js';
31
43
  import { getAgentsConfigPath, loadAgentsConfig, updateAgentsConfig } from '../agents/config-store.js';
32
44
  import {
33
45
  buildEffectiveRoleModels,
@@ -35,7 +47,13 @@ import {
35
47
  normalizeModelId,
36
48
  sanitizeRoleModels,
37
49
  } from '../agents/model-selection.js';
38
- import { ensureCollabDependencies, hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
50
+ import {
51
+ ensureCollabDependencies,
52
+ hasCommand,
53
+ resolveCommandPath,
54
+ resolveManagedZellijPath,
55
+ installManagedZellijLatest,
56
+ } from '../services/dependency-installer.js';
39
57
 
40
58
  function output(data, json) {
41
59
  if (json) {
@@ -60,18 +78,81 @@ async function ensureSessionId(required = true) {
60
78
  function printStartSummary(state) {
61
79
  console.log(chalk.green(`✓ ${COLLAB_SYSTEM_NAME} session started`));
62
80
  console.log(chalk.dim(` Session: ${state.sessionId}`));
63
- console.log(chalk.dim(` tmux: ${state.tmuxSessionName}`));
81
+ const multiplexer = state.multiplexer || 'auto';
82
+ const muxSessionName = state.multiplexerSessionName || state.tmuxSessionName || '-';
83
+ console.log(chalk.dim(` Multiplexer: ${multiplexer}`));
84
+ console.log(chalk.dim(` Session name: ${muxSessionName}`));
64
85
  console.log(chalk.dim(` Task: ${state.task}`));
65
86
  console.log(chalk.dim(` Roles: ${state.roles.join(', ')}`));
66
87
  console.log();
67
88
  console.log(chalk.cyan('Attach with:'));
68
- console.log(chalk.white(` tmux attach -t ${state.tmuxSessionName}`));
89
+ if (multiplexer === 'zellij') {
90
+ console.log(chalk.white(` zellij attach ${muxSessionName}`));
91
+ } else {
92
+ console.log(chalk.white(` tmux attach -t ${muxSessionName}`));
93
+ }
69
94
  console.log(chalk.white(' acfm agents live'));
70
95
  console.log();
71
96
  console.log(chalk.cyan('Interact with:'));
72
97
  console.log(chalk.white(' acfm agents send "your message"'));
73
98
  }
74
99
 
100
+ function validateMultiplexer(value) {
101
+ const normalized = String(value || '').trim().toLowerCase();
102
+ if (!['auto', 'zellij', 'tmux'].includes(normalized)) {
103
+ throw new Error('--mux must be one of: auto|zellij|tmux');
104
+ }
105
+ return normalized;
106
+ }
107
+
108
+ function sessionMuxName(state) {
109
+ return state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
110
+ }
111
+
112
+ async function sessionExistsForMux(multiplexer, sessionName, zellijPath = null) {
113
+ if (multiplexer === 'zellij') return zellijSessionExists(sessionName, zellijPath);
114
+ return tmuxSessionExists(sessionName);
115
+ }
116
+
117
+ function resolveConfiguredZellijPath(config) {
118
+ const strategy = config?.agents?.zellij?.strategy || 'auto';
119
+ if (strategy === 'system') {
120
+ return resolveCommandPath('zellij');
121
+ }
122
+ const managed = resolveManagedZellijPath(config);
123
+ if (managed) return managed;
124
+ return resolveCommandPath('zellij');
125
+ }
126
+
127
+ function shouldUseManagedZellij(config) {
128
+ const strategy = config?.agents?.zellij?.strategy || 'auto';
129
+ if (strategy === 'managed') return true;
130
+ if (strategy === 'system') return false;
131
+ return true;
132
+ }
133
+
134
+ function resolveMultiplexerWithPaths(config, requestedMux = 'auto') {
135
+ const zellijPath = resolveConfiguredZellijPath(config);
136
+ const tmuxPath = resolveCommandPath('tmux');
137
+ const selected = resolveMultiplexer(requestedMux, Boolean(tmuxPath), Boolean(zellijPath));
138
+ return {
139
+ selected,
140
+ zellijPath,
141
+ tmuxPath,
142
+ };
143
+ }
144
+
145
+ async function attachToMux(multiplexer, sessionName, readonly = false, zellijPath = null) {
146
+ if (multiplexer === 'zellij') {
147
+ await runZellij(['attach', sessionName], { stdio: 'inherit', binaryPath: zellijPath });
148
+ return;
149
+ }
150
+ const args = ['attach'];
151
+ if (readonly) args.push('-r');
152
+ args.push('-t', sessionName);
153
+ await runTmux('tmux', args, { stdio: 'inherit' });
154
+ }
155
+
75
156
  function toMarkdownTranscript(state, transcript) {
76
157
  const displayedRound = Math.min(state.round, state.maxRounds);
77
158
  const lines = [
@@ -161,6 +242,18 @@ async function readSessionArtifact(sessionId, filename) {
161
242
  return readFile(path, 'utf8');
162
243
  }
163
244
 
245
+ async function collectArtifactStatus(sessionId) {
246
+ await ensureSessionArtifacts(sessionId);
247
+ const paths = sessionArtifactPaths(sessionId);
248
+ return {
249
+ sessionId,
250
+ checkedAt: new Date().toISOString(),
251
+ artifacts: Object.fromEntries(
252
+ Object.entries(paths).map(([key, value]) => [key, { path: value, exists: existsSync(value) }])
253
+ ),
254
+ };
255
+ }
256
+
164
257
  async function preflightModel({ opencodeBin, model, cwd }) {
165
258
  const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
166
259
  try {
@@ -181,31 +274,96 @@ export function agentsCommand() {
181
274
  const agents = new Command('agents')
182
275
  .description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
183
276
 
277
+ agents.addHelpText('after', `
278
+ Examples:
279
+ acfm agents start --task "Implement auth flow" --mux auto
280
+ acfm agents setup
281
+ acfm agents artifacts
282
+ acfm agents runtime get
283
+ acfm agents runtime install-zellij
284
+ acfm agents runtime set zellij
285
+ acfm agents model choose
286
+ acfm agents model list
287
+ acfm agents transcript --role all --limit 40
288
+ acfm agents summary
289
+ acfm agents export --format md --out ./session.md
290
+ `);
291
+
184
292
  agents
185
293
  .command('setup')
186
- .description('Install optional collaboration dependencies (OpenCode + tmux)')
294
+ .description('Install optional collaboration dependencies (OpenCode + zellij/tmux)')
295
+ .option('--yes', 'Install dependencies without interactive confirmation', false)
187
296
  .option('--json', 'Output as JSON')
188
297
  .action(async (opts) => {
189
- const result = ensureCollabDependencies();
298
+ let installZellij = true;
299
+ let installTmux = true;
300
+
301
+ if (!opts.yes && !opts.json) {
302
+ const answers = await inquirer.prompt([
303
+ {
304
+ type: 'confirm',
305
+ name: 'installZellij',
306
+ message: 'Install zellij (recommended, multiplatform backend)?',
307
+ default: true,
308
+ },
309
+ {
310
+ type: 'confirm',
311
+ name: 'installTmux',
312
+ message: 'Install tmux as fallback backend?',
313
+ default: true,
314
+ },
315
+ ]);
316
+ installZellij = Boolean(answers.installZellij);
317
+ installTmux = Boolean(answers.installTmux);
318
+ }
319
+
320
+ const result = ensureCollabDependencies({
321
+ installZellij,
322
+ installTmux,
323
+ preferManagedZellij: installZellij,
324
+ });
325
+ const awaited = await result;
190
326
  let collabMcp = null;
191
327
 
192
- if (result.success) {
328
+ if (awaited.success) {
193
329
  const { detectAndInstallMCPs } = await import('../services/mcp-installer.js');
194
330
  collabMcp = detectAndInstallMCPs({ target: 'collab' });
195
331
  }
196
332
 
197
- const payload = { ...result, collabMcp };
333
+ const payload = { ...awaited, collabMcp };
198
334
  output(payload, opts.json);
199
335
  if (!opts.json) {
200
- const oLabel = result.opencode.success ? chalk.green('ok') : chalk.red('failed');
201
- const tLabel = result.tmux.success ? chalk.green('ok') : chalk.red('failed');
202
- console.log(`OpenCode: ${oLabel} - ${result.opencode.message}`);
203
- console.log(`tmux: ${tLabel} - ${result.tmux.message}`);
336
+ const oLabel = awaited.opencode.success ? chalk.green('ok') : chalk.red('failed');
337
+ const tLabel = awaited.tmux.success ? chalk.green('ok') : chalk.red('failed');
338
+ const zLabel = awaited.zellij.success ? chalk.green('ok') : chalk.red('failed');
339
+ console.log(`OpenCode: ${oLabel} - ${awaited.opencode.message}`);
340
+ console.log(`zellij: ${zLabel} - ${awaited.zellij.message}`);
341
+ if (awaited.zellij.binaryPath) {
342
+ console.log(chalk.dim(` ${awaited.zellij.binaryPath}`));
343
+ }
344
+ console.log(`tmux: ${tLabel} - ${awaited.tmux.message}`);
204
345
  if (collabMcp) {
205
346
  console.log(`Collab MCP: ${chalk.green('ok')} - installed ${collabMcp.success}/${collabMcp.installed} on detected assistants`);
206
347
  }
207
348
  }
208
- if (!result.success) process.exit(1);
349
+
350
+ if (awaited.zellij.success && awaited.zellij.source === 'managed' && awaited.zellij.binaryPath) {
351
+ await updateAgentsConfig((current) => ({
352
+ agents: {
353
+ defaultModel: current.agents.defaultModel,
354
+ defaultRoleModels: { ...current.agents.defaultRoleModels },
355
+ multiplexer: current.agents.multiplexer || 'auto',
356
+ zellij: {
357
+ strategy: 'managed',
358
+ binaryPath: awaited.zellij.binaryPath,
359
+ version: awaited.zellij.version || null,
360
+ source: 'managed',
361
+ },
362
+ },
363
+ }));
364
+ }
365
+
366
+ if (!awaited.success) process.exit(1);
209
367
  });
210
368
 
211
369
  agents
@@ -286,10 +444,10 @@ export function agentsCommand() {
286
444
  }
287
445
  console.log(chalk.bold('SynapseGrid Sessions'));
288
446
  for (const item of sessions) {
289
- console.log(
447
+ console.log(
290
448
  `${chalk.cyan(item.sessionId.slice(0, 8))} ${item.status.padEnd(10)} ` +
291
449
  `round ${String(item.round).padStart(2)}/${String(item.maxRounds).padEnd(2)} ` +
292
- `${item.tmuxSessionName || '-'} ${item.task}`
450
+ `${item.multiplexer || 'auto'}:${item.multiplexerSessionName || item.tmuxSessionName || '-'} ${item.task}`
293
451
  );
294
452
  }
295
453
  }
@@ -302,15 +460,19 @@ export function agentsCommand() {
302
460
 
303
461
  agents
304
462
  .command('attach')
305
- .description('Attach terminal to active SynapseGrid tmux session')
463
+ .description('Attach terminal to active SynapseGrid multiplexer session')
306
464
  .action(async () => {
307
465
  try {
308
466
  const sessionId = await ensureSessionId(true);
309
467
  const state = await loadSessionState(sessionId);
310
- if (!state.tmuxSessionName) {
311
- throw new Error('No tmux session registered for active collaborative session');
468
+ const cfg = await loadAgentsConfig();
469
+ const zellijPath = resolveConfiguredZellijPath(cfg);
470
+ const multiplexer = state.multiplexer || 'tmux';
471
+ const muxSessionName = sessionMuxName(state);
472
+ if (!muxSessionName) {
473
+ throw new Error('No multiplexer session registered for active collaborative session');
312
474
  }
313
- await runTmux('tmux', ['attach', '-t', state.tmuxSessionName], { stdio: 'inherit' });
475
+ await attachToMux(multiplexer, muxSessionName, false, zellijPath);
314
476
  } catch (error) {
315
477
  console.error(chalk.red(`Error: ${error.message}`));
316
478
  process.exit(1);
@@ -319,23 +481,24 @@ export function agentsCommand() {
319
481
 
320
482
  agents
321
483
  .command('live')
322
- .description('Attach to live tmux collaboration view (all agent panes)')
484
+ .description('Attach to live collaboration view (all agent panes)')
323
485
  .option('--readonly', 'Attach in read-only mode', false)
324
486
  .action(async (opts) => {
325
487
  try {
326
488
  const sessionId = await ensureSessionId(true);
327
489
  const state = await loadSessionState(sessionId);
328
- if (!state.tmuxSessionName) {
329
- throw new Error('No tmux session registered for active collaborative session');
490
+ const cfg = await loadAgentsConfig();
491
+ const zellijPath = resolveConfiguredZellijPath(cfg);
492
+ const multiplexer = state.multiplexer || 'tmux';
493
+ const muxSessionName = sessionMuxName(state);
494
+ if (!muxSessionName) {
495
+ throw new Error('No multiplexer session registered for active collaborative session');
330
496
  }
331
- const tmuxExists = await tmuxSessionExists(state.tmuxSessionName);
332
- if (!tmuxExists) {
333
- throw new Error(`tmux session ${state.tmuxSessionName} no longer exists. Run: acfm agents resume`);
497
+ const sessionExists = await sessionExistsForMux(multiplexer, muxSessionName, zellijPath);
498
+ if (!sessionExists) {
499
+ throw new Error(`${multiplexer} session ${muxSessionName} no longer exists. Run: acfm agents resume`);
334
500
  }
335
- const args = ['attach'];
336
- if (opts.readonly) args.push('-r');
337
- args.push('-t', state.tmuxSessionName);
338
- await runTmux('tmux', args, { stdio: 'inherit' });
501
+ await attachToMux(multiplexer, muxSessionName, Boolean(opts.readonly), zellijPath);
339
502
  } catch (error) {
340
503
  console.error(chalk.red(`Error: ${error.message}`));
341
504
  process.exit(1);
@@ -344,48 +507,68 @@ export function agentsCommand() {
344
507
 
345
508
  agents
346
509
  .command('resume')
347
- .description('Resume a previous session and optionally recreate tmux workers')
510
+ .description('Resume a previous session and optionally recreate multiplexer workers')
348
511
  .option('--session <id>', 'Session ID to resume (defaults to current)')
349
- .option('--no-recreate', 'Do not recreate tmux session/workers when missing')
350
- .option('--no-attach', 'Do not attach tmux after resume')
512
+ .option('--no-recreate', 'Do not recreate multiplexer session/workers when missing')
513
+ .option('--no-attach', 'Do not attach multiplexer after resume')
351
514
  .option('--json', 'Output as JSON')
352
515
  .action(async (opts) => {
353
516
  try {
354
517
  const sessionId = opts.session || await ensureSessionId(true);
355
518
  let state = await loadSessionState(sessionId);
519
+ const multiplexer = state.multiplexer || 'tmux';
520
+ const cfg = await loadAgentsConfig();
521
+ const zellijPath = resolveConfiguredZellijPath(cfg);
522
+ const tmuxPath = resolveCommandPath('tmux');
523
+
524
+ if (multiplexer === 'zellij' && !zellijPath) {
525
+ throw new Error('zellij is not installed. Run: acfm agents setup');
526
+ }
527
+ if (multiplexer === 'tmux' && !tmuxPath) {
528
+ throw new Error('tmux is not installed. Run: acfm agents setup');
529
+ }
356
530
 
357
- const tmuxSessionName = state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
358
- const tmuxExists = hasCommand('tmux') ? await tmuxSessionExists(tmuxSessionName) : false;
531
+ const muxSessionName = sessionMuxName(state);
532
+ const muxExists = await sessionExistsForMux(multiplexer, muxSessionName, zellijPath);
359
533
 
360
- if (!tmuxExists && opts.recreate) {
361
- if (!hasCommand('tmux')) {
362
- throw new Error('tmux is not installed. Run: acfm agents setup');
363
- }
534
+ if (!muxExists && opts.recreate) {
364
535
  const sessionDir = getSessionDir(state.sessionId);
365
- await spawnTmuxSession({ sessionName: tmuxSessionName, sessionDir, sessionId: state.sessionId });
536
+ if (multiplexer === 'zellij') {
537
+ await spawnZellijSession({
538
+ sessionName: muxSessionName,
539
+ sessionDir,
540
+ sessionId: state.sessionId,
541
+ binaryPath: zellijPath,
542
+ });
543
+ } else {
544
+ await spawnTmuxSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
545
+ }
366
546
  }
367
547
 
368
548
  state = await saveSessionState({
369
549
  ...state,
370
550
  status: 'running',
371
- tmuxSessionName,
551
+ multiplexer,
552
+ multiplexerSessionName: muxSessionName,
553
+ tmuxSessionName: multiplexer === 'tmux' ? muxSessionName : (state.tmuxSessionName || null),
372
554
  });
373
555
  await setCurrentSession(state.sessionId);
374
556
 
375
557
  output({
376
558
  sessionId: state.sessionId,
377
559
  status: state.status,
378
- tmuxSessionName,
379
- recreatedTmux: !tmuxExists && Boolean(opts.recreate),
560
+ multiplexer,
561
+ multiplexerSessionName: muxSessionName,
562
+ recreatedSession: !muxExists && Boolean(opts.recreate),
380
563
  }, opts.json);
381
564
 
382
565
  if (!opts.json) {
383
566
  console.log(chalk.green(`✓ Resumed session ${state.sessionId}`));
384
- console.log(chalk.dim(` tmux: ${tmuxSessionName}`));
567
+ console.log(chalk.dim(` ${multiplexer}: ${muxSessionName}`));
385
568
  }
386
569
 
387
570
  if (opts.attach) {
388
- await runTmux('tmux', ['attach', '-t', tmuxSessionName], { stdio: 'inherit' });
571
+ await attachToMux(multiplexer, muxSessionName, false, zellijPath);
389
572
  }
390
573
  } catch (error) {
391
574
  output({ error: error.message }, opts.json);
@@ -453,6 +636,129 @@ export function agentsCommand() {
453
636
  .command('model')
454
637
  .description('Manage default SynapseGrid model configuration');
455
638
 
639
+ const runtime = agents
640
+ .command('runtime')
641
+ .description('Manage SynapseGrid runtime backend settings');
642
+
643
+ runtime
644
+ .command('get')
645
+ .description('Show configured multiplexer backend')
646
+ .option('--json', 'Output as JSON')
647
+ .action(async (opts) => {
648
+ try {
649
+ const cfg = await loadAgentsConfig();
650
+ const configured = validateMultiplexer(cfg.agents.multiplexer || 'auto');
651
+ const zellijPath = resolveConfiguredZellijPath(cfg);
652
+ const tmuxPath = resolveCommandPath('tmux');
653
+ const resolved = resolveMultiplexer(configured, Boolean(tmuxPath), Boolean(zellijPath));
654
+ const payload = {
655
+ configPath: getAgentsConfigPath(),
656
+ multiplexer: configured,
657
+ resolved,
658
+ available: {
659
+ zellij: Boolean(zellijPath),
660
+ tmux: Boolean(tmuxPath),
661
+ },
662
+ zellij: cfg.agents.zellij,
663
+ zellijPath,
664
+ };
665
+ output(payload, opts.json);
666
+ if (!opts.json) {
667
+ console.log(chalk.bold('SynapseGrid runtime backend'));
668
+ console.log(chalk.dim(`Config: ${payload.configPath}`));
669
+ console.log(chalk.dim(`Configured: ${configured}`));
670
+ console.log(chalk.dim(`Resolved: ${resolved || 'none'}`));
671
+ console.log(chalk.dim(`zellij=${payload.available.zellij} tmux=${payload.available.tmux}`));
672
+ if (payload.zellijPath) {
673
+ console.log(chalk.dim(`zellij path: ${payload.zellijPath}`));
674
+ }
675
+ }
676
+ } catch (error) {
677
+ output({ error: error.message }, opts.json);
678
+ if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
679
+ process.exit(1);
680
+ }
681
+ });
682
+
683
+ runtime
684
+ .command('set <mux>')
685
+ .description('Set multiplexer backend: auto|zellij|tmux')
686
+ .option('--json', 'Output as JSON')
687
+ .action(async (mux, opts) => {
688
+ try {
689
+ const selected = validateMultiplexer(mux);
690
+ const updated = await updateAgentsConfig((current) => ({
691
+ agents: {
692
+ defaultModel: current.agents.defaultModel,
693
+ defaultRoleModels: { ...current.agents.defaultRoleModels },
694
+ multiplexer: selected,
695
+ zellij: {
696
+ ...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
697
+ },
698
+ },
699
+ }));
700
+ const zellijPath = resolveConfiguredZellijPath(updated);
701
+ const tmuxPath = resolveCommandPath('tmux');
702
+ const resolved = resolveMultiplexer(updated.agents.multiplexer, Boolean(tmuxPath), Boolean(zellijPath));
703
+ const payload = {
704
+ success: true,
705
+ configPath: getAgentsConfigPath(),
706
+ multiplexer: updated.agents.multiplexer,
707
+ resolved,
708
+ };
709
+ output(payload, opts.json);
710
+ if (!opts.json) {
711
+ console.log(chalk.green('✓ SynapseGrid runtime backend updated'));
712
+ console.log(chalk.dim(` Configured: ${payload.multiplexer}`));
713
+ console.log(chalk.dim(` Resolved: ${payload.resolved || 'none'}`));
714
+ }
715
+ } catch (error) {
716
+ output({ error: error.message }, opts.json);
717
+ if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
718
+ process.exit(1);
719
+ }
720
+ });
721
+
722
+ runtime
723
+ .command('install-zellij')
724
+ .description('Install latest zellij release managed by AC Framework')
725
+ .option('--json', 'Output as JSON')
726
+ .action(async (opts) => {
727
+ try {
728
+ const result = await installManagedZellijLatest();
729
+ if (!result.success) {
730
+ output(result, opts.json);
731
+ if (!opts.json) console.error(chalk.red(`Error: ${result.message}`));
732
+ process.exit(1);
733
+ }
734
+
735
+ await updateAgentsConfig((current) => ({
736
+ agents: {
737
+ defaultModel: current.agents.defaultModel,
738
+ defaultRoleModels: { ...current.agents.defaultRoleModels },
739
+ multiplexer: current.agents.multiplexer || 'auto',
740
+ zellij: {
741
+ strategy: result.source === 'system' ? 'system' : 'managed',
742
+ binaryPath: result.binaryPath,
743
+ version: result.version || null,
744
+ source: result.source || 'managed',
745
+ },
746
+ },
747
+ }));
748
+
749
+ output(result, opts.json);
750
+ if (!opts.json) {
751
+ console.log(chalk.green('✓ Managed zellij ready'));
752
+ console.log(chalk.dim(` Version: ${result.version || 'unknown'}`));
753
+ console.log(chalk.dim(` Binary: ${result.binaryPath}`));
754
+ }
755
+ } catch (error) {
756
+ output({ error: error.message }, opts.json);
757
+ if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
758
+ process.exit(1);
759
+ }
760
+ });
761
+
456
762
  model
457
763
  .command('list')
458
764
  .description('List available OpenCode models grouped by provider')
@@ -510,6 +816,7 @@ export function agentsCommand() {
510
816
  configPath: getAgentsConfigPath(),
511
817
  defaultModel: config.agents.defaultModel,
512
818
  defaultRoleModels: config.agents.defaultRoleModels,
819
+ multiplexer: config.agents.multiplexer,
513
820
  };
514
821
  output(payload, opts.json);
515
822
  if (!opts.json) {
@@ -519,6 +826,7 @@ export function agentsCommand() {
519
826
  for (const role of COLLAB_ROLES) {
520
827
  console.log(chalk.dim(`- ${role}: ${payload.defaultRoleModels[role] || '(none)'}`));
521
828
  }
829
+ console.log(chalk.dim(`Multiplexer: ${payload.multiplexer || 'auto'}`));
522
830
  }
523
831
  } catch (error) {
524
832
  output({ error: error.message }, opts.json);
@@ -594,6 +902,10 @@ export function agentsCommand() {
594
902
  agents: {
595
903
  defaultModel: current.agents.defaultModel,
596
904
  defaultRoleModels: { ...current.agents.defaultRoleModels },
905
+ multiplexer: current.agents.multiplexer || 'auto',
906
+ zellij: {
907
+ ...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
908
+ },
597
909
  },
598
910
  };
599
911
 
@@ -654,6 +966,10 @@ export function agentsCommand() {
654
966
  agents: {
655
967
  defaultModel: current.agents.defaultModel,
656
968
  defaultRoleModels: { ...current.agents.defaultRoleModels },
969
+ multiplexer: current.agents.multiplexer || 'auto',
970
+ zellij: {
971
+ ...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
972
+ },
657
973
  },
658
974
  };
659
975
  if (role === 'all') {
@@ -702,6 +1018,10 @@ export function agentsCommand() {
702
1018
  agents: {
703
1019
  defaultModel: current.agents.defaultModel,
704
1020
  defaultRoleModels: { ...current.agents.defaultRoleModels },
1021
+ multiplexer: current.agents.multiplexer || 'auto',
1022
+ zellij: {
1023
+ ...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
1024
+ },
705
1025
  },
706
1026
  };
707
1027
  if (role === 'all') {
@@ -810,6 +1130,66 @@ export function agentsCommand() {
810
1130
  }
811
1131
  });
812
1132
 
1133
+ agents
1134
+ .command('artifacts')
1135
+ .description('Show SynapseGrid artifact paths and existence status')
1136
+ .option('--session <id>', 'Session ID (defaults to current)')
1137
+ .option('--watch', 'Continuously watch artifact status', false)
1138
+ .option('--interval <ms>', 'Polling interval in milliseconds for --watch', '1500')
1139
+ .option('--json', 'Output as JSON')
1140
+ .action(async (opts) => {
1141
+ try {
1142
+ const sessionId = opts.session || await ensureSessionId(true);
1143
+ const intervalMs = Number.parseInt(opts.interval, 10);
1144
+ if (!Number.isInteger(intervalMs) || intervalMs <= 0) {
1145
+ throw new Error('--interval must be a positive integer');
1146
+ }
1147
+
1148
+ const printSnapshot = async () => {
1149
+ const snapshot = await collectArtifactStatus(sessionId);
1150
+ if (opts.json) {
1151
+ process.stdout.write(JSON.stringify(snapshot) + '\n');
1152
+ return;
1153
+ }
1154
+
1155
+ console.log(chalk.bold('SynapseGrid artifacts'));
1156
+ console.log(chalk.dim(`Session: ${snapshot.sessionId}`));
1157
+ console.log(chalk.dim(`Checked: ${snapshot.checkedAt}`));
1158
+ for (const [key, meta] of Object.entries(snapshot.artifacts)) {
1159
+ console.log(chalk.dim(`${key}: ${meta.exists ? 'ok' : 'missing'} -> ${meta.path}`));
1160
+ }
1161
+ };
1162
+
1163
+ if (!opts.watch) {
1164
+ const snapshot = await collectArtifactStatus(sessionId);
1165
+ output(snapshot, opts.json);
1166
+ if (!opts.json) {
1167
+ console.log(chalk.bold('SynapseGrid artifacts'));
1168
+ for (const [key, meta] of Object.entries(snapshot.artifacts)) {
1169
+ console.log(chalk.dim(`${key}: ${meta.exists ? 'ok' : 'missing'} -> ${meta.path}`));
1170
+ }
1171
+ }
1172
+ return;
1173
+ }
1174
+
1175
+ if (!opts.json) {
1176
+ console.log(chalk.cyan('Watching artifacts (Ctrl+C to stop)\n'));
1177
+ }
1178
+
1179
+ while (true) {
1180
+ if (!opts.json) {
1181
+ process.stdout.write('\x1Bc');
1182
+ }
1183
+ await printSnapshot();
1184
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
1185
+ }
1186
+ } catch (error) {
1187
+ output({ error: error.message }, opts.json);
1188
+ if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
1189
+ process.exit(1);
1190
+ }
1191
+ });
1192
+
813
1193
  agents
814
1194
  .command('export')
815
1195
  .description('Export collaborative transcript')
@@ -865,17 +1245,15 @@ export function agentsCommand() {
865
1245
  .option('--model-critic <id>', 'Model for critic role (provider/model)')
866
1246
  .option('--model-coder <id>', 'Model for coder role (provider/model)')
867
1247
  .option('--model-reviewer <id>', 'Model for reviewer role (provider/model)')
1248
+ .option('--mux <name>', 'Multiplexer backend: auto|zellij|tmux')
868
1249
  .option('--cwd <path>', 'Working directory for agents', process.cwd())
869
- .option('--attach', 'Attach tmux immediately after start', false)
1250
+ .option('--attach', 'Attach multiplexer immediately after start', false)
870
1251
  .option('--json', 'Output as JSON')
871
1252
  .action(async (opts) => {
872
1253
  try {
873
1254
  if (!hasCommand('opencode')) {
874
1255
  throw new Error('OpenCode is not installed. Run: acfm agents setup');
875
1256
  }
876
- if (!hasCommand('tmux')) {
877
- throw new Error('tmux is not installed. Run: acfm agents setup');
878
- }
879
1257
  const opencodeBin = resolveCommandPath('opencode');
880
1258
  if (!opencodeBin) {
881
1259
  throw new Error('OpenCode binary not found. Run: acfm agents setup');
@@ -888,6 +1266,35 @@ export function agentsCommand() {
888
1266
  }
889
1267
 
890
1268
  const config = await loadAgentsConfig();
1269
+ const configuredMux = validateMultiplexer(opts.mux || config.agents.multiplexer || 'auto');
1270
+ const muxResolution = resolveMultiplexerWithPaths(config, configuredMux);
1271
+ let selectedMux = muxResolution.selected;
1272
+ let zellijPath = muxResolution.zellijPath;
1273
+ if (!selectedMux) {
1274
+ if (configuredMux !== 'tmux' && shouldUseManagedZellij(config)) {
1275
+ const installResult = await installManagedZellijLatest();
1276
+ if (installResult.success && installResult.binaryPath) {
1277
+ await updateAgentsConfig((current) => ({
1278
+ agents: {
1279
+ defaultModel: current.agents.defaultModel,
1280
+ defaultRoleModels: { ...current.agents.defaultRoleModels },
1281
+ multiplexer: current.agents.multiplexer || 'auto',
1282
+ zellij: {
1283
+ strategy: 'managed',
1284
+ binaryPath: installResult.binaryPath,
1285
+ version: installResult.version || null,
1286
+ source: 'managed',
1287
+ },
1288
+ },
1289
+ }));
1290
+ zellijPath = installResult.binaryPath;
1291
+ selectedMux = resolveMultiplexer(configuredMux, Boolean(resolveCommandPath('tmux')), Boolean(zellijPath));
1292
+ }
1293
+ }
1294
+ }
1295
+ if (!selectedMux) {
1296
+ throw new Error('No multiplexer found. Install zellij or tmux with: acfm agents setup');
1297
+ }
891
1298
  const cliModel = assertValidModelIdOrNull('--model', opts.model || null);
892
1299
  const cliRoleModels = parseRoleModelOptions(opts);
893
1300
  for (const [role, model] of Object.entries(cliRoleModels)) {
@@ -922,30 +1329,48 @@ export function agentsCommand() {
922
1329
  maxRounds,
923
1330
  model: globalModel,
924
1331
  roleModels,
1332
+ multiplexer: selectedMux,
925
1333
  workingDirectory: resolve(opts.cwd),
926
1334
  opencodeBin,
927
1335
  });
928
- const tmuxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
1336
+ const muxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
929
1337
  const sessionDir = getSessionDir(state.sessionId);
1338
+
1339
+ if (selectedMux === 'zellij') {
1340
+ await spawnZellijSession({
1341
+ sessionName: muxSessionName,
1342
+ sessionDir,
1343
+ sessionId: state.sessionId,
1344
+ binaryPath: zellijPath,
1345
+ });
1346
+ } else {
1347
+ await spawnTmuxSession({
1348
+ sessionName: muxSessionName,
1349
+ sessionDir,
1350
+ sessionId: state.sessionId,
1351
+ });
1352
+ }
1353
+
930
1354
  const updated = await saveSessionState({
931
1355
  ...state,
932
- tmuxSessionName,
933
- });
934
-
935
- await spawnTmuxSession({
936
- sessionName: tmuxSessionName,
937
- sessionDir,
938
- sessionId: state.sessionId,
1356
+ multiplexer: selectedMux,
1357
+ multiplexerSessionName: muxSessionName,
1358
+ tmuxSessionName: selectedMux === 'tmux' ? muxSessionName : null,
939
1359
  });
940
1360
 
941
- output({ sessionId: updated.sessionId, tmuxSessionName, status: updated.status }, opts.json);
1361
+ output({
1362
+ sessionId: updated.sessionId,
1363
+ multiplexer: selectedMux,
1364
+ multiplexerSessionName: muxSessionName,
1365
+ status: updated.status,
1366
+ }, opts.json);
942
1367
  if (!opts.json) {
943
1368
  printStartSummary(updated);
944
1369
  printModelConfig(updated);
945
1370
  }
946
1371
 
947
1372
  if (opts.attach) {
948
- await runTmux('tmux', ['attach', '-t', tmuxSessionName], { stdio: 'inherit' });
1373
+ await attachToMux(selectedMux, muxSessionName, false, zellijPath);
949
1374
  }
950
1375
  } catch (error) {
951
1376
  output({ error: error.message }, opts.json);
@@ -981,6 +1406,7 @@ export function agentsCommand() {
981
1406
  try {
982
1407
  const sessionId = await ensureSessionId(true);
983
1408
  const state = await loadSessionState(sessionId);
1409
+ await ensureSessionArtifacts(sessionId, state);
984
1410
  const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
985
1411
  output({ ...state, effectiveRoleModels }, opts.json);
986
1412
  if (!opts.json) {
@@ -996,14 +1422,20 @@ export function agentsCommand() {
996
1422
  console.log(chalk.dim(`Run error: ${summary.lastError.message}`));
997
1423
  }
998
1424
  console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
1425
+ console.log(chalk.dim(`Multiplexer: ${state.multiplexer || 'auto'} (${sessionMuxName(state)})`));
999
1426
  for (const role of COLLAB_ROLES) {
1000
1427
  const configured = state.roleModels?.[role] || '-';
1001
1428
  const effective = effectiveRoleModels[role] || '(opencode default)';
1002
1429
  console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
1003
1430
  }
1004
- if (state.tmuxSessionName) {
1005
- console.log(chalk.dim(`tmux: ${state.tmuxSessionName}`));
1006
- }
1431
+ const meetingLogPath = resolve(getSessionDir(state.sessionId), 'meeting-log.md');
1432
+ const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
1433
+ const turnsDirPath = resolve(getSessionDir(state.sessionId), 'turns');
1434
+ const rawDirPath = resolve(getSessionDir(state.sessionId), 'turns', 'raw');
1435
+ console.log(chalk.dim(`meeting-log: ${existsSync(meetingLogPath) ? meetingLogPath : 'not generated yet'}`));
1436
+ console.log(chalk.dim(`meeting-summary: ${existsSync(meetingSummaryPath) ? meetingSummaryPath : 'not generated yet'}`));
1437
+ console.log(chalk.dim(`turns: ${existsSync(turnsDirPath) ? turnsDirPath : 'not generated yet'}`));
1438
+ console.log(chalk.dim(`turns/raw: ${existsSync(rawDirPath) ? rawDirPath : 'not generated yet'}`));
1007
1439
  }
1008
1440
  } catch (error) {
1009
1441
  output({ error: error.message }, opts.json);
@@ -1020,6 +1452,7 @@ export function agentsCommand() {
1020
1452
  try {
1021
1453
  const sessionId = await ensureSessionId(true);
1022
1454
  let state = await loadSessionState(sessionId);
1455
+ const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
1023
1456
  state = await stopSession(state, 'stopped');
1024
1457
  if (state.run && state.run.status === 'running') {
1025
1458
  state = await saveSessionState({
@@ -1036,15 +1469,53 @@ export function agentsCommand() {
1036
1469
  },
1037
1470
  });
1038
1471
  }
1039
- if (state.tmuxSessionName && hasCommand('tmux')) {
1472
+
1473
+ if (!existsSync(meetingSummaryPath)) {
1474
+ const fallbackSummary = [
1475
+ '# SynapseGrid Meeting Summary',
1476
+ '',
1477
+ `Session: ${state.sessionId}`,
1478
+ `Status: ${state.status}`,
1479
+ '',
1480
+ 'This summary was auto-generated at stop time because the run did not complete normally.',
1481
+ '',
1482
+ '## Last message',
1483
+ state.messages?.[state.messages.length - 1]?.content || '(none)',
1484
+ '',
1485
+ ].join('\n');
1486
+ await writeMeetingSummary(state.sessionId, fallbackSummary);
1487
+ if (state.run && !state.run.finalSummary) {
1488
+ state = await saveSessionState({
1489
+ ...state,
1490
+ run: {
1491
+ ...state.run,
1492
+ finalSummary: fallbackSummary,
1493
+ },
1494
+ });
1495
+ }
1496
+ }
1497
+
1498
+ const multiplexer = state.multiplexer || 'tmux';
1499
+ const muxSessionName = sessionMuxName(state);
1500
+ const cfg = await loadAgentsConfig();
1501
+ const zellijPath = resolveConfiguredZellijPath(cfg);
1502
+ if (multiplexer === 'zellij' && muxSessionName && zellijPath) {
1040
1503
  try {
1041
- await runTmux('tmux', ['kill-session', '-t', state.tmuxSessionName]);
1504
+ await runZellij(['delete-session', muxSessionName], { binaryPath: zellijPath });
1505
+ } catch {
1506
+ // ignore if already closed
1507
+ }
1508
+ }
1509
+ if (multiplexer === 'tmux' && muxSessionName && hasCommand('tmux')) {
1510
+ try {
1511
+ await runTmux('tmux', ['kill-session', '-t', muxSessionName]);
1042
1512
  } catch {
1043
1513
  // ignore if already closed
1044
1514
  }
1045
1515
  }
1046
1516
  output({ sessionId: state.sessionId, status: state.status }, opts.json);
1047
1517
  if (!opts.json) console.log(chalk.green('✓ Collaborative session stopped'));
1518
+
1048
1519
  } catch (error) {
1049
1520
  output({ error: error.message }, opts.json);
1050
1521
  if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
@@ -1143,10 +1614,19 @@ export function agentsCommand() {
1143
1614
  const opencodeBin = resolveCommandPath('opencode');
1144
1615
  const tmuxInstalled = hasCommand('tmux');
1145
1616
  const cfg = await loadAgentsConfig();
1617
+ const zellijPath = resolveConfiguredZellijPath(cfg);
1618
+ const zellijInstalled = Boolean(zellijPath);
1146
1619
  const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
1620
+ const configuredMux = validateMultiplexer(cfg.agents.multiplexer || 'auto');
1621
+ const resolvedMux = resolveMultiplexer(configuredMux, tmuxInstalled, zellijInstalled);
1147
1622
  const result = {
1148
1623
  opencodeBin,
1149
1624
  tmuxInstalled,
1625
+ zellijInstalled,
1626
+ zellijPath,
1627
+ zellijConfig: cfg.agents.zellij,
1628
+ configuredMultiplexer: configuredMux,
1629
+ resolvedMultiplexer: resolvedMux,
1150
1630
  defaultModel,
1151
1631
  defaultRoleModels: cfg.agents.defaultRoleModels,
1152
1632
  preflight: null,
@@ -1166,12 +1646,15 @@ export function agentsCommand() {
1166
1646
  if (!opts.json) {
1167
1647
  console.log(chalk.bold('SynapseGrid doctor'));
1168
1648
  console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
1649
+ console.log(chalk.dim(`zellij: ${zellijInstalled ? 'installed' : 'not installed'}`));
1650
+ if (zellijPath) console.log(chalk.dim(`zellij path: ${zellijPath}`));
1169
1651
  console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
1652
+ console.log(chalk.dim(`multiplexer: configured=${configuredMux} resolved=${resolvedMux || 'none'}`));
1170
1653
  console.log(chalk.dim(`default model: ${defaultModel}`));
1171
1654
  console.log(chalk.dim(`preflight: ${result.preflight?.ok ? 'ok' : `failed - ${result.preflight?.error || 'unknown error'}`}`));
1172
1655
  }
1173
1656
 
1174
- if (!result.preflight?.ok) process.exit(1);
1657
+ if (!result.preflight?.ok || !result.resolvedMultiplexer) process.exit(1);
1175
1658
  } catch (error) {
1176
1659
  output({ error: error.message }, opts.json);
1177
1660
  if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));