@ijfw/memory-server 1.4.3 → 1.5.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.
Files changed (233) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1171 -10
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-planning.html +273 -0
  131. package/src/dashboard-client-waves.html +304 -0
  132. package/src/dashboard-client.html +17 -2
  133. package/src/dashboard-server.js +152 -0
  134. package/src/deploy-alerts.js +150 -0
  135. package/src/design/iframe-bridge.js +242 -0
  136. package/src/design-companion.js +144 -0
  137. package/src/dispatch/checkpoint-cli.js +97 -0
  138. package/src/dispatch/colon-syntax.js +81 -1
  139. package/src/dispatch/extension.js +27 -1
  140. package/src/dispatch/registry-cli.js +4 -1
  141. package/src/dispatch/wave-cli.js +323 -0
  142. package/src/dispatch/worktree-cli.js +40 -0
  143. package/src/dispatch-planner.js +97 -2
  144. package/src/dream/runner.mjs +47 -11
  145. package/src/dream/stage-runner.js +40 -0
  146. package/src/dream/state-file.js +102 -0
  147. package/src/extension-installer.js +70 -24
  148. package/src/extension-quota-tracker.js +4 -2
  149. package/src/extension-registry.js +289 -35
  150. package/src/feedback-detector.js +26 -0
  151. package/src/fs-lock.js +259 -7
  152. package/src/gate-result.js +95 -1
  153. package/src/hero-line.js +86 -5
  154. package/src/intent-router.js +35 -0
  155. package/src/lib/a11y-contract.js +117 -0
  156. package/src/lib/atomic-io.js +29 -8
  157. package/src/lib/cache-keepalive.js +150 -0
  158. package/src/lib/jsonl-rotation.js +104 -0
  159. package/src/lib/lighthouse-pillar.js +121 -0
  160. package/src/lib/llm-call.js +121 -0
  161. package/src/lib/playwright-baseline.js +205 -0
  162. package/src/lib/rekor-bridge.js +221 -0
  163. package/src/lib/repo-map.js +392 -0
  164. package/src/lib/shasum-verify.js +164 -0
  165. package/src/lib/sketches-gc.js +132 -0
  166. package/src/lib/tmp-suffix.js +62 -0
  167. package/src/lib/ui-review-runner.js +554 -0
  168. package/src/lib/uispec-drift.js +301 -0
  169. package/src/lib/uispec-intake.js +381 -0
  170. package/src/lib/worktree-guards.js +118 -0
  171. package/src/lib/worktree-recovery.js +100 -0
  172. package/src/memory/auto-linker.js +152 -0
  173. package/src/memory/benchmark.js +498 -0
  174. package/src/memory/dedup.js +126 -0
  175. package/src/memory/embedding-cache.js +136 -0
  176. package/src/memory/fact-extractor.js +168 -0
  177. package/src/memory/fts5.js +65 -1
  178. package/src/memory/migrations/004-bitemporal.js +91 -0
  179. package/src/memory/migrations/005-vector-cache.js +61 -0
  180. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  181. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  182. package/src/memory/migrations/008-write-provenance.js +41 -0
  183. package/src/memory/obsidian-parser.js +91 -0
  184. package/src/memory/query-dataview.js +86 -0
  185. package/src/memory/search.js +10 -0
  186. package/src/memory/temporal.js +529 -0
  187. package/src/memory/tokenize.js +10 -0
  188. package/src/memory-facts-handler.js +37 -0
  189. package/src/memory-feedback.js +260 -2
  190. package/src/model-refresh.js +292 -0
  191. package/src/observability/cost-anomaly.js +166 -0
  192. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  193. package/src/observability/trace-id.js +163 -0
  194. package/src/orchestrator/agents-md-blackboard.js +152 -0
  195. package/src/orchestrator/checkpoint-contract.md +140 -0
  196. package/src/orchestrator/debug-trident.js +570 -0
  197. package/src/orchestrator/merge-block-aware.js +350 -0
  198. package/src/orchestrator/plan-checker.js +475 -0
  199. package/src/orchestrator/post-done-runner.js +249 -0
  200. package/src/orchestrator/review.js +136 -0
  201. package/src/orchestrator/runtime-loop.js +430 -0
  202. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  203. package/src/orchestrator/skill-telemetry.js +37 -0
  204. package/src/orchestrator/state-events.js +459 -0
  205. package/src/orchestrator/state-sdk.js +1764 -0
  206. package/src/orchestrator/status-protocol.js +235 -0
  207. package/src/orchestrator/subagent-telemetry.js +452 -0
  208. package/src/orchestrator/termination.js +160 -0
  209. package/src/orchestrator/verification-gate.js +281 -0
  210. package/src/orchestrator/wave-state.js +564 -0
  211. package/src/orchestrator/worktree-provision.js +77 -0
  212. package/src/override-use-registry.js +111 -5
  213. package/src/receipts.js +36 -4
  214. package/src/recovery/checkpoint.js +56 -3
  215. package/src/recovery/code-fixer.js +656 -0
  216. package/src/recovery/truncation.js +317 -0
  217. package/src/redactor.js +75 -6
  218. package/src/runtime-mediator.js +15 -0
  219. package/src/sanitizer.js +10 -0
  220. package/src/search-hybrid.js +139 -0
  221. package/src/server.js +603 -59
  222. package/src/swarm/worktree.js +27 -4
  223. package/src/swarm-config.js +113 -12
  224. package/src/team/domain-templates/book.json +51 -0
  225. package/src/team/domain-templates/business.json +41 -0
  226. package/src/team/domain-templates/content.json +50 -0
  227. package/src/team/domain-templates/design.json +44 -0
  228. package/src/team/domain-templates/research.json +41 -0
  229. package/src/team/domain-templates/software.json +40 -0
  230. package/src/team/generator.js +278 -3
  231. package/src/team/modify.js +203 -0
  232. package/src/team/schemas.js +48 -0
  233. package/src/update-apply.js +19 -3
