@ijfw/memory-server 1.3.0 → 1.4.1

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.
Files changed (68) hide show
  1. package/README.md +67 -0
  2. package/fixtures/team/book.json +47 -0
  3. package/fixtures/team/business.json +47 -0
  4. package/fixtures/team/content.json +47 -0
  5. package/fixtures/team/design.json +47 -0
  6. package/fixtures/team/mixed.json +59 -0
  7. package/fixtures/team/research.json +47 -0
  8. package/fixtures/team/software.json +47 -0
  9. package/package.json +1 -9
  10. package/src/.registry-meta-key.pem +3 -0
  11. package/src/active-extension-writer.js +142 -0
  12. package/src/blackboard.js +360 -0
  13. package/src/cli-run.js +91 -0
  14. package/src/codex-agents.js +177 -0
  15. package/src/compute/extract.js +3 -0
  16. package/src/compute/fts5.js +4 -4
  17. package/src/compute/graph-lock.js +0 -2
  18. package/src/compute/migrations/003-tier-semantic.js +3 -3
  19. package/src/compute/runner.js +44 -15
  20. package/src/compute/schema.sql +1 -1
  21. package/src/cross-orchestrator-cli.js +974 -13
  22. package/src/cross-orchestrator.js +9 -1
  23. package/src/dashboard-client.html +353 -1
  24. package/src/dashboard-server.js +318 -2
  25. package/src/design-intelligence.js +721 -0
  26. package/src/dispatch/colon-syntax.js +31 -3
  27. package/src/dispatch/domain-manifest.js +251 -0
  28. package/src/dispatch/extension.js +637 -0
  29. package/src/dispatch/override.js +221 -0
  30. package/src/dispatch-planner.js +1 -0
  31. package/src/dream/runner.mjs +3 -3
  32. package/src/extension-installer.js +1269 -0
  33. package/src/extension-manifest-schema.js +301 -0
  34. package/src/extension-permission-check.mjs +79 -0
  35. package/src/extension-registry.js +619 -0
  36. package/src/extension-signer.js +905 -0
  37. package/src/gate-result-formatter.js +95 -0
  38. package/src/gate-result-schema.js +274 -0
  39. package/src/gate-result.js +195 -0
  40. package/src/intent-router.js +2 -0
  41. package/src/lib/npm-view.js +1 -0
  42. package/src/memory/fts5.js +3 -3
  43. package/src/memory/migrations/002-tier-semantic.js +2 -2
  44. package/src/memory/staleness.js +1 -1
  45. package/src/memory/tier-promotion.js +6 -6
  46. package/src/memory/tokenize.js +1 -1
  47. package/src/memory-feedback.js +372 -0
  48. package/src/override-manifest-schema.js +146 -0
  49. package/src/override-resolver.js +699 -0
  50. package/src/override-use-registry.js +307 -0
  51. package/src/overrides/presets/academic.md +101 -0
  52. package/src/overrides/presets/book.md +87 -0
  53. package/src/overrides/presets/campaign.md +95 -0
  54. package/src/overrides/presets/screenplay.md +99 -0
  55. package/src/recovery/checkpoint.js +191 -0
  56. package/src/redactor.js +2 -0
  57. package/src/runtime-mediator.js +207 -0
  58. package/src/sandbox.js +17 -3
  59. package/src/server.js +94 -2
  60. package/src/swarm/dispatch-prompt.js +154 -0
  61. package/src/swarm/planner.js +399 -0
  62. package/src/swarm/review.js +136 -0
  63. package/src/swarm/worktree.js +239 -0
  64. package/src/team/generator.js +119 -0
  65. package/src/team/schemas.js +341 -0
  66. package/src/trident/dispatch.js +47 -0
  67. package/src/update-check.js +1 -1
  68. package/src/vectors.js +7 -8
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Zero external deps. Parse argv manually.
9
9
 
10
- import { readFileSync, existsSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync, readdirSync, rmSync, realpathSync } from 'node:fs';
10
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync, readdirSync, rmSync, realpathSync, copyFileSync } from 'node:fs';
11
11
  import { fileURLToPath, pathToFileURL } from 'node:url';
12
12
  import { join, dirname, basename, isAbsolute, resolve } from 'node:path';
13
13
  import { homedir } from 'node:os';
@@ -19,6 +19,44 @@ import { renderHeroLine } from './hero-line.js';
19
19
  import { ROSTER, isInstalled, isReachable } from './audit-roster.js';
20
20
  import { aggregatePortfolioFindings } from './cross-project-search.js';
21
21
  import { runImport, runImportAll, listImporters } from './importers/cli.js';
22
+ import { validateToken } from './lib/token.js';
23
+ import { isVersionStringValid } from './lib/npm-view.js';
24
+ import {
25
+ addBlackboardNote,
26
+ blackboardStatus,
27
+ claimArtifact,
28
+ initBlackboard,
29
+ readBlackboard,
30
+ releaseClaim,
31
+ writeHandoff,
32
+ } from './blackboard.js';
33
+ import { createTeamAssembly, readTeamAssembly } from './team/generator.js';
34
+ import {
35
+ blockSwarmTask,
36
+ buildSwarmPlan,
37
+ completeSwarmTask,
38
+ listSwarmTasks,
39
+ prepareSwarmTasks,
40
+ readySwarmTask,
41
+ startSwarmTask,
42
+ swarmPlanSummary,
43
+ } from './swarm/planner.js';
44
+ import { createCheckpoint, latestCheckpoint, recoveryStatus } from './recovery/checkpoint.js';
45
+ import {
46
+ cleanupTaskWorktree,
47
+ createTaskWorktree,
48
+ integrateTaskWorktree,
49
+ listTaskWorktrees,
50
+ } from './swarm/worktree.js';
51
+ import { renderSwarmDispatchPrompt } from './swarm/dispatch-prompt.js';
52
+ import { syncCodexAgents } from './codex-agents.js';
53
+ import {
54
+ DESIGN_ACTIONS,
55
+ auditDesignText,
56
+ designActionGuide,
57
+ initialDesignMarkdown,
58
+ loadDesignContext,
59
+ } from './design-intelligence.js';
22
60
 
23
61
  // ---------------------------------------------------------------------------
24
62
  // Auditor error translator (1.2.5)
@@ -169,11 +207,111 @@ function parseArgs(argv) {
169
207
  return out;
170
208
  }
171
209
 
