@ijfw/memory-server 1.4.4 → 1.5.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 (245) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  23. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  24. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  26. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  27. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  28. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  29. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  47. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  48. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  49. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  50. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  52. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  53. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  54. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  68. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  69. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  71. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  72. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  73. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  74. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  92. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  93. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  94. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  95. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  96. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  97. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  98. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  99. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  115. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  116. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  117. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  118. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  119. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  120. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  121. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  122. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  123. package/package.json +6 -3
  124. package/src/active-extension-writer.js +144 -64
  125. package/src/api-client.js +43 -5
  126. package/src/audit-roster.js +80 -5
  127. package/src/blackboard.js +298 -6
  128. package/src/cli-run.js +33 -5
  129. package/src/codex-agents.js +96 -5
  130. package/src/cost/aggregator.js +39 -9
  131. package/src/cost/pricing.js +57 -0
  132. package/src/cost/readers/gemini.js +1 -1
  133. package/src/cross-audit-chunker.js +189 -0
  134. package/src/cross-dispatcher.js +124 -21
  135. package/src/cross-orchestrator-cli.js +754 -159
  136. package/src/cross-orchestrator.js +1065 -17
  137. package/src/cross-project-search.js +195 -9
  138. package/src/dashboard-client-waves.html +304 -0
  139. package/src/dashboard-client.html +5 -1
  140. package/src/dashboard-server.js +73 -0
  141. package/src/deploy-alerts.js +150 -0
  142. package/src/design/iframe-bridge.js +242 -0
  143. package/src/design-companion.js +144 -0
  144. package/src/dispatch/checkpoint-cli.js +97 -0
  145. package/src/dispatch/colon-syntax.js +81 -1
  146. package/src/dispatch/extension.js +26 -2
  147. package/src/dispatch/registry-cli.js +4 -1
  148. package/src/dispatch/wave-cli.js +201 -6
  149. package/src/dispatch/worktree-cli.js +40 -0
  150. package/src/dispatch-planner.js +97 -2
  151. package/src/dream/runner.mjs +47 -11
  152. package/src/dream/stage-runner.js +40 -0
  153. package/src/dream/state-file.js +102 -0
  154. package/src/extension-installer.js +70 -24
  155. package/src/extension-quota-tracker.js +4 -2
  156. package/src/extension-registry.js +289 -35
  157. package/src/feedback-detector.js +26 -0
  158. package/src/fs-lock.js +259 -7
  159. package/src/gate-result.js +95 -1
  160. package/src/hardware-signer.js +4 -2
  161. package/src/hero-line.js +86 -5
  162. package/src/intent-router.js +35 -0
  163. package/src/lib/a11y-contract.js +117 -0
  164. package/src/lib/atomic-io.js +29 -8
  165. package/src/lib/cache-keepalive.js +150 -0
  166. package/src/lib/jsonl-rotation.js +104 -0
  167. package/src/lib/lighthouse-pillar.js +121 -0
  168. package/src/lib/llm-call.js +121 -0
  169. package/src/lib/playwright-baseline.js +205 -0
  170. package/src/lib/rekor-bridge.js +221 -0
  171. package/src/lib/repo-map.js +392 -0
  172. package/src/lib/shasum-verify.js +164 -0
  173. package/src/lib/sketches-gc.js +132 -0
  174. package/src/lib/tmp-suffix.js +62 -0
  175. package/src/lib/ui-review-runner.js +595 -0
  176. package/src/lib/uispec-drift.js +301 -0
  177. package/src/lib/uispec-intake.js +381 -0
  178. package/src/lib/worktree-guards.js +118 -0
  179. package/src/lib/worktree-recovery.js +100 -0
  180. package/src/memory/auto-linker.js +267 -0
  181. package/src/memory/benchmark.js +498 -0
  182. package/src/memory/dedup.js +126 -0
  183. package/src/memory/embedding-cache.js +136 -0
  184. package/src/memory/fact-extractor.js +168 -0
  185. package/src/memory/fts5.js +65 -1
  186. package/src/memory/migration-runner.js +6 -1
  187. package/src/memory/migrations/004-bitemporal.js +91 -0
  188. package/src/memory/migrations/005-vector-cache.js +61 -0
  189. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  190. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  191. package/src/memory/migrations/008-write-provenance.js +41 -0
  192. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  193. package/src/memory/obsidian-parser.js +152 -0
  194. package/src/memory/query-dataview.js +86 -0
  195. package/src/memory/search.js +46 -15
  196. package/src/memory/temporal.js +529 -0
  197. package/src/memory/tokenize.js +10 -0
  198. package/src/memory-facts-handler.js +37 -0
  199. package/src/memory-feedback.js +260 -2
  200. package/src/model-refresh.js +292 -0
  201. package/src/observability/cost-anomaly.js +166 -0
  202. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  203. package/src/observability/trace-id.js +163 -0
  204. package/src/orchestrator/agents-md-blackboard.js +152 -0
  205. package/src/orchestrator/checkpoint-contract.md +140 -0
  206. package/src/orchestrator/debug-trident-trigger.js +374 -0
  207. package/src/orchestrator/debug-trident.js +570 -0
  208. package/src/orchestrator/merge-block-aware.js +350 -0
  209. package/src/orchestrator/plan-checker.js +475 -0
  210. package/src/orchestrator/post-done-runner.js +277 -0
  211. package/src/orchestrator/review.js +38 -3
  212. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  213. package/src/orchestrator/skill-telemetry.js +37 -0
  214. package/src/orchestrator/state-events.js +459 -0
  215. package/src/orchestrator/state-sdk.js +1932 -0
  216. package/src/orchestrator/status-protocol.js +84 -17
  217. package/src/orchestrator/subagent-telemetry.js +471 -0
  218. package/src/orchestrator/termination.js +160 -0
  219. package/src/orchestrator/verification-gate.js +200 -16
  220. package/src/orchestrator/wave-state.js +332 -23
  221. package/src/orchestrator/worktree-provision.js +77 -0
  222. package/src/override-resolver.js +5 -3
  223. package/src/override-use-registry.js +111 -5
  224. package/src/receipts.js +36 -4
  225. package/src/recovery/checkpoint.js +56 -3
  226. package/src/recovery/code-fixer.js +961 -0
  227. package/src/recovery/truncation.js +317 -0
  228. package/src/redactor.js +75 -6
  229. package/src/runtime-mediator.js +15 -1
  230. package/src/sanitizer.js +10 -0
  231. package/src/search-hybrid.js +139 -0
  232. package/src/server.js +795 -112
  233. package/src/swarm/worktree.js +27 -4
  234. package/src/swarm-config.js +102 -17
  235. package/src/team/domain-templates/book.json +51 -0
  236. package/src/team/domain-templates/business.json +44 -0
  237. package/src/team/domain-templates/content.json +50 -0
  238. package/src/team/domain-templates/design.json +44 -0
  239. package/src/team/domain-templates/research.json +44 -0
  240. package/src/team/domain-templates/software.json +40 -0
  241. package/src/team/generator.js +440 -3
  242. package/src/team/modify.js +203 -0
  243. package/src/team/schemas.js +48 -0
  244. package/src/update-apply.js +19 -3
  245. package/src/dashboard-charts.js +0 -239
@@ -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,
@@ -50,6 +62,8 @@ import {
50
62
  } from './swarm/worktree.js';
51
63
  import { renderSwarmDispatchPrompt } from './swarm/dispatch-prompt.js';
52
64
  import { syncCodexAgents } from './codex-agents.js';