@@ -14,6 +14,7 @@ import { homedir } from 'node:os';
14
14
  import { spawnSync } from 'node:child_process';
15
15
  import { writeAtomic } from './lib/atomic-io.js';
16
16
  import { runCrossOp } from './cross-orchestrator.js';
17
+ import { chunkText, mergeFindings, CHUNKER_DEFAULTS } from './cross-audit-chunker.js';
17
18
  import { readReceipts, purgeReceipts } from './receipts.js';
18
19
  import { renderHeroLine } from './hero-line.js';
19
20
  import { ROSTER, isInstalled, isReachable } from './audit-roster.js';
@@ -21,16 +22,27 @@ import { aggregatePortfolioFindings } from './cross-project-search.js';
21
22
  import { runImport, runImportAll, listImporters } from './importers/cli.js';
22
23
  import { validateToken } from './lib/token.js';
23
24
  import { isVersionStringValid } from './lib/npm-view.js';
25
+ import { verifyShasumCrossSource } from './lib/shasum-verify.js';
24
26
  import {
25
27
  addBlackboardNote,
26
28
  blackboardStatus,
27
29
  claimArtifact,
30
+ DEFAULT_CLAIM_TTL_MS,
31
+ evictOrphanedClaims,
28
32
  initBlackboard,
29
33
  readBlackboard,
30
34
  releaseClaim,
35
+ updateClaimHeartbeat,
31
36
  writeHandoff,
32
37
  } from './blackboard.js';
33
38
  import { createTeamAssembly, readTeamAssembly } from './team/generator.js';
39
+ import {
40
+ addTeamRole,
41
+ checkTeamAssembly,
42
+ listTeamRoles,
43
+ removeTeamRole,
44
+ swapTeamRole,
45
+ } from './team/modify.js';
34
46
  import {
35
47
  blockSwarmTask,
36
48
  buildSwarmPlan,
@@ -274,17 +286,19 @@ function parseCrossAlias(mode, args) {
274
286
  let only = null;
275
287
  let confirm = false;
276
288
  let expand = false;
289
+ let chunk = false;
277
290
  const positional = [];
278
291
  for (let i = 1; i < args.length; i++) {
279
292
  const arg = args[i];
280
293
  if (arg === '--confirm') confirm = true;
281
294
  else if (arg === '--expand') expand = true;
295
+ else if (arg === '--chunk') chunk = true; // v1.5.1 H1.6 — wire chunker
282
296
  else if (arg === '--with' && args[i + 1]) only = args[++i];
283
297
  else if (arg.startsWith('--with=')) only = arg.slice('--with='.length);
284
298
  else if (!arg.startsWith('--')) positional.push(arg);
285
299
  }
286
300
  const target = mode === 'research' ? positional.join(' ').trim() : positional[0];
287
- return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand };
301
+ return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand, chunk };
288
302
  }
289
303
 