210
+ const COMMAND_ALIAS_HELP = {
211
+ workflow: {
212
+ title: 'IJFW workflow',
213
+ usage: 'Use the ijfw-workflow skill in agents. Terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare.',
214
+ },
215
+ handoff: {
216
+ title: 'IJFW handoff',
217
+ usage: 'Use the ijfw-handoff skill in agents, or record swarm handoff text with: ijfw blackboard handoff --message "<summary>".',
218
+ },
219
+ compress: {
220
+ title: 'IJFW compress',
221
+ usage: 'Use the ijfw-compress skill in agents. Terminal context compression is host-specific and should preserve exact paths, commands, versions, and decisions.',
222
+ },
223
+ consolidate: {
224
+ title: 'IJFW consolidate',
225
+ usage: 'Use the ijfw-handoff or ijfw-memory-audit skill to consolidate decisions into memory. For swarm state, run: ijfw memory checkpoint <label>.',
226
+ },
227
+ 'ijfw-audit': {
228
+ title: 'IJFW audit',
229
+ usage: 'Run verification with: ijfw preflight. For multi-model review, run: ijfw cross audit <target>.',
230
+ },
231
+ 'ijfw-execute': {
232
+ title: 'IJFW execute',
233
+ usage: 'Use ijfw-workflow in agents, then terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare, ijfw swarm start <task-id>.',
234
+ },
235
+ 'ijfw-help': {
236
+ title: 'IJFW help',
237
+ usage: 'Run: ijfw help. Add --browser for the rendered local guide.',
238
+ },
239
+ 'ijfw-plan': {
240
+ title: 'IJFW plan',
241
+ usage: 'Use ijfw-workflow for planning. Terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare --reviews.',
242
+ },
243
+ 'ijfw-ship': {
244
+ title: 'IJFW ship',
245
+ usage: 'Run: ijfw preflight. Do not publish or tag until your release gate is explicitly cleared.',
246
+ },
247
+ 'ijfw-verify': {
248
+ title: 'IJFW verify',
249
+ usage: 'Run: ijfw preflight. For focused review, run: ijfw cross audit <target>.',
250
+ },
251
+ 'memory-audit': {
252
+ title: 'IJFW memory audit',
253
+ usage: 'Use the ijfw-memory-audit skill in agents. Terminal safety net: ijfw recover status and ijfw memory checkpoint <label>.',
254
+ },
255
+ 'memory-consent': {
256
+ title: 'IJFW memory consent',
257
+ usage: 'Use IJFW memory tools only for explicit project memory. Terminal checkpoint: ijfw memory checkpoint <label>.',
258
+ },
259
+ 'memory-why': {
260
+ title: 'IJFW memory why',
261
+ usage: 'Use ijfw-recall or ijfw-memory-audit in agents to inspect why memory exists. Terminal recovery state: ijfw recover latest.',
262
+ },
263
+ metrics: {
264
+ title: 'IJFW metrics',
265
+ usage: 'Open the dashboard with: ijfw dashboard start. Agent-side metrics are available through ijfw_metrics.',
266
+ },
267
+ mode: {
268
+ title: 'IJFW mode',
269
+ usage: 'Inspect configuration with: ijfw config --audit. Statusline mode helpers: ijfw statusline --status, --compose, or --disable.',
270
+ },
271
+ };
272
+
273
+ function parseCrossAlias(mode, args) {
274
+ let only = null;
275
+ let confirm = false;
276
+ let expand = false;
277
+ const positional = [];
278
+ for (let i = 1; i < args.length; i++) {
279
+ const arg = args[i];
280
+ if (arg === '--confirm') confirm = true;
281
+ else if (arg === '--expand') expand = true;
282
+ else if (arg === '--with' && args[i + 1]) only = args[++i];
283
+ else if (arg.startsWith('--with=')) only = arg.slice('--with='.length);
284
+ else if (!arg.startsWith('--')) positional.push(arg);
285
+ }
286
+ const target = mode === 'research' ? positional.join(' ').trim() : positional[0];
287
+ return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand };
288
+ }
289
+
290
+ function parseCommandAlias(args) {
291
+ const name = args[0];
292
+ if (name === 'cross-audit') {
293
+ return parseCrossAlias('audit', args);
294
+ }
295
+ if (name === 'cross-critique') {
296
+ return parseCrossAlias('critique', args);
297
+ }
298
+ if (name === 'cross-research') {
299
+ return parseCrossAlias('research', args);
300
+ }
301
+ if (Object.prototype.hasOwnProperty.call(COMMAND_ALIAS_HELP, name)) {
302
+ return { cmd: 'command-alias', alias: name };
303
+ }
304
+ return null;
305
+ }
306
+
172
307
  function parseArgsInner(args) {
173
308
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
174
309
  return { cmd: 'help' };
175
310
  }
176
311
 
312
+ const alias = parseCommandAlias(args);
313
+ if (alias) return alias;
314
+
177
315
  if (args[0] === '--version' || args[0] === '-v') {
178
316
  return { cmd: 'version', verbose: args.includes('--verbose') };
179
317
  }
@@ -254,6 +392,40 @@ function parseArgsInner(args) {
254
392
  return { cmd: 'dashboard', sub: args[1] || 'status' };
255
393
  }
256
394
 
395
+ if (args[0] === 'design') {
396
+ return { cmd: 'design', sub: args[1] || 'status' };
397
+ }
398
+
399
+ if (args[0] === 'blackboard') {
400
+ return { cmd: 'blackboard', sub: args[1] || 'status' };
401
+ }
402
+
403
+ if (args[0] === 'team') {
404
+ return { cmd: 'team', sub: args[1] || 'status' };
405
+ }
406
+ if (args[0] === 'override') {
407
+ return { cmd: 'override', sub: args[1] || 'list', rest: args.slice(2) };
408
+ }
409
+ if (args[0] === 'extension') {
410
+ return { cmd: 'extension', sub: args[1] || 'list', rest: args.slice(2) };
411
+ }
412
+
413
+ if (args[0] === 'swarm') {
414
+ return { cmd: 'swarm', sub: args[1] || 'status' };
415
+ }
416
+
417
+ if (args[0] === 'codex') {
418
+ return { cmd: 'codex', sub: args[1] || 'doctor' };
419
+ }
420
+
421
+ if (args[0] === 'memory' && args[1] === 'checkpoint') {
422
+ return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
423
+ }
424
+
425
+ if (args[0] === 'recover') {
426
+ return { cmd: 'recover', sub: args[1] || 'status' };
427
+ }
428
+
257
429
  if (args[0] === 'receipt') {
258
430
  return { cmd: 'receipt', sub: args[1] || 'last' };
259
431
  }
@@ -319,6 +491,14 @@ Usage:
319
491
  ijfw uninstall
320
492
  ijfw preflight
321
493
  ijfw dashboard [start|stop|status]
494
+ ijfw design [start|open|status|stop|push|clear|init|plan|audit|critique|polish|normalize|bolder|quieter|handoff]
495
+ ijfw blackboard [init|status|claim|release|note|handoff]
496
+ ijfw team [init|status]
497
+ ijfw swarm [plan|prepare|tasks|prompt|start|complete|block|ready|status]
498
+ ijfw swarm worktree [create|list|integrate|cleanup]
499
+ ijfw codex [doctor|sync-agents]
500
+ ijfw memory checkpoint <label>
501
+ ijfw recover [status|latest]
322
502
  ijfw cross <mode> <target> [options]
323
503
  ijfw cross project-audit <rule-file> [--dry-run]
324
504
  ijfw import <tool> [--all] [--dry-run] [--force] [--path <p>]
@@ -332,8 +512,13 @@ Usage:
332
512
  Commands:
333
513
  install Install IJFW into your AI coding agents.
334
514
  uninstall Remove IJFW and revert AI-agent configs. Same as: ijfw off
335
- preflight Run the 12-gate quality pipeline (blocking + advisory).
515
+ preflight Run the 11-gate quality pipeline (blocking + advisory).
336
516
  dashboard Control the dashboard server (start, stop, status).
517
+ design Control the live visual design companion.
518
+ blackboard Coordinate project-local swarm state and artifact claims.
519
+ team Assemble project agents, charter, and workflow manifest.
520
+ swarm Plan artifact-aware parallel work from the team manifest.
521
+ recover Show the latest checkpoint and next recovery step.
337
522
  demo 30-second live tour of the Trident (fires real auditors).
338
523
  cross Fire external auditors at a target. Try: ijfw cross audit README.md
339
524
  import Pull memory in from another tool. Try: ijfw import claude-mem --all