65
+ // v1.5.1 W2.H — memory benchmark harness (T22). Surfaced via `ijfw metrics --benchmark`.
66
+ import { runBenchmark } from './memory/benchmark.js';
53
67
  import {
54
68
  DESIGN_ACTIONS,
55
69
  auditDesignText,
@@ -57,6 +71,12 @@ import {
57
71
  initialDesignMarkdown,
58
72
  loadDesignContext,
59
73
  } from './design-intelligence.js';
74
+ // v1.5.1 W3.A.4 — registry-driven usage + alias help.
75
+ // SINGLE SOURCE OF TRUTH lives at installer/src/command-registry.js;
76
+ // import sibling-package style, matching extension-installer.js precedent.
77
+ import {
78
+ COMMAND_REGISTRY,
79
+ } from '../../installer/src/command-registry.js';
60
80
 
61
81
  // ---------------------------------------------------------------------------
62
82
  // Auditor error translator (1.2.5)
@@ -207,84 +227,37 @@ function parseArgs(argv) {
207
227
  return out;
208
228
  }
209
229
 
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
- };
230
+ // v1.5.1 W3.A.4 — COMMAND_ALIAS_HELP is now derived from the command-registry
231
+ // (entries where tier === 'pointer-stub'). Each entry's deprecatedReason
232
+ // becomes the usage line; title is auto-derived from the canonical name.
233
+ // To change the help text or add a new pointer-stub: edit
234
+ // installer/src/command-registry.js.
235
+ const COMMAND_ALIAS_HELP = Object.freeze(Object.fromEntries(
236
+ COMMAND_REGISTRY
237
+ .filter(e => e.tier === 'pointer-stub')
238
+ .map(e => [e.name, {
239
+ title: `IJFW ${e.name}`,
240
+ usage: e.deprecatedReason,
241
+ }])
242
+ ));
272
243
 
273
244
  function parseCrossAlias(mode, args) {
274
245
  let only = null;
275
246
  let confirm = false;
276
247
  let expand = false;
248
+ let chunk = false;
277
249
  const positional = [];
278
250
  for (let i = 1; i < args.length; i++) {
279
251
  const arg = args[i];
280
252
  if (arg === '--confirm') confirm = true;
281
253
  else if (arg === '--expand') expand = true;
254
+ else if (arg === '--chunk') chunk = true; // v1.5.1 H1.6 — wire chunker
282
255
  else if (arg === '--with' && args[i + 1]) only = args[++i];
283
256
  else if (arg.startsWith('--with=')) only = arg.slice('--with='.length);
284
257
  else if (!arg.startsWith('--')) positional.push(arg);
285
258
  }
286
259
  const target = mode === 'research' ? positional.join(' ').trim() : positional[0];
287
- return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand };
260
+ return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand, chunk };
288
261
  }
289
262
 
