@ijfw/memory-server 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1171 -10
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-planning.html +273 -0
  131. package/src/dashboard-client-waves.html +304 -0
  132. package/src/dashboard-client.html +17 -2
  133. package/src/dashboard-server.js +152 -0
  134. package/src/deploy-alerts.js +150 -0
  135. package/src/design/iframe-bridge.js +242 -0
  136. package/src/design-companion.js +144 -0
  137. package/src/dispatch/checkpoint-cli.js +97 -0
  138. package/src/dispatch/colon-syntax.js +81 -1
  139. package/src/dispatch/extension.js +27 -1
  140. package/src/dispatch/registry-cli.js +4 -1
  141. package/src/dispatch/wave-cli.js +323 -0
  142. package/src/dispatch/worktree-cli.js +40 -0
  143. package/src/dispatch-planner.js +97 -2
  144. package/src/dream/runner.mjs +47 -11
  145. package/src/dream/stage-runner.js +40 -0
  146. package/src/dream/state-file.js +102 -0
  147. package/src/extension-installer.js +70 -24
  148. package/src/extension-quota-tracker.js +4 -2
  149. package/src/extension-registry.js +289 -35
  150. package/src/feedback-detector.js +26 -0
  151. package/src/fs-lock.js +259 -7
  152. package/src/gate-result.js +95 -1
  153. package/src/hero-line.js +86 -5
  154. package/src/intent-router.js +35 -0
  155. package/src/lib/a11y-contract.js +117 -0
  156. package/src/lib/atomic-io.js +29 -8
  157. package/src/lib/cache-keepalive.js +150 -0
  158. package/src/lib/jsonl-rotation.js +104 -0
  159. package/src/lib/lighthouse-pillar.js +121 -0
  160. package/src/lib/llm-call.js +121 -0
  161. package/src/lib/playwright-baseline.js +205 -0
  162. package/src/lib/rekor-bridge.js +221 -0
  163. package/src/lib/repo-map.js +392 -0
  164. package/src/lib/shasum-verify.js +164 -0
  165. package/src/lib/sketches-gc.js +132 -0
  166. package/src/lib/tmp-suffix.js +62 -0
  167. package/src/lib/ui-review-runner.js +554 -0
  168. package/src/lib/uispec-drift.js +301 -0
  169. package/src/lib/uispec-intake.js +381 -0
  170. package/src/lib/worktree-guards.js +118 -0
  171. package/src/lib/worktree-recovery.js +100 -0
  172. package/src/memory/auto-linker.js +152 -0
  173. package/src/memory/benchmark.js +498 -0
  174. package/src/memory/dedup.js +126 -0
  175. package/src/memory/embedding-cache.js +136 -0
  176. package/src/memory/fact-extractor.js +168 -0
  177. package/src/memory/fts5.js +65 -1
  178. package/src/memory/migrations/004-bitemporal.js +91 -0
  179. package/src/memory/migrations/005-vector-cache.js +61 -0
  180. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  181. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  182. package/src/memory/migrations/008-write-provenance.js +41 -0
  183. package/src/memory/obsidian-parser.js +91 -0
  184. package/src/memory/query-dataview.js +86 -0
  185. package/src/memory/search.js +10 -0
  186. package/src/memory/temporal.js +529 -0
  187. package/src/memory/tokenize.js +10 -0
  188. package/src/memory-facts-handler.js +37 -0
  189. package/src/memory-feedback.js +260 -2
  190. package/src/model-refresh.js +292 -0
  191. package/src/observability/cost-anomaly.js +166 -0
  192. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  193. package/src/observability/trace-id.js +163 -0
  194. package/src/orchestrator/agents-md-blackboard.js +152 -0
  195. package/src/orchestrator/checkpoint-contract.md +140 -0
  196. package/src/orchestrator/debug-trident.js +570 -0
  197. package/src/orchestrator/merge-block-aware.js +350 -0
  198. package/src/orchestrator/plan-checker.js +475 -0
  199. package/src/orchestrator/post-done-runner.js +249 -0
  200. package/src/orchestrator/review.js +136 -0
  201. package/src/orchestrator/runtime-loop.js +430 -0
  202. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  203. package/src/orchestrator/skill-telemetry.js +37 -0
  204. package/src/orchestrator/state-events.js +459 -0
  205. package/src/orchestrator/state-sdk.js +1764 -0
  206. package/src/orchestrator/status-protocol.js +235 -0
  207. package/src/orchestrator/subagent-telemetry.js +452 -0
  208. package/src/orchestrator/termination.js +160 -0
  209. package/src/orchestrator/verification-gate.js +281 -0
  210. package/src/orchestrator/wave-state.js +564 -0
  211. package/src/orchestrator/worktree-provision.js +77 -0
  212. package/src/override-use-registry.js +111 -5
  213. package/src/receipts.js +36 -4
  214. package/src/recovery/checkpoint.js +56 -3
  215. package/src/recovery/code-fixer.js +656 -0
  216. package/src/recovery/truncation.js +317 -0
  217. package/src/redactor.js +75 -6
  218. package/src/runtime-mediator.js +15 -0
  219. package/src/sanitizer.js +10 -0
  220. package/src/search-hybrid.js +139 -0
  221. package/src/server.js +603 -59
  222. package/src/swarm/worktree.js +27 -4
  223. package/src/swarm-config.js +113 -12
  224. package/src/team/domain-templates/book.json +51 -0
  225. package/src/team/domain-templates/business.json +41 -0
  226. package/src/team/domain-templates/content.json +50 -0
  227. package/src/team/domain-templates/design.json +44 -0
  228. package/src/team/domain-templates/research.json +41 -0
  229. package/src/team/domain-templates/software.json +40 -0
  230. package/src/team/generator.js +278 -3
  231. package/src/team/modify.js +203 -0
  232. package/src/team/schemas.js +48 -0
  233. package/src/update-apply.js +19 -3