@@ -376,6 +561,18 @@ Examples:
376
561
  `.trim());
377
562
  }
378
563
 
564
+ function cmdCommandAlias(alias) {
565
+ const info = COMMAND_ALIAS_HELP[alias];
566
+ if (!info) {
567
+ console.error(`Unknown command alias: ${alias}`);
568
+ process.exit(1);
569
+ }
570
+ console.log(`${info.title}`);
571
+ console.log('');
572
+ console.log(info.usage);
573
+ process.exit(0);
574
+ }
575
+
379
576
  async function cmdStatus(projectDir, opts = {}) {
380
577
  const receipts = readReceipts(projectDir);
381
578
  const last = receipts[receipts.length - 1];
@@ -552,8 +749,10 @@ const INTEGRATION_DEPTH = {
552
749
  label: 'Codex',
553
750
  checks: [
554
751
  { name: 'native skills', detect: () => existsSync(join(homedir(), '.codex', 'skills')) },
752
+ { name: 'command aliases', detect: () => existsSync(join(homedir(), '.codex', 'commands', 'ijfw.md')) && existsSync(join(homedir(), '.codex', 'commands', 'cross-audit.md')) },
555
753
  { name: 'hooks', detect: () => existsSync(join(homedir(), '.codex', 'hooks.json')) },
556
754
  { name: 'context file', detect: () => existsSync(join(homedir(), '.codex', 'IJFW.md')) },
755
+ { name: 'project agents', detect: () => existsSync(join(process.cwd(), '.codex', 'agents')) },
557
756
  { name: 'MCP', detect: () => { try { const t = readFileSync(join(homedir(), '.codex', 'config.toml'), 'utf8'); return t.includes('ijfw-memory'); } catch { return false; } } },
558
757
  ],
559
758
  },
@@ -1091,6 +1290,7 @@ function npmViewVersion(pkg = '@ijfw/install') {
1091
1290
  return { ok: false, message: stderr || `npm view exited ${r.status} with no stderr` };
1092
1291
  }
1093
1292
  const raw = (r.stdout || '').trim().replace(/^"|"$/g, '');
1293
+ // eslint-disable-next-line security/detect-unsafe-regex -- raw is npm's short version string response and is truncated in the error path.
1094
1294
  if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(raw)) return { ok: false, message: `malformed: ${raw.slice(0, 80)}` };
1095
1295
  return { ok: true, version: raw };
1096
1296
  }
@@ -1180,7 +1380,7 @@ function cmdUpdateChangelog() {
1180
1380
  console.error(`could not fetch latest version: ${r.message}`);
1181
1381
  process.exit(1);
1182
1382
  }
1183
- const url = `https://api.github.com/repos/therealseandonahoe/ijfw/releases/tags/v${r.version}`;
1383
+ const url = `https://gitlab.com/api/v4/projects/therealseandonahoe%2Fijfw/releases/v${r.version}`;
1184
1384
  const fetchRes = spawnSync('curl', ['-fsSL', '-H', 'User-Agent: ijfw', url], { encoding: 'utf8', timeout: 10_000 });