290
263
  function parseCommandAlias(args) {
@@ -299,6 +272,16 @@ function parseCommandAlias(args) {
299
272
  return parseCrossAlias('research', args);
300
273
  }
301
274
  if (Object.prototype.hasOwnProperty.call(COMMAND_ALIAS_HELP, name)) {
275
+ // v1.5.1 W2.H — `ijfw metrics` is a deprecated pointer-stub, but
276
+ // `ijfw metrics --benchmark` surfaces the memory benchmark harness (T22).
277
+ // Bare `ijfw metrics` still falls through to the deprecation redirect.
278
+ if (name === 'metrics' && args.includes('--benchmark')) {
279
+ const opts = { cmd: 'metrics-benchmark', json: args.includes('--json'), write: true };
280
+ for (let i = 1; i < args.length; i++) {
281
+ if (args[i] === '--no-write') opts.write = false;
282
+ }
283
+ return opts;
284
+ }
302
285
  return { cmd: 'command-alias', alias: name };
303
286
  }
304
287
  return null;
@@ -396,6 +379,23 @@ function parseArgsInner(args) {
396
379
  return { cmd: 'design', sub: args[1] || 'status' };
397
380
  }
398
381
 
382
+ // v1.5.0 wire-W1.D — `ijfw ui-review --spec <path> --scope <dirs>`
383
+ if (args[0] === 'ui-review') {
384
+ const opts = { cmd: 'ui-review', spec: null, scope: null, write: true, gcSketches: false, peerInputs: null };
385
+ for (let i = 1; i < args.length; i++) {
386
+ const a = args[i];
387
+ if ((a === '--spec' || a === '-s') && args[i + 1]) { opts.spec = args[++i]; }
388
+ else if (a.startsWith('--spec=')) opts.spec = a.slice('--spec='.length);
389
+ else if ((a === '--scope' || a === '-S') && args[i + 1]) { opts.scope = args[++i]; }
390
+ else if (a.startsWith('--scope=')) opts.scope = a.slice('--scope='.length);
391
+ else if (a === '--no-write') opts.write = false;
392
+ else if (a === '--gc-sketches') opts.gcSketches = true;
393
+ else if ((a === '--peer-inputs') && args[i + 1]) { opts.peerInputs = args[++i]; }
394
+ else if (a.startsWith('--peer-inputs=')) opts.peerInputs = a.slice('--peer-inputs='.length);
395
+ }
396
+ return opts;
397
+ }
398
+
399
399
  if (args[0] === 'blackboard') {
400
400
  return { cmd: 'blackboard', sub: args[1] || 'status' };
401
401
  }
@@ -418,8 +418,25 @@ function parseArgsInner(args) {
418
418
  return { cmd: 'codex', sub: args[1] || 'doctor' };
419
419
  }
420
420
 
421
- if (args[0] === 'memory' && args[1] === 'checkpoint') {
422
- return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
421
+ if (args[0] === 'memory') {
422
+ // `ijfw memory` / `ijfw memory --help` / `ijfw memory -h` → namespace help
423
+ if (args.length === 1 || args[1] === '--help' || args[1] === '-h') {
424
+ return { cmd: 'memory-help' };
425
+ }
426
+ if (args[1] === 'checkpoint') {
427
+ return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
428
+ }
429
+ if (args[1] === 'reindex') {
430
+ // `ijfw memory reindex` -> M1 backfill (free, obsidian indexing)
431
+ // `ijfw memory reindex --m2` -> also run M2 A-Mem auto-link backfill
432
+ // (budget-gated; needs IJFW_AUTOLINK_*)
433
+ let m2 = false;
434
+ for (let i = 2; i < args.length; i++) {
435
+ if (args[i] === '--m2' || args[i] === '--autolink') m2 = true;
436
+ }
437
+ return { cmd: 'memory-reindex', m2 };
438
+ }
439
+ return { cmd: 'memory-unknown', sub: args[1] };
423
440
  }
424
441
 
425
442
  if (args[0] === 'recover') {
@@ -464,14 +481,16 @@ function parseArgsInner(args) {
464
481
  let only = null;
465
482
  let confirm = false;
466
483
  let expand = false;
484
+ let chunk = false;
467
485
 
468
486
  for (let i = 3; i < args.length; i++) {
469
487
  if (args[i] === '--confirm') { confirm = true; }
470
488
  else if (args[i] === '--expand') { expand = true; }
489
+ else if (args[i] === '--chunk') { chunk = true; } // v1.5.1 H1.6 — wire chunker
471
490
  else if (args[i] === '--with' && args[i + 1]) { only = args[++i]; }
472
491
  }
473
492
 
474
- return { cmd: 'cross', mode, target, only, confirm, expand };
493
+ return { cmd: 'cross', mode, target, only, confirm, expand, chunk };
475
494
  }
476
495
 
477
496
  return { cmd: 'unknown', raw: args[0] };
@@ -481,86 +500,32 @@ function parseArgsInner(args) {
481
500
  // Commands
482
501
  // ---------------------------------------------------------------------------
483
502
 
484
- function printUsage() {
503
+ function printMemoryHelp() {
485
504
  console.log(`
486
- ijfw -- It Just Fucking Works CLI
487
- Fire 2-4 AIs at any target. Receipts logged. Cache hits tracked. Memory follows you.
505
+ ijfw memory -- project memory namespace
488
506
 
489
507
  Usage:
490
- ijfw install
491
- ijfw uninstall
492
- ijfw preflight
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]
502
- ijfw cross <mode> <target> [options]
503
- ijfw cross project-audit <rule-file> [--dry-run]
504
- ijfw import <tool> [--all] [--dry-run] [--force] [--path <p>]
505
- ijfw status
506
- ijfw doctor
507
- ijfw update
508
- ijfw receipt last
509
- ijfw --purge-receipts
510
- ijfw --help
511
-
512
- Commands:
513
- install Install IJFW into your AI coding agents.
514
- uninstall Remove IJFW and revert AI-agent configs. Same as: ijfw off
515
- preflight Run the 11-gate quality pipeline (blocking + advisory).
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.
522
- demo 30-second live tour of the Trident (fires real auditors).
523
- cross Fire external auditors at a target. Try: ijfw cross audit README.md
524
- import Pull memory in from another tool. Try: ijfw import claude-mem --all
525
- status Show recent cross-audit activity. Try: ijfw status
526
- doctor Probe which CLIs and API keys are reachable. Try: ijfw doctor
527
- update Pull latest IJFW + reinstall merge-safely. Try: ijfw update
528
- update --check Non-invasive check. Exits 0 always; prints "update-available: <ver>" when an update exists (grep-safe).
529
- receipt last Print a redacted, shareable block from the last Trident run.
530
- --purge-receipts Clear the cross-runs receipt log. Try: ijfw --purge-receipts
531
-
532
- Modes (for ijfw cross):
533
- audit Adversarial review of a file, module, or path
534
- research Multi-source research on a topic
535
- critique Structured counter-argument generation
536
- project-audit Run the same audit across every registered IJFW project
537
- Usage: ijfw cross project-audit <rule-file> [--dry-run]
538
-
539
- Options for ijfw cross:
540
- --with <id> Force a specific auditor (comma-separated for multiple)
541
- --confirm Prompt for confirmation before firing
542
- --expand Include extended swarm when available
543
-
544
- Global flags:
545
- --json Emit JSON instead of human output. status and doctor auto-JSON
546
- on non-TTY (gh-CLI convention); version stays one-line on pipe
547
- and only JSON-ifies with explicit --json. Other commands ignore.
548
-
549
- Environment:
550
- IJFW_AUDIT_BUDGET_USD Session spend cap (default $2.00). First call is always
551
- allowed (no cap). Cap enforced from the 2nd call on.
552
-
553
- Examples:
554
- ijfw demo
555
- ijfw cross audit README.md
556
- ijfw cross research "vector search approaches"
557
- ijfw cross critique HEAD~3..HEAD
558
- ijfw cross audit CLAUDE.md --with codex,gemini
559
- ijfw status
560
- ijfw doctor
508
+ ijfw memory checkpoint <label> Snapshot current swarm/memory state under <label>.
509
+ <label> defaults to "manual" if omitted.
510
+ ijfw memory reindex [--m2] Backfill M1 obsidian indexing (wikilinks,
511
+ #tags, [k:: v] metadata) over the whole
512
+ memory db. Free + idempotent. Add --m2 to
513
+ also run the A-Mem auto-link backfill --
514
+ budget-gated (set IJFW_AUTOLINK_BUDGET_USD
515
+ and IJFW_AUTOLINK_BACKFILL=1).
516
+
517
+ Related:
518
+ ijfw recover [status|latest] Inspect checkpoints and recovery state.
519
+ ijfw --help Top-level user-facing commands.
520
+ ijfw commands Full command surface (all verbs).
561
521
  `.trim());
562
522
  }
563
523
 
524
+ function printUnknownCommand(raw) {
525
+ console.error(`Unknown command: ${raw}`);
526
+ console.error('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
527
+ }
528
+
564
529
  function cmdCommandAlias(alias) {
565
530
  const info = COMMAND_ALIAS_HELP[alias];
566
531
  if (!info) {
@@ -573,6 +538,41 @@ function cmdCommandAlias(alias) {
573
538
  process.exit(0);
574
539
  }
575
540
 
541
+ // v1.5.1 W2.H — `ijfw metrics --benchmark`: run the memory benchmark harness
542
+ // (T22) against IJFW's own 3-tier store and report recall@k, MRR/NDCG-style
543
+ // retrieval quality, throughput, and p50/p95/p99 latency. See
544
+ // docs/MEMORY-BENCHMARK.md for the axes and how to interpret the numbers.
545
+ async function cmdMetricsBenchmark(opts = {}) {
546
+ const results = await runBenchmark({
547
+ root: process.cwd(),
548
+ write: opts.write !== false,
549
+ });
550
+
551
+ if (wantsJson(opts)) {
552
+ emitJson(results);
553
+ return;
554
+ }
555
+
556
+ const q = results.axes.query_warm_fts5;
557
+ const ing = results.axes.ingest;
558
+ const recallPairs = Object.entries(q.recall || {});
559
+ console.log('IJFW memory benchmark (T22)');
560
+ console.log('');
561
+ console.log(` corpus ${results.corpus.docs} docs / ${results.corpus.queries} queries / ${results.corpus.total_query_samples} timed samples`);
562
+ console.log(` ingest throughput ${ing.throughput_rps} rows/s`);
563
+ console.log(` ingest latency p50 ${ing.latency_ms.p50}ms p95 ${ing.latency_ms.p95}ms p99 ${ing.latency_ms.p99}ms`);
564
+ console.log(` query latency p50 ${q.latency_ms.p50}ms p95 ${q.latency_ms.p95}ms p99 ${q.latency_ms.p99}ms`);
565
+ console.log(` recall ${recallPairs.map(([k, v]) => `${k}=${v.toFixed(3)}`).join(' ')}`);
566
+ console.log(` storage ${results.axes.storage.bytes_per_memory} bytes/memory (${results.axes.storage.rows_indexed} rows)`);
567
+ console.log(` cold tier ${results.axes.query_cold_vector.available ? 'available' : 'reserved (no embedding model)'}`);
568
+ if (results.artifact_path) {
569
+ console.log('');
570
+ console.log(` artifact ${results.artifact_path}`);
571
+ }
572
+ console.log('');
573
+ console.log('Axes explained: docs/MEMORY-BENCHMARK.md');
574
+ }
575
+
576
576
  async function cmdStatus(projectDir, opts = {}) {
577
577
  const receipts = readReceipts(projectDir);
578
578
  const last = receipts[receipts.length - 1];
@@ -663,8 +663,6 @@ async function cmdDemo() {
663
663
 
664
664
  let result;
665
665
  try {
666
- // TODO post-merge: perAuditorTimeoutSec, minResponses, quiet are added by Item 2 agent.
667
- // Passed through here; current orchestrator silently ignores unknown params.
668
666
  result = await runCrossOp({
669
667
  mode: 'audit',
670
668
  target,
@@ -877,6 +875,15 @@ function cmdPurgeReceipts(projectDir) {
877
875
  // ranges, and non-existent paths pass through unchanged.
878
876
  const TARGET_FILE_SIZE_CAP = 64 * 1024; // 64 KB -- leaves prompt headroom
879
877
 
878
+ // r17.1 — size thresholds for pre-flight advisory. Inputs under WARN are
879
+ // silent; WARN..CAP get a one-line "this might be slow" advisory; CAP..MAX
880
+ // get a "this WILL be truncated, consider chunking" warning before we fire
881
+ // any auditor (so the user can cancel rather than burn wall time). Anything
882
+ // over MAX would be silently truncated by resolveTarget anyway — the
883
+ // pre-flight check makes that loud.
884
+ const TARGET_FILE_SIZE_WARN = 32 * 1024;
885
+ const TARGET_FILE_SIZE_MAX = 256 * 1024; // beyond this, advise chunking explicitly
886
+
880
887
  export function resolveTarget(raw, opts = {}) {
881
888
  const cap = typeof opts.sizeCap === 'number' ? opts.sizeCap : TARGET_FILE_SIZE_CAP;
882
889
  if (typeof raw !== 'string' || !raw) return raw;
@@ -921,7 +928,44 @@ export function resolveTarget(raw, opts = {}) {
921
928
  return `File: ${raw}\n\n${contents}`;
922
929
  }
923
930
 
924
- async function cmdCross({ mode, target, only, confirm, expand }) {
931
+ // v1.5.1 H1.6 chunked-dispatch helpers (audit finding token-optimization.md
932
+ // HIGH-H4 + trident.md HIGH-1, 2/2 consensus). The chunker has shipped (with
933
+ // tests) since r17.1 but was never wired into the CLI. `--chunk` now triggers
934
+ // per-chunk dispatch through runCrossOp + a final Jaccard-dedupe merge.
935
+
936
+ /**
937
+ * Decide whether a target absolute path is large enough to benefit from
938
+ * chunking. Exported for unit-tests.
939
+ */
940
+ export function shouldChunkFile(absPath, opts = {}) {
941
+ const threshold = typeof opts.threshold === 'number' ? opts.threshold : TARGET_FILE_SIZE_CAP;
942
+ try {
943
+ const st = statSync(absPath);
944
+ if (!st.isFile()) return false;
945
+ return st.size > threshold;
946
+ } catch {
947
+ return false;
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Read a file and produce the per-chunk target strings the auditors will see.
953
+ * Each chunk is annotated with its index so cross-chunk findings can be
954
+ * reconciled. Exported for unit-tests.
955
+ */
956
+ export function buildChunkedTargets(absPath, rawTarget, opts = {}) {
957
+ const content = readFileSync(absPath, 'utf8');
958
+ const chunks = chunkText(content, opts);
959
+ return chunks.map((c, i) => ({
960
+ chunkIndex: i,
961
+ total: chunks.length,
962
+ bytesStart: c.start,
963
+ bytesEnd: c.end,
964
+ target: `File: ${rawTarget} [chunk ${i + 1}/${chunks.length}, bytes ${c.start}-${c.end}]\n\n${c.text}`,
965
+ }));
966
+ }
967
+
968
+ async function cmdCross({ mode, target, only, confirm, expand, chunk }) {
925
969
  const VALID_MODES = ['audit', 'research', 'critique'];
926
970
  if (!mode || !VALID_MODES.includes(mode)) {
927
971
  console.error(`ijfw cross requires a mode: ${VALID_MODES.join(', ')}. Example: ijfw cross audit <file>`);
@@ -935,6 +979,114 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
935
979
  // Issue #6 fix: substitute file contents for path string when target is a
936
980
  // regular file. Keep the raw target for the user-facing echo line.
937
981
  const rawTarget = target;
982
+
983
+ // r17.1 — pre-flight size advisory. Run BEFORE resolveTarget truncates,
984
+ // so the user sees the real number and can decide to abort + chunk the
985
+ // input themselves rather than getting a silently-truncated audit.
986
+ // r17-M3: resolve relative paths against cwd FIRST, matching the same
987
+ // resolution resolveTarget() uses. Without this, `ijfw cross audit foo.md`
988
+ // (a relative path that exists) would skip the advisory because
989
+ // existsSync(rawTarget) probes against the wrong cwd-anchor.
990
+ try {
991
+ let probePath = null;
992
+ if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
993
+ const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
994
+ if (existsSync(resolved)) probePath = resolved;
995
+ }
996
+ if (probePath) {
997
+ const st = statSync(probePath);
998
+ if (st.isFile()) {
999
+ if (st.size > TARGET_FILE_SIZE_MAX) {
1000
+ console.log('');
1001
+ 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.`);
1002
+ 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\`).`);
1003
+ } else if (st.size > TARGET_FILE_SIZE_CAP) {
1004
+ console.log('');
1005
+ 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.`);
1006
+ } else if (st.size > TARGET_FILE_SIZE_WARN) {
1007
+ console.log('');
1008
+ console.log(`Note: target is ${(st.size / 1024).toFixed(1)} KB; expect a slower wall time (gemini in particular may push the 90s budget).`);
1009
+ }
1010
+ }
1011
+ }
1012
+ } catch { /* statSync failure is non-fatal; size advisory is best-effort */ }
1013
+
1014
+ // v1.5.1 H1.6 — chunked dispatch path. When --chunk is set AND the target
1015
+ // is a file larger than the size cap, split via the cross-audit-chunker
1016
+ // (boundary-aware, 10% overlap, Jaccard-dedupe merge) and dispatch each
1017
+ // chunk through runCrossOp separately. Merge findings at the end. Opt-in
1018
+ // because cost scales linearly with chunk count.
1019
+ if (chunk) {
1020
+ let absPath = null;
1021
+ try {
1022
+ if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
1023
+ const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
1024
+ if (existsSync(resolved) && statSync(resolved).isFile()) absPath = resolved;
1025
+ }
1026
+ } catch { /* */ }
1027
+
1028
+ if (!absPath) {
1029
+ console.log('');
1030
+ console.log('--chunk requires a file target. Topics, git ranges, and missing paths cannot be chunked.');
1031
+ // Fall through to normal path -- --chunk silently no-ops for non-files
1032
+ } else if (!shouldChunkFile(absPath)) {
1033
+ console.log('');
1034
+ 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.`);
1035
+ // Fall through to normal path
1036
+ } else {
1037
+ const chunks = buildChunkedTargets(absPath, rawTarget);
1038
+ console.log('');
1039
+ 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).`);
1040
+ console.log(`Trident dispatches: ${chunks.length} × per-chunk audit. Cost scales linearly.`);
1041
+
1042
+ const perChunkResults = [];
1043
+ const auditorIds = new Set();
1044
+ const projectDir = process.cwd();
1045
+ let firedAny = false;
1046
+ for (const { chunkIndex, total, target: chunkTarget } of chunks) {
1047
+ console.log('');
1048
+ console.log(`[chunk ${chunkIndex + 1}/${total}] dispatching...`);
1049
+ try {
1050
+ const r = await runCrossOp({
1051
+ mode, target: chunkTarget, projectDir,
1052
+ runStamp: new Date().toISOString(), only, confirm, expand,
1053
+ });
1054
+ const findings = Array.isArray(r.merged) ? r.merged : [];
1055
+ perChunkResults.push({ chunkIndex, findings });
1056
+ for (const p of (r.picks || [])) auditorIds.add(p.id);
1057
+ if ((r.picks || []).length > 0) firedAny = true;
1058
+ console.log(`[chunk ${chunkIndex + 1}/${total}] ${findings.length} finding(s) from ${(r.picks || []).length} auditor(s).`);
1059
+ } catch (err) {
1060
+ console.log(`[chunk ${chunkIndex + 1}/${total}] dispatch error: ${err.message}`);
1061
+ perChunkResults.push({ chunkIndex, findings: [] });
1062
+ }
1063
+ }
1064
+
1065
+ const merged = mergeFindings(perChunkResults);
1066
+ console.log('');
1067
+ console.log(`=== Chunked audit complete: ${merged.length} unique finding(s) across ${chunks.length} chunks ===`);
1068
+ console.log(`Auditors fired (union): ${[...auditorIds].join(', ') || '(none)'}`);
1069
+ if (!firedAny) {
1070
+ console.log('No auditors fired -- run `ijfw doctor` to see the install hints.');
1071
+ process.exit(2); // r17.1 — degraded exit code
1072
+ }
1073
+ for (const f of merged) {
1074
+ const sev = (f.severity || 'note').toUpperCase();
1075
+ const cluster = f.clusterSize > 1 ? ` [×${f.clusterSize}]` : '';
1076
+ const tgt = f.target ? ` ${f.target} —` : '';
1077
+ // v1.5.0 wire-W4: widen field fallback to cover description/issue/
1078
+ // detail/note/summary keys auditors emit. Closes the r19 "(no detail)"
1079
+ // dropout that made adjudication a guessing game.
1080
+ const text = f.finding || f.text || f.message ||
1081
+ f.description || f.issue ||
1082
+ f.detail || f.details || f.note || f.summary ||
1083
+ '(no detail)';
1084
+ console.log(` ${sev}${cluster}${tgt} ${text}`);
1085
+ }
1086
+ return;
1087
+ }
1088
+ }
1089
+
938
1090
  target = resolveTarget(target);
939
1091
 
940
1092
  // Polish 6: pre-flight reachability check. If no auditor is wired, give a
@@ -1006,6 +1158,18 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
1006
1158
  printFindings(mode, merged);
1007
1159
 
1008
1160
  console.log('\nReceipt logged -- run `ijfw status` to see it.');
1161
+
1162
+ // r17.1 — structured exit code so CI scripts + orchestrator-LLM callers
1163
+ // can detect degraded runs without scraping console output.
1164
+ // exit 0 — all picks contributed productively
1165
+ // exit 2 — at least one pick didn't contribute (timeout / failure / aborted)
1166
+ // exit 3 — zero picks contributed productively (INCONCLUSIVE verdict)
1167
+ // exit 1 is reserved for argv / usage errors (already used above).
1168
+ if (auditorResults && Array.isArray(auditorResults)) {
1169
+ const productive = auditorResults.filter(r => r.counted === true || r.status === null || r.status === 'fallback-used');
1170
+ if (productive.length === 0) process.exit(3);
1171
+ if (productive.length < auditorResults.length) process.exit(2);
1172
+ }
1009
1173
  }
1010
1174
 
1011
1175
  // ---------------------------------------------------------------------------
@@ -1314,16 +1478,100 @@ function cmpSemver(a, b) {
1314
1478
 
1315
1479
  function readState() { return readJsonSafe(join(ijfwHome(), 'state.json')) || {}; }
1316
1480
  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);
1481
+
1482
+ // v1.5.0 audit M11 (F-REL-1): writeStateFields was best-effort — readState +
1483
+ // merge + writeAtomic was a TOCTOU window where a parallel `ijfw update`
1484
+ // completion could clobber another writer's `last_applied_version`. If the
1485
+ // write failed silently, the re-entrancy guard (last_applied_version >=
1486
+ // last_latest_seen) would never fire and every subsequent session would
1487
+ // nag-loop until manual `ijfw doctor`.
1488
+ //
1489
+ // Fix: serialise read-modify-write under a sync directory lock so the merge
1490
+ // happens against the latest disk state. We use a tiny inlined sync version
1491
+ // of `withFsLock` (mkdir-with-EEXIST is atomic on POSIX + NTFS) rather than
1492
+ // importing the async `fs-lock.js` — this whole CLI is sync top-to-bottom and
1493
+ // converting just-this-callsite to async would force every caller to await.
1494
+ //
1495
+ // On lock-acquire failure we still attempt the write (better than refusing to
1496
+ // persist) and surface the lock failure as a clearer error.
1497
+ const STATE_LOCK_DIR = () => join(ijfwHome(), '.state.lock');
1498
+ const STATE_LOCK_ACQUIRE_TIMEOUT_MS = 5000;
1499
+ const STATE_LOCK_STALE_MS = 30000;
1500
+ const STATE_LOCK_BACKOFF_START_MS = 25;
1501
+ const STATE_LOCK_BACKOFF_MAX_MS = 250;
1502
+
1503
+ function withStateLockSync(fn) {
1504
+ const lockDir = STATE_LOCK_DIR();
1505
+ // Ensure parent exists; tolerate races.
1506
+ try { mkdirSync(dirname(lockDir), { recursive: true, mode: 0o700 }); } catch { /* */ }
1507
+
1508
+ const deadline = Date.now() + STATE_LOCK_ACQUIRE_TIMEOUT_MS;
1509
+ let staleRecoveryUsed = false;
1510
+ let backoff = STATE_LOCK_BACKOFF_START_MS;
1511
+ let acquired = false;
1512
+
1513
+ while (Date.now() < deadline) {
1514
+ try {
1515
+ mkdirSync(lockDir, { recursive: false });
1516
+ acquired = true;
1517
+ break;
1518
+ } catch (err) {
1519
+ if (err && err.code !== 'EEXIST') {
1520
+ // Real FS error (EACCES, ENOENT on parent we couldn't create) —
1521
+ // surface so the caller can fall back to best-effort write.
1522
+ throw err;
1523
+ }
1524
+ // Stale recovery: if the lock dir is older than STATE_LOCK_STALE_MS,
1525
+ // a previous holder crashed mid-write. Remove + retry once.
1526
+ if (!staleRecoveryUsed) {
1527
+ try {
1528
+ const st = statSync(lockDir);
1529
+ if (Date.now() - st.mtimeMs > STATE_LOCK_STALE_MS) {
1530
+ staleRecoveryUsed = true;
1531
+ rmSync(lockDir, { recursive: true, force: true });
1532
+ continue;
1533
+ }
1534
+ } catch { /* lock vanished mid-stat; retry */ }
1535
+ }
1536
+ // Bounded busy-wait. Sync sleep via Atomics is heavy; use a tight loop
1537
+ // with deadline check (typical contention is microseconds in practice).
1538
+ const waitUntil = Date.now() + Math.min(backoff, STATE_LOCK_BACKOFF_MAX_MS, deadline - Date.now());
1539
+ // eslint-disable-next-line no-empty
1540
+ while (Date.now() < waitUntil) {}
1541
+ backoff = Math.min(backoff * 2, STATE_LOCK_BACKOFF_MAX_MS);
1542
+ }
1543
+ }
1544
+
1545
+ if (!acquired) {
1546
+ // Couldn't acquire — fall back to caller's fn anyway. Better to risk a
1547
+ // racy write than to lose the re-entrancy guard entry.
1548
+ try { return fn(); }
1549
+ finally { /* no lock to release */ }
1550
+ }
1551
+
1320
1552
  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}`);
1553
+ return fn();
1554
+ } finally {
1555
+ try { rmSync(lockDir, { recursive: true, force: true }); } catch { /* */ }
1324
1556
  }
1325
1557
  }
1326
1558
 
1559
+ function writeStateFields(updates) {
1560
+ const path = join(ijfwHome(), 'state.json');
1561
+ withStateLockSync(() => {
1562
+ // Re-read INSIDE the lock so we don't merge against a stale snapshot.
1563
+ const state = Object.assign(readState(), updates);
1564
+ try {
1565
+ writeAtomic(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
1566
+ } catch (e) {
1567
+ // M11: persist failure is now visible AND surfaces which field would
1568
+ // not propagate. Re-entrancy guard relies on last_applied_version;
1569
+ // log explicitly so `ijfw doctor` / a user reading logs can spot it.
1570
+ console.error(`could not persist state.json (re-entrancy guard may not fire next session): ${e.message}`);
1571
+ }
1572
+ });
1573
+ }
1574
+
1327
1575
  function cmdUpdateCheck() {
1328
1576
  const state = readState();
1329
1577
  const current = state.installed_version || '0.0.0';
@@ -1370,6 +1618,19 @@ function cmdUpdateVerify() {
1370
1618
  } else {
1371
1619
  console.log(` provenance: NOT VERIFIED (audit signatures exited ${sig.status})`);
1372
1620
  }
1621
+ // Second factor: shasum cross-verify (F-SEC-7). Dry-run: report but don't
1622
+ // exit non-zero -- caller is asking "what would happen if I updated?", and
1623
+ // the interactive flow already fails closed on mismatch.
1624
+ const shasumDry = verifyShasumCrossSource(r.version);
1625
+ if (shasumDry.mode === 'verified') {
1626
+ console.log(` shasum cross-verify: VERIFIED (${shasumDry.npmShasum.slice(0, 12)}...)`);
1627
+ } else if (shasumDry.mode === 'mismatch') {
1628
+ console.log(` shasum cross-verify: MISMATCH (npm=${shasumDry.npmShasum} release=${shasumDry.releaseShasum}) -- install would be REFUSED`);
1629
+ } else if (shasumDry.mode === 'advisory') {
1630
+ console.log(` shasum cross-verify: ADVISORY (${shasumDry.message})`);
1631
+ } else {
1632
+ console.log(` shasum cross-verify: ERROR (${shasumDry.message})`);
1633
+ }
1373
1634
  console.log('Verification complete.');
1374
1635
  process.exit(0);
1375
1636
  }
@@ -1545,6 +1806,37 @@ function cmdUpdateInteractive(opts = {}) {
1545
1806
  return 1;
1546
1807
  }
1547
1808
  }
1809
+ // Shasum cross-verify (F-SEC-7): independent second factor on top of
1810
+ // npm-side signatures. Fetches the GitLab release asset shasum and
1811
+ // compares it against npm's dist.shasum for the same version. Mismatch
1812
+ // means we refuse to install; advisory (release shasum unavailable)
1813
+ // requires explicit --yes to proceed.
1814
+ const shasum = verifyShasumCrossSource(r.version);
1815
+ if (shasum.mode === 'verified') {
1816
+ console.log(` Shasum: verified (${shasum.npmShasum.slice(0, 12)}...)`);
1817
+ } else if (shasum.mode === 'mismatch') {
1818
+ console.error(' Shasum: MISMATCH -- refusing install.');
1819
+ console.error(` npm : ${shasum.npmShasum}`);
1820
+ console.error(` release : ${shasum.releaseShasum}`);
1821
+ console.error(' The npm tarball does NOT match the GitLab release asset.');
1822
+ console.error(' This could indicate a compromised registry or release. Aborting.');
1823
+ return 1;
1824
+ } else if (shasum.mode === 'error') {
1825
+ console.error(` Shasum: error -- ${shasum.message}`);
1826
+ console.error(' Refusing install: cannot establish second-factor integrity.');
1827
+ return 1;
1828
+ } else if (shasum.mode === 'advisory') {
1829
+ console.error(` Shasum: ADVISORY -- ${shasum.message}`);
1830
+ if (!opts.yes) {
1831
+ console.error(' Continuing requires --yes (acknowledge missing release shasum).');
1832
+ return 1;
1833
+ }
1834
+ console.error(' Proceeding due to --yes; release shasum could not be verified.');
1835
+ } else {
1836
+ // Unknown mode: fail closed.
1837
+ console.error(` Shasum: unknown mode "${shasum.mode}" -- refusing install.`);
1838
+ return 1;
1839
+ }
1548
1840
  // Method dispatch
1549
1841
  const method = state.install_method || 'manual';
1550
1842
  console.log(` install_method: ${method}`);
@@ -1637,8 +1929,17 @@ function cmdUpdateInteractive(opts = {}) {
1637
1929
  console.error(`Update did not complete (exit ${installRes.status}). State not written.`);
1638
1930
  return 1;
1639
1931
  }
1640
- // Persist both fields atomically -- single write avoids concurrent-reader inconsistency
1641
- writeStateFields({ last_applied_version: r.version, installed_version: r.version });
1932
+ // Persist both fields atomically -- single write avoids concurrent-reader inconsistency.
1933
+ // last_good_shasum records the shasum we just successfully cross-verified +
1934
+ // installed (per docs/SECURITY.md "last_good_shasum is a one-way 'what did
1935
+ // we actually install' record"). Only written when shasum was actually
1936
+ // verified (mode === 'verified'); advisory paths leave the previous value
1937
+ // so we don't poison the record with an unverified hash.
1938
+ const stateUpdate = { last_applied_version: r.version, installed_version: r.version };
1939
+ if (shasum && shasum.mode === 'verified' && shasum.npmShasum) {
1940
+ stateUpdate.last_good_shasum = shasum.npmShasum;
1941
+ }
1942
+ writeStateFields(stateUpdate);
1642
1943
  console.log('');
1643
1944
  console.log(`IJFW updated to v${r.version}. Run \`ijfw status\` to confirm.`);
1644
1945
  return 0;
@@ -2017,10 +2318,25 @@ if (isMainModule) {
2017
2318
  }
2018
2319
 
2019
2320
  if (parsed.cmd === 'help') {
2020
- printUsage();
2321
+ // v1.5.1 W1.D+E: orchestrator-side help is handled by the installer
2322
+ // (`ijfw --help` for the primary surface, `ijfw commands` for full).
2323
+ // Print a pointer instead of the old stale Usage block.
2324
+ console.log('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
2325
+ process.exit(0);
2326
+ }
2327
+
2328
+ if (parsed.cmd === 'memory-help') {
2329
+ printMemoryHelp();
2021
2330
  process.exit(0);
2022
2331
  }
2023
2332
 
2333
+ if (parsed.cmd === 'memory-unknown') {
2334
+ console.error(`Unknown memory subcommand: ${parsed.sub}`);
2335
+ console.error('');
2336
+ printMemoryHelp();
2337
+ process.exit(1);
2338
+ }
2339
+
2024
2340
  if (parsed.cmd === 'status') {
2025
2341
  cmdStatus(process.cwd(), parsed).catch(err => { console.error(err.message); process.exit(1); });
2026
2342
  } else if (parsed.cmd === 'demo') {
@@ -2031,6 +2347,8 @@ if (isMainModule) {
2031
2347
  cmdCrossProjectAudit(parsed).catch(err => { console.error(err.message); process.exit(1); });
2032
2348
  } else if (parsed.cmd === 'command-alias') {
2033
2349
  cmdCommandAlias(parsed.alias);
2350
+ } else if (parsed.cmd === 'metrics-benchmark') {
2351
+ cmdMetricsBenchmark(parsed).catch(err => { console.error(err.message); process.exit(1); });
2034
2352
  } else if (parsed.cmd === 'import') {
2035
2353
  cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
2036
2354
  } else if (parsed.cmd === 'doctor') {
@@ -2059,6 +2377,8 @@ if (isMainModule) {
2059
2377
  cmdDashboard(parsed.sub);
2060
2378
  } else if (parsed.cmd === 'design') {
2061
2379
  cmdDesign(parsed.sub);
2380
+ } else if (parsed.cmd === 'ui-review') {
2381
+ cmdUiReview(parsed).catch(err => { console.error(err.message); process.exit(1); });
2062
2382
  } else if (parsed.cmd === 'blackboard') {
2063
2383
  cmdBlackboard(parsed.sub);
2064
2384
  } else if (parsed.cmd === 'team') {
@@ -2073,11 +2393,13 @@ if (isMainModule) {
2073
2393
  cmdCodex(parsed.sub);
2074
2394
  } else if (parsed.cmd === 'memory-checkpoint') {
2075
2395
  cmdMemoryCheckpoint(parsed.label);
2396
+ } else if (parsed.cmd === 'memory-reindex') {
2397
+ cmdMemoryReindex(parsed).catch(err => { console.error(err.message); process.exit(1); });
2076
2398
  } else if (parsed.cmd === 'recover') {
2077
2399
  cmdRecover(parsed.sub);
2078
2400
  } else {
2079
- console.error(`Unknown command: ${parsed.raw}`);
2080
- printUsage();
2401
+ // v1.5.1 W1.D+E: clean unknown-command message; no stale usage dump.
2402
+ printUnknownCommand(parsed.raw);
2081
2403
  process.exit(1);
2082
2404
  }
2083
2405
  }
@@ -2158,6 +2480,69 @@ function openDesignUrl(url) {
2158
2480
  return res.status ?? 0;
2159
2481
  }
2160
2482
 
2483
+ // v1.5.0 wire-W1.D — `ijfw ui-review` production CLI. Wires the 7-pillar
2484
+ // audit runner into a user-facing command. Args:
2485
+ // --spec <UI-SPEC.md path> required
2486
+ // --scope <comma-sep dirs> required (e.g. "src,components")
2487
+ // --no-write skip writing UI-REVIEW.md (preview mode)
2488
+ // --gc-sketches run sketches-gc as the finalizer
2489
+ // --peer-inputs <json-path> optional axe / lighthouse / playwright
2490
+ // pre-computed outputs (JSON file)
2491
+ // --json machine-readable output (skip narrative)
2492
+ async function cmdUiReview(parsed) {
2493
+ if (!parsed.spec) {
2494
+ console.error('Usage: ijfw ui-review --spec <UI-SPEC.md> --scope <dirs> [--no-write] [--gc-sketches] [--peer-inputs <path>]');
2495
+ process.exit(1);
2496
+ }
2497
+ if (!parsed.scope) {
2498
+ console.error('ui-review: --scope is required (comma-separated dirs, e.g. "src,components")');
2499
+ process.exit(1);
2500
+ }
2501
+ const specPath = isAbsolute(parsed.spec) ? parsed.spec : resolve(process.cwd(), parsed.spec);
2502
+ if (!existsSync(specPath)) {
2503
+ console.error(`ui-review: UI-SPEC not found at ${specPath}`);
2504
+ process.exit(1);
2505
+ }
2506
+
2507
+ let peerInputs = {};
2508
+ if (parsed.peerInputs) {
2509
+ const ppath = isAbsolute(parsed.peerInputs) ? parsed.peerInputs : resolve(process.cwd(), parsed.peerInputs);
2510
+ try { peerInputs = JSON.parse(readFileSync(ppath, 'utf8')); }
2511
+ catch (err) {
2512
+ console.error(`ui-review: failed to read --peer-inputs JSON: ${err.message}`);
2513
+ process.exit(1);
2514
+ }
2515
+ }
2516
+
2517
+ const { runUiReview } = await import('./lib/ui-review-runner.js');
2518
+ const result = await runUiReview({
2519
+ uiSpecPath: specPath,
2520
+ sourceScope: parsed.scope,
2521
+ projectRoot: process.cwd(),
2522
+ peerInputs,
2523
+ write: parsed.write !== false,
2524
+ gcSketches: !!parsed.gcSketches,
2525
+ });
2526
+
2527
+ if (parsed.json) {
2528
+ console.log(JSON.stringify({
2529
+ topVerdict: result.topVerdict,
2530
+ pillarVerdicts: result.pillarVerdicts,
2531
+ reviewPath: result.reviewPath,
2532
+ parallel: result.parallel,
2533
+ }, null, 2));
2534
+ } else {
2535
+ console.log(`UI review: top-level ${result.topVerdict}`);
2536
+ for (const [pillar, verdict] of Object.entries(result.pillarVerdicts)) {
2537
+ console.log(` - ${pillar.padEnd(12)} ${verdict}`);
2538
+ }
2539
+ if (result.reviewPath) console.log(`Review written to: ${result.reviewPath}`);
2540
+ console.log(`Parallel grader run: wall=${result.parallel.wallMs}ms parallelism=${result.parallel.parallelism}`);
2541
+ }
2542
+ // Exit code: PASS=0, FLAG=0 (advisory), BLOCK=2 (ship-blocker)
2543
+ process.exit(result.topVerdict === 'BLOCK' ? 2 : 0);
2544
+ }
2545
+
2161
2546
  function cmdDesign(sub) {
2162
2547
  const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
2163
2548
  mkdirSync(contentDir, { recursive: true });
@@ -2369,8 +2754,11 @@ function cmdTeam(sub) {
2369
2754
  if (sub === 'init' || sub === 'create') {
2370
2755
  const archetype = optionValue(args, ['--archetype', '--type', '-t']);
2371
2756
  const teamName = optionValue(args, ['--name']);
2757
+ // F-FUN-1: --brief lets the Discovery hand-off carry domain signal
2758
+ // when filesystem detection would otherwise collapse to mixed/unknown.
2759
+ const brief = optionValue(args, ['--brief', '-b']) || '';
2372
2760
  const force = args.includes('--force');
2373
- const result = createTeamAssembly(process.cwd(), { archetype, teamName, force });
2761
+ const result = createTeamAssembly(process.cwd(), { archetype, teamName, brief, force });
2374
2762
  if (!result.ok) {
2375
2763
  if (result.error === 'exists') {
2376
2764
  console.error('Team assembly already exists. Re-run with --force to replace .ijfw/team/charter.json and workflow.json.');
@@ -2405,7 +2793,101 @@ function cmdTeam(sub) {
2405
2793
  process.exit(0);
2406
2794
  }
2407
2795
 
2408
- console.log('Usage: ijfw team init [--archetype <type>] [--name <team-name>] [--force] | status');
2796
+ // F-FUN-4 (audit-MED-teams-#7): ijfw team list -- enumerate roles.
2797
+ if (sub === 'list') {
2798
+ const result = listTeamRoles(process.cwd());
2799
+ if (!result.ok) {
2800
+ console.error(`team list failed: ${result.error}`);
2801
+ process.exit(1);
2802
+ }
2803
+ console.log(`Team: ${result.team_name || '(unnamed)'}`);
2804
+ if (result.project_archetypes.length) console.log(`Archetypes: ${result.project_archetypes.join(', ')}`);
2805
+ console.log(`Roles (${result.roles.length}):`);
2806
+ for (const role of result.roles) {
2807
+ console.log(` - ${role.name} [${role.role_type}] model=${role.model} effort=${role.effort}`);
2808
+ }
2809
+ process.exit(0);
2810
+ }
2811
+
2812
+ // F-FUN-4: ijfw team add <role-name> --charter <path>
2813
+ if (sub === 'add') {
2814
+ const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
2815
+ const charterPath = optionValue(args, ['--charter', '-c']);
2816
+ if (!charterPath) {
2817
+ console.error('Usage: ijfw team add <role-name> --charter <path-to-role.json>');
2818
+ process.exit(1);
2819
+ }
2820
+ const result = addTeamRole(process.cwd(), { charterPath });
2821
+ if (!result.ok) {
2822
+ console.error(`team add failed: ${result.error}`);
2823
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2824
+ process.exit(1);
2825
+ }
2826
+ console.log(`Added role: ${result.role.name}`);
2827
+ if (name && name !== result.role.name) {
2828
+ console.log(` note: charter declared name "${result.role.name}", ignoring CLI argument "${name}"`);
2829
+ }
2830
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2831
+ process.exit(0);
2832
+ }
2833
+
2834
+ // F-FUN-4: ijfw team remove <role-name>
2835
+ if (sub === 'remove' || sub === 'rm') {
2836
+ const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
2837
+ if (!name) {
2838
+ console.error('Usage: ijfw team remove <role-name>');
2839
+ process.exit(1);
2840
+ }
2841
+ const result = removeTeamRole(process.cwd(), { name });
2842
+ if (!result.ok) {
2843
+ console.error(`team remove failed: ${result.error}`);
2844
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2845
+ process.exit(1);
2846
+ }
2847
+ console.log(`Removed role: ${result.removed}`);
2848
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2849
+ process.exit(0);
2850
+ }
2851
+
2852
+ // F-FUN-4: ijfw team swap <old-role-name> --charter <path>
2853
+ if (sub === 'swap') {
2854
+ const oldName = args[0] && !args[0].startsWith('--') ? args[0] : null;
2855
+ const charterPath = optionValue(args, ['--charter', '-c']);
2856
+ if (!oldName || !charterPath) {
2857
+ console.error('Usage: ijfw team swap <old-role-name> --charter <path-to-role.json>');
2858
+ process.exit(1);
2859
+ }
2860
+ const result = swapTeamRole(process.cwd(), { oldName, charterPath });
2861
+ if (!result.ok) {
2862
+ console.error(`team swap failed: ${result.error}`);
2863
+ if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
2864
+ process.exit(1);
2865
+ }
2866
+ console.log(`Swapped ${result.swapped.old} -> ${result.swapped.new}`);
2867
+ if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
2868
+ process.exit(0);
2869
+ }
2870
+
2871
+ // F-FUN-5 (audit-MED-teams-#13): ijfw team check -- standalone validator.
2872
+ if (sub === 'check' || sub === 'validate') {
2873
+ const report = checkTeamAssembly(process.cwd());
2874
+ if (report.ok) {
2875
+ console.log(`Team assembly OK: ${report.role_count} role(s), ${report.artifact_count} artifact(s)`);
2876
+ process.exit(0);
2877
+ }
2878
+ console.error('Team assembly has issues:');
2879
+ if (!report.has_charter || !report.charter.ok) {
2880
+ console.error(' charter.json:');
2881
+ for (const err of report.charter.errors) console.error(` - ${err}`);
2882
+ }
2883
+ if (!report.has_workflow || !report.workflow.ok) {
2884
+ console.error(' workflow.json:');
2885
+ for (const err of report.workflow.errors) console.error(` - ${err}`);
2886
+ }
2887
+ process.exit(1);
2888
+ }
2889
+
2890
+ 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
2891
  process.exit(1);
2410
2892
  }
2411
2893
 
@@ -2483,11 +2965,23 @@ function codexDoctor(projectRoot) {
2483
2965
  fix: 'restore codex/.codex/hooks.json and hook scripts',
2484
2966
  });
2485
2967
 
2968
+ // C11 — the message MUST track the same condition `ok` does. Previously the
2969
+ // message said "ijfw-memory configured" whenever config.toml merely existed,
2970
+ // so a config.toml present-but-missing-the-ijfw-memory-block printed the
2971
+ // [ !! ] failure glyph (ok=false) next to success text. Branch all three
2972
+ // states: file absent, file present-but-unconfigured, file configured.
2973
+ const _codexConfigExists = existsSync(configPath);
2974
+ const _codexMemoryConfigured =
2975
+ _codexConfigExists && readFileSync(configPath, 'utf8').includes('ijfw-memory');
2486
2976
  checks.push({
2487
2977
  name: 'MCP config',
2488
- ok: existsSync(configPath) && readFileSync(configPath, 'utf8').includes('ijfw-memory'),
2978
+ ok: _codexMemoryConfigured,
2489
2979
  required: true,
2490
- message: existsSync(configPath) ? 'ijfw-memory configured' : 'missing config.toml',
2980
+ message: _codexMemoryConfigured
2981
+ ? 'ijfw-memory configured'
2982
+ : _codexConfigExists
2983
+ ? 'config.toml present but ijfw-memory not configured'
2984
+ : 'missing config.toml',
2491
2985
  fix: 'run ijfw install or restore codex/.codex/config.toml',
2492
2986
  });
2493
2987
 
@@ -2730,7 +3224,45 @@ function cmdSwarm(sub) {
2730
3224
  process.exit(0);
2731
3225
  }
2732
3226
 
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');
3227
+ // F-REL-1 (H5.3): orphan eviction. Walks active claims and releases any
3228
+ // whose freshness anchor (claimed_at or heartbeat_at) is older than the
3229
+ // TTL. Default TTL is 30 minutes; override with --ttl-min N.
3230
+ if (sub === 'evict-orphans' || sub === 'evict') {
3231
+ const minRaw = optionValue(args, ['--ttl-min', '--ttl', '-t']);
3232
+ const min = Number.parseFloat(minRaw);
3233
+ const ttlMs = Number.isFinite(min) && min > 0 ? Math.floor(min * 60 * 1000) : DEFAULT_CLAIM_TTL_MS;
3234
+ const result = evictOrphanedClaims(process.cwd(), { ttlMs });
3235
+ if (!result.ok) {
3236
+ console.log(`Swarm evict halted: ${result.error}`);
3237
+ process.exit(1);
3238
+ }
3239
+ console.log(`Evicted ${result.count} orphan claim(s) (TTL ${Math.round(ttlMs / 60000)}min)`);
3240
+ for (const item of result.evicted) {
3241
+ console.log(` ${item.id} (${item.agent} -> ${item.artifact_id}, age ${Math.round(item.age_ms / 1000)}s)`);
3242
+ }
3243
+ process.exit(0);
3244
+ }
3245
+
3246
+ // F-REL-1 (H5.3): manual heartbeat ping. Subagents normally invoke this
3247
+ // via the programmatic surface (updateClaimHeartbeat); the CLI form is
3248
+ // for forensic dogfooding from tests + the dashboard preview.
3249
+ if (sub === 'heartbeat') {
3250
+ const taskOrClaim = args[0];
3251
+ const owner = optionValue(args.slice(1), ['--owner', '-o']);
3252
+ const claimId = optionValue(args.slice(1), ['--claim', '--id']);
3253
+ const input = claimId
3254
+ ? { claim_id: claimId }
3255
+ : { artifact_id: taskOrClaim, agent: owner };
3256
+ const result = updateClaimHeartbeat(process.cwd(), input);
3257
+ if (!result.ok) {
3258
+ console.log(`Heartbeat halted: ${result.error}`);
3259
+ process.exit(1);
3260
+ }
3261
+ console.log(`Heartbeat ${result.claim.id} at ${result.claim.heartbeat_at}`);
3262
+ process.exit(0);
3263
+ }
3264
+
3265
+ 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
3266
  process.exit(1);
2735
3267
  }
2736
3268
 
@@ -2819,6 +3351,69 @@ function cmdMemoryCheckpoint(label) {
2819
3351
  process.exit(0);
2820
3352
  }
2821
3353
 
3354
+ // v1.5.1 R5-1.2 -- `ijfw memory reindex [--m2]`. Closes Trident r5 finding
3355
+ // 1.2: memory written during v1.5.0 (before Round-4 Fix-1 wired M1/M2 into
3356
+ // the production write path) has empty memory_links / memory_tags /
3357
+ // memory_meta. Migration 009 already backfills M1 once on upgrade; this verb
3358
+ // is the manual re-run path AND the only way to opt into the M2 (A-Mem
3359
+ // auto-link) backfill, which is budget-gated because it makes one LLM call
3360
+ // per row.
3361
+ async function cmdMemoryReindex(parsed) {
3362
+ const projectRoot = process.cwd();
3363
+ // Lazy import: better-sqlite3 is heavy; only pay for it on this verb.
3364
+ const { openDb, closeDb, dbPathFor } = await import('./memory/fts5.js');
3365
+ const { backfillObsidianIndex } = await import('./memory/obsidian-parser.js');
3366
+ const { backfillAutoLink } = await import('./memory/auto-linker.js');
3367
+
3368
+ let db;
3369
+ try {
3370
+ db = await openDb(projectRoot);
3371
+ } catch (e) {
3372
+ console.error(`Memory db unavailable: ${e.message}`);
3373
+ process.exit(1);
3374
+ }
3375
+
3376
+ try {
3377
+ console.log(`Reindexing memory at ${dbPathFor(projectRoot)}`);
3378
+ // M1 -- always. Free + idempotent obsidian indexing.
3379
+ const m1 = backfillObsidianIndex(db);
3380
+ console.log(
3381
+ `M1 obsidian-index backfill: ${m1.rows} entries re-indexed ` +
3382
+ `(${m1.links} links, ${m1.tags} tags, ${m1.meta} meta` +
3383
+ `${m1.errors ? `, ${m1.errors} errors` : ''}).`,
3384
+ );
3385
+
3386
+ // M2 -- opt-in via --m2. Budget-gated; backfillAutoLink internally
3387
+ // forces past the IJFW_AUTOLINK_BACKFILL opt-in (the --m2 flag IS the
3388
+ // explicit opt-in) but still honours IJFW_AUTOLINK_OFF, the budget cap,
3389
+ // and the API-key requirement.
3390
+ if (parsed.m2) {
3391
+ const m2 = await backfillAutoLink(db, { force: true });
3392
+ if (m2.skipped) {
3393
+ console.log(
3394
+ `M2 auto-link backfill skipped (${m2.reason}). ` +
3395
+ `M2 backfill needs a positive IJFW_AUTOLINK_BUDGET_USD cap and an ` +
3396
+ `API key (IJFW_AUTOLINK_API_KEY or ANTHROPIC_API_KEY).`,
3397
+ );
3398
+ } else {
3399
+ console.log(
3400
+ `M2 auto-link backfill: ${m2.linked}/${m2.rows} entries linked ` +
3401
+ `(${m2.links_added} links, ${m2.neighbor_tags_added} neighbor tags)` +
3402
+ `${m2.stopped_early ? ' -- stopped early (budget / kill switch)' : ''}.`,
3403
+ );
3404
+ }
3405
+ } else {
3406
+ console.log(
3407
+ 'M2 auto-link backfill not run. Re-run with --m2 (budget-gated) to ' +
3408
+ 'auto-link old entries via the A-Mem LLM pass.',
3409
+ );
3410
+ }
3411
+ } finally {
3412
+ closeDb(db);
3413
+ }
3414
+ process.exit(0);
3415
+ }
3416
+
2822
3417
  function cmdRecover(sub) {
2823
3418
  if (sub === 'latest') {
2824
3419
  const latest = latestCheckpoint(process.cwd());