290
304
  function parseCommandAlias(args) {
@@ -396,6 +410,23 @@ function parseArgsInner(args) {
396
410
  return { cmd: 'design', sub: args[1] || 'status' };
397
411
  }
398
412
 
413
+ // v1.5.0 wire-W1.D — `ijfw ui-review --spec <path> --scope <dirs>`
414
+ if (args[0] === 'ui-review') {
415
+ const opts = { cmd: 'ui-review', spec: null, scope: null, write: true, gcSketches: false, peerInputs: null };
416
+ for (let i = 1; i < args.length; i++) {
417
+ const a = args[i];
418
+ if ((a === '--spec' || a === '-s') && args[i + 1]) { opts.spec = args[++i]; }
419
+ else if (a.startsWith('--spec=')) opts.spec = a.slice('--spec='.length);
420
+ else if ((a === '--scope' || a === '-S') && args[i + 1]) { opts.scope = args[++i]; }
421
+ else if (a.startsWith('--scope=')) opts.scope = a.slice('--scope='.length);
422
+ else if (a === '--no-write') opts.write = false;
423
+ else if (a === '--gc-sketches') opts.gcSketches = true;
424
+ else if ((a === '--peer-inputs') && args[i + 1]) { opts.peerInputs = args[++i]; }
425
+ else if (a.startsWith('--peer-inputs=')) opts.peerInputs = a.slice('--peer-inputs='.length);
426
+ }
427
+ return opts;
428
+ }
429
+
399
430
  if (args[0] === 'blackboard') {
400
431
  return { cmd: 'blackboard', sub: args[1] || 'status' };
401
432
  }
@@ -464,14 +495,16 @@ function parseArgsInner(args) {
464
495
  let only = null;
465
496
  let confirm = false;
466
497
  let expand = false;
498
+ let chunk = false;
467
499
 
468
500
  for (let i = 3; i < args.length; i++) {
469
501
  if (args[i] === '--confirm') { confirm = true; }
470
502
  else if (args[i] === '--expand') { expand = true; }
503
+ else if (args[i] === '--chunk') { chunk = true; } // v1.5.1 H1.6 — wire chunker
471
504
  else if (args[i] === '--with' && args[i + 1]) { only = args[++i]; }
472
505
  }
473
506
 
474
- return { cmd: 'cross', mode, target, only, confirm, expand };
507
+ return { cmd: 'cross', mode, target, only, confirm, expand, chunk };
475
508
  }
476
509
 
477
510
  return { cmd: 'unknown', raw: args[0] };
@@ -877,6 +910,15 @@ function cmdPurgeReceipts(projectDir) {
877
910
  // ranges, and non-existent paths pass through unchanged.
878
911
  const TARGET_FILE_SIZE_CAP = 64 * 1024; // 64 KB -- leaves prompt headroom
879
912
 
913
+ // r17.1 — size thresholds for pre-flight advisory. Inputs under WARN are
914
+ // silent; WARN..CAP get a one-line "this might be slow" advisory; CAP..MAX
915
+ // get a "this WILL be truncated, consider chunking" warning before we fire
916
+ // any auditor (so the user can cancel rather than burn wall time). Anything
917
+ // over MAX would be silently truncated by resolveTarget anyway — the
918
+ // pre-flight check makes that loud.
919
+ const TARGET_FILE_SIZE_WARN = 32 * 1024;
920
+ const TARGET_FILE_SIZE_MAX = 256 * 1024; // beyond this, advise chunking explicitly
921
+
880
922
  export function resolveTarget(raw, opts = {}) {
881
923
  const cap = typeof opts.sizeCap === 'number' ? opts.sizeCap : TARGET_FILE_SIZE_CAP;
882
924
  if (typeof raw !== 'string' || !raw) return raw;
@@ -921,7 +963,44 @@ export function resolveTarget(raw, opts = {}) {
921
963
  return `File: ${raw}\n\n${contents}`;
922
964
  }
923
965
 
924
- async function cmdCross({ mode, target, only, confirm, expand }) {
966
+ // v1.5.1 H1.6 chunked-dispatch helpers (audit finding token-optimization.md
967
+ // HIGH-H4 + trident.md HIGH-1, 2/2 consensus). The chunker has shipped (with
968
+ // tests) since r17.1 but was never wired into the CLI. `--chunk` now triggers
969
+ // per-chunk dispatch through runCrossOp + a final Jaccard-dedupe merge.
970
+
971
+ /**
972
+ * Decide whether a target absolute path is large enough to benefit from
973
+ * chunking. Exported for unit-tests.
974
+ */
975
+ export function shouldChunkFile(absPath, opts = {}) {
976
+ const threshold = typeof opts.threshold === 'number' ? opts.threshold : TARGET_FILE_SIZE_CAP;
977
+ try {
978
+ const st = statSync(absPath);
979
+ if (!st.isFile()) return false;
980
+ return st.size > threshold;
981
+ } catch {
982
+ return false;
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Read a file and produce the per-chunk target strings the auditors will see.
988
+ * Each chunk is annotated with its index so cross-chunk findings can be
989
+ * reconciled. Exported for unit-tests.
990
+ */
991
+ export function buildChunkedTargets(absPath, rawTarget, opts = {}) {
992
+ const content = readFileSync(absPath, 'utf8');
993
+ const chunks = chunkText(content, opts);
994
+ return chunks.map((c, i) => ({
995
+ chunkIndex: i,
996
+ total: chunks.length,
997
+ bytesStart: c.start,
998
+ bytesEnd: c.end,
999
+ target: `File: ${rawTarget} [chunk ${i + 1}/${chunks.length}, bytes ${c.start}-${c.end}]\n\n${c.text}`,
1000
+ }));
1001
+ }
1002
+
1003
+ async function cmdCross({ mode, target, only, confirm, expand, chunk }) {
925
1004
  const VALID_MODES = ['audit', 'research', 'critique'];
926
1005
  if (!mode || !VALID_MODES.includes(mode)) {
927
1006
  console.error(`ijfw cross requires a mode: ${VALID_MODES.join(', ')}. Example: ijfw cross audit <file>`);
@@ -935,6 +1014,114 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
935
1014
  // Issue #6 fix: substitute file contents for path string when target is a
936
1015
  // regular file. Keep the raw target for the user-facing echo line.
937
1016
  const rawTarget = target;
1017
+
1018
+ // r17.1 — pre-flight size advisory. Run BEFORE resolveTarget truncates,
1019
+ // so the user sees the real number and can decide to abort + chunk the
1020
+ // input themselves rather than getting a silently-truncated audit.
1021
+ // r17-M3: resolve relative paths against cwd FIRST, matching the same
1022
+ // resolution resolveTarget() uses. Without this, `ijfw cross audit foo.md`
1023
+ // (a relative path that exists) would skip the advisory because
1024
+ // existsSync(rawTarget) probes against the wrong cwd-anchor.
1025
+ try {
1026
+ let probePath = null;
1027
+ if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
1028
+ const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
1029
+ if (existsSync(resolved)) probePath = resolved;
1030
+ }
1031
+ if (probePath) {
1032
+ const st = statSync(probePath);
1033
+ if (st.isFile()) {
1034
+ if (st.size > TARGET_FILE_SIZE_MAX) {
1035
+ console.log('');
1036
+ console.log(`Heads up -- target is ${(st.size / 1024).toFixed(1)} KB, larger than the ${(TARGET_FILE_SIZE_MAX / 1024).toFixed(0)} KB recommended max.`);
1037
+ console.log(`Auditors will see only the first ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB (truncated). For full coverage, chunk the target into smaller files and audit each, OR pass --chunk to auto-split (see \`ijfw cross --help\`).`);
1038
+ } else if (st.size > TARGET_FILE_SIZE_CAP) {
1039
+ console.log('');
1040
+ console.log(`Note: target is ${(st.size / 1024).toFixed(1)} KB; auditors will see the first ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB and a truncation marker. Findings beyond that point will be missed.`);
1041
+ } else if (st.size > TARGET_FILE_SIZE_WARN) {
1042
+ console.log('');
1043
+ console.log(`Note: target is ${(st.size / 1024).toFixed(1)} KB; expect a slower wall time (gemini in particular may push the 90s budget).`);
1044
+ }
1045
+ }
1046
+ }
1047
+ } catch { /* statSync failure is non-fatal; size advisory is best-effort */ }
1048
+
1049
+ // v1.5.1 H1.6 — chunked dispatch path. When --chunk is set AND the target
1050
+ // is a file larger than the size cap, split via the cross-audit-chunker
1051
+ // (boundary-aware, 10% overlap, Jaccard-dedupe merge) and dispatch each
1052
+ // chunk through runCrossOp separately. Merge findings at the end. Opt-in
1053
+ // because cost scales linearly with chunk count.
1054
+ if (chunk) {
1055
+ let absPath = null;
1056
+ try {
1057
+ if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
1058
+ const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
1059
+ if (existsSync(resolved) && statSync(resolved).isFile()) absPath = resolved;
1060
+ }
1061
+ } catch { /* */ }
1062
+
1063
+ if (!absPath) {
1064
+ console.log('');
1065
+ console.log('--chunk requires a file target. Topics, git ranges, and missing paths cannot be chunked.');
1066
+ // Fall through to normal path -- --chunk silently no-ops for non-files
1067
+ } else if (!shouldChunkFile(absPath)) {
1068
+ console.log('');
1069
+ console.log(`--chunk: target is ${(statSync(absPath).size / 1024).toFixed(1)} KB, under the ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB chunk threshold; running single-pass audit instead.`);
1070
+ // Fall through to normal path
1071
+ } else {
1072
+ const chunks = buildChunkedTargets(absPath, rawTarget);
1073
+ console.log('');
1074
+ console.log(`--chunk: splitting ${rawTarget} into ${chunks.length} chunks (≈${(CHUNKER_DEFAULTS.chunkSize / 1024).toFixed(0)} KB each, ${(CHUNKER_DEFAULTS.overlap / 1024).toFixed(0)} KB overlap).`);
1075
+ console.log(`Trident dispatches: ${chunks.length} × per-chunk audit. Cost scales linearly.`);
1076
+
1077
+ const perChunkResults = [];
1078
+ const auditorIds = new Set();
1079
+ const projectDir = process.cwd();
1080
+ let firedAny = false;
1081
+ for (const { chunkIndex, total, target: chunkTarget } of chunks) {
1082
+ console.log('');
1083
+ console.log(`[chunk ${chunkIndex + 1}/${total}] dispatching...`);
1084
+ try {
1085
+ const r = await runCrossOp({
1086
+ mode, target: chunkTarget, projectDir,
1087
+ runStamp: new Date().toISOString(), only, confirm, expand,
1088
+ });
1089
+ const findings = Array.isArray(r.merged) ? r.merged : [];
1090
+ perChunkResults.push({ chunkIndex, findings });
1091
+ for (const p of (r.picks || [])) auditorIds.add(p.id);
1092
+ if ((r.picks || []).length > 0) firedAny = true;
1093
+ console.log(`[chunk ${chunkIndex + 1}/${total}] ${findings.length} finding(s) from ${(r.picks || []).length} auditor(s).`);
1094
+ } catch (err) {
1095
+ console.log(`[chunk ${chunkIndex + 1}/${total}] dispatch error: ${err.message}`);
1096
+ perChunkResults.push({ chunkIndex, findings: [] });
1097
+ }
1098
+ }
1099
+
1100
+ const merged = mergeFindings(perChunkResults);
1101
+ console.log('');
1102
+ console.log(`=== Chunked audit complete: ${merged.length} unique finding(s) across ${chunks.length} chunks ===`);
1103
+ console.log(`Auditors fired (union): ${[...auditorIds].join(', ') || '(none)'}`);
1104
+ if (!firedAny) {
1105
+ console.log('No auditors fired -- run `ijfw doctor` to see the install hints.');
1106
+ process.exit(2); // r17.1 — degraded exit code
1107
+ }
1108
+ for (const f of merged) {
1109
+ const sev = (f.severity || 'note').toUpperCase();
1110
+ const cluster = f.clusterSize > 1 ? ` [×${f.clusterSize}]` : '';
1111
+ const tgt = f.target ? ` ${f.target} —` : '';
1112
+ // v1.5.0 wire-W4: widen field fallback to cover description/issue/
1113
+ // detail/note/summary keys auditors emit. Closes the r19 "(no detail)"
1114
+ // dropout that made adjudication a guessing game.
1115
+ const text = f.finding || f.text || f.message ||
1116
+ f.description || f.issue ||
1117
+ f.detail || f.details || f.note || f.summary ||
1118
+ '(no detail)';
1119
+ console.log(` ${sev}${cluster}${tgt} ${text}`);
1120
+ }
1121
+ return;
1122
+ }
1123
+ }
1124
+
938
1125
  target = resolveTarget(target);
939
1126
 
940
1127
  // Polish 6: pre-flight reachability check. If no auditor is wired, give a
@@ -1006,6 +1193,18 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
1006
1193
  printFindings(mode, merged);
1007
1194
 
1008
1195
  console.log('\nReceipt logged -- run `ijfw status` to see it.');
1196
+
1197
+ // r17.1 — structured exit code so CI scripts + orchestrator-LLM callers
1198
+ // can detect degraded runs without scraping console output.
1199
+ // exit 0 — all picks contributed productively
1200
+ // exit 2 — at least one pick didn't contribute (timeout / failure / aborted)
1201
+ // exit 3 — zero picks contributed productively (INCONCLUSIVE verdict)
1202
+ // exit 1 is reserved for argv / usage errors (already used above).
1203
+ if (auditorResults && Array.isArray(auditorResults)) {
1204
+ const productive = auditorResults.filter(r => r.counted === true || r.status === null || r.status === 'fallback-used');
1205
+ if (productive.length === 0) process.exit(3);
1206
+ if (productive.length < auditorResults.length) process.exit(2);
1207
+ }
1009
1208
  }
1010
1209
 
1011
1210
  // ---------------------------------------------------------------------------
@@ -1314,16 +1513,100 @@ function cmpSemver(a, b) {
1314
1513
 
1315
1514
  function readState() { return readJsonSafe(join(ijfwHome(), 'state.json')) || {}; }
1316
1515
  function readSettings() { return readJsonSafe(join(ijfwHome(), 'settings.json')) || {}; }
1317
- function writeStateFields(updates) {
1318
- const path = join(ijfwHome(), 'state.json');
1319
- const state = Object.assign(readState(), updates);
1516
+
1517
+ // v1.5.0 audit M11 (F-REL-1): writeStateFields was best-effort — readState +
1518
+ // merge + writeAtomic was a TOCTOU window where a parallel `ijfw update`
1519
+ // completion could clobber another writer's `last_applied_version`. If the
1520
+ // write failed silently, the re-entrancy guard (last_applied_version >=
1521
+ // last_latest_seen) would never fire and every subsequent session would
1522
+ // nag-loop until manual `ijfw doctor`.
1523
+ //
1524
+ // Fix: serialise read-modify-write under a sync directory lock so the merge
1525
+ // happens against the latest disk state. We use a tiny inlined sync version
1526
+ // of `withFsLock` (mkdir-with-EEXIST is atomic on POSIX + NTFS) rather than
1527
+ // importing the async `fs-lock.js` — this whole CLI is sync top-to-bottom and
1528
+ // converting just-this-callsite to async would force every caller to await.
1529
+ //
1530
+ // On lock-acquire failure we still attempt the write (better than refusing to
1531
+ // persist) and surface the lock failure as a clearer error.
1532
+ const STATE_LOCK_DIR = () => join(ijfwHome(), '.state.lock');
1533
+ const STATE_LOCK_ACQUIRE_TIMEOUT_MS = 5000;
1534
+ const STATE_LOCK_STALE_MS = 30000;
1535
+ const STATE_LOCK_BACKOFF_START_MS = 25;
1536
+ const STATE_LOCK_BACKOFF_MAX_MS = 250;
1537
+
1538
+ function withStateLockSync(fn) {
1539
+ const lockDir = STATE_LOCK_DIR();
1540
+ // Ensure parent exists; tolerate races.
1541
+ try { mkdirSync(dirname(lockDir), { recursive: true, mode: 0o700 }); } catch { /* */ }
1542
+
1543
+ const deadline = Date.now() + STATE_LOCK_ACQUIRE_TIMEOUT_MS;
1544
+ let staleRecoveryUsed = false;
1545
+ let backoff = STATE_LOCK_BACKOFF_START_MS;
1546
+ let acquired = false;
1547
+
1548
+ while (Date.now() < deadline) {
1549
+ try {
1550
+ mkdirSync(lockDir, { recursive: false });
1551
+ acquired = true;
1552
+ break;
1553
+ } catch (err) {
1554
+ if (err && err.code !== 'EEXIST') {
1555
+ // Real FS error (EACCES, ENOENT on parent we couldn't create) —
1556
+ // surface so the caller can fall back to best-effort write.
1557
+ throw err;
1558
+ }
1559
+ // Stale recovery: if the lock dir is older than STATE_LOCK_STALE_MS,
1560
+ // a previous holder crashed mid-write. Remove + retry once.
1561
+ if (!staleRecoveryUsed) {
1562
+ try {
1563
+ const st = statSync(lockDir);
1564
+ if (Date.now() - st.mtimeMs > STATE_LOCK_STALE_MS) {
1565
+ staleRecoveryUsed = true;
1566
+ rmSync(lockDir, { recursive: true, force: true });
1567
+ continue;
1568
+ }
1569
+ } catch { /* lock vanished mid-stat; retry */ }
1570
+ }
1571
+ // Bounded busy-wait. Sync sleep via Atomics is heavy; use a tight loop
1572
+ // with deadline check (typical contention is microseconds in practice).
1573
+ const waitUntil = Date.now() + Math.min(backoff, STATE_LOCK_BACKOFF_MAX_MS, deadline - Date.now());
1574
+ // eslint-disable-next-line no-empty
1575
+ while (Date.now() < waitUntil) {}
1576
+ backoff = Math.min(backoff * 2, STATE_LOCK_BACKOFF_MAX_MS);
1577
+ }
1578
+ }
1579
+
1580
+ if (!acquired) {
1581
+ // Couldn't acquire — fall back to caller's fn anyway. Better to risk a
1582
+ // racy write than to lose the re-entrancy guard entry.
1583
+ try { return fn(); }
1584
+ finally { /* no lock to release */ }
1585
+ }
1586
+
1320
1587
  try {
1321
- writeAtomic(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
1322
- } catch (e) {
1323
- console.error(`could not persist state.json: ${e.message}`);
1588
+ return fn();
1589
+ } finally {
1590
+ try { rmSync(lockDir, { recursive: true, force: true }); } catch { /* */ }
1324
1591
  }
1325
1592
  }
1326
1593
 
1594
+ function writeStateFields(updates) {
1595
+ const path = join(ijfwHome(), 'state.json');
1596
+ withStateLockSync(() => {
1597
+ // Re-read INSIDE the lock so we don't merge against a stale snapshot.
1598
+ const state = Object.assign(readState(), updates);
1599
+ try {
1600
+ writeAtomic(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
1601
+ } catch (e) {
1602
+ // M11: persist failure is now visible AND surfaces which field would
1603
+ // not propagate. Re-entrancy guard relies on last_applied_version;
1604
+ // log explicitly so `ijfw doctor` / a user reading logs can spot it.
1605
+ console.error(`could not persist state.json (re-entrancy guard may not fire next session): ${e.message}`);
1606
+ }
1607
+ });
1608
+ }
1609
+
1327
1610
  function cmdUpdateCheck() {
1328
1611
  const state = readState();
1329
1612
  const current = state.installed_version || '0.0.0';
@@ -1370,6 +1653,19 @@ function cmdUpdateVerify() {
1370
1653
  } else {
1371
1654
  console.log(` provenance: NOT VERIFIED (audit signatures exited ${sig.status})`);
1372
1655
  }
1656
+ // Second factor: shasum cross-verify (F-SEC-7). Dry-run: report but don't
1657
+ // exit non-zero -- caller is asking "what would happen if I updated?", and
1658
+ // the interactive flow already fails closed on mismatch.
1659
+ const shasumDry = verifyShasumCrossSource(r.version);
1660
+ if (shasumDry.mode === 'verified') {
1661
+ console.log(` shasum cross-verify: VERIFIED (${shasumDry.npmShasum.slice(0, 12)}...)`);
1662
+ } else if (shasumDry.mode === 'mismatch') {
1663
+ console.log(` shasum cross-verify: MISMATCH (npm=${shasumDry.npmShasum} release=${shasumDry.releaseShasum}) -- install would be REFUSED`);
1664
+ } else if (shasumDry.mode === 'advisory') {
1665
+ console.log(` shasum cross-verify: ADVISORY (${shasumDry.message})`);
1666
+ } else {
1667
+ console.log(` shasum cross-verify: ERROR (${shasumDry.message})`);
1668
+ }
1373
1669
  console.log('Verification complete.');
1374
1670
  process.exit(0);
1375
1671
  }
@@ -1545,6 +1841,37 @@ function cmdUpdateInteractive(opts = {}) {
1545
1841
  return 1;
1546
1842
  }
1547
1843
  }
1844
+ // Shasum cross-verify (F-SEC-7): independent second factor on top of
1845
+ // npm-side signatures. Fetches the GitLab release asset shasum and
1846
+ // compares it against npm's dist.shasum for the same version. Mismatch
1847
+ // means we refuse to install; advisory (release shasum unavailable)
1848
+ // requires explicit --yes to proceed.
1849
+ const shasum = verifyShasumCrossSource(r.version);
1850
+ if (shasum.mode === 'verified') {
1851
+ console.log(` Shasum: verified (${shasum.npmShasum.slice(0, 12)}...)`);
1852
+ } else if (shasum.mode === 'mismatch') {
1853
+ console.error(' Shasum: MISMATCH -- refusing install.');
1854
+ console.error(` npm : ${shasum.npmShasum}`);
1855
+ console.error(` release : ${shasum.releaseShasum}`);
1856
+ console.error(' The npm tarball does NOT match the GitLab release asset.');
1857
+ console.error(' This could indicate a compromised registry or release. Aborting.');
1858
+ return 1;
1859
+ } else if (shasum.mode === 'error') {
1860
+ console.error(` Shasum: error -- ${shasum.message}`);
1861
+ console.error(' Refusing install: cannot establish second-factor integrity.');
1862
+ return 1;
1863
+ } else if (shasum.mode === 'advisory') {
1864
+ console.error(` Shasum: ADVISORY -- ${shasum.message}`);
1865
+ if (!opts.yes) {
1866
+ console.error(' Continuing requires --yes (acknowledge missing release shasum).');
1867
+ return 1;
1868
+ }
1869
+ console.error(' Proceeding due to --yes; release shasum could not be verified.');
1870
+ } else {
1871
+ // Unknown mode: fail closed.
1872
+ console.error(` Shasum: unknown mode "${shasum.mode}" -- refusing install.`);
1873
+ return 1;
1874
+ }
1548
1875
  // Method dispatch
1549
1876
  const method = state.install_method || 'manual';
1550
1877
  console.log(` install_method: ${method}`);
@@ -1637,8 +1964,17 @@ function cmdUpdateInteractive(opts = {}) {
1637
1964
  console.error(`Update did not complete (exit ${installRes.status}). State not written.`);
1638
1965
  return 1;
1639
1966
  }
1640
- // Persist both fields atomically -- single write avoids concurrent-reader inconsistency
1641
- writeStateFields({ last_applied_version: r.version, installed_version: r.version });
1967
+ // Persist both fields atomically -- single write avoids concurrent-reader inconsistency.
1968
+ // last_good_shasum records the shasum we just successfully cross-verified +
1969
+ // installed (per docs/SECURITY.md "last_good_shasum is a one-way 'what did
1970
+ // we actually install' record"). Only written when shasum was actually
1971
+ // verified (mode === 'verified'); advisory paths leave the previous value
1972
+ // so we don't poison the record with an unverified hash.
1973
+ const stateUpdate = { last_applied_version: r.version, installed_version: r.version };
1974
+ if (shasum && shasum.mode === 'verified' && shasum.npmShasum) {
1975
+ stateUpdate.last_good_shasum = shasum.npmShasum;
1976
+ }
1977
+ writeStateFields(stateUpdate);
1642
1978
  console.log('');
1643
1979
  console.log(`IJFW updated to v${r.version}. Run \`ijfw status\` to confirm.`);
1644
1980
  return 0;
@@ -2059,6 +2395,8 @@ if (isMainModule) {
2059
2395
  cmdDashboard(parsed.sub);
2060
2396
  } else if (parsed.cmd === 'design') {
2061
2397
  cmdDesign(parsed.sub);
2398
+ } else if (parsed.cmd === 'ui-review') {
2399
+ cmdUiReview(parsed).catch(err => { console.error(err.message); process.exit(1); });
2062
2400
  } else if (parsed.cmd === 'blackboard') {
2063
2401
  cmdBlackboard(parsed.sub);
2064
2402
  } else if (parsed.cmd === 'team') {
@@ -2158,6 +2496,69 @@ function openDesignUrl(url) {
2158
2496
  return res.status ?? 0;
2159
2497
  }
2160
2498
 
2499
+ // v1.5.0 wire-W1.D — `ijfw ui-review` production CLI. Wires the 7-pillar
2500
+ // audit runner into a user-facing command. Args:
2501
+ // --spec <UI-SPEC.md path> required
2502
+ // --scope <comma-sep dirs> required (e.g. "src,components")
2503
+ // --no-write skip writing UI-REVIEW.md (preview mode)
2504
+ // --gc-sketches run sketches-gc as the finalizer
2505
+ // --peer-inputs <json-path> optional axe / lighthouse / playwright
2506
+ // pre-computed outputs (JSON file)
2507
+ // --json machine-readable output (skip narrative)
2508
+ async function cmdUiReview(parsed) {
2509
+ if (!parsed.spec) {
2510
+ console.error('Usage: ijfw ui-review --spec <UI-SPEC.md> --scope <dirs> [--no-write] [--gc-sketches] [--peer-inputs <path>]');
2511
+ process.exit(1);
2512
+ }
2513
+ if (!parsed.scope) {
2514
+ console.error('ui-review: --scope is required (comma-separated dirs, e.g. "src,components")');
2515
+ process.exit(1);
2516
+ }
2517
+ const specPath = isAbsolute(parsed.spec) ? parsed.spec : resolve(process.cwd(), parsed.spec);
2518
+ if (!existsSync(specPath)) {
2519
+ console.error(`ui-review: UI-SPEC not found at ${specPath}`);
2520
+ process.exit(1);
2521
+ }
2522
+
2523
+ let peerInputs = {};
2524
+ if (parsed.peerInputs) {
2525
+ const ppath = isAbsolute(parsed.peerInputs) ? parsed.peerInputs : resolve(process.cwd(), parsed.peerInputs);
2526
+ try { peerInputs = JSON.parse(readFileSync(ppath, 'utf8')); }
2527
+ catch (err) {
2528
+ console.error(`ui-review: failed to read --peer-inputs JSON: ${err.message}`);
2529
+ process.exit(1);
2530
+ }
2531
+ }
2532
+
2533
+ const { runUiReview } = await import('./lib/ui-review-runner.js');
2534
+ const result = await runUiReview({
2535
+ uiSpecPath: specPath,
2536
+ sourceScope: parsed.scope,
2537
+ projectRoot: process.cwd(),
2538
+ peerInputs,
2539
+ write: parsed.write !== false,
2540
+ gcSketches: !!parsed.gcSketches,
2541
+ });
2542
+
2543
+ if (parsed.json) {
2544
+ console.log(JSON.stringify({
2545
+ topVerdict: result.topVerdict,
2546
+ pillarVerdicts: result.pillarVerdicts,
2547
+ reviewPath: result.reviewPath,
2548
+ parallel: result.parallel,
2549
+ }, null, 2));
2550
+ } else {
2551
+ console.log(`UI review: top-level ${result.topVerdict}`);
2552
+ for (const [pillar, verdict] of Object.entries(result.pillarVerdicts)) {
2553
+ console.log(` - ${pillar.padEnd(12)} ${verdict}`);
2554
+ }
2555
+ if (result.reviewPath) console.log(`Review written to: ${result.reviewPath}`);
2556
+ console.log(`Parallel grader run: wall=${result.parallel.wallMs}ms parallelism=${result.parallel.parallelism}`);
2557
+ }
2558
+ // Exit code: PASS=0, FLAG=0 (advisory), BLOCK=2 (ship-blocker)
2559
+ process.exit(result.topVerdict === 'BLOCK' ? 2 : 0);
2560
+ }
2561
+
2161
2562
  function cmdDesign(sub) {
2162
2563
  const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
2163
2564
  mkdirSync(contentDir, { recursive: true });
@@ -2369,8 +2770,11 @@ function cmdTeam(sub) {
2369
2770
  if (sub === 'init' || sub === 'create') {
2370
2771
  const archetype = optionValue(args, ['--archetype', '--type', '-t']);
2371
2772
  const teamName = optionValue(args, ['--name']);
2773
+ // F-FUN-1: --brief lets the Discovery hand-off carry domain signal
2774
+ // when filesystem detection would otherwise collapse to mixed/unknown.
2775
+ const brief = optionValue(args, ['--brief', '-b']) || '';
2372
2776
  const force = args.includes('--force');
2373
- const result = createTeamAssembly(process.cwd(), { archetype, teamName, force });
2777
+ const result = createTeamAssembly(process.cwd(), { archetype, teamName, brief, force });
2374
2778
  if (!result.ok) {
2375
2779
  if (result.error === 'exists') {
2376
2780
  console.error('Team assembly already exists. Re-run with --force to replace .ijfw/team/charter.json and workflow.json.');
@@ -2405,7 +2809,101 @@ function cmdTeam(sub) {
2405
2809
  process.exit(0);
2406
2810
  }
2407
2811
 
2408
- console.log('Usage: ijfw team init [--archetype <type>] [--name <team-name>] [--force] | status');
2812
+ // F-FUN-4 (audit-MED-teams-#7): ijfw team list -- enumerate roles.
2813
+ if (sub === 'list') {
2814
+ const result = listTeamRoles(process.cwd());
2815
+ if (!result.ok) {
2816
+ console.error(`team list failed: ${result.error}`);
2817
+ process.exit(1);
2818
+ }
2819
+ console.log(`Team: ${result.team_name || '(unnamed)'}`);
2820
+ if (result.project_archetypes.length) console.log(`Archetypes: ${result.project_archetypes.join(', ')}`);
2821
+ console.log(`Roles (${result.roles.length}):`);
2822
+ for (const role of result.roles) {
2823
+ console.log(` - ${role.name} [${role.role_type}] model=${role.model} effort=${role.effort}`);
2824
+ }
2825
+ process.exit(0);
2826
+ }
2827
+
2828
+ // F-FUN-4: ijfw team add <role-name> --charter <path>
2829
+ if (sub === 'add') {
2830
+ const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
2831
+ const charterPath = optionValue(args, ['--charter', '-c']);
2832
+ if (!charterPath) {
2833
+ console.error('Usage: ijfw team add <role-name> --charter <path-to-role.json>');
2834
+ process.exit(1);
2835
+ }
2836
+ const result = addTeamRole(process.cwd(), { charterPath });
2837
+ if (!result.ok) {
2838
+ console.error(`team add failed: ${result.error}`);
2839
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2840
+ process.exit(1);
2841
+ }
2842
+ console.log(`Added role: ${result.role.name}`);
2843
+ if (name && name !== result.role.name) {
2844
+ console.log(` note: charter declared name "${result.role.name}", ignoring CLI argument "${name}"`);
2845
+ }
2846
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2847
+ process.exit(0);
2848
+ }
2849
+
2850
+ // F-FUN-4: ijfw team remove <role-name>
2851
+ if (sub === 'remove' || sub === 'rm') {
2852
+ const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
2853
+ if (!name) {
2854
+ console.error('Usage: ijfw team remove <role-name>');
2855
+ process.exit(1);
2856
+ }
2857
+ const result = removeTeamRole(process.cwd(), { name });
2858
+ if (!result.ok) {
2859
+ console.error(`team remove failed: ${result.error}`);
2860
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2861
+ process.exit(1);
2862
+ }
2863
+ console.log(`Removed role: ${result.removed}`);
2864
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2865
+ process.exit(0);
2866
+ }
2867
+
2868
+ // F-FUN-4: ijfw team swap <old-role-name> --charter <path>
2869
+ if (sub === 'swap') {
2870
+ const oldName = args[0] && !args[0].startsWith('--') ? args[0] : null;
2871
+ const charterPath = optionValue(args, ['--charter', '-c']);
2872
+ if (!oldName || !charterPath) {
2873
+ console.error('Usage: ijfw team swap <old-role-name> --charter <path-to-role.json>');
2874
+ process.exit(1);
2875
+ }
2876
+ const result = swapTeamRole(process.cwd(), { oldName, charterPath });
2877
+ if (!result.ok) {
2878
+ console.error(`team swap failed: ${result.error}`);
2879
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2880
+ process.exit(1);
2881
+ }
2882
+ console.log(`Swapped ${result.swapped.old} -> ${result.swapped.new}`);
2883
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2884
+ process.exit(0);
2885
+ }
2886
+
2887
+ // F-FUN-5 (audit-MED-teams-#13): ijfw team check -- standalone validator.
2888
+ if (sub === 'check' || sub === 'validate') {
2889
+ const report = checkTeamAssembly(process.cwd());
2890
+ if (report.ok) {
2891
+ console.log(`Team assembly OK: ${report.role_count} role(s), ${report.artifact_count} artifact(s)`);
2892
+ process.exit(0);
2893
+ }
2894
+ console.error('Team assembly has issues:');
2895
+ if (!report.has_charter || !report.charter.ok) {
2896
+ console.error(' charter.json:');
2897
+ for (const err of report.charter.errors) console.error(` - ${err}`);
2898
+ }
2899
+ if (!report.has_workflow || !report.workflow.ok) {
2900
+ console.error(' workflow.json:');
2901
+ for (const err of report.workflow.errors) console.error(` - ${err}`);
2902
+ }
2903
+ process.exit(1);
2904
+ }
2905
+
2906
+ console.log('Usage: ijfw team init [--archetype <type>] [--name <team-name>] [--force] | status | list | add <role> --charter <path> | remove <role> | swap <old> --charter <path> | check');
2409
2907
  process.exit(1);
2410
2908
  }
2411
2909
 
@@ -2730,7 +3228,45 @@ function cmdSwarm(sub) {
2730
3228
  process.exit(0);
2731
3229
  }
2732
3230
 
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');
3231
+ // F-REL-1 (H5.3): orphan eviction. Walks active claims and releases any
3232
+ // whose freshness anchor (claimed_at or heartbeat_at) is older than the
3233
+ // TTL. Default TTL is 30 minutes; override with --ttl-min N.
3234
+ if (sub === 'evict-orphans' || sub === 'evict') {
3235
+ const minRaw = optionValue(args, ['--ttl-min', '--ttl', '-t']);
3236
+ const min = Number.parseFloat(minRaw);
3237
+ const ttlMs = Number.isFinite(min) && min > 0 ? Math.floor(min * 60 * 1000) : DEFAULT_CLAIM_TTL_MS;
3238
+ const result = evictOrphanedClaims(process.cwd(), { ttlMs });
3239
+ if (!result.ok) {
3240
+ console.log(`Swarm evict halted: ${result.error}`);
3241
+ process.exit(1);
3242
+ }
3243
+ console.log(`Evicted ${result.count} orphan claim(s) (TTL ${Math.round(ttlMs / 60000)}min)`);
3244
+ for (const item of result.evicted) {
3245
+ console.log(` ${item.id} (${item.agent} -> ${item.artifact_id}, age ${Math.round(item.age_ms / 1000)}s)`);
3246
+ }
3247
+ process.exit(0);
3248
+ }
3249
+
3250
+ // F-REL-1 (H5.3): manual heartbeat ping. Subagents normally invoke this
3251
+ // via the programmatic surface (updateClaimHeartbeat); the CLI form is
3252
+ // for forensic dogfooding from tests + the dashboard preview.
3253
+ if (sub === 'heartbeat') {
3254
+ const taskOrClaim = args[0];
3255
+ const owner = optionValue(args.slice(1), ['--owner', '-o']);
3256
+ const claimId = optionValue(args.slice(1), ['--claim', '--id']);
3257
+ const input = claimId
3258
+ ? { claim_id: claimId }
3259
+ : { artifact_id: taskOrClaim, agent: owner };
3260
+ const result = updateClaimHeartbeat(process.cwd(), input);
3261
+ if (!result.ok) {
3262
+ console.log(`Heartbeat halted: ${result.error}`);
3263
+ process.exit(1);
3264
+ }
3265
+ console.log(`Heartbeat ${result.claim.id} at ${result.claim.heartbeat_at}`);
3266
+ process.exit(0);
3267
+ }
3268
+
3269
+ 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 | evict-orphans [--ttl-min N] | heartbeat <artifact-id> --owner <agent>');
2734
3270
  process.exit(1);
2735
3271
  }
2736
3272