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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-framework",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Agentic Coding Framework - Multi-assistant configuration system with OpenSpec workflows",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -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
- return `bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`;
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
- await runner(command, ['--layout', layoutPath, 'attach', '--create-background', sessionName], {
151
- cwd: sessionDir,
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) => line === sessionName || line.startsWith(`${sessionName} `));
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
  }
@@ -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
- if (selectedMux === 'zellij') {
1340
- await spawnZellijSession({
1341
- sessionName: muxSessionName,
1342
- sessionDir,
1343
- sessionId: state.sessionId,
1344
- binaryPath: zellijPath,
1345
- });
1346
- } else {
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: selectedMux,
1387
+ multiplexer: activeMux,
1357
1388
  multiplexerSessionName: muxSessionName,
1358
- tmuxSessionName: selectedMux === 'tmux' ? muxSessionName : null,
1389
+ tmuxSessionName: activeMux === 'tmux' ? muxSessionName : null,
1359
1390
  });
1360
1391
 
1361
1392
  output({
1362
1393
  sessionId: updated.sessionId,
1363
- multiplexer: selectedMux,
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(selectedMux, muxSessionName, false, zellijPath);
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,
@@ -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({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
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({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
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 });