@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
@@ -36,6 +36,92 @@ import os from 'node:os';
36
36
 
37
37
  const SCHEMA_VERSION = '1.0';
38
38
 
39
+ // Maximum path length the registry will accept. Practical limits across the
40
+ // supported platforms are PATH_MAX=4096 (Linux), 1024 (macOS), 32767 (Windows
41
+ // long-path). 4096 is a comfortable cap that won't reject any real path but
42
+ // will reject a flood-of-bytes prompt-injection payload.
43
+ const MAX_PROJECT_ROOT_LEN = 4096;
44
+
45
+ // Unicode tag-block range (U+E0000–U+E007F) — invisible "ASCII Smuggler" code
46
+ // points historically used to hide prompt-injection payloads inside text the
47
+ // model sees but the human doesn't. Closed at the prelude in H1.4; we also
48
+ // reject at registry-write time to prevent contamination at the source.
49
+ // eslint-disable-next-line security/detect-unsafe-regex -- single-char Unicode range class; no quantifier; not backtrack-exploitable
50
+ const UNICODE_TAG_BLOCK_RE = /[\u{E0000}-\u{E007F}]/u;
51
+
52
+ // ASCII control characters (excluding nothing — \n \r \t \0 etc are ALL
53
+ // rejected when present in a path key).
54
+ // eslint-disable-next-line no-control-regex -- this regex EXISTS to reject control chars; lint hit is a false positive on the gate itself
55
+ const CONTROL_CHAR_RE = /[\x00-\x1F\x7F]/;
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Validation
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * F-SEC-4 (HIGH, update-install-trust audit v1.5.0):
63
+ * recordOverrideUse persisted any non-empty string as a project key and that
64
+ * value flows verbatim into the cross-project promote suggestion that lands
65
+ * in the prelude the model reads — a prompt-injection vector.
66
+ *
67
+ * Validate projectRoot is shaped like a real absolute filesystem path with
68
+ * no `..` segments, no control characters, no Unicode tag-block smuggling
69
+ * code points, and within a sane length cap. Returns `{ ok: true }` on pass
70
+ * or `{ ok: false, reason }` on fail. We do NOT throw — the caller in
71
+ * override-resolver.js wraps the registry write in a non-fatal try/catch
72
+ * and we want a structured signal instead of a thrown Error so future
73
+ * callers can surface a clean rejection without try/catch noise.
74
+ *
75
+ * @param {unknown} projectRoot
76
+ * @returns {{ ok: true } | { ok: false, reason: string }}
77
+ */
78
+ function validateProjectRoot(projectRoot) {
79
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
80
+ return { ok: false, reason: 'projectRoot must be a non-empty string' };
81
+ }
82
+ if (projectRoot.length > MAX_PROJECT_ROOT_LEN) {
83
+ return {
84
+ ok: false,
85
+ reason: `projectRoot exceeds max length ${MAX_PROJECT_ROOT_LEN}`,
86
+ };
87
+ }
88
+ if (!path.isAbsolute(projectRoot)) {
89
+ return { ok: false, reason: 'projectRoot must be an absolute path' };
90
+ }
91
+ // Reject any `..` segment in the RAW path before normalization. path.normalize
92
+ // would collapse interior `..` (e.g. `/a/../b` -> `/b`) and silently rewrite
93
+ // the registry key, defeating the validation. A real filesystem path passed
94
+ // by override-resolver.js is already canonical, so a `..` is a tampering
95
+ // signal. Split on both POSIX and Windows separators so /a/../b and \a\..\b
96
+ // are both caught regardless of host platform.
97
+ const rawSegments = projectRoot.split(/[\\/]/);
98
+ if (rawSegments.includes('..')) {
99
+ return { ok: false, reason: 'projectRoot must not contain `..` segments' };
100
+ }
101
+ if (CONTROL_CHAR_RE.test(projectRoot)) {
102
+ return {
103
+ ok: false,
104
+ reason: 'projectRoot must not contain control characters',
105
+ };
106
+ }
107
+ if (UNICODE_TAG_BLOCK_RE.test(projectRoot)) {
108
+ return {
109
+ ok: false,
110
+ reason: 'projectRoot must not contain Unicode tag-block characters',
111
+ };
112
+ }
113
+ // Reject prompt-injection / markdown / HTML tokens that are not meaningful
114
+ // in a real path but ARE meaningful when rendered into the prelude. These
115
+ // are not legal in any reasonable filesystem path.
116
+ if (/[<>`]/.test(projectRoot)) {
117
+ return {
118
+ ok: false,
119
+ reason: 'projectRoot must not contain `<`, `>`, or backtick',
120
+ };
121
+ }
122
+ return { ok: true };
123
+ }
124
+
39
125
  // ---------------------------------------------------------------------------
40
126
  // Path helpers — read os.homedir()/HOME at call time so tests can swap HOME
41
127
  // via process.env between calls.
@@ -111,21 +197,31 @@ async function readSettings() {
111
197
  * Idempotent — re-recording the same (project, preset) updates applied_at
112
198
  * rather than duplicating the entry.
113
199
  *
114
- * @param {string} projectRoot
200
+ * Returns `{ ok: true }` on success or `{ ok: false, reason }` on validation
201
+ * failure. Does NOT throw on bad input — the value flows into the prelude
202
+ * the model reads, so we want every untrusted projectRoot rejected loudly
203
+ * via the return contract instead of via an unhandled Error.
204
+ *
205
+ * @param {string} projectRoot MUST be an absolute path, no `..`, no
206
+ * control / Unicode tag-block / HTML chars.
115
207
  * @param {string} preset
116
208
  * @param {string} scope 'base' | 'user' | 'org' | 'project'
117
209
  * @param {string} [project_type] auto-detected project type, defaults to
118
210
  * whatever is already on file or 'unknown'.
211
+ * @returns {Promise<{ ok: true } | { ok: false, reason: string }>}
119
212
  */
120
213
  export async function recordOverrideUse(projectRoot, preset, scope, project_type) {
121
- if (typeof projectRoot !== 'string' || !projectRoot) {
122
- throw new Error('recordOverrideUse: projectRoot must be a non-empty string');
214
+ // F-SEC-4 (HIGH): full path validation on projectRoot before it enters the
215
+ // registry. See validateProjectRoot above for the threat-model rationale.
216
+ const pathCheck = validateProjectRoot(projectRoot);
217
+ if (!pathCheck.ok) {
218
+ return pathCheck;
123
219
  }
124
220
  if (typeof preset !== 'string' || !preset) {
125
- throw new Error('recordOverrideUse: preset must be a non-empty string');
221
+ return { ok: false, reason: 'preset must be a non-empty string' };
126
222
  }
127
223
  if (typeof scope !== 'string' || !scope) {
128
- throw new Error('recordOverrideUse: scope must be a non-empty string');
224
+ return { ok: false, reason: 'scope must be a non-empty string' };
129
225
  }
130
226
 
131
227
  const state = await readRegistry();
@@ -153,6 +249,7 @@ export async function recordOverrideUse(projectRoot, preset, scope, project_type
153
249
  }
154
250
  state.projects[projectRoot] = proj;
155
251
  await writeRegistry(state);
252
+ return { ok: true };
156
253
  }
157
254
 
158
255
  /**
@@ -186,6 +283,9 @@ export async function findProjectsWithOverride(preset) {
186
283
  const out = [];
187
284
  for (const [project, entry] of Object.entries(state.projects || {})) {
188
285
  if (!entry || !Array.isArray(entry.active_overrides)) continue;
286
+ // F-SEC-4 (HIGH) defense-in-depth: legacy registries written before
287
+ // v1.5.1 validation could contain unsafe keys. Refuse to surface them.
288
+ if (!validateProjectRoot(project).ok) continue;
189
289
  const hit = entry.active_overrides.find(
190
290
  (o) => o && o.preset === preset
191
291
  );
@@ -226,6 +326,10 @@ export async function findProjectsWithSimilarOverrideSet(
226
326
  for (const [project, entry] of Object.entries(state.projects || {})) {
227
327
  if (exclude && project === exclude) continue;
228
328
  if (!entry || !Array.isArray(entry.active_overrides)) continue;
329
+ // F-SEC-4 (HIGH) defense-in-depth: legacy registries written before
330
+ // v1.5.1 validation could contain unsafe keys that would flow into the
331
+ // promote suggestion text and the prelude. Refuse to surface them.
332
+ if (!validateProjectRoot(project).ok) continue;
229
333
  const theirs = new Set(
230
334
  entry.active_overrides
231
335
  .filter((o) => o && typeof o.preset === 'string')
@@ -304,4 +408,6 @@ export const __test = {
304
408
  settingsPath,
305
409
  readRegistry,
306
410
  writeRegistry,
411
+ validateProjectRoot,
412
+ MAX_PROJECT_ROOT_LEN,
307
413
  };
package/src/receipts.js CHANGED
@@ -9,6 +9,9 @@
9
9
 
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ // v1.5.0 N4.obs M1: tag every receipt with the orchestrator's trace_id so the
13
+ // dashboard can roll up sessions->traces->observations like Langfuse / Helicone.
14
+ import { getTraceId } from './observability/trace-id.js';
12
15
 
13
16
  export function RECEIPTS_FILE(projectDir) {
14
17
  return path.join(projectDir, '.ijfw', 'receipts', 'cross-runs.jsonl');
@@ -24,7 +27,13 @@ export function writeReceipt(projectDir, record) {
24
27
  const dest = RECEIPTS_FILE(projectDir);
25
28
  const dir = path.dirname(dest);
26
29
  fs.mkdirSync(dir, { recursive: true });
27
- fs.appendFileSync(dest, JSON.stringify(record) + '\n');
30
+ // v1.5.0 N4.obs M1: tag with trace_id if one is set + caller hasn't supplied
31
+ // one. Never overwrite an explicit caller-supplied trace_id.
32
+ const traceId = getTraceId();
33
+ const enriched = (traceId && record && typeof record === 'object' && !record.trace_id)
34
+ ? { ...record, trace_id: traceId }
35
+ : record;
36
+ fs.appendFileSync(dest, JSON.stringify(enriched) + '\n');
28
37
  _pruneReceipts(dest);
29
38
  }
30
39
 
@@ -46,8 +55,27 @@ export function purgeReceipts(projectDir) {
46
55
  return count;
47
56
  }
48
57
 
49
- // Anthropic cache-read savings rate (mirrors hero-line.js constant).
50
- const CACHE_SAVINGS_PER_TOKEN = 2.70 / 1_000_000;
58
+ // v1.5.0 audit-LOW-obs-L2: per-tier cache-read savings rate.
59
+ //
60
+ // Anthropic prompt caching: a cached read costs ~10% of an uncached input
61
+ // token. Savings are computed as (uncached_input_rate - cached_read_rate),
62
+ // which collapses to (0.9 * input_rate) per cached-read token. Values are
63
+ // derived from the canonical pricing table (mcp-server/src/cost/pricing.js).
64
+ //
65
+ // Default tier remains sonnet (matches the prior single-constant behaviour
66
+ // and is the dominant model on the hero-line surface), but cache_stats
67
+ // records that carry a `model` field now dispatch to the right rate.
68
+ const SONNET_CACHE_SAVINGS_PER_TOKEN = 2.70 / 1_000_000;
69
+ const OPUS_CACHE_SAVINGS_PER_TOKEN = 13.50 / 1_000_000;
70
+ const HAIKU_CACHE_SAVINGS_PER_TOKEN = 0.72 / 1_000_000;
71
+
72
+ function cacheSavingsPerTokenFor(model) {
73
+ if (typeof model !== 'string' || !model) return SONNET_CACHE_SAVINGS_PER_TOKEN;
74
+ const m = model.toLowerCase();
75
+ if (m.includes('opus')) return OPUS_CACHE_SAVINGS_PER_TOKEN;
76
+ if (m.includes('haiku')) return HAIKU_CACHE_SAVINGS_PER_TOKEN;
77
+ return SONNET_CACHE_SAVINGS_PER_TOKEN;
78
+ }
51
79
 
52
80
  // renderReceipt(record, phaseWave?, stepNum?)
53
81
  // phaseWave -- caller-supplied label for the narration header. Default is
@@ -101,7 +129,11 @@ export function renderReceipt(record, phaseWave = 'Trident', stepNum = 1) {
101
129
  lines.push(`Step ${stepNum}.4 -- cache created: ${cs.cache_creation_input_tokens} tokens`);
102
130
  }
103
131
  if (typeof cs.cache_read_input_tokens === 'number') {
104
- const saved = cs.cache_read_input_tokens * CACHE_SAVINGS_PER_TOKEN;
132
+ // v1.5.0 audit-LOW-obs-L2: dispatch on cache_stats.model when set;
133
+ // fall back to receipt-level model; finally fall back to sonnet.
134
+ const modelForRate = cs.model || record.model || null;
135
+ const rate = cacheSavingsPerTokenFor(modelForRate);
136
+ const saved = cs.cache_read_input_tokens * rate;
105
137
  const savedStr = saved >= 0.01 ? ` (~$${saved.toFixed(2)} saved)` : '';
106
138
  lines.push(`Step ${stepNum}.5 -- cache read: ${cs.cache_read_input_tokens} tokens${savedStr}`);
107
139
  }
@@ -4,10 +4,15 @@
4
4
  // not a replacement for IJFW memory, but they make recovery possible when chat
5
5
  // context or a generated memory summary goes missing.
6
6
 
7
- import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
8
8
  import { basename, join, resolve } from 'node:path';
9
9
  import { writeAtomic } from '../lib/atomic-io.js';
10
- import { appendBlackboardEvent, blackboardStatus, readBlackboard } from '../blackboard.js';
10
+ import {
11
+ appendBlackboardEvent,
12
+ blackboardPaths,
13
+ blackboardStatus,
14
+ readBlackboard,
15
+ } from '../blackboard.js';
11
16
  import { readTeamAssembly } from '../team/generator.js';
12
17
  import { buildSwarmPlan } from '../swarm/planner.js';
13
18
 
@@ -89,13 +94,43 @@ export function listCheckpoints(projectRoot = process.cwd()) {
89
94
  .map((file) => join(paths.dir, file));
90
95
  }
91
96
 
97
+ // v1.5.0 audit-LOW-work-L2: memoise buildSnapshot per (projectRoot, ms).
98
+ // Snapshot construction reads team + plan + blackboard, which are themselves
99
+ // I/O-heavy reads. The bb mtime cache already shortcuts the inner reads, but
100
+ // when a caller does back-to-back createCheckpoint() calls (e.g. on a wave
101
+ // boundary) we still re-build the wrapper N times. Memo is keyed on the
102
+ // project root + blackboard mtimes + ts so any state change invalidates;
103
+ // hot-path cache size is capped at 8 entries to stay tiny.
104
+ const SNAPSHOT_CACHE = new Map();
105
+ const SNAPSHOT_CACHE_MAX = 8;
106
+
107
+ function snapshotCacheKey(projectRoot, ts) {
108
+ const paths = blackboardPaths(projectRoot);
109
+ let tasksMtime = 0, claimsMtime = 0;
110
+ try { tasksMtime = statSync(paths.tasks).mtimeMs; } catch { /* default 0 */ }
111
+ try { claimsMtime = statSync(paths.claims).mtimeMs; } catch { /* default 0 */ }
112
+ return `${paths.root}::${ts}::${tasksMtime}::${claimsMtime}`;
113
+ }
114
+
92
115
  function buildSnapshot(projectRoot, meta) {
116
+ const cacheKey = snapshotCacheKey(projectRoot, meta.ts);
117
+ const cached = SNAPSHOT_CACHE.get(cacheKey);
118
+ if (cached) {
119
+ // Cached body is independent of meta.id / meta.label / meta.message;
120
+ // those are reapplied from the current call.
121
+ return {
122
+ ...cached,
123
+ id: meta.id,
124
+ label: meta.label,
125
+ message: meta.message || null,
126
+ };
127
+ }
93
128
  const blackboard = readBlackboard(projectRoot);
94
129
  const team = readTeamAssembly(projectRoot);
95
130
  const plan = buildSwarmPlan(projectRoot);
96
131
  const status = blackboardStatus(projectRoot);
97
132
  const tasks = blackboard.tasks.data.tasks || [];
98
- return {
133
+ const snapshot = {
99
134
  schema_version: 'ijfw-checkpoint/v1',
100
135
  id: meta.id,
101
136
  label: meta.label,
@@ -122,6 +157,24 @@ function buildSnapshot(projectRoot, meta) {
122
157
  recent: blackboard.recent,
123
158
  next: recommendedNext(team, plan, tasks),
124
159
  };
160
+ // Stash a meta-agnostic copy in the cache (id/label/message reapplied on hit).
161
+ SNAPSHOT_CACHE.set(cacheKey, {
162
+ ...snapshot,
163
+ id: null,
164
+ label: null,
165
+ message: null,
166
+ });
167
+ // Narrow LRU: cap the cache size and drop the oldest insertion when over.
168
+ if (SNAPSHOT_CACHE.size > SNAPSHOT_CACHE_MAX) {
169
+ const firstKey = SNAPSHOT_CACHE.keys().next().value;
170
+ if (firstKey !== undefined) SNAPSHOT_CACHE.delete(firstKey);
171
+ }
172
+ return snapshot;
173
+ }
174
+
175
+ // Exposed for tests / cache invalidation hooks.
176
+ export function _resetSnapshotCache() {
177
+ SNAPSHOT_CACHE.clear();
125
178
  }
126
179
 
127
180
  function renderCheckpoint(snapshot) {