1185
1385
  if (fetchRes.status !== 0) {
1186
1386
  console.log(`No release notes available for v${r.version}.`);
@@ -1190,7 +1390,7 @@ function cmdUpdateChangelog() {
1190
1390
  let body = '';
1191
1391
  try {
1192
1392
  const data = JSON.parse(fetchRes.stdout || '{}');
1193
- body = data.body || '(no body)';
1393
+ body = data.description || '(no body)';
1194
1394
  } catch { body = '(could not parse release JSON)'; }
1195
1395
  // ANSI strip + cap 4KB. Control-char regex is intentional -- defangs
1196
1396
  // CHANGELOG bytes fetched over HTTPS so paste into the terminal can't
@@ -1235,21 +1435,49 @@ function cmdUpdateConfirm(token) {
1235
1435
  process.exit(1);
1236
1436
  }
1237
1437
  let sentinelPath = null;
1438
+ let pending = null;
1439
+ let sessionId = null;
1440
+ let sawPending = false;
1238
1441
  try {
1239
1442
  for (const dir of readdirSync(runRoot)) {
1240
1443
  const candidate = join(runRoot, dir, 'update-pending.json');
1241
- if (existsSync(candidate)) { sentinelPath = candidate; break; }
1444
+ if (!existsSync(candidate)) continue;
1445
+ sawPending = true;
1446
+ const candidatePending = readJsonSafe(candidate);
1447
+ if (candidatePending && candidatePending.token === token) {
1448
+ sentinelPath = candidate;
1449
+ pending = candidatePending;
1450
+ sessionId = dir;
1451
+ break;
1452
+ }
1242
1453
  }
1243
1454
  } catch { /* */ }
1244
1455
  if (!sentinelPath) {
1245
- console.error('No pending update sentinel found. The MCP `ijfw_update_apply` tool issues sentinels.');
1456
+ console.error(
1457
+ sawPending
1458
+ ? 'Token mismatch -- run ijfw_update_check + ijfw_update_apply via your AI to issue a fresh token.'
1459
+ : 'No pending update sentinel found. The MCP `ijfw_update_apply` tool issues sentinels.'
1460
+ );
1246
1461
  process.exit(1);
1247
1462
  }
1248
- const pending = readJsonSafe(sentinelPath);
1249
- if (!pending || pending.token !== token) {
1250
- console.error('Token mismatch -- run ijfw_update_check + ijfw_update_apply via your AI to issue a fresh token.');
1463
+
1464
+ if (!pending || !isVersionStringValid(pending.target_version)) {
1465
+ try { rmSync(sentinelPath, { force: true }); } catch { /* */ }
1466
+ console.error('Pending update sentinel is malformed -- run ijfw_update_check + ijfw_update_apply again.');
1251
1467
  process.exit(1);
1252
1468
  }
1469
+ const tokenCheck = validateToken(sessionId, token);
1470
+ if (!tokenCheck.ok || tokenCheck.target_version !== pending.target_version) {
1471
+ try { rmSync(sentinelPath, { force: true }); } catch { /* */ }
1472
+ const why =
1473
+ tokenCheck.error === 'expired' ? 'Token expired' :
1474
+ tokenCheck.error === 'already-consumed' ? 'Token already consumed' :
1475
+ tokenCheck.error === 'mismatch' ? 'Token mismatch' :
1476
+ 'Token validation failed';
1477
+ console.error(`${why} -- run ijfw_update_check + ijfw_update_apply to issue a fresh token.`);
1478
+ process.exit(1);
1479
+ }
1480
+
1253
1481
  if (process.env.IJFW_FROM_MCP === '1') {
1254
1482
  console.error('Refusing: --confirm must be invoked from a terminal, not an MCP-spawned subprocess.');
1255
1483
  process.exit(1);
@@ -1288,6 +1516,13 @@ function cmdUpdateInteractive(opts = {}) {
1288
1516
  console.error(`Update check failed: ${r.message}`);
1289
1517
  return 1;
1290
1518
  }
1519
+ if (opts._confirmedFromToken && r.version !== opts._confirmedFromToken) {
1520
+ console.error(
1521
+ `Update target changed from v${opts._confirmedFromToken} to v${r.version}. ` +
1522
+ 'Re-run ijfw_update_check so the terminal confirmation matches the package being installed.'
1523
+ );
1524
+ return 1;
1525
+ }
1291
1526
  const cmp = cmpSemver(current, r.version);
1292
1527
  if (cmp >= 0) {
1293
1528
  console.log(`IJFW is up to date (v${current}). Nothing to do.`);
@@ -1409,7 +1644,7 @@ function cmdUpdateInteractive(opts = {}) {
1409
1644
  return 0;
1410
1645
  }
1411
1646
 
1412
- // `ijfw --version` (pure) and `ijfw --version --verbose` per v3 section MEDIUM
1647
+ // `ijfw --version` (pure) and `ijfw --version --verbose` per v3 section MEDIUM
1413
1648
  function cmdVersion(opts = {}) {
1414
1649
  const root = repoRootFromCli();
1415
1650
  const pkgPath = join(root, 'installer', 'package.json');
@@ -1794,6 +2029,8 @@ if (isMainModule) {
1794
2029
  cmdCross(parsed).catch(err => { console.error(err.message); process.exit(1); });
1795
2030
  } else if (parsed.cmd === 'cross-project-audit') {
1796
2031
  cmdCrossProjectAudit(parsed).catch(err => { console.error(err.message); process.exit(1); });
2032
+ } else if (parsed.cmd === 'command-alias') {
2033
+ cmdCommandAlias(parsed.alias);
1797
2034
  } else if (parsed.cmd === 'import') {
1798
2035
  cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
1799
2036
  } else if (parsed.cmd === 'doctor') {
@@ -1820,6 +2057,24 @@ if (isMainModule) {
1820
2057
  cmdPreflight();
1821
2058
  } else if (parsed.cmd === 'dashboard') {
1822
2059
  cmdDashboard(parsed.sub);
2060
+ } else if (parsed.cmd === 'design') {
2061
+ cmdDesign(parsed.sub);
2062
+ } else if (parsed.cmd === 'blackboard') {
2063
+ cmdBlackboard(parsed.sub);
2064
+ } else if (parsed.cmd === 'team') {
2065
+ cmdTeam(parsed.sub);
2066
+ } else if (parsed.cmd === 'override') {
2067
+ cmdOverride(parsed.sub, parsed.rest || []);
2068
+ } else if (parsed.cmd === 'extension') {
2069
+ cmdExtension(parsed.sub, parsed.rest || []);
2070
+ } else if (parsed.cmd === 'swarm') {
2071
+ cmdSwarm(parsed.sub);
2072
+ } else if (parsed.cmd === 'codex') {
2073
+ cmdCodex(parsed.sub);
2074
+ } else if (parsed.cmd === 'memory-checkpoint') {
2075
+ cmdMemoryCheckpoint(parsed.label);
2076
+ } else if (parsed.cmd === 'recover') {
2077
+ cmdRecover(parsed.sub);
1823
2078
  } else {
1824
2079
  console.error(`Unknown command: ${parsed.raw}`);
1825
2080
  printUsage();
@@ -1866,12 +2121,12 @@ function cmdUninstall() {
1866
2121
  process.exit(res.status ?? 1);
1867
2122
  }
1868
2123
  function cmdPreflight() {
1869
- const script = findCliAsset('scripts', 'check-all.sh');
2124
+ const script = findCliAsset('installer', 'src', 'preflight.js');
1870
2125
  if (!script) {
1871
- console.error('check-all.sh not found. Run `ijfw-install` to deploy ~/.ijfw/, or set IJFW_HOME to your IJFW tree.');
2126
+ console.error('preflight.js not found. Run `ijfw-install` to deploy ~/.ijfw/, or set IJFW_HOME to your IJFW tree.');
1872
2127
  process.exit(1);
1873
2128
  }
1874
- const res = spawnSync('bash', [script, ...process.argv.slice(3)], { stdio: 'inherit' });
2129
+ const res = spawnSync(process.execPath, [script, ...process.argv.slice(3)], { stdio: 'inherit' });
1875
2130
  process.exit(res.status ?? 1);
1876
2131
  }
1877
2132
  function cmdDashboard(sub) {
@@ -1883,3 +2138,709 @@ function cmdDashboard(sub) {
1883
2138
  const res = spawnSync(process.execPath, [script, sub], { stdio: 'inherit' });
1884
2139
  process.exit(res.status ?? 1);
1885
2140
  }
2141
+
2142
+ function dashboardPort() {
2143
+ try {
2144
+ const n = Number.parseInt(readFileSync(join(homedir(), '.ijfw', 'dashboard.port'), 'utf8').trim(), 10);
2145
+ return Number.isFinite(n) ? n : 37891;
2146
+ } catch {
2147
+ return 37891;
2148
+ }
2149
+ }
2150
+
2151
+ function openDesignUrl(url) {
2152
+ if (process.env.CI || process.env.NO_OPEN) return;
2153
+ const res = process.platform === 'darwin'
2154
+ ? spawnSync('open', [url], { stdio: 'ignore' })
2155
+ : process.platform === 'win32'
2156
+ ? spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' })
2157
+ : spawnSync('xdg-open', [url], { stdio: 'ignore' });
2158
+ return res.status ?? 0;
2159
+ }
2160
+
2161
+ function cmdDesign(sub) {
2162
+ const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
2163
+ mkdirSync(contentDir, { recursive: true });
2164
+
2165
+ if (sub === 'start' || sub === 'open') {
2166
+ const dash = findCliAsset('mcp-server', 'bin', 'ijfw-dashboard');
2167
+ if (!dash) {
2168
+ console.error('Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or set IJFW_HOME to your IJFW tree.');
2169
+ process.exit(1);
2170
+ }
2171
+ const noOpen = process.argv.includes('--no-open');
2172
+ const res = spawnSync(process.execPath, [dash, 'start', '--no-open'], { stdio: sub === 'start' ? 'inherit' : 'ignore' });
2173
+ if ((res.status ?? 1) !== 0) process.exit(res.status ?? 1);
2174
+ const url = `http://localhost:${dashboardPort()}/design`;
2175
+ if (!noOpen) openDesignUrl(url);
2176
+ console.log(`Design companion running at ${url}`);
2177
+ process.exit(0);
2178
+ }
2179
+
2180
+ if (sub === 'status' || sub === 'stop') {
2181
+ const dash = findCliAsset('mcp-server', 'bin', 'ijfw-dashboard');
2182
+ if (!dash) {
2183
+ console.error('Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or set IJFW_HOME to your IJFW tree.');
2184
+ process.exit(1);
2185
+ }
2186
+ const res = spawnSync(process.execPath, [dash, sub === 'status' ? 'status' : 'stop'], { stdio: 'inherit' });
2187
+ if (sub === 'status' && (res.status ?? 1) === 0) console.log(`Design companion URL: http://localhost:${dashboardPort()}/design`);
2188
+ process.exit(res.status ?? 0);
2189
+ }
2190
+
2191
+ if (sub === 'push') {
2192
+ const filePaths = process.argv.slice(4);
2193
+ if (filePaths.length === 0) {
2194
+ console.error('Usage: ijfw design push <file.html> [more.html ...]');
2195
+ process.exit(1);
2196
+ }
2197
+ for (const filePath of filePaths) {
2198
+ const abs = resolve(filePath);
2199
+ if (!abs.toLowerCase().endsWith('.html')) {
2200
+ console.error('Design companion accepts standalone .html files.');
2201
+ process.exit(1);
2202
+ }
2203
+ if (!existsSync(abs)) {
2204
+ console.error(`File not found: ${abs}`);
2205
+ process.exit(1);
2206
+ }
2207
+ const dest = join(contentDir, basename(abs));
2208
+ copyFileSync(abs, dest);
2209
+ console.log(`Design pushed: ${dest}`);
2210
+ }
2211
+ console.log(`Preview: http://localhost:${dashboardPort()}/design`);
2212
+ process.exit(0);
2213
+ }
2214
+
2215
+ if (sub === 'clear') {
2216
+ for (const f of readdirSync(contentDir)) rmSync(join(contentDir, f), { force: true });
2217
+ console.log('Design companion content cleared.');
2218
+ process.exit(0);
2219
+ }
2220
+
2221
+ if (sub === 'init') {
2222
+ const path = join(process.cwd(), 'DESIGN.md');
2223
+ if (existsSync(path) && !process.argv.includes('--force')) {
2224
+ console.log(`DESIGN.md already exists: ${path}`);
2225
+ console.log('Use --force to replace it.');
2226
+ process.exit(1);
2227
+ }
2228
+ const name = optionValue(process.argv.slice(4), ['--name']) || basename(process.cwd());
2229
+ const direction = optionValue(process.argv.slice(4), ['--direction']) || '';
2230
+ writeFileSync(path, initialDesignMarkdown({ projectName: name, direction }), { mode: 0o644 });
2231
+ console.log(`Created ${path}`);
2232
+ process.exit(0);
2233
+ }
2234
+
2235
+ if (DESIGN_ACTIONS.includes(sub)) {
2236
+ const context = loadDesignContext(process.cwd());
2237
+ const target = optionValue(process.argv.slice(4), ['--file', '-f']);
2238
+ const guide = designActionGuide(sub, context);
2239
+ console.log(`Design ${sub}`);
2240
+ console.log(`DESIGN.md: ${context.exists ? context.path : 'not found'}`);
2241
+ for (const line of guide.guidance) console.log(`- ${line}`);
2242
+ console.log(`- ${guide.reminder}`);
2243
+ if (context.exists) {
2244
+ console.log('');
2245
+ console.log(context.summary);
2246
+ }
2247
+ if (sub === 'audit' && target) {
2248
+ const abs = resolve(target);
2249
+ if (!existsSync(abs)) {
2250
+ console.error(`File not found: ${abs}`);
2251
+ process.exit(1);
2252
+ }
2253
+ const audit = auditDesignText(readFileSync(abs, 'utf8'));
2254
+ console.log('');
2255
+ console.log(`Static audit: ${audit.summary}`);
2256
+ for (const item of audit.findings) console.log(`- [${item.severity}] ${item.rule}: ${item.message}`);
2257
+ }
2258
+ process.exit(0);
2259
+ }
2260
+
2261
+ console.log('Usage: ijfw design start [--no-open] | open | status | stop | push <file.html> [more.html ...] | clear | init [--force] [--name <name>] [--direction <text>] | plan|audit|critique|polish|normalize|bolder|quieter|handoff [--file <artifact>]');
2262
+ process.exit(1);
2263
+ }
2264
+
2265
+ function optionValue(args, names) {
2266
+ for (let i = 0; i < args.length; i++) {
2267
+ if (names.includes(args[i]) && args[i + 1]) return args[i + 1];
2268
+ for (const name of names) {
2269
+ const prefix = `${name}=`;
2270
+ if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
2271
+ }
2272
+ }
2273
+ return null;
2274
+ }
2275
+
2276
+ function printBlackboardStatus(status) {
2277
+ console.log(`Blackboard: ${status.initialized ? status.dir : 'not initialized'}`);
2278
+ console.log(`Tasks: ${status.tasks.open} open / ${status.tasks.total} total`);
2279
+ console.log(`Claims: ${status.claims.active} active / ${status.claims.total} total`);
2280
+ for (const claim of status.claims.active_items) {
2281
+ const claimPaths = claim.paths.length ? ` (${claim.paths.join(', ')})` : '';
2282
+ console.log(` ${claim.artifact_id} -> ${claim.agent}${claimPaths}`);
2283
+ }
2284
+ const blockerCount = status.recent.blockers.length;
2285
+ if (blockerCount) console.log(`Recent blockers: ${blockerCount}`);
2286
+ if (status.health.tasks !== 'ok' || status.health.claims !== 'ok') {
2287
+ console.log(`Health: tasks=${status.health.tasks}, claims=${status.health.claims}`);
2288
+ }
2289
+ }
2290
+
2291
+ function cmdBlackboard(sub) {
2292
+ const args = process.argv.slice(4);
2293
+
2294
+ if (sub === 'init') {
2295
+ const result = initBlackboard(process.cwd());
2296
+ console.log(`Blackboard initialized: ${result.dir}`);
2297
+ process.exit(0);
2298
+ }
2299
+
2300
+ if (sub === 'status') {
2301
+ printBlackboardStatus(blackboardStatus(process.cwd()));
2302
+ process.exit(0);
2303
+ }
2304
+
2305
+ if (sub === 'claim') {
2306
+ const artifact = optionValue(args, ['--artifact', '-a']) || args[0];
2307
+ const owner = optionValue(args, ['--owner', '-o']);
2308
+ const paths = optionValue(args, ['--paths', '-p']);
2309
+ const note = optionValue(args, ['--note']);
2310
+ const result = claimArtifact(process.cwd(), { artifact, owner, paths, note });
2311
+ if (!result.ok) {
2312
+ if (result.error === 'conflict') {
2313
+ console.error(`Claim conflict for ${artifact}:`);
2314
+ for (const conflict of result.conflicts) {
2315
+ console.error(` ${(conflict.artifact_id || conflict.artifact)} -> ${(conflict.agent || conflict.owner)}`);
2316
+ }
2317
+ } else {
2318
+ console.error(`Claim failed: ${result.error}`);
2319
+ }
2320
+ process.exit(1);
2321
+ }
2322
+ console.log(`Claimed ${result.claim.artifact_id} for ${result.claim.agent}`);
2323
+ process.exit(0);
2324
+ }
2325
+
2326
+ if (sub === 'release') {
2327
+ const artifact = optionValue(args, ['--artifact', '-a']) || args[0];
2328
+ const owner = optionValue(args, ['--owner', '-o']);
2329
+ const result = releaseClaim(process.cwd(), { artifact, owner });
2330
+ if (!result.ok) {
2331
+ console.error(`Release failed: ${result.error}`);
2332
+ process.exit(1);
2333
+ }
2334
+ console.log(`Released ${result.released} claim(s) for ${artifact}`);
2335
+ process.exit(0);
2336
+ }
2337
+
2338
+ if (sub === 'note' || sub === 'finding' || sub === 'decision' || sub === 'blocker') {
2339
+ const kind = sub === 'note' ? (optionValue(args, ['--kind', '-k']) || 'note') : sub;
2340
+ const author = optionValue(args, ['--author', '--owner', '-o']) || 'cli';
2341
+ const artifact = optionValue(args, ['--artifact', '-a']);
2342
+ const message = optionValue(args, ['--message', '-m']) || args.filter((arg) => !arg.startsWith('--')).join(' ');
2343
+ const result = addBlackboardNote(process.cwd(), { kind, author, artifact, message });
2344
+ if (!result.ok) {
2345
+ console.error(`Note failed: ${result.error}`);
2346
+ process.exit(1);
2347
+ }
2348
+ console.log(`Recorded ${result.entry.kind}: ${result.entry.message}`);
2349
+ process.exit(0);
2350
+ }
2351
+
2352
+ if (sub === 'handoff') {
2353
+ const message = optionValue(args, ['--message', '-m']) || args.join(' ');
2354
+ const result = writeHandoff(process.cwd(), message);
2355
+ if (!result.ok) {
2356
+ console.error(`Handoff failed: ${result.error}`);
2357
+ process.exit(1);
2358
+ }
2359
+ console.log(`Handoff written: ${result.path}`);
2360
+ process.exit(0);
2361
+ }
2362
+
2363
+ console.log('Usage: ijfw blackboard init | status | claim --artifact <id> --owner <agent> [--paths <globs>] | release --artifact <id> [--owner <agent>] | note|finding|decision|blocker --message <text> | handoff --message <markdown>');
2364
+ process.exit(1);
2365
+ }
2366
+
2367
+ function cmdTeam(sub) {
2368
+ const args = process.argv.slice(4);
2369
+ if (sub === 'init' || sub === 'create') {
2370
+ const archetype = optionValue(args, ['--archetype', '--type', '-t']);
2371
+ const teamName = optionValue(args, ['--name']);
2372
+ const force = args.includes('--force');
2373
+ const result = createTeamAssembly(process.cwd(), { archetype, teamName, force });
2374
+ if (!result.ok) {
2375
+ if (result.error === 'exists') {
2376
+ console.error('Team assembly already exists. Re-run with --force to replace .ijfw/team/charter.json and workflow.json.');
2377
+ } else {
2378
+ console.error(`Team assembly failed: ${result.error}`);
2379
+ }
2380
+ process.exit(1);
2381
+ }
2382
+ console.log(`Team ready: ${result.team_name}`);
2383
+ console.log(`Archetype: ${result.archetype}`);
2384
+ console.log(`Agents: ${result.agentFiles.length} saved to ${result.agentsDir}`);
2385
+ if (result.codexAgents?.ok) console.log(`Codex agents: ${result.codexAgents.count} saved to ${result.codexAgents.agentsDir}`);
2386
+ console.log(`Charter: ${result.charterPath}`);
2387
+ console.log(`Workflow: ${result.workflowPath}`);
2388
+ createCheckpoint(process.cwd(), 'team-init', { actor: 'ijfw', message: 'Team assembly initialized' });
2389
+ process.exit(0);
2390
+ }
2391
+
2392
+ if (sub === 'status') {
2393
+ const state = readTeamAssembly(process.cwd());
2394
+ if (!state.ok) {
2395
+ console.log('No complete team assembly found. Run: ijfw team init');
2396
+ if (state.validation.charter.errors.length) console.log(`Charter: ${state.validation.charter.errors.join('; ')}`);
2397
+ if (state.validation.workflow.errors.length) console.log(`Workflow: ${state.validation.workflow.errors.join('; ')}`);
2398
+ process.exit(1);
2399
+ }
2400
+ console.log(`Team: ${state.charter.team_name}`);
2401
+ console.log(`Archetypes: ${state.charter.project_archetypes.join(', ')}`);
2402
+ console.log(`Roles: ${state.charter.roles.map((role) => role.name).join(', ')}`);
2403
+ console.log(`Artifacts: ${state.workflow.artifacts.length}`);
2404
+ console.log(`Agents: ${state.agents.length} in ${state.agentsDir}`);
2405
+ process.exit(0);
2406
+ }
2407
+
2408
+ console.log('Usage: ijfw team init [--archetype <type>] [--name <team-name>] [--force] | status');
2409
+ process.exit(1);
2410
+ }
2411
+
2412
+ function cmdCodex(sub) {
2413
+ if (sub === 'sync-agents') {
2414
+ const result = syncCodexAgents(process.cwd());
2415
+ if (!result.ok) {
2416
+ console.log(`Codex agent sync halted: ${result.error}`);
2417
+ console.log('Run: ijfw team init');
2418
+ process.exit(1);
2419
+ }
2420
+ console.log(`Codex agents synced: ${result.count}`);
2421
+ console.log(`Directory: ${result.agentsDir}`);
2422
+ process.exit(0);
2423
+ }
2424
+
2425
+ if (sub === 'doctor' || sub === 'status') {
2426
+ const result = codexDoctor(process.cwd());
2427
+ console.log('IJFW Codex doctor');
2428
+ for (const item of result.checks) {
2429
+ const mark = item.ok ? '[ ok ]' : item.required ? '[ !! ]' : '[ .. ]';
2430
+ console.log(` ${mark} ${item.name} -- ${item.message}`);
2431
+ if (!item.ok && item.fix) console.log(` fix: ${item.fix}`);
2432
+ }
2433
+ process.exit(result.ok ? 0 : 1);
2434
+ }
2435
+
2436
+ console.log('Usage: ijfw codex doctor | sync-agents');
2437
+ process.exit(1);
2438
+ }
2439
+
2440
+ function codexDoctor(projectRoot) {
2441
+ const root = resolve(projectRoot);
2442
+ const checks = [];
2443
+ const pluginPath = findFirstExisting(
2444
+ join(root, 'codex', '.codex-plugin', 'plugin.json'),
2445
+ findCliAsset('codex', '.codex-plugin', 'plugin.json'),
2446
+ );
2447
+ const hooksPath = findFirstExisting(
2448
+ join(homedir(), '.codex', 'hooks.json'),
2449
+ join(root, 'codex', '.codex', 'hooks.json'),
2450
+ findCliAsset('codex', '.codex', 'hooks.json'),
2451
+ );
2452
+ const configPath = findFirstExisting(
2453
+ join(homedir(), '.codex', 'config.toml'),
2454
+ join(root, 'codex', '.codex', 'config.toml'),
2455
+ findCliAsset('codex', '.codex', 'config.toml'),
2456
+ );
2457
+ const skillsDirs = [
2458
+ join(homedir(), '.codex', 'skills'),
2459
+ join(root, 'codex', 'skills'),
2460
+ findCliAsset('codex', 'skills'),
2461
+ ].filter(Boolean);
2462
+ const projectAgents = join(root, '.codex', 'agents');
2463
+ const repoAgents = join(root, 'codex', '.codex', 'agents');
2464
+ const agentsMd = join(root, 'AGENTS.md');
2465
+
2466
+ const plugin = readJsonFile(pluginPath);
2467
+ checks.push({
2468
+ name: 'plugin metadata',
2469
+ ok: plugin?.version === '1.3.2',
2470
+ required: true,
2471
+ message: plugin ? `version ${plugin.version}` : 'missing plugin.json',
2472
+ fix: 'update codex/.codex-plugin/plugin.json',
2473
+ });
2474
+
2475
+ const hooks = readJsonFile(hooksPath);
2476
+ const hookEvents = hooks?.hooks && typeof hooks.hooks === 'object' ? Object.keys(hooks.hooks) : [];
2477
+ const missingHooks = ['SessionStart', 'Stop', 'UserPromptSubmit', 'PreToolUse', 'PermissionRequest', 'PostToolUse'].filter((event) => !hookEvents.includes(event));
2478
+ checks.push({
2479
+ name: 'hooks',
2480
+ ok: missingHooks.length === 0,
2481
+ required: true,
2482
+ message: missingHooks.length ? `missing ${missingHooks.join(', ')}` : 'six Codex hook events configured',
2483
+ fix: 'restore codex/.codex/hooks.json and hook scripts',
2484
+ });
2485
+
2486
+ checks.push({
2487
+ name: 'MCP config',
2488
+ ok: existsSync(configPath) && readFileSync(configPath, 'utf8').includes('ijfw-memory'),
2489
+ required: true,
2490
+ message: existsSync(configPath) ? 'ijfw-memory configured' : 'missing config.toml',
2491
+ fix: 'run ijfw install or restore codex/.codex/config.toml',
2492
+ });
2493
+
2494
+ const skills = maxSkillCount(skillsDirs);
2495
+ checks.push({
2496
+ name: 'skills',
2497
+ ok: skills.count >= 19,
2498
+ required: true,
2499
+ message: `${skills.count} skill(s) found${skills.dir ? ` in ${skills.dir}` : ''}`,
2500
+ fix: 'run ijfw install or restore codex/skills',
2501
+ });
2502
+
2503
+ checks.push({
2504
+ name: 'custom agents',
2505
+ ok: existsSync(projectAgents) || existsSync(repoAgents),
2506
+ required: false,
2507
+ message: existsSync(projectAgents) ? '.codex/agents present' : existsSync(repoAgents) ? 'repo agent templates present' : 'not generated yet',
2508
+ fix: 'run ijfw team init or ijfw codex sync-agents',
2509
+ });
2510
+
2511
+ checks.push({
2512
+ name: 'AGENTS.md',
2513
+ ok: existsSync(agentsMd) && readFileSync(agentsMd, 'utf8').includes('IJFW-MEMORY-START'),
2514
+ required: false,
2515
+ message: existsSync(agentsMd) ? 'AGENTS.md memory block present' : 'missing AGENTS.md',
2516
+ fix: 'start a new IJFW-enabled session or run ijfw install',
2517
+ });
2518
+
2519
+ return { ok: checks.every((item) => item.ok || !item.required), checks };
2520
+ }
2521
+
2522
+ function findFirstExisting(...paths) {
2523
+ return paths.filter(Boolean).find((path) => existsSync(path)) || paths.filter(Boolean)[0] || '';
2524
+ }
2525
+
2526
+ function maxSkillCount(dirs) {
2527
+ let best = { count: 0, dir: null };
2528
+ for (const dir of dirs) {
2529
+ if (!dir || !existsSync(dir)) continue;
2530
+ const count = readdirSync(dir).filter((name) => existsSync(join(dir, name, 'SKILL.md'))).length;
2531
+ if (count > best.count) best = { count, dir };
2532
+ }
2533
+ return best;
2534
+ }
2535
+
2536
+ function readJsonFile(path) {
2537
+ try {
2538
+ if (!existsSync(path)) return null;
2539
+ return JSON.parse(readFileSync(path, 'utf8'));
2540
+ } catch {
2541
+ return null;
2542
+ }
2543
+ }
2544
+
2545
+ // IJFW v1.4.0 (W3/t16): override + extension CLI commands.
2546
+ // Delegate to the colon-syntax dispatch handlers for the actual logic;
2547
+ // this thin wrapper only handles argv parsing + result printing.
2548
+ function cmdOverride(sub, rest) {
2549
+ const projectRoot = process.env.IJFW_PROJECT_DIR || process.cwd();
2550
+ const args = (rest || []).join(' ');
2551
+ import('./dispatch/override.js')
2552
+ .then(m => m.overrideDispatch({ command: sub, args, projectRoot }))
2553
+ .then(r => {
2554
+ if (r && r.ok === false) {
2555
+ console.error(`[ijfw override] ${r.command}: ${r.error}`);
2556
+ process.exit(1);
2557
+ }
2558
+ console.log(JSON.stringify(r, null, 2));
2559
+ })
2560
+ .catch(err => {
2561
+ console.error(`[ijfw override] ${err.message}`);
2562
+ process.exit(1);
2563
+ });
2564
+ }
2565
+
2566
+ function cmdExtension(sub, rest) {
2567
+ const projectRoot = process.env.IJFW_PROJECT_DIR || process.cwd();
2568
+ const args = (rest || []).join(' ');
2569
+ import('./dispatch/extension.js')
2570
+ .then(m => m.extensionDispatch({ command: sub, args, projectRoot }))
2571
+ .then(r => {
2572
+ if (r && r.ok === false) {
2573
+ console.error(`[ijfw extension] ${r.command}: ${r.error}`);
2574
+ process.exit(1);
2575
+ }
2576
+ console.log(JSON.stringify(r, null, 2));
2577
+ })
2578
+ .catch(err => {
2579
+ console.error(`[ijfw extension] ${err.message}`);
2580
+ process.exit(1);
2581
+ });
2582
+ }
2583
+
2584
+ function cmdSwarm(sub) {
2585
+ const args = process.argv.slice(4);
2586
+ if (sub === 'worktree') {
2587
+ cmdSwarmWorktree(args[0] || 'list', args.slice(1));
2588
+ return;
2589
+ }
2590
+
2591
+ if (sub === 'plan') {
2592
+ const plan = buildSwarmPlan(process.cwd());
2593
+ console.log(swarmPlanSummary(plan));
2594
+ process.exit(plan.ok ? 0 : 1);
2595
+ }
2596
+
2597
+ if (sub === 'status') {
2598
+ const plan = buildSwarmPlan(process.cwd());
2599
+ if (!plan.ok) {
2600
+ console.log(swarmPlanSummary(plan));
2601
+ process.exit(1);
2602
+ }
2603
+ const blackboard = readBlackboard(process.cwd());
2604
+ const preparedTasks = blackboard.tasks.data.tasks || [];
2605
+ const ready = preparedTasks.length
2606
+ ? preparedTasks.filter((task) => task.status === 'ready').length
2607
+ : plan.waves.reduce((n, wave) => n + wave.tasks.filter((task) => !task.blocked).length, 0);
2608
+ const blocked = preparedTasks.length
2609
+ ? preparedTasks.filter((task) => task.status === 'blocked').length
2610
+ : plan.waves.reduce((n, wave) => n + wave.tasks.filter((task) => task.blocked).length, 0);
2611
+ const inProgress = preparedTasks.filter((task) => task.status === 'in_progress').length;
2612
+ const done = preparedTasks.filter((task) => task.status === 'done').length;
2613
+ console.log(`Swarm: planned, not executing`);
2614
+ console.log(`Team: ${plan.team_name}`);
2615
+ console.log(`Prepared tasks: ${preparedTasks.length}`);
2616
+ console.log(`Ready tasks: ${ready}`);
2617
+ if (preparedTasks.length) console.log(`In progress tasks: ${inProgress}`);
2618
+ if (preparedTasks.length) console.log(`Done tasks: ${done}`);
2619
+ console.log(`Blocked tasks: ${blocked}`);
2620
+ console.log(`Next: run 'ijfw swarm plan' for the wave breakdown, or dispatch ready tasks.`);
2621
+ process.exit(blocked ? 2 : 0);
2622
+ }
2623
+
2624
+ if (sub === 'prepare') {
2625
+ const replace = !process.argv.includes('--append');
2626
+ const includeReviews = process.argv.includes('--reviews');
2627
+ const result = prepareSwarmTasks(process.cwd(), { replace, includeReviews });
2628
+ if (!result.ok) {
2629
+ console.log(result.message || `Swarm prepare failed: ${result.error}`);
2630
+ process.exit(1);
2631
+ }
2632
+ const ready = result.tasks.filter((task) => task.status === 'ready').length;
2633
+ const blocked = result.tasks.filter((task) => task.status === 'blocked').length;
2634
+ console.log(`Swarm tasks prepared: ${result.written}`);
2635
+ console.log(`Ready: ${ready}`);
2636
+ console.log(`Blocked: ${blocked}`);
2637
+ console.log(`Blackboard tasks: ${result.total}`);
2638
+ createCheckpoint(process.cwd(), 'swarm-prepare', { actor: 'ijfw', message: 'Swarm tasks prepared' });
2639
+ process.exit(blocked ? 2 : 0);
2640
+ }
2641
+
2642
+ if (sub === 'tasks' || sub === 'list') {
2643
+ const result = listSwarmTasks(process.cwd());
2644
+ if (!result.ok) {
2645
+ console.log(`Swarm tasks unavailable: ${result.error}`);
2646
+ process.exit(1);
2647
+ }
2648
+ if (!result.tasks.length) {
2649
+ console.log('No prepared swarm tasks. Run: ijfw swarm prepare');
2650
+ process.exit(0);
2651
+ }
2652
+ for (const task of result.tasks) {
2653
+ const artifacts = (task.artifact_ids || []).join(',');
2654
+ console.log(`${task.id} [${task.status}] ${task.owner} -> ${artifacts}`);
2655
+ }
2656
+ process.exit(0);
2657
+ }
2658
+
2659
+ if (sub === 'prompt' || sub === 'dispatch-prompt') {
2660
+ const taskId = args[0];
2661
+ const codex = args.includes('--codex');
2662
+ const result = listSwarmTasks(process.cwd());
2663
+ if (!result.ok) {
2664
+ console.log(`Swarm tasks unavailable: ${result.error}`);
2665
+ process.exit(1);
2666
+ }
2667
+ const task = result.tasks.find((item) => item.id === taskId);
2668
+ if (!task) {
2669
+ console.log(`Swarm prompt unavailable: task-not-found`);
2670
+ process.exit(1);
2671
+ }
2672
+ console.log(renderSwarmDispatchPrompt(task, { projectRoot: process.cwd(), codex }));
2673
+ process.exit(0);
2674
+ }
2675
+
2676
+ if (sub === 'start') {
2677
+ const taskId = args[0];
2678
+ const owner = optionValue(args.slice(1), ['--owner', '-o']);
2679
+ const result = startSwarmTask(process.cwd(), taskId, { owner });
2680
+ if (!result.ok) {
2681
+ console.log(`Swarm start halted: ${result.error}`);
2682
+ if (result.dependency) console.log(`Dependency pending: ${result.dependency.id} [${result.dependency.status}]`);
2683
+ if (result.claim?.conflicts) {
2684
+ for (const conflict of result.claim.conflicts) console.log(`Claim held: ${conflict.artifact_id} -> ${conflict.agent}`);
2685
+ }
2686
+ process.exit(1);
2687
+ }
2688
+ console.log(`Started ${result.task.id} for ${result.task.active_owner || result.task.owner}`);
2689
+ createCheckpoint(process.cwd(), 'task-start', { actor: result.task.active_owner || result.task.owner, message: result.task.id });
2690
+ process.exit(0);
2691
+ }
2692
+
2693
+ if (sub === 'complete' || sub === 'done') {
2694
+ const taskId = args[0];
2695
+ const owner = optionValue(args.slice(1), ['--owner', '-o']);
2696
+ const message = optionValue(args.slice(1), ['--message', '-m']) || positionalMessage(args.slice(1), ['--owner', '-o']);
2697
+ const result = completeSwarmTask(process.cwd(), taskId, { owner, message });
2698
+ if (!result.ok) {
2699
+ console.log(`Swarm complete halted: ${result.error}`);
2700
+ process.exit(1);
2701
+ }
2702
+ console.log(`Completed ${result.task.id}`);
2703
+ createCheckpoint(process.cwd(), 'task-complete', { actor: result.task.active_owner || result.task.owner, message: result.task.id });
2704
+ process.exit(0);
2705
+ }
2706
+
2707
+ if (sub === 'block') {
2708
+ const taskId = args[0];
2709
+ const owner = optionValue(args.slice(1), ['--owner', '-o']);
2710
+ const message = optionValue(args.slice(1), ['--message', '-m']) || args.slice(1).filter((arg) => !arg.startsWith('--')).join(' ');
2711
+ const result = blockSwarmTask(process.cwd(), taskId, { owner, message });
2712
+ if (!result.ok) {
2713
+ console.log(`Swarm block halted: ${result.error}`);
2714
+ process.exit(1);
2715
+ }
2716
+ console.log(`Blocked ${result.task.id}: ${result.task.blocker}`);
2717
+ createCheckpoint(process.cwd(), 'task-block', { actor: owner || result.task.owner, message: result.task.id });
2718
+ process.exit(0);
2719
+ }
2720
+
2721
+ if (sub === 'ready') {
2722
+ const taskId = args[0];
2723
+ const result = readySwarmTask(process.cwd(), taskId);
2724
+ if (!result.ok) {
2725
+ console.log(`Swarm ready halted: ${result.error}`);
2726
+ process.exit(1);
2727
+ }
2728
+ console.log(`Ready ${result.task.id}`);
2729
+ createCheckpoint(process.cwd(), 'task-ready', { actor: 'ijfw', message: result.task.id });
2730
+ process.exit(0);
2731
+ }
2732
+
2733
+ console.log('Usage: ijfw swarm plan | prepare [--append] [--reviews] | tasks | prompt <task-id> [--codex] | start <task-id> [--owner <agent>] | complete <task-id> [--message <text>] | block <task-id> --message <why> | ready <task-id> | status');
2734
+ process.exit(1);
2735
+ }
2736
+
2737
+ function positionalMessage(args, valueOptions = []) {
2738
+ const out = [];
2739
+ for (let i = 0; i < args.length; i++) {
2740
+ const arg = args[i];
2741
+ if (valueOptions.includes(arg)) {
2742
+ i++;
2743
+ continue;
2744
+ }
2745
+ if (valueOptions.some((name) => arg.startsWith(`${name}=`))) continue;
2746
+ if (!arg.startsWith('--')) out.push(arg);
2747
+ }
2748
+ return out.join(' ').trim() || null;
2749
+ }
2750
+
2751
+ function cmdSwarmWorktree(sub, args) {
2752
+ if (sub === 'create') {
2753
+ const taskId = args[0];
2754
+ const force = args.includes('--force');
2755
+ const allowDirty = args.includes('--allow-dirty');
2756
+ const result = createTaskWorktree(process.cwd(), taskId, { force, allowDirty });
2757
+ if (!result.ok) {
2758
+ console.log(`Worktree create halted: ${result.error}`);
2759
+ if (result.detail) for (const line of result.detail.slice(0, 10)) console.log(` ${line}`);
2760
+ process.exit(1);
2761
+ }
2762
+ console.log(`Worktree created: ${result.path}`);
2763
+ console.log(`Branch: ${result.branch}`);
2764
+ process.exit(0);
2765
+ }
2766
+
2767
+ if (sub === 'list') {
2768
+ const result = listTaskWorktrees(process.cwd());
2769
+ if (!result.worktrees.length) {
2770
+ console.log('No swarm worktrees recorded.');
2771
+ process.exit(0);
2772
+ }
2773
+ for (const wt of result.worktrees) console.log(`${wt.task_id} [${wt.status}] ${wt.branch} -> ${wt.path}`);
2774
+ process.exit(0);
2775
+ }
2776
+
2777
+ if (sub === 'integrate') {
2778
+ const taskId = args[0];
2779
+ const cleanup = args.includes('--cleanup');
2780
+ const result = integrateTaskWorktree(process.cwd(), taskId, { cleanup });
2781
+ if (!result.ok) {
2782
+ console.log(`Worktree integrate halted: ${result.error}`);
2783
+ if (result.stderr) console.log(result.stderr.trim());
2784
+ if (result.stdout) console.log(result.stdout.trim());
2785
+ process.exit(1);
2786
+ }
2787
+ console.log(`Worktree integrated: ${taskId}`);
2788
+ process.exit(0);
2789
+ }
2790
+
2791
+ if (sub === 'cleanup') {
2792
+ const taskId = args[0];
2793
+ const force = args.includes('--force');
2794
+ const result = cleanupTaskWorktree(process.cwd(), taskId, { force });
2795
+ if (!result.ok) {
2796
+ console.log(`Worktree cleanup halted: ${result.error}`);
2797
+ process.exit(1);
2798
+ }
2799
+ console.log(`Worktree cleaned: ${taskId}`);
2800
+ process.exit(0);
2801
+ }
2802
+
2803
+ console.log('Usage: ijfw swarm worktree create <task-id> [--force] [--allow-dirty] | list | integrate <task-id> [--cleanup] | cleanup <task-id> [--force]');
2804
+ process.exit(1);
2805
+ }
2806
+
2807
+ function cmdMemoryCheckpoint(label) {
2808
+ const result = createCheckpoint(process.cwd(), label || 'manual', {
2809
+ actor: 'cli',
2810
+ message: process.argv.slice(4).join(' ') || undefined,
2811
+ });
2812
+ if (!result.ok) {
2813
+ console.log(`Checkpoint unavailable: ${result.error}`);
2814
+ process.exit(1);
2815
+ }
2816
+ console.log(`Checkpoint created: ${result.id}`);
2817
+ console.log(`Markdown: ${result.mdPath}`);
2818
+ console.log(`JSON: ${result.jsonPath}`);
2819
+ process.exit(0);
2820
+ }
2821
+
2822
+ function cmdRecover(sub) {
2823
+ if (sub === 'latest') {
2824
+ const latest = latestCheckpoint(process.cwd());
2825
+ if (!latest.ok) {
2826
+ console.log('No checkpoint found. Run: ijfw memory checkpoint <label>');
2827
+ process.exit(1);
2828
+ }
2829
+ console.log(latest.markdown || `Latest checkpoint: ${latest.id}`);
2830
+ process.exit(0);
2831
+ }
2832
+
2833
+ if (sub === 'status') {
2834
+ const status = recoveryStatus(process.cwd());
2835
+ console.log(`Recovery status`);
2836
+ console.log(`Latest checkpoint: ${status.latest ? status.latest.id : 'none'}`);
2837
+ console.log(`Team: ${status.team.ok ? status.team.name : 'not assembled'}`);
2838
+ console.log(`Tasks: ${status.tasks.ready} ready, ${status.tasks.in_progress} in progress, ${status.tasks.blocked} blocked, ${status.tasks.done} done`);
2839
+ console.log(`Active claims: ${status.claims.active}`);
2840
+ console.log(`Next: ${status.next}`);
2841
+ process.exit(0);
2842
+ }
2843
+
2844
+ console.log('Usage: ijfw recover status | latest');
2845
+ process.exit(1);
2846
+ }