@@ -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
+ }
@@ -0,0 +1,281 @@
1
+ /**
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
+ *
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.
12
+ *
13
+ * Violations are persisted to .ijfw/memory/verification-violations.jsonl
14
+ * so the memory-feedback system (v1.4.1 B10) can pattern-detect over time.
15
+ *
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).
18
+ */
19
+
20
+ import { appendFile, mkdir } from 'node:fs/promises';
21
+ import { dirname, join } from 'node:path';
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
+
48
+ // ---------------------------------------------------------------------------
49
+ // Detection patterns
50
+ // ---------------------------------------------------------------------------
51
+
52
+ // r13-M-01 + r13-M-04: dropped bare `complete`, lowercase `done`, AND lowercase
53
+ // `pass(?:es)?` — all three fired falsely on common neutral language ("not yet
54
+ // complete", "to be done in v1.5", "pass the context"). Detection list is now:
55
+ // protocol literal `DONE`, `completed`/`shipped` (deliberate completion verbs),
56
+ // uppercase `PASS` (verdict literal), `✅` emoji, and explicit completion phrases
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.
66
+ const COMPLETION_PATTERNS = [
67
+ /\b(?:DONE|completed|shipped|PASS)\b/,
68
+ /✅/,
69
+ /\b(?:all tests pass|build succeeded|deployed|ready to ship)\b/i,
70
+ ];
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
+
83
+ // Bash tool calls that count as fresh verification evidence.
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
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Core gate
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Check whether a message that contains a completion claim also has fresh
127
+ * verification evidence (a Bash tool call running tests/build).
128
+ *
129
+ * @param {string} message Full text of the agent message.
130
+ * @param {Array<{tool: string, input?: {command?: string}}>} toolCallsInMessage
131
+ * Tool calls that appeared in the same message turn.
132
+ *
133
+ * @returns {{ ok: true } | { ok: false, violation: string, claim: string }}
134
+ */
135
+ export function checkVerificationGate(message, toolCallsInMessage) {
136
+ const claims = COMPLETION_PATTERNS.flatMap((p) => message.match(p) ?? []);
137
+ if (claims.length === 0) return { ok: true };
138
+
139
+ const verificationCalls = toolCallsInMessage.filter(
140
+ (t) => t.tool === 'Bash' && isVerificationCommand(t.input?.command ?? ''),
141
+ );
142
+
143
+ if (verificationCalls.length === 0) {
144
+ return {
145
+ ok: false,
146
+ violation: `Completion claim "${claims[0]}" without fresh verification in same message`,
147
+ claim: claims[0],
148
+ };
149
+ }
150
+
151
+ return { ok: true };
152
+ }
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
+
213
+ // ---------------------------------------------------------------------------
214
+ // Violation recorder
215
+ // ---------------------------------------------------------------------------
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
+
234
+ /**
235
+ * Append a violation record to .ijfw/memory/verification-violations.jsonl.
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.
241
+ *
242
+ * @param {{ violation: string, claim: string, [key: string]: unknown }} violation
243
+ * @param {string} projectRoot Absolute path to the project root.
244
+ * @returns {Promise<{ ok: true, path: string } | { ok: false, path: string, error: string }>}
245
+ */
246
+ export async function recordViolation(violation, projectRoot) {
247
+ const file = join(projectRoot, '.ijfw', 'memory', 'verification-violations.jsonl');
248
+ try {
249
+ await mkdir(dirname(file), { recursive: true });
250
+ await appendFile(
251
+ file,
252
+ JSON.stringify({ ...violation, recorded_at: new Date().toISOString() }) + '\n',
253
+ );
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 };
269
+ }
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
+ }