@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,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-03",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-3",
5
+ "subId": "errT3",
6
+ "notes": "error-terminated with overwrite partial — rollback restores baseline",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-3\"}"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "verbId": "v-errT-3-partial",
3
+ "targets": [
4
+ {
5
+ "relPath": ".ijfw/state/workflow.json",
6
+ "absPath": "<<ABS>>",
7
+ "existed": true,
8
+ "content": "{\"phase\":\"errT-pre-3\"}"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ {"seq":1,"verb":"subagent.checkpoint","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-ckpt","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-4-ckpt"}
2
+ {"seq":2,"verb":"decision.add","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-4-partial"}
@@ -0,0 +1,3 @@
1
+ {"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/wave-W20-errT-4/subagent-errT4.checkpoint.json"],"payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
2
+ {"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
3
+ {"verb":"decision.add","verbId":"v-errT-4-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/blackboard/decisions.jsonl"],"payloadDigest":"sha256-fixture-v-errT-4-partial","kind":"append","dedupKey":"dk-errT-4"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-04",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-4",
5
+ "subId": "errT4",
6
+ "notes": "error-terminated with append partial — seal-in-place preserves record",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/blackboard/decisions.jsonl": "{\"id\":\"d-errT-4\",\"text\":\"errT 4\",\"dedupKey\":\"dk-errT-4\"}\n"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1 @@
1
+ {"id":"d-errT-4","text":"errT 4","dedupKey":"dk-errT-4"}
@@ -0,0 +1,2 @@
1
+ {"seq":1,"verb":"workflow.set-phase","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-ok","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-5-ok"}
2
+ {"seq":2,"verb":"wave.advance","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-5-partial"}
@@ -0,0 +1,3 @@
1
+ {"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
2
+ {"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
3
+ {"verb":"wave.advance","verbId":"v-errT-5-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-partial","kind":"overwrite"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "fx-05-error-terminated-05",
3
+ "category": "error-terminated",
4
+ "waveId": "W20-errT-5",
5
+ "subId": "errT5",
6
+ "notes": "error-terminated with overwrite partial — rollback restores baseline",
7
+ "expectedDetection": {
8
+ "truncated": "error-terminated",
9
+ "reasonContains": "outcome='error'"
10
+ },
11
+ "expectedRecovery": {
12
+ "recovered": true
13
+ },
14
+ "expectedFinalState": {
15
+ ".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-5\"}"
16
+ },
17
+ "expectedTerminalVerb": "subagent.post-done"
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "verbId": "v-errT-5-partial",
3
+ "targets": [
4
+ {
5
+ "relPath": ".ijfw/state/workflow.json",
6
+ "absPath": "<<ABS>>",
7
+ "existed": true,
8
+ "content": "{\"phase\":\"errT-pre-5\"}"
9
+ }
10
+ ]
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/memory-server",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Cross-platform persistent memory server for IJFW. 10 MCP tools (memory + admin/update). Works with 13 MCP-using platforms (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw) plus Aider via rules-only tier.",
5
5
  "author": "Sean Donahoe",
6
6
  "license": "MIT",
@@ -5,14 +5,38 @@
5
5
  * clears it on deactivate. Used by:
6
6
  * - `ijfw_run extension:activate <name>` CLI command
7
7
  * - installExtension when opts.activate is set
8
+ *
9
+ * v1.5.0 T10 — Migrated to the state-SDK. The legacy
10
+ * `writeFile`/`rename`/`unlink` direct-write path is replaced by a call to
11
+ * `query('extension.set-active', ...)`. The state-SDK is the single mutation
12
+ * surface for `~/.ijfw/state/active-extension.json`; this writer now adapts
13
+ * the SDK's verb-returns to the writer's `{ok, path, error}` /
14
+ * `{ok, removed}` external API so every existing caller keeps working without
15
+ * change (`extension-installer.js`, `dispatch/active-cli.js`,
16
+ * `dispatch/extension.js`, the test suite).
17
+ *
18
+ * Side-effects retained verbatim from the v1.4.0/W7.1 contract:
19
+ * - quota counters are reset on activate (B16/SEC-M-02) and clear (B16) via
20
+ * `resetExtensionQuotas` — best-effort, never blocks the verb result.
21
+ * - B18 cross-IDE last-seen / divergence detection (`detectCrossIdeDivergence`)
22
+ * stays as-is — it is a read-only consumer of the homedir state file and
23
+ * does not need the SDK.
24
+ * - `findInstalledManifest` stays as-is — it reads project/org/user-scope
25
+ * manifest.json files; it is a read of installed extensions, not a write
26
+ * to the homedir state file, so it is outside the state-SDK surface.
8
27
  */
9
28
 
10
29
  import { readFile, writeFile, unlink, mkdir, readdir, stat } from 'node:fs/promises';
30
+ import { existsSync } from 'node:fs';
11
31
  import { join, dirname } from 'node:path';
12
32
  import { homedir } from 'node:os';
13
- import { randomBytes } from 'node:crypto';
14
33
 
15
34
  import { resetExtensionQuotas } from './extension-quota-tracker.js';
35
+ // v1.5.0 audit-LOW-update-#13: shared tmp-suffix helper.
36
+ import { tmpSuffix } from './lib/tmp-suffix.js';
37
+ // v1.5.0 T10: route the canonical write/clear of ~/.ijfw/state/active-extension.json
38
+ // through the state-SDK verb.
39
+ import { query } from './orchestrator/state-sdk.js';
16
40
 
17
41
  const STATE_PATH_REL = ['.ijfw', 'state', 'active-extension.json'];
18
42
 
@@ -34,16 +58,37 @@ function stateDir(home) {
34
58
  return join(home || homedir(), '.ijfw', 'state');
35
59
  }
36
60
 
61
+ /**
62
+ * Pick a state-SDK projectRoot to satisfy the verb's `ctx.projectRoot`
63
+ * requirement. The writer's external callers don't carry a project root
64
+ * (extension activation is a homedir-scoped operation), so we fall back to
65
+ * `process.cwd()`. The SDK uses this only for the intent-journal lock path
66
+ * (`<projectRoot>/.ijfw/state/intent-journal.jsonl`) — it never mutates any
67
+ * project file under it. opts.projectRoot, when supplied, takes precedence.
68
+ */
69
+ function pickProjectRoot(opts) {
70
+ if (opts && typeof opts.projectRoot === 'string' && opts.projectRoot.length > 0) {
71
+ return opts.projectRoot;
72
+ }
73
+ return process.cwd();
74
+ }
75
+
37
76
  /**
38
77
  * Write the active-extension state file from a manifest + scope.
39
- * Validates required fields before write. Atomic write via tmp+rename.
78
+ * Validates required fields before write. Atomic write via the state-SDK
79
+ * `extension.set-active` verb (tmp+rename via atomic-io).
40
80
  *
41
- * @param {{ name: string, permissions: { reads: string[], writes: string[] } }} manifest
81
+ * @param {{ name: string, permissions: { reads: string[], writes: string[] }, quotas?: object }} manifest
42
82
  * @param {'project'|'org'|'user'} scope
43
- * @param {{ homeDir?: string, ideId?: string|null }} [opts]
83
+ * @param {{ homeDir?: string, ideId?: string|null, projectRoot?: string }} [opts]
44
84
  * @returns {Promise<{ ok: boolean, path?: string, error?: string }>}
45
85
  */
46
86
  export async function writeActiveExtension(manifest, scope, opts = {}) {
87
+ // Validation — external API contract (returns ok:false, never throws).
88
+ // Mirrors the pre-v1.5.0 behavior so every existing caller keeps the same
89
+ // error-handling shape. The SDK verb would throw on the same inputs; the
90
+ // adapter catches that downstream too, but explicit pre-checks give the
91
+ // historical error messages.
47
92
  if (!manifest || typeof manifest !== 'object') {
48
93
  return { ok: false, error: 'manifest must be an object' };
49
94
  }
@@ -56,57 +101,73 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
56
101
  if (!manifest.permissions || typeof manifest.permissions !== 'object') {
57
102
  return { ok: false, error: 'manifest.permissions required' };
58
103
  }
59
- const reads = Array.isArray(manifest.permissions.reads) ? manifest.permissions.reads : [];
60
- const writes = Array.isArray(manifest.permissions.writes) ? manifest.permissions.writes : [];
61
- const activatedAt = new Date().toISOString();
104
+
105
+ const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
106
+
62
107
  // B18: stamp activated_by_ide + activated_by_pid when ideId is provided.
63
108
  // Caller (CLI) is responsible for calling detectIde() and threading the
64
- // value in. When opts.ideId is null/undefined, fields are omitted (so the
65
- // file stays back-compatible with v1.4.1 readers).
109
+ // value in. When opts.ideId is null/undefined or invalid, fields are
110
+ // omitted (so the file stays back-compatible with v1.4.1 readers).
66
111
  const ideId = (typeof opts.ideId === 'string' && IDE_ID_PATTERN.test(opts.ideId))
67
112
  ? opts.ideId
68
113
  : null;
69
- const out = {
114
+
115
+ // Normalize permissions + quotas in the manifest payload so the SDK verb
116
+ // sees the exact shape it expects. The verb itself filters quotas to
117
+ // positive integers — we leave that filtering to it (single writer rule).
118
+ const reads = Array.isArray(manifest.permissions.reads) ? manifest.permissions.reads : [];
119
+ const writes = Array.isArray(manifest.permissions.writes) ? manifest.permissions.writes : [];
120
+ const verbManifest = {
70
121
  name: manifest.name,
71
- scope,
72
122
  permissions: { reads, writes },
73
- activated_at: activatedAt,
74
123
  };
75
- if (ideId) {
76
- out.activated_by_ide = ideId;
77
- out.activated_by_pid = process.pid;
78
- }
79
- // R12-H-01: persist manifest.quotas so the tier-2 hook
80
- // (extension-permission-check.mjs) can enforce quotas on Edit/Write/Bash
81
- // dispatch. Without this the tier-2 hook reads `active.quotas` as undefined
82
- // and silently bypasses the v1.4.3 quota gate that the server-side
83
- // gatePermissionAndQuota path enforces. Schema (extension-manifest-schema.js):
84
- // optional object whose values are positive integers — currently
85
- // max_files_written / max_bytes_written / max_wall_clock_ms (forward-compat:
86
- // unknown dimensions are kept as-is — schema rejects unknowns at install).
87
124
  if (
88
125
  manifest.quotas !== undefined &&
89
126
  manifest.quotas !== null &&
90
127
  typeof manifest.quotas === 'object' &&
91
128
  !Array.isArray(manifest.quotas)
92
129
  ) {
93
- const cleanQuotas = {};
94
- let copied = 0;
95
- for (const [k, v] of Object.entries(manifest.quotas)) {
96
- if (typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v) && v > 0) {
97
- cleanQuotas[k] = v;
98
- copied++;
99
- }
100
- }
101
- if (copied > 0) out.quotas = cleanQuotas;
130
+ verbManifest.quotas = manifest.quotas;
102
131
  }
103
- const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
104
- const path = statePath(home);
105
- await mkdir(dirname(path), { recursive: true });
106
- const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
107
- await writeFile(tmp, JSON.stringify(out, null, 2) + '\n', 'utf8');
108
- const { rename } = await import('node:fs/promises');
109
- await rename(tmp, path);
132
+
133
+ const payload = {
134
+ manifest: verbManifest,
135
+ scope,
136
+ homeDir: home,
137
+ };
138
+ if (ideId) {
139
+ payload.activated_by_ide = ideId;
140
+ payload.activated_by_pid = process.pid;
141
+ }
142
+
143
+ let result;
144
+ try {
145
+ result = await query('extension.set-active', payload, {
146
+ projectRoot: pickProjectRoot(opts),
147
+ homeDir: home,
148
+ });
149
+ } catch (err) {
150
+ // The SDK verb throws on protocol violations. Surface as the writer's
151
+ // {ok:false, error} contract so callers don't have to learn a new shape.
152
+ return { ok: false, error: err && err.message ? err.message : String(err) };
153
+ }
154
+ if (!result || result.ok !== true) {
155
+ return { ok: false, error: 'state-sdk extension.set-active returned non-ok' };
156
+ }
157
+
158
+ // Recover the activated_at timestamp the SDK stamped so we can pass it to
159
+ // resetExtensionQuotas (parity with the legacy writer's wall_clock_ms window).
160
+ // Best-effort: if the read fails, omit activated_at and the tracker resets
161
+ // counters without the window stamp.
162
+ let activatedAt;
163
+ try {
164
+ const raw = await readFile(result.path, 'utf8');
165
+ const parsed = JSON.parse(raw);
166
+ if (parsed && typeof parsed.activated_at === 'string') {
167
+ activatedAt = parsed.activated_at;
168
+ }
169
+ } catch { /* best-effort */ }
170
+
110
171
  // B16/SEC-M-02: reset quota counters on activate; stamp activated_at so
111
172
  // wall_clock_ms can be computed against this activation window.
112
173
  try {
@@ -115,21 +176,24 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
115
176
  // Quota reset failure must not block activation. Counters will self-heal
116
177
  // on next deactivate or the next activate of the same name.
117
178
  }
118
- return { ok: true, path };
179
+
180
+ return { ok: true, path: result.path };
119
181
  }
120
182
 
121
183
  /**
122
184
  * Clear the active-extension state file. Idempotent -- succeeds if file is absent.
123
185
  *
124
- * @param {{ homeDir?: string }} [opts]
186
+ * @param {{ homeDir?: string, projectRoot?: string }} [opts]
125
187
  * @returns {Promise<{ ok: boolean, removed: boolean }>}
126
188
  */
127
189
  export async function clearActiveExtension(opts = {}) {
128
190
  const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
129
- // B16/SEC-M-02: read the active extension name BEFORE unlinking so we can
130
- // clear its quota counters. Best-effort: if the file is missing or
191
+
192
+ // B16/SEC-M-02: read the active extension name BEFORE clearing so we can
193
+ // reset its quota counters. Best-effort: if the file is missing or
131
194
  // malformed, deactivate still succeeds.
132
195
  let extName = null;
196
+ const wasPresent = existsSync(statePath(home));
133
197
  try {
134
198
  const raw = await readFile(statePath(home), 'utf8');
135
199
  const parsed = JSON.parse(raw);
@@ -139,27 +203,42 @@ export async function clearActiveExtension(opts = {}) {
139
203
  } catch {
140
204
  // ignore — extName stays null
141
205
  }
206
+
207
+ // Drive the canonical clear through the SDK verb (manifest:null is the
208
+ // documented clear semantics — see STATE-SDK-CONTRACT.md §7).
209
+ // The verb is idempotent on an absent file (best-effort unlink).
210
+ let verbOk = false;
142
211
  try {
143
- await unlink(statePath(home));
144
- if (extName) {
145
- try {
146
- await resetExtensionQuotas(extName, { homeDir: home });
147
- } catch {
148
- // best-effort
149
- }
150
- }
151
- return { ok: true, removed: true };
152
- } catch (err) {
153
- if (err && err.code === 'ENOENT') {
154
- if (extName) {
155
- try {
156
- await resetExtensionQuotas(extName, { homeDir: home });
157
- } catch { /* best-effort */ }
158
- }
159
- return { ok: true, removed: false };
160
- }
161
- return { ok: false, removed: false };
212
+ const r = await query('extension.set-active', {
213
+ manifest: null,
214
+ // The verb requires a valid scope. 'project' is a safe sentinel — the
215
+ // clear branch does not consult `scope` for any state-file content
216
+ // (clear unlinks the file unconditionally). Any of the three valid
217
+ // values would work.
218
+ scope: 'project',
219
+ homeDir: home,
220
+ }, {
221
+ projectRoot: pickProjectRoot(opts),
222
+ homeDir: home,
223
+ });
224
+ verbOk = !!(r && r.ok);
225
+ } catch {
226
+ verbOk = false;
227
+ }
228
+
229
+ // Always reset quota counters if we know the ext name, regardless of
230
+ // whether the verb succeeded or the file was present — mirrors the
231
+ // legacy writer's ENOENT-tolerant best-effort reset.
232
+ if (extName) {
233
+ try {
234
+ await resetExtensionQuotas(extName, { homeDir: home });
235
+ } catch { /* best-effort */ }
236
+ }
237
+
238
+ if (verbOk) {
239
+ return { ok: true, removed: wasPresent };
162
240
  }
241
+ return { ok: false, removed: false };
163
242
  }
164
243
 
165
244
  /**
@@ -247,7 +326,8 @@ async function writeLastSeen(ideId, opts = {}) {
247
326
  try {
248
327
  await mkdir(dirname(path), { recursive: true });
249
328
  const body = JSON.stringify({ ide: ideId, last_seen_at: new Date().toISOString() }, null, 2) + '\n';
250
- const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
329
+ // v1.5.0 audit-LOW-update-#13: tmpSuffix() replaces inline randomBytes call.
330
+ const tmp = `${path}.tmp.${tmpSuffix({ bytes: 4, includePid: false })}`;
251
331
  await writeFile(tmp, body, 'utf8');
252
332
  const { rename } = await import('node:fs/promises');
253
333
  await rename(tmp, path);
package/src/api-client.js CHANGED
@@ -8,6 +8,19 @@ import { getTemplate } from './cross-dispatcher.js';
8
8
 
9
9
  const DEFAULT_TIMEOUT_MS = 30_000;
10
10
 
11
+ // v1.5.0 audit-DISPUTED-1 — user-message cache_control threshold.
12
+ //
13
+ // Anthropic prompt-caching has a server-side minimum (~1024 tokens). We
14
+ // gate the user-content-block split on a conservative 2KB byte cutoff so
15
+ // short audits (which would never recover the cache-write overhead)
16
+ // remain plain strings, while large diff/transcript targets (which DO
17
+ // amortize the cache) get split into a cacheable prefix + ephemeral tail.
18
+ //
19
+ // Pairs with the H4.2 ordering invariant: cycleSummary (or any per-turn
20
+ // content) MUST land AFTER the cacheable block so it never busts the
21
+ // cache prefix. See ADJUDICATIONS.md DISPUTED-1.
22
+ const CACHE_USER_THRESHOLD_BYTES = 2048;
23
+
11
24
  // ---------------------------------------------------------------------------
12
25
  // Provider request builders
13
26
  // ---------------------------------------------------------------------------
@@ -68,7 +81,7 @@ function buildGemini(system, user, model, key, timeoutMs, endpoint) {
68
81
  // Sonnet 4.5 prompt-caching threshold: 1024 tokens (rough: chars / 4).
69
82
  const CACHE_TOKEN_THRESHOLD = 1024;
70
83
 
71
- function buildAnthropic(system, user, model, key, timeoutMs) {
84
+ function buildAnthropic(system, user, model, key, timeoutMs, cycleSummary = null) {
72
85
  const promptChars = system.length + user.length;
73
86
  const estimatedTokens = Math.floor(promptChars / 4);
74
87
  const cacheEligible = estimatedTokens >= CACHE_TOKEN_THRESHOLD;
@@ -77,6 +90,26 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
77
90
  ? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
78
91
  : system;
79
92
 
93
+ // v1.5.0 audit-DISPUTED-1 — user-message cache_control.
94
+ //
95
+ // When the user content is large enough to amortize the cache-write
96
+ // overhead, split into a content-blocks array: the stable target gets
97
+ // cache_control:{type:'ephemeral'}, and any per-turn tail (cycleSummary)
98
+ // follows as a plain text block so it never busts the cache prefix.
99
+ //
100
+ // Below the threshold we keep the legacy plain-string form -- no need
101
+ // to spend cache-write tokens we can't recover.
102
+ const userBytes = Buffer.byteLength(user, 'utf8');
103
+ let userContent;
104
+ if (userBytes >= CACHE_USER_THRESHOLD_BYTES) {
105
+ const cacheableBlock = { type: 'text', text: user, cache_control: { type: 'ephemeral' } };
106
+ userContent = cycleSummary
107
+ ? [cacheableBlock, { type: 'text', text: cycleSummary }]
108
+ : [cacheableBlock];
109
+ } else {
110
+ userContent = user;
111
+ }
112
+
80
113
  return {
81
114
  url: 'https://api.anthropic.com/v1/messages',
82
115
  options: {
@@ -91,7 +124,7 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
91
124
  model,
92
125
  max_tokens: 4096,
93
126
  system: systemBlock,
94
- messages: [{ role: 'user', content: user }],
127
+ messages: [{ role: 'user', content: userContent }],
95
128
  }),
96
129
  signal: AbortSignal.timeout(timeoutMs),
97
130
  },
@@ -138,9 +171,13 @@ function extractCacheStats(json, cacheEligible) {
138
171
  // Main export
139
172
  // ---------------------------------------------------------------------------
140
173
 
141
- // runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?)
174
+ // runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?, opts?)
142
175
  // Returns { status: 'ok', raw, model } or { status: 'failed', error, model }.
143
- export async function runViaApi(pick, mode, angle, target, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, abortSignal = null) {
176
+ //
177
+ // opts.cycleSummary (string|null): when set on Anthropic calls, lands in a
178
+ // trailing ephemeral content block AFTER the cacheable user block so it
179
+ // never busts the cache prefix. Ignored for non-Anthropic providers.
180
+ export async function runViaApi(pick, mode, angle, target, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, abortSignal = null, opts = {}) {
144
181
  const fb = pick.apiFallback;
145
182
  if (!fb) return { status: 'failed', error: 'no API fallback configured', model: '' };
146
183
 
@@ -149,6 +186,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
149
186
 
150
187
  const { system, format } = getTemplate(mode, angle);
151
188
  const user = `${format}\n\n## Target\n\n${target}`;
189
+ const cycleSummary = opts.cycleSummary ?? null;
152
190
 
153
191
  // Combine caller abort signal with our per-call timeout signal.
154
192
  const timeoutSig = AbortSignal.timeout(timeoutMs);
@@ -164,7 +202,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
164
202
  } else if (fb.provider === 'google') {
165
203
  req = buildGemini(system, user, fb.model, key, timeoutMs, fb.endpoint);
166
204
  } else if (fb.provider === 'anthropic') {
167
- req = buildAnthropic(system, user, fb.model, key, timeoutMs);
205
+ req = buildAnthropic(system, user, fb.model, key, timeoutMs, cycleSummary);
168
206
  } else {
169
207
  return { status: 'failed', error: `unknown provider: ${fb.provider}`, model: fb.model };
170
208
  }
@@ -12,6 +12,38 @@
12
12
  // gets filtered as "self."
13
13
 
14
14
  import { spawnSync } from 'node:child_process';
15
+ import { getLatestModel } from './model-refresh.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // v1.5.0 F5 -- audit-rotation v0 schema (schema-only; runtime ships in v1.6.0)
19
+ // ---------------------------------------------------------------------------
20
+ //
21
+ // We are NOT shipping rotation logic in v1.5.0. We are shipping the schema
22
+ // commitment so callers / future code have a stable contract to encode
23
+ // against. Auto-rotation flips on in v1.6.0 once we have telemetry to
24
+ // decide rotation policy (cost-weighted, win-rate-weighted, round-robin).
25
+ //
26
+ // Contract:
27
+ // - ROTATION_SCHEMA_VERSION = 1: any future schema change bumps this
28
+ // integer; consumers MUST refuse to apply policy from a higher version
29
+ // than they support.
30
+ // - defaultRotationPolicy = 'manual': v0 behavior is "no rotation;
31
+ // caller (or operator) picks the auditor explicitly via pickAuditors
32
+ // `only:` or default-priority strategy." Other policy values reserved
33
+ // for v1.6.0: 'round-robin', 'cost-weighted', 'win-rate-weighted'.
34
+ //
35
+ // Shape (reserved; not consumed yet):
36
+ // {
37
+ // schema: ROTATION_SCHEMA_VERSION,
38
+ // policy: defaultRotationPolicy,
39
+ // window_days: 7, // reserved -- look-back for win-rate
40
+ // min_picks_per_auditor: 1, // reserved -- floor on usage
41
+ // last_rotated: <ISO>, // reserved -- persistence anchor
42
+ // }
43
+ //
44
+ /** @typedef {{ schema: number, policy: string, window_days?: number, min_picks_per_auditor?: number, last_rotated?: string }} RotationPolicy */
45
+ export const ROTATION_SCHEMA_VERSION = 1;
46
+ export const defaultRotationPolicy = 'manual';
15
47
 
16
48
  export const ROSTER = [
17
49
  {
@@ -19,7 +51,22 @@ export const ROSTER = [
19
51
  family: 'openai',
20
52
  model: '',
21
53
  name: 'Codex CLI',
54
+ // Prompt-via-stdin path. Proven working 2026-05-18 with codex-cli 0.130.0.
22
55
  invoke: 'codex exec --skip-git-repo-check --sandbox read-only -c approval_policy="never" -c mcp_servers.ijfw-memory.enabled=false -',
56
+ // Dedicated review subcommand path. Use when an audit target is a git ref
57
+ // (HEAD~N, branch name, or commit SHA). The -c mcp_servers.ijfw-memory.enabled=false
58
+ // override is LOAD-BEARING: without it, codex review hangs indefinitely on
59
+ // the ijfw_memory_prelude MCP tool autostart (cycle: codex spawns IJFW MCP
60
+ // server, prelude tool waits on a response, IJFW MCP server is itself the
61
+ // child of the codex session). Verified 2026-05-18, codex-cli 0.130.0.
62
+ // {REF} is the substitution token the caller swaps for the base git ref.
63
+ reviewInvoke: 'codex review --base {REF} -c approval_policy="never" -c mcp_servers.ijfw-memory.enabled=false',
64
+ // 8 min default per-auditor budget for review work. codex review against
65
+ // HEAD~5 with MCP disabled completed in ~75s during S7 reproduction;
66
+ // larger diffs and reasoning-heavy targets need headroom. The existing
67
+ // PROVIDER_TIMEOUT_MS['codex'] in cross-orchestrator.js is 120s (2 min),
68
+ // which is fine for exec-mode quick prompts but too tight for review.
69
+ timeoutMs: 8 * 60 * 1000,
23
70
  note: 'Different training lineage; fast on review tasks. The - flag reads prompt from stdin. --skip-git-repo-check bypasses the trusted-directory gate added in codex-cli 0.118.0. --sandbox read-only blocks file WRITES on the host (verified Codex 0.122.0: `echo > /tmp/x` returns `operation not permitted`); it does NOT block shell exec or subprocess launching -- a `read-only` sandbox can still run `ls`, `curl`, or `gemini`. The defense against codex going meta and shelling out to other auditors is the prompt-layer "Operating constraints" block in cross-dispatcher.js buildRequest, not the sandbox flag. The model layer additionally refuses to read explicitly-secret files like ~/.ssh/id_rsa or ~/.codex/auth.json even when prompt-injected to do so. The visibility surface in cross-orchestrator-cli.js cmdCross catches any residual silent failure. approval_policy="never" auto-approves without an interactive prompt. mcp_servers.ijfw-memory.enabled=false disables IJFW MCP for this session because Codex in `codex exec` mode under a non-bypass sandbox auto-cancels MCP tool calls -- the cancellation noise wastes tokens and the audit does not need IJFW memory recall (the brief contains the full target inline).',
24
71
  // CODEX_SESSION_ID is set by codex itself when running INSIDE a codex
25
72
  // session; CODEX_HOME is a config-path env var that's set whenever codex
@@ -28,7 +75,11 @@ export const ROSTER = [
28
75
  // installed but where the caller is something else (Claude Code, Cursor,
29
76
  // etc.). Surface noted by carrmjw during the qwen roster review (#11).
30
77
  detect: (env) => Boolean(env.CODEX_SESSION_ID) || /codex/i.test(env._ || ''),
31
- apiFallback: { provider: 'openai', model: 'gpt-4o-mini', authEnv: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1/chat/completions' },
78
+ // model is resolved at call-time via model-refresh.js (24h-cached probe of
79
+ // /v1/models). Hardcoded value below is the offline fallback only.
80
+ get apiFallback() {
81
+ return { provider: 'openai', model: getLatestModel('openai'), authEnv: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1/chat/completions' };
82
+ },
32
83
  },
33
84
  {
34
85
  id: 'gemini',
@@ -38,7 +89,10 @@ export const ROSTER = [
38
89
  invoke: 'gemini',
39
90
  note: 'Strong on security + architectural patterns. Auto-detects piped stdin for headless mode.',
40
91
  detect: (env) => Boolean(env.GEMINI_CLI || env.GOOGLE_CLOUD_PROJECT_GEMINI) || /gemini-cli/i.test(env._ || ''),
41
- apiFallback: { provider: 'google', model: 'gemini-2.0-flash', authEnv: 'GEMINI_API_KEY', endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent' },
92
+ // model is resolved at call-time via model-refresh.js (24h-cached probe).
93
+ get apiFallback() {
94
+ return { provider: 'google', model: getLatestModel('google'), authEnv: 'GEMINI_API_KEY', endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent' };
95
+ },
42
96
  },
43
97
  {
44
98
  id: 'qwen',
@@ -108,7 +162,10 @@ export const ROSTER = [
108
162
  invoke: 'claude -p',
109
163
  note: 'Anthropic; useful when you want a second Claude pass in a fresh session.',
110
164
  detect: (env) => Boolean(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT || env.CLAUDE_PLUGIN_ROOT),
111
- apiFallback: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001', authEnv: 'ANTHROPIC_API_KEY', endpoint: 'https://api.anthropic.com/v1/messages' },
165
+ // model is resolved at call-time via model-refresh.js (24h-cached probe).
166
+ get apiFallback() {
167
+ return { provider: 'anthropic', model: getLatestModel('anthropic'), authEnv: 'ANTHROPIC_API_KEY', endpoint: 'https://api.anthropic.com/v1/messages' };
168
+ },
112
169
  },
113
170
  ];
114
171
 
@@ -122,9 +179,27 @@ export function detectSelf(env = process.env) {
122
179
 
123
180
  // Probe whether the auditor's CLI is on PATH. Cached per process.
124
181
  // Exported so tests can prime the cache for deterministic behavior.
182
+ //
183
+ // v1.5.0 audit-LOW-trident-L1: cache entries now carry a timestamp and
184
+ // expire after 5 minutes. A long-running orchestrator session that installs
185
+ // an auditor mid-session (e.g. `npm install -g @google/gemini-cli`) was
186
+ // otherwise stuck with the stale "not installed" verdict for the rest of
187
+ // the process lifetime. 5min is comfortably longer than a typical Trident
188
+ // fan-out (which probes every auditor up front) but short enough that a
189
+ // mid-session install is detected on the next round.
125
190
  export const _installedCache = new Map();
191
+ const INSTALLED_CACHE_TTL_MS = 5 * 60 * 1000;
126
192
  export function isInstalled(id) {
127
- if (_installedCache.has(id)) return _installedCache.get(id);
193
+ const cached = _installedCache.get(id);
194
+ if (cached !== undefined) {
195
+ // Legacy entries (primed by tests as a raw boolean) are honoured
196
+ // forever — tests rely on that contract. New entries carry {value, ts}.
197
+ if (typeof cached === 'boolean') return cached;
198
+ if (cached && typeof cached === 'object' && cached.ts + INSTALLED_CACHE_TTL_MS > Date.now()) {
199
+ return cached.value;
200
+ }
201
+ // expired — fall through to re-probe
202
+ }
128
203
  const entry = ROSTER.find(e => e.id === id);
129
204
  if (!entry) return false;
130
205
  // First word of invoke is the binary; the rest are args.
@@ -133,7 +208,7 @@ export function isInstalled(id) {
133
208
  // works reliably across macOS + Linux. spawnSync exit code = 0 → present.
134
209
  const r = spawnSync('bash', ['-lc', `command -v ${JSON.stringify(bin)} >/dev/null 2>&1`], { timeout: 2000 });
135
210
  const installed = r.status === 0;
136
- _installedCache.set(id, installed);
211
+ _installedCache.set(id, { value: installed, ts: Date.now() });
137
212
  return installed;
138
213
  }
139
214