@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
@@ -0,0 +1,160 @@
1
+ /**
2
+ * termination.js — Composable termination conditions for orchestrator loops
3
+ * (v1.5.0 audit-MED-work-M3, AutoGen-style API).
4
+ *
5
+ * Every condition is a function `(iter: number, state: object) => boolean`
6
+ * returning TRUE iff the loop should stop. Conditions compose via `or` (any
7
+ * fires) and `and` (all fire). The `runtime-loop.js` wrappers accept an
8
+ * optional `termination:` predicate; default behaviour is `MaxAttempts(N)`.
9
+ *
10
+ * State shape passed by the loop caller — fields are read defensively so
11
+ * conditions can be combined without worrying about which is "responsible"
12
+ * for which field:
13
+ * {
14
+ * startTimestamp?: number, // unix ms (or seconds — see WallClockTimeout)
15
+ * tokensUsed?: number, // running total
16
+ * findings?: Array<{ severity: 'HIGH'|'MEDIUM'|'LOW'|'INFO' }>,
17
+ * }
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Atomic conditions
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Stop after `maxAttempts` iterations (iter is 0-indexed; condition fires
26
+ * when `iter + 1 >= maxAttempts`, i.e. after the Nth attempt completes).
27
+ *
28
+ * @param {number} maxAttempts positive integer
29
+ * @returns {(iter: number, state?: object) => boolean}
30
+ */
31
+ export function MaxAttempts(maxAttempts) {
32
+ if (!Number.isFinite(maxAttempts) || maxAttempts < 1) {
33
+ throw new TypeError(`MaxAttempts: maxAttempts must be >= 1, got ${maxAttempts}`);
34
+ }
35
+ return (iter /* , _state */) => iter + 1 >= maxAttempts;
36
+ }
37
+
38
+ /**
39
+ * Stop after `budgetMs` milliseconds of wall-clock have elapsed since
40
+ * `state.startTimestamp` (which the caller must initialise — usually
41
+ * `Date.now()` before the first iteration).
42
+ *
43
+ * Defensive: missing/non-number startTimestamp falls back to never-firing
44
+ * (a misconfigured timeout shouldn't accidentally terminate the loop).
45
+ *
46
+ * @param {number} budgetMs
47
+ * @returns {(iter: number, state: object) => boolean}
48
+ */
49
+ export function WallClockTimeout(budgetMs) {
50
+ if (!Number.isFinite(budgetMs) || budgetMs < 0) {
51
+ throw new TypeError(`WallClockTimeout: budgetMs must be >= 0, got ${budgetMs}`);
52
+ }
53
+ return (_iter, state) => {
54
+ const start = state && typeof state.startTimestamp === 'number' ? state.startTimestamp : null;
55
+ if (start === null) return false;
56
+ return Date.now() - start >= budgetMs;
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Stop once `state.tokensUsed` reaches or exceeds `maxTokens`. Defensive:
62
+ * non-number tokensUsed never fires (so callers can opt in without poisoning
63
+ * loops that don't track tokens).
64
+ *
65
+ * @param {number} maxTokens
66
+ * @returns {(iter: number, state: object) => boolean}
67
+ */
68
+ export function TokenBudget(maxTokens) {
69
+ if (!Number.isFinite(maxTokens) || maxTokens < 0) {
70
+ throw new TypeError(`TokenBudget: maxTokens must be >= 0, got ${maxTokens}`);
71
+ }
72
+ return (_iter, state) => {
73
+ const used = state && typeof state.tokensUsed === 'number' ? state.tokensUsed : null;
74
+ if (used === null) return false;
75
+ return used >= maxTokens;
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Stop when `state.findings` contains at least one finding of `severity` or
81
+ * higher. Severity ladder (high → low): HIGH > MEDIUM > LOW > INFO.
82
+ *
83
+ * @param {'HIGH'|'MEDIUM'|'LOW'|'INFO'} severity
84
+ * @returns {(iter: number, state: object) => boolean}
85
+ */
86
+ export function FindingSeverity(severity) {
87
+ const rank = { HIGH: 3, MEDIUM: 2, LOW: 1, INFO: 0 };
88
+ const threshold = rank[severity];
89
+ if (threshold === undefined) {
90
+ throw new TypeError(
91
+ `FindingSeverity: severity must be one of HIGH|MEDIUM|LOW|INFO, got ${severity}`,
92
+ );
93
+ }
94
+ return (_iter, state) => {
95
+ const findings = state && Array.isArray(state.findings) ? state.findings : null;
96
+ if (!findings) return false;
97
+ return findings.some((f) => {
98
+ const sev = f && typeof f.severity === 'string' ? f.severity.toUpperCase() : null;
99
+ return sev !== null && (rank[sev] ?? -1) >= threshold;
100
+ });
101
+ };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Composition combinators
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Compose: stop iff ANY of the given conditions stop. Mirrors AutoGen's `|`
110
+ * operator on termination conditions.
111
+ *
112
+ * @param {...((iter: number, state: object) => boolean)} conds
113
+ * @returns {(iter: number, state: object) => boolean}
114
+ */
115
+ export function or(...conds) {
116
+ return (iter, state) => conds.some((c) => c(iter, state));
117
+ }
118
+
119
+ /**
120
+ * Compose: stop iff ALL of the given conditions stop. Mirrors AutoGen's `&`
121
+ * operator on termination conditions.
122
+ *
123
+ * @param {...((iter: number, state: object) => boolean)} conds
124
+ * @returns {(iter: number, state: object) => boolean}
125
+ */
126
+ export function and(...conds) {
127
+ // Empty-and is true (trivially satisfied), but we treat the empty-arg case
128
+ // as "never stop" — explicit-is-better-than-implicit.
129
+ if (conds.length === 0) return () => false;
130
+ return (iter, state) => conds.every((c) => c(iter, state));
131
+ }
132
+
133
+ /**
134
+ * Negation — stop iff `cond` does NOT stop. Rarely useful alone; included
135
+ * for parity with the boolean algebra.
136
+ *
137
+ * @param {(iter: number, state: object) => boolean} cond
138
+ * @returns {(iter: number, state: object) => boolean}
139
+ */
140
+ export function not(cond) {
141
+ return (iter, state) => !cond(iter, state);
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Helpers for callers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Default termination used by `runtime-loop.js` callers that didn't pass an
150
+ * explicit predicate. Currently MaxAttempts(REVIEW_MAX_ITERATIONS) — matches
151
+ * the v1.4.4 N3 behaviour (re-review caps at 3 iterations).
152
+ *
153
+ * Caller is free to pass their own predicate via the `termination:` arg.
154
+ *
155
+ * @param {number} [maxAttempts=3]
156
+ * @returns {(iter: number, state?: object) => boolean}
157
+ */
158
+ export function defaultTermination(maxAttempts = 3) {
159
+ return MaxAttempts(maxAttempts);
160
+ }
@@ -1,20 +1,50 @@
1
1
  /**
2
- * verification-gate.js — Advisory lint: detects completion claims in a
3
- * message that lack fresh verification evidence (a Bash test/build call
4
- * in the same message).
2
+ * verification-gate.js — Iron-Law completion lint: detects completion
3
+ * claims in a message that lack fresh verification evidence (a Bash
4
+ * test/build call in the same message).
5
5
  *
6
- * ADVISORY ONLY never throws, never blocks. Returns { ok: true } or
7
- * { ok: false, violation: string, claim: string }.
6
+ * Gate enforcement: callers receive { ok, violation, claim, enforce }.
7
+ * When `enforce: 'strict'` (default in post-done-runner.js as of W12-F/F4),
8
+ * the caller MUST refuse to advance on ok=false. Use `enforceVerificationGate`
9
+ * (default strict) to get a thrown `VerificationGateViolation` on failure;
10
+ * use the lower-level `checkVerificationGate` for advisory-only callers
11
+ * that prefer to inspect the result themselves.
8
12
  *
9
13
  * Violations are persisted to .ijfw/memory/verification-violations.jsonl
10
14
  * so the memory-feedback system (v1.4.1 B10) can pattern-detect over time.
11
15
  *
12
- * Landed in W10-A2 (v1.4.4 — N5).
16
+ * Landed in W10-A2 (v1.4.4 — N5). Promoted to strict-by-default in
17
+ * W12-F/F4 (v1.5.0-major — RT2-H1).
13
18
  */
14
19
 
15
20
  import { appendFile, mkdir } from 'node:fs/promises';
16
21
  import { dirname, join } from 'node:path';
17
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Error subclass — thrown by enforceVerificationGate in strict mode.
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Thrown by `enforceVerificationGate` when the gate fails and strict mode
29
+ * is in effect (default). Carries the structured violation alongside the
30
+ * Error contract so callers can branch on `instanceof VerificationGateViolation`.
31
+ */
32
+ export class VerificationGateViolation extends Error {
33
+ /**
34
+ * @param {{ violation: string, claim: string }} outcome
35
+ * The failure result returned by `checkVerificationGate`.
36
+ */
37
+ constructor(outcome) {
38
+ const reason = outcome && outcome.violation
39
+ ? String(outcome.violation)
40
+ : 'Verification gate violation';
41
+ super(reason);
42
+ this.name = 'VerificationGateViolation';
43
+ this.violation = reason;
44
+ this.claim = outcome && outcome.claim ? String(outcome.claim) : '';
45
+ }
46
+ }
47
+
18
48
  // ---------------------------------------------------------------------------
19
49
  // Detection patterns
20
50
  // ---------------------------------------------------------------------------
@@ -25,15 +55,68 @@ import { dirname, join } from 'node:path';
25
55
  // protocol literal `DONE`, `completed`/`shipped` (deliberate completion verbs),
26
56
  // uppercase `PASS` (verdict literal), `✅` emoji, and explicit completion phrases
27
57
  // ("all tests pass" / "build succeeded" / "deployed" / "ready to ship").
58
+ //
59
+ // v1.5.0 audit-MED-work-M8: documented the rationale + introduced a SECOND
60
+ // "low-confidence" tier (LOW_CONFIDENCE_PATTERNS) intended for advise-mode-
61
+ // only callers. The strict-by-default gate (enforceVerificationGate) keeps
62
+ // using COMPLETION_PATTERNS so the existing iron-law stays unchanged; tools
63
+ // that want to surface "you might have claimed completion" without blocking
64
+ // can call `checkVerificationGateLowConfidence`. See CHANGELOG v1.5.0 for
65
+ // the policy entry.
28
66
  const COMPLETION_PATTERNS = [
29
67
  /\b(?:DONE|completed|shipped|PASS)\b/,
30
68
  /✅/,
31
69
  /\b(?:all tests pass|build succeeded|deployed|ready to ship)\b/i,
32
70
  ];
33
71
 
72
+ /**
73
+ * Low-confidence completion signals — words that USED to be in
74
+ * COMPLETION_PATTERNS but were removed in r13-M-01 / r13-M-04 because they
75
+ * fired on neutral language. These should NEVER block (strict-mode default
76
+ * uses COMPLETION_PATTERNS only), but advise-mode callers can use them to
77
+ * nudge an agent that's drifting toward a claim without evidence.
78
+ */
79
+ export const LOW_CONFIDENCE_PATTERNS = [
80
+ /\b(?:done|complete|passes|works|finished)\b/i,
81
+ ];
82
+
34
83
  // Bash tool calls that count as fresh verification evidence.
35
- const VERIFICATION_COMMAND_RE =
36
- /(?:npm test|node --test|cargo test|pytest|preflight|ijfw preflight|build)/i;
84
+ //
85
+ // v1.5.1 H1.1 (audit HIGH-S2): the bare `build` substring let non-build
86
+ // commands like `Bash("ls build/")` clear the Iron Law without running tests.
87
+ //
88
+ // v1.5.1 H1.1-followup (Trident r18 finding): the first cut used `[\s;&|]`
89
+ // as a command-start marker, but plain whitespace inside arguments still let
90
+ // `echo npm test` and `printf 'npm run build'` satisfy the gate. The fix is
91
+ // structural, not regex-stretching: split the bash command into chain-
92
+ // segments on real shell separators (`;`, `&&`, `||`, `|`) and require a
93
+ // verify verb at the START of at least one segment (after an optional
94
+ // env-var prefix like `NODE_ENV=production`).
95
+ /* eslint-disable security/detect-unsafe-regex --
96
+ * Matches verify-cmd strings from developer-authored plan/spec files (not
97
+ * network input). Fixed-suffix alternations + bounded env-var prefix loop
98
+ * are not backtrack-exploitable.
99
+ */
100
+ const VERIFY_VERB_RE =
101
+ /^(?:[A-Z_][A-Z0-9_]*=\S+\s+)*(?:npm test|node --test|cargo test|pytest|preflight|ijfw preflight|npm run build|yarn build|pnpm build|bun build|cargo build|tsc --build|tsc -b|make(?:\s|$))/i;
102
+ /* eslint-enable security/detect-unsafe-regex */
103
+
104
+ const SHELL_SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||;|\|)\s*/;
105
+
106
+ /**
107
+ * Return true iff `command` runs a recognized test/build verb as the head of
108
+ * at least one chain-segment. `echo npm test` does NOT match (echo is the
109
+ * head); `cd foo && npm test` DOES (the second segment is `npm test`).
110
+ * Exported for unit-tests of the bash-segment splitter.
111
+ */
112
+ export function isVerificationCommand(command) {
113
+ if (typeof command !== 'string' || !command) return false;
114
+ const segments = command.trim().split(SHELL_SEGMENT_SPLIT_RE);
115
+ for (const seg of segments) {
116
+ if (VERIFY_VERB_RE.test(seg.trim())) return true;
117
+ }
118
+ return false;
119
+ }
37
120
 
38
121
  // ---------------------------------------------------------------------------
39
122
  // Core gate
@@ -54,9 +137,7 @@ export function checkVerificationGate(message, toolCallsInMessage) {
54
137
  if (claims.length === 0) return { ok: true };
55
138
 
56
139
  const verificationCalls = toolCallsInMessage.filter(
57
- (t) =>
58
- t.tool === 'Bash' &&
59
- VERIFICATION_COMMAND_RE.test(t.input?.command ?? ''),
140
+ (t) => t.tool === 'Bash' && isVerificationCommand(t.input?.command ?? ''),
60
141
  );
61
142
 
62
143
  if (verificationCalls.length === 0) {
@@ -70,18 +151,97 @@ export function checkVerificationGate(message, toolCallsInMessage) {
70
151
  return { ok: true };
71
152
  }
72
153
 
154
+ /**
155
+ * Low-confidence advisory variant of `checkVerificationGate`. Uses the
156
+ * LOW_CONFIDENCE_PATTERNS list (lowercase `done`/`complete`/etc.) so callers
157
+ * can detect drift toward completion claims that the strict gate (rightly)
158
+ * ignores. ADVISORY ONLY — never wired into the iron-law enforcement path.
159
+ *
160
+ * Returns the same shape as `checkVerificationGate`. Skipping evidence here
161
+ * is NOT a violation in the strict-enforcement sense; callers decide what
162
+ * to do (typically: surface as INFO finding to the operator).
163
+ *
164
+ * @param {string} message
165
+ * @param {Array<{tool: string, input?: {command?: string}}>} toolCallsInMessage
166
+ * @returns {{ ok: true } | { ok: false, violation: string, claim: string, lowConfidence: true }}
167
+ */
168
+ export function checkVerificationGateLowConfidence(message, toolCallsInMessage) {
169
+ const claims = LOW_CONFIDENCE_PATTERNS.flatMap((p) => message.match(p) ?? []);
170
+ if (claims.length === 0) return { ok: true };
171
+
172
+ const verificationCalls = (Array.isArray(toolCallsInMessage) ? toolCallsInMessage : []).filter(
173
+ (t) => t.tool === 'Bash' && isVerificationCommand(t.input?.command ?? ''),
174
+ );
175
+ if (verificationCalls.length === 0) {
176
+ return {
177
+ ok: false,
178
+ violation: `Low-confidence completion signal "${claims[0]}" without fresh verification (advisory)`,
179
+ claim: claims[0],
180
+ lowConfidence: true,
181
+ };
182
+ }
183
+ return { ok: true };
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Strict enforcement wrapper (W12-F/F4 — RT2-H1)
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * Enforce the verification gate. By default this is strict: a failed gate
192
+ * throws `VerificationGateViolation`. Pass `{ strict: false }` for advisory
193
+ * behavior (returns the same shape as `checkVerificationGate`).
194
+ *
195
+ * @param {string} messageText
196
+ * @param {Array<{tool: string, input?: {command?: string}}>} toolCalls
197
+ * @param {{ strict?: boolean }} [opts]
198
+ * @returns {{ ok: true } | { ok: false, violation: string, claim: string }}
199
+ * @throws {VerificationGateViolation} when strict and gate fails.
200
+ */
201
+ export function enforceVerificationGate(messageText, toolCalls, opts = {}) {
202
+ const strict = opts.strict !== false; // default strict
203
+ const outcome = checkVerificationGate(
204
+ typeof messageText === 'string' ? messageText : '',
205
+ Array.isArray(toolCalls) ? toolCalls : [],
206
+ );
207
+ if (!outcome.ok && strict) {
208
+ throw new VerificationGateViolation(outcome);
209
+ }
210
+ return outcome;
211
+ }
212
+
73
213
  // ---------------------------------------------------------------------------
74
214
  // Violation recorder
75
215
  // ---------------------------------------------------------------------------
76
216
 
217
+ /**
218
+ * Set of target paths whose write failure has already been logged to stderr
219
+ * this process. Prevents stderr flooding when the memory dir is unwritable
220
+ * and a high-frequency caller (e.g. a stuck subagent loop) keeps retrying.
221
+ *
222
+ * v1.5.0 audit-H4.3 (HIGH-Rel1): recordViolation used to silently swallow
223
+ * write failures, defeating the gate's whole job (surfacing violations to
224
+ * the memory-feedback system). The fix:
225
+ * 1. Emit ONE stderr line per failure (with the claim, so the violation
226
+ * isn't fully lost), then
227
+ * 2. Return a result shape so callers can observe success/failure, but
228
+ * 3. NEVER throw — recordViolation remains advisory in posture.
229
+ * Failures that used to be invisible are now AUDIBLE (stderr) +
230
+ * OBSERVABLE (return shape), but still non-fatal.
231
+ */
232
+ const RECORD_FAILURE_LOGGED = new Set();
233
+
77
234
  /**
78
235
  * Append a violation record to .ijfw/memory/verification-violations.jsonl.
79
- * Auto-creates parent directories. Advisory — errors are silently swallowed
80
- * so a write failure never blocks the orchestrator.
236
+ * Auto-creates parent directories.
237
+ *
238
+ * Advisory in posture (never throws), but no longer silent: write failures
239
+ * are logged to stderr (once per target-path, dedup'd via a module-level
240
+ * Set) and returned in the result shape so callers can observe them.
81
241
  *
82
242
  * @param {{ violation: string, claim: string, [key: string]: unknown }} violation
83
243
  * @param {string} projectRoot Absolute path to the project root.
84
- * @returns {Promise<void>}
244
+ * @returns {Promise<{ ok: true, path: string } | { ok: false, path: string, error: string }>}
85
245
  */
86
246
  export async function recordViolation(violation, projectRoot) {
87
247
  const file = join(projectRoot, '.ijfw', 'memory', 'verification-violations.jsonl');
@@ -91,7 +251,31 @@ export async function recordViolation(violation, projectRoot) {
91
251
  file,
92
252
  JSON.stringify({ ...violation, recorded_at: new Date().toISOString() }) + '\n',
93
253
  );
94
- } catch {
95
- // Advisory never propagate write errors.
254
+ return { ok: true, path: file };
255
+ } catch (err) {
256
+ const errMsg = err && err.message ? String(err.message) : String(err);
257
+ // Dedup: only one stderr line per target path per process, to avoid
258
+ // flooding when the memory dir is unwritable for a long-running loop.
259
+ if (!RECORD_FAILURE_LOGGED.has(file)) {
260
+ RECORD_FAILURE_LOGGED.add(file);
261
+ const claim = violation && violation.claim ? String(violation.claim) : '<unknown>';
262
+ // One-line stderr log so the violation isn't fully lost. Includes the
263
+ // claim string so a downstream operator can correlate.
264
+ process.stderr.write(
265
+ `[verification-gate] recordViolation write failed path=${file} claim=${JSON.stringify(claim)} err=${errMsg}\n`,
266
+ );
267
+ }
268
+ return { ok: false, path: file, error: errMsg };
96
269
  }
97
270
  }
271
+
272
+ /**
273
+ * Reset the recordViolation failure-dedup Set. Test-only helper so each
274
+ * test can assert "one stderr line per failure" independently of other
275
+ * tests in the same process.
276
+ *
277
+ * @internal
278
+ */
279
+ export function _resetRecordViolationDedup() {
280
+ RECORD_FAILURE_LOGGED.clear();
281
+ }