@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,166 @@
1
+ /**
2
+ * observability/cost-anomaly.js -- v1.5.0 N4.obs M4.
3
+ *
4
+ * Rolling z-score anomaly detection on the daily cost series. No LLM call,
5
+ * no external service. Designed for the dashboard cost tile: when today is
6
+ * unusually expensive, surface it ("Today is 3.4x yesterday's average; top
7
+ * driver = ijfw_memory_search calls, 54% of cost").
8
+ *
9
+ * Inputs (per the dashboard's existing /api/data shape):
10
+ * - daily: { date: 'YYYY-MM-DD', cost: number }[] (oldest -> newest OR
11
+ * newest -> oldest;
12
+ * we sort defensively)
13
+ * - todayDrivers: { name: string, cost: number }[] (optional;
14
+ * top-driver
15
+ * attribution)
16
+ *
17
+ * Output:
18
+ * {
19
+ * anomalous: boolean, // true when today > mean + 2*stdev of trailing window
20
+ * today: number, // today's cost
21
+ * baseline: { mean, stdev, days, window } // baseline stats
22
+ * factor: number|null, // today / yesterday-baseline-mean (1.0 = on par)
23
+ * z: number|null, // (today - mean) / stdev (∞-bounded; null if stdev=0)
24
+ * topDriver: { name, cost, sharePct }|null
25
+ * reason: string // human-readable summary for the tile
26
+ * }
27
+ *
28
+ * Tuning knobs:
29
+ * - windowDays: rolling window length (default 7)
30
+ * - threshold: z above which we mark anomalous (default 2.0; equivalent to
31
+ * "more than 2 standard deviations above the trailing mean")
32
+ *
33
+ * Edge cases the dashboard relies on:
34
+ * - Fewer than `windowDays` historical days => not anomalous, return
35
+ * `{anomalous:false, reason:'insufficient_history', ...}` so the tile can
36
+ * hide gracefully.
37
+ * - Zero stdev (flat baseline) => fall back to factor-based check
38
+ * (today > baseline mean by >= 2x).
39
+ * - Empty/malformed series => anomalous=false, reason='no_data'.
40
+ */
41
+
42
+ const DEFAULT_WINDOW = 7;
43
+ const DEFAULT_THRESHOLD = 2.0;
44
+ // When stdev=0 but today exceeds mean by this multiple, still flag.
45
+ const FLAT_BASELINE_FACTOR = 2.0;
46
+
47
+ /**
48
+ * Detect a daily-cost anomaly.
49
+ *
50
+ * @param {{daily: Array<{date:string, cost:number}>, todayDrivers?: Array<{name:string,cost:number}>, windowDays?: number, threshold?: number}} input
51
+ * @returns {object} result described above
52
+ */
53
+ export function detectCostAnomaly(input = {}) {
54
+ const windowDays = Number.isFinite(input.windowDays) && input.windowDays > 0
55
+ ? Math.floor(input.windowDays)
56
+ : DEFAULT_WINDOW;
57
+ const threshold = Number.isFinite(input.threshold) && input.threshold > 0
58
+ ? input.threshold
59
+ : DEFAULT_THRESHOLD;
60
+
61
+ const daily = Array.isArray(input.daily) ? input.daily : [];
62
+ const cleaned = daily
63
+ .filter((d) => d && typeof d.date === 'string' && Number.isFinite(d.cost))
64
+ .map((d) => ({ date: d.date, cost: Math.max(0, d.cost) }))
65
+ // Sort oldest -> newest, defensively.
66
+ .sort((a, b) => a.date.localeCompare(b.date));
67
+
68
+ if (cleaned.length === 0) {
69
+ return {
70
+ anomalous: false,
71
+ today: 0,
72
+ baseline: { mean: 0, stdev: 0, days: 0, window: windowDays },
73
+ factor: null,
74
+ z: null,
75
+ topDriver: null,
76
+ reason: 'no_data',
77
+ };
78
+ }
79
+
80
+ // Today = last entry; trailing window = the windowDays entries BEFORE it.
81
+ const todayEntry = cleaned[cleaned.length - 1];
82
+ const trailing = cleaned.slice(Math.max(0, cleaned.length - 1 - windowDays), cleaned.length - 1);
83
+
84
+ if (trailing.length < Math.min(3, windowDays)) {
85
+ return {
86
+ anomalous: false,
87
+ today: todayEntry.cost,
88
+ baseline: { mean: 0, stdev: 0, days: trailing.length, window: windowDays },
89
+ factor: null,
90
+ z: null,
91
+ topDriver: pickTopDriver(input.todayDrivers, todayEntry.cost),
92
+ reason: 'insufficient_history',
93
+ };
94
+ }
95
+
96
+ // mean/stdev (sample variance, ddof=1 when n>1)
97
+ const n = trailing.length;
98
+ const sum = trailing.reduce((s, d) => s + d.cost, 0);
99
+ const mean = sum / n;
100
+ const variance = n > 1
101
+ ? trailing.reduce((s, d) => s + (d.cost - mean) ** 2, 0) / (n - 1)
102
+ : 0;
103
+ const stdev = Math.sqrt(variance);
104
+
105
+ const today = todayEntry.cost;
106
+ const factor = mean > 0 ? today / mean : null;
107
+ const z = stdev > 0 ? (today - mean) / stdev : null;
108
+
109
+ let anomalous = false;
110
+ let reason;
111
+ if (z !== null) {
112
+ anomalous = z >= threshold;
113
+ if (anomalous) {
114
+ reason = `Today is ${factor != null ? factor.toFixed(1) : '?'}x the ${windowDays}-day average (z=${z.toFixed(1)}).`;
115
+ } else {
116
+ reason = `Within normal range (z=${z.toFixed(1)}, threshold=${threshold}).`;
117
+ }
118
+ } else if (mean > 0 && factor !== null && factor >= FLAT_BASELINE_FACTOR) {
119
+ // Flat baseline -- mean > 0 but stdev == 0. Still flag if we more than
120
+ // doubled.
121
+ anomalous = true;
122
+ reason = `Today is ${factor.toFixed(1)}x the ${windowDays}-day flat baseline.`;
123
+ } else if (mean === 0) {
124
+ // Baseline was zero -- today is anomalous iff there's non-zero spend.
125
+ anomalous = today > 0;
126
+ reason = anomalous
127
+ ? `First non-zero day after a ${windowDays}-day quiet stretch.`
128
+ : 'No spend in window.';
129
+ } else {
130
+ reason = 'Within normal range (flat baseline, factor < 2).';
131
+ }
132
+
133
+ const topDriver = pickTopDriver(input.todayDrivers, today);
134
+ if (anomalous && topDriver) {
135
+ reason += ` Top driver: ${topDriver.name} (${topDriver.sharePct}%).`;
136
+ }
137
+
138
+ return {
139
+ anomalous,
140
+ today,
141
+ baseline: {
142
+ mean,
143
+ stdev,
144
+ days: n,
145
+ window: windowDays,
146
+ },
147
+ factor,
148
+ z,
149
+ topDriver,
150
+ reason,
151
+ };
152
+ }
153
+
154
+ function pickTopDriver(drivers, total) {
155
+ if (!Array.isArray(drivers) || drivers.length === 0) return null;
156
+ if (!Number.isFinite(total) || total <= 0) return null;
157
+ let top = null;
158
+ for (const d of drivers) {
159
+ if (!d || typeof d.name !== 'string' || !Number.isFinite(d.cost)) continue;
160
+ if (d.cost <= 0) continue;
161
+ if (!top || d.cost > top.cost) top = d;
162
+ }
163
+ if (!top) return null;
164
+ const sharePct = Math.round((top.cost / total) * 100);
165
+ return { name: top.name, cost: top.cost, sharePct };
166
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * observability/evaluator-checkpoint-contract.js -- v1.5.0 N4.obs M3.
3
+ *
4
+ * First shipped pre-built evaluator. Story for the audit / dashboard / docs:
5
+ *
6
+ * "IJFW ships evaluators. Even without integrating with Weave / Phoenix /
7
+ * Langfuse, you get N built-in checks that score every subagent report
8
+ * against the orchestrator's contracts."
9
+ *
10
+ * THIS evaluator scores a subagent's final Status block against the v1.4.4 N2
11
+ * status-protocol contract -- the four MANDATORY sections every implementer
12
+ * agent is required to emit on completion:
13
+ *
14
+ * 1. Status: -- one of DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
15
+ * 2. SHAs: -- list of commit SHAs landed (or "(none)")
16
+ * 3. Files: -- list of files touched (or "(none)")
17
+ * 4. Tests: -- "<pass>/<total>" or "(none)"
18
+ *
19
+ * The four-section contract is enforced by:
20
+ * - orchestrator/checkpoint-contract.md (cadence + size cap)
21
+ * - orchestrator/status-protocol.js (parseAgentReport regex)
22
+ * - the N4.obs M3 brief instructions all implementer subagents receive
23
+ *
24
+ * Returns:
25
+ * {
26
+ * valid: boolean, // true iff missing[] is empty
27
+ * missing: string[], // section names that were absent
28
+ * details: { [section]: 'present'|'absent' },
29
+ * reason?: string, // when valid=false, short rationale
30
+ * }
31
+ *
32
+ * Designed to be cheap (regex-only, no LLM call) so dispatch-planner and
33
+ * plan-checker can run it inline on every report.
34
+ *
35
+ * Future evaluators can be added next to this file under the same exported
36
+ * `evaluators` registry shape so the dashboard can enumerate them.
37
+ */
38
+
39
+ /** Canonical evaluator id -- stable, matches the file slug. */
40
+ export const ID = 'checkpoint-contract';
41
+
42
+ /** The four sections required by v1.4.4 N2. Stable order = stable UI. */
43
+ export const REQUIRED_SECTIONS = Object.freeze([
44
+ 'STATUS',
45
+ 'SHAS',
46
+ 'FILES',
47
+ 'TESTS',
48
+ ]);
49
+
50
+ /**
51
+ * Synonyms tolerated for each section heading. Implementer agents have shipped
52
+ * a few minor variants ("SHA(s):", "Files:", "Tests:"), so the evaluator
53
+ * accepts them all. The status-protocol parser is stricter; we want this
54
+ * evaluator to be a high-recall first-pass so it doesn't false-flag obviously
55
+ * compliant reports.
56
+ */
57
+ const SECTION_PATTERNS = Object.freeze({
58
+ // STATUS must be on its own line; allow trailing space + colon + value.
59
+ STATUS: /^\s*Status\s*:\s*(?:DONE(?:_WITH_CONCERNS)?|NEEDS_CONTEXT|BLOCKED)\b/im,
60
+ SHAS: /^\s*SHA(?:s|\(s\))?\s*:/im,
61
+ FILES: /^\s*Files?\s*:/im,
62
+ TESTS: /^\s*Tests?\s*:/im,
63
+ });
64
+
65
+ /**
66
+ * Evaluate a subagent's final report against the checkpoint-contract.
67
+ *
68
+ * @param {string} statusBlock the agent's final Status block (or full report)
69
+ * @returns {{valid: boolean, missing: string[], details: Record<string, 'present'|'absent'>, reason?: string}}
70
+ */
71
+ export function evaluateCheckpointContract(statusBlock) {
72
+ if (typeof statusBlock !== 'string' || statusBlock.length === 0) {
73
+ return {
74
+ valid: false,
75
+ missing: [...REQUIRED_SECTIONS],
76
+ details: Object.fromEntries(REQUIRED_SECTIONS.map((s) => [s, 'absent'])),
77
+ reason: 'empty or non-string report',
78
+ };
79
+ }
80
+
81
+ const details = {};
82
+ const missing = [];
83
+ for (const section of REQUIRED_SECTIONS) {
84
+ const re = SECTION_PATTERNS[section];
85
+ const present = re ? re.test(statusBlock) : false;
86
+ details[section] = present ? 'present' : 'absent';
87
+ if (!present) missing.push(section);
88
+ }
89
+
90
+ const valid = missing.length === 0;
91
+ return {
92
+ valid,
93
+ missing,
94
+ details,
95
+ ...(valid ? {} : { reason: `missing required sections: ${missing.join(', ')}` }),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Evaluator registry entry. Loaders (dashboard tile, plan-checker, dispatcher)
101
+ * import this to enumerate built-in evaluators without name-spelunking.
102
+ */
103
+ export const evaluator = Object.freeze({
104
+ id: ID,
105
+ title: 'Checkpoint contract (4 required sections)',
106
+ description:
107
+ 'Scores a subagent\'s final Status block against the v1.4.4 N2 four-section contract: Status / SHAs / Files / Tests.',
108
+ // Per-evaluator weights live with the evaluator so dashboard can show them
109
+ // alongside the score without an external registry.
110
+ weight: 1.0,
111
+ run: evaluateCheckpointContract,
112
+ });
113
+
114
+ /**
115
+ * Registry shape. Adding evaluators later is "import + push".
116
+ */
117
+ export const evaluators = Object.freeze([evaluator]);
@@ -0,0 +1,163 @@
1
+ /**
2
+ * observability/trace-id.js -- v1.5.0 N4.obs M1
3
+ *
4
+ * Session-scoped trace IDs (Langfuse / Helicone-style sessions->traces->observations
5
+ * rollup). One UUID per orchestrator session, propagated to subagent worktrees
6
+ * via the IJFW_TRACE_ID env var, and recorded on every checkpoint / receipt /
7
+ * observation / session row.
8
+ *
9
+ * Discovery order (caller-side):
10
+ * 1. process.env.IJFW_TRACE_ID (set explicitly by orchestrator or subagent parent)
11
+ * 2. lazy-init: generate one and cache in module state
12
+ *
13
+ * Zero deps -- Node built-in crypto only.
14
+ *
15
+ * Threading model: a single Node process holds at most ONE current trace id at a
16
+ * time. Subagents inherit via env var; if they call ensureTraceId() they keep
17
+ * the parent's id. resetTraceId() exists for tests only.
18
+ */
19
+
20
+ import { randomUUID } from 'node:crypto';
21
+
22
+ const ENV_VAR = 'IJFW_TRACE_ID';
23
+ // RFC 4122 v4 UUID -- 32 hex chars + 4 dashes
24
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25
+
26
+ let _cached = null;
27
+
28
+ /**
29
+ * Hierarchical observation path (Helicone-style) -- v1.5.0 N4.obs M2.
30
+ *
31
+ * The orchestrator-LLM walks down `/wave-<waveId>/sub-<subId>/tool-<toolName>`
32
+ * via pushPath/popPath wrappers (NOT exported -- callers compose the segment
33
+ * themselves so the runtime doesn't have to track a stack across async ops).
34
+ */
35
+ const PATH_SEPARATOR = '/';
36
+
37
+ /**
38
+ * Validate a UUID-shaped trace id. Reject anything else so a poisoned env
39
+ * var (e.g. `IJFW_TRACE_ID=$(rm -rf /)`) doesn't flow into receipt files.
40
+ */
41
+ export function isValidTraceId(value) {
42
+ return typeof value === 'string' && UUID_PATTERN.test(value);
43
+ }
44
+
45
+ /**
46
+ * Get the current trace id, generating one if none exists. Idempotent within a
47
+ * process: subsequent calls return the same id unless `resetTraceId` is called.
48
+ *
49
+ * If process.env.IJFW_TRACE_ID is set and valid, it is adopted (this is how
50
+ * worktree subagents inherit the orchestrator's trace).
51
+ *
52
+ * @returns {string} current trace id (RFC 4122 v4 UUID).
53
+ */
54
+ export function ensureTraceId() {
55
+ if (_cached && isValidTraceId(_cached)) return _cached;
56
+ const fromEnv = process.env[ENV_VAR];
57
+ if (isValidTraceId(fromEnv)) {
58
+ _cached = fromEnv;
59
+ return _cached;
60
+ }
61
+ _cached = randomUUID();
62
+ // Reflect into env so a child process spawned without an explicit env arg
63
+ // still inherits via the standard mechanism.
64
+ process.env[ENV_VAR] = _cached;
65
+ return _cached;
66
+ }
67
+
68
+ /**
69
+ * Read the current trace id without generating one. Returns null if neither the
70
+ * cache nor the env var has a valid id. Useful for "tag if available, don't
71
+ * fabricate" call sites.
72
+ *
73
+ * @returns {string|null}
74
+ */
75
+ export function getTraceId() {
76
+ if (_cached && isValidTraceId(_cached)) return _cached;
77
+ const fromEnv = process.env[ENV_VAR];
78
+ if (isValidTraceId(fromEnv)) {
79
+ _cached = fromEnv;
80
+ return _cached;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Adopt a trace id explicitly. Used when an orchestrator dispatches a subagent
87
+ * and wants to set the env var into the spawn options.
88
+ *
89
+ * @param {string} id
90
+ * @returns {string} the adopted id (throws if invalid)
91
+ */
92
+ export function setTraceId(id) {
93
+ if (!isValidTraceId(id)) {
94
+ throw new Error(`trace-id: refusing to adopt invalid trace id "${id}"`);
95
+ }
96
+ _cached = id;
97
+ process.env[ENV_VAR] = id;
98
+ return _cached;
99
+ }
100
+
101
+ /**
102
+ * Test-only: clear the cached trace id and the env var. Caller-side tests
103
+ * import this to reset state between cases.
104
+ */
105
+ export function resetTraceId() {
106
+ _cached = null;
107
+ delete process.env[ENV_VAR];
108
+ }
109
+
110
+ /**
111
+ * Build the env object to hand to child_process.spawn / Agent worktree dispatch
112
+ * so the child inherits the current trace id. Pass-through clones existing env
113
+ * keys; callers MAY override.
114
+ *
115
+ * @param {object} [extra] extra env keys to merge in
116
+ * @returns {object}
117
+ */
118
+ export function traceEnv(extra = {}) {
119
+ const id = ensureTraceId();
120
+ return { ...process.env, [ENV_VAR]: id, ...extra };
121
+ }
122
+
123
+ /**
124
+ * Compose a hierarchical observation path -- v1.5.0 N4.obs M2.
125
+ *
126
+ * Convention: `/wave-<waveId>/sub-<subId>/tool-<toolName>`. Each segment is
127
+ * sanitised to `[A-Za-z0-9_-]` so the path is safe to render in a dashboard
128
+ * tree without escaping. Empty/missing segments are skipped.
129
+ *
130
+ * Example:
131
+ * composePath({ waveId: 'W12-A', subId: 'N05', tool: 'Bash' })
132
+ * -> "/wave-W12-A/sub-N05/tool-Bash"
133
+ *
134
+ * @param {{waveId?: string, subId?: string, tool?: string, segments?: string[]}} parts
135
+ * @returns {string}
136
+ */
137
+ export function composePath(parts = {}) {
138
+ const segs = [];
139
+ if (parts && typeof parts === 'object') {
140
+ if (typeof parts.waveId === 'string' && parts.waveId.length > 0) {
141
+ segs.push(`wave-${sanitiseSegment(parts.waveId)}`);
142
+ }
143
+ if (typeof parts.subId === 'string' && parts.subId.length > 0) {
144
+ segs.push(`sub-${sanitiseSegment(parts.subId)}`);
145
+ }
146
+ if (typeof parts.tool === 'string' && parts.tool.length > 0) {
147
+ segs.push(`tool-${sanitiseSegment(parts.tool)}`);
148
+ }
149
+ if (Array.isArray(parts.segments)) {
150
+ for (const s of parts.segments) {
151
+ if (typeof s !== 'string' || s.length === 0) continue;
152
+ segs.push(sanitiseSegment(s));
153
+ }
154
+ }
155
+ }
156
+ if (segs.length === 0) return '';
157
+ return PATH_SEPARATOR + segs.join(PATH_SEPARATOR);
158
+ }
159
+
160
+ function sanitiseSegment(s) {
161
+ // Collapse anything outside [A-Za-z0-9_-] to '_'. Cap at 64 chars.
162
+ return String(s).replace(/[^A-Za-z0-9_-]+/g, '_').slice(0, 64);
163
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * agents-md-blackboard.js — populate the AGENTS.md BLACKBOARD marker block.
3
+ *
4
+ * v1.5.0 S4 (initial landing): shelled out to
5
+ * claude/skills/ijfw-agents-md/scripts/merge-block-aware.sh
6
+ * under `withFsLock(.AGENTS.md.lock)` via `execFile('bash', …)`. That
7
+ * pattern HELD an fs-lock across a subprocess spawn — a STATE-SDK-CONTRACT
8
+ * §3 violation:
9
+ * "No lock is held across a subprocess spawn. `merge-block-aware.sh` is
10
+ * ported to in-process JS (T8); any unavoidable spawn pre-renders its
11
+ * payload and runs outside the lock."
12
+ *
13
+ * v1.5.0 T8 (this rewrite):
14
+ * - The shell script is ported to in-process JS at
15
+ * `./merge-block-aware.js` (single-pair AGENTS.md block merge with
16
+ * parity-tested semantics — see `test-merge-block-aware.js`).
17
+ * - The AGENTS.md write happens under `withFsLock(lockPathFor(<AGENTS.md>))`
18
+ * — i.e. the STATE-SDK-CONTRACT §3 #8 tier lock — using the SAME lock
19
+ * primitive every other state writer uses. No subprocess is ever
20
+ * spawned anywhere in the call graph; the critical section is
21
+ * read → mergeBlocks() → writeAtomic().
22
+ * - The blackboard refresh is registered with the SDK as a fire-and-forget
23
+ * observability event via `query('event.emit', …)` AFTER the lock is
24
+ * released. This is the SDK-routing requirement from the T8 brief
25
+ * ("Blackboard writes route through verbs"): every blackboard write
26
+ * emits an `agents-md.blackboard.set` event, classifiable by replay.
27
+ *
28
+ * SDK-VERB GAP — T8-followup-1 (mirror of T7-followup-1):
29
+ * The frozen 20-verb state-SDK contract (`.planning/v150-gap-closure/
30
+ * STATE-SDK-CONTRACT.md` §7+§8) does NOT include a verb that writes
31
+ * `<projectRoot>/AGENTS.md`. The contract §1 table lists this module
32
+ * ("agents-md-blackboard.js (T8)") as the sole writer for the BLACKBOARD
33
+ * marker block, and the contract §3 lock hierarchy assigns AGENTS.md to
34
+ * tier #8 — but no verb signature emits that write today. T8 closes the
35
+ * subprocess-under-lock violation in-process; absorbing the actual file
36
+ * write into a future `agents-md.blackboard.set` verb is deferred to a
37
+ * later contract amendment (do-not-touch state-sdk.js per the T8 brief).
38
+ * The §3 #8 lock IS taken, the §3 §4 ordering invariant holds (no other
39
+ * tier locks are acquired by this writer), and the SDK is notified of the
40
+ * write via the `event.emit` verb after release — so the absence of a
41
+ * dedicated verb is purely a contract-API gap, not a correctness gap.
42
+ *
43
+ * Failure handling: non-throwing. Returns `{ok, reason?, error?}` so a
44
+ * missing AGENTS.md / missing template / write-error degrades the blackboard
45
+ * refresh to advisory (the wave-state checkpoint must never be blocked by an
46
+ * AGENTS.md hiccup — see `wave-state.js#checkpointWave`).
47
+ */
48
+
49
+ import { join } from 'node:path';
50
+
51
+ import { withFsLock, lockPathFor } from '../fs-lock.js';
52
+ import { readWaveState } from './wave-state.js';
53
+ import { mergeFile, MergeBlockAwareError } from './merge-block-aware.js';
54
+ import { query } from './state-sdk.js';
55
+
56
+ /**
57
+ * Render the BLACKBOARD marker-block payload from a wave's STATE.md
58
+ * frontmatter slice. Stable shape — consumers (humans + the dashboard's
59
+ * BLACKBOARD lens) read these keys directly.
60
+ *
61
+ * @param {string} waveId
62
+ * @param {object} state parsed STATE.md (`readWaveState` result)
63
+ * @returns {string} pretty-printed JSON (2-space indent — matches v1.5.0 S4)
64
+ */
65
+ function renderBlackboardPayload(waveId, state) {
66
+ const fm = state?.frontmatter || {};
67
+ return JSON.stringify({
68
+ wave_id: waveId,
69
+ state_path: `.ijfw/wave-${waveId}/STATE.md`,
70
+ status: fm.status,
71
+ claims_active: fm.claims_active ?? 0,
72
+ blockers_open: fm.blockers_open ?? [],
73
+ findings_recent: fm.findings_recent ?? [],
74
+ updated_at: new Date().toISOString(),
75
+ }, null, 2);
76
+ }
77
+
78
+ /**
79
+ * Refresh the BLACKBOARD marker block in `<projectRoot>/AGENTS.md` from the
80
+ * given wave's STATE.md. Held under the §3 #8 AGENTS.md lock; in-process
81
+ * (no spawn). Best-effort SDK event emit after lock release.
82
+ *
83
+ * Return shapes:
84
+ * `{ ok: true }` — wrote AGENTS.md
85
+ * `{ ok: false, reason: 'no-state' }` — wave STATE.md absent (skip)
86
+ * `{ ok: false, reason: 'merge-error', error }` — merger threw
87
+ *
88
+ * @param {string} waveId
89
+ * @param {string} projectRoot
90
+ * @returns {Promise<{ok: boolean, reason?: string, error?: string}>}
91
+ */
92
+ export async function populateBlackboardBlock(waveId, projectRoot) {
93
+ const state = await readWaveState(waveId, projectRoot);
94
+ if (!state) return { ok: false, reason: 'no-state' };
95
+
96
+ const payload = renderBlackboardPayload(waveId, state);
97
+ const agentsMdPath = join(projectRoot, 'AGENTS.md');
98
+ const lockPath = lockPathFor(agentsMdPath);
99
+
100
+ let mergeResult;
101
+ let mergeError = null;
102
+ try {
103
+ mergeResult = await withFsLock(lockPath, async () => mergeFile(
104
+ agentsMdPath,
105
+ [{ block: 'BLACKBOARD', content: payload }],
106
+ ));
107
+ } catch (err) {
108
+ mergeError = err;
109
+ }
110
+
111
+ if (mergeError) {
112
+ // Surface the same shape callers expected from the legacy
113
+ // `execFile`-of-merger failure mode: ok:false + a reason + an error
114
+ // string. `reason` is the canonical T8 replacement for the v1.5.0 S4
115
+ // `merger-failed`/`merger-missing` strings (the merger is now in-
116
+ // process — the failure surface is "merge-error").
117
+ const code = mergeError instanceof MergeBlockAwareError ? mergeError.code : null;
118
+ return {
119
+ ok: false,
120
+ reason: code === 'ERR_TEMPLATE_MISSING' ? 'template-missing' : 'merge-error',
121
+ error: String(mergeError.message || mergeError),
122
+ };
123
+ }
124
+
125
+ // SDK-routed observability event — fire-and-forget AFTER the lock has been
126
+ // released. STATE-SDK-CONTRACT §5 (Model 3): the event tap is observability,
127
+ // not state. Failure here is swallowed (advisory by design).
128
+ //
129
+ // `event.emit` requires `subagentId` + `waveId` (both stable from the
130
+ // checkpoint context). `dedupKey` uses the file bytes + waveId so two
131
+ // identical refreshes within the same wave are idempotent on the event
132
+ // log (matches the §6 append/dedup semantics).
133
+ try {
134
+ await query('event.emit', {
135
+ subagentId: 'parent',
136
+ waveId,
137
+ eventType: 'agents-md.blackboard.set',
138
+ data: {
139
+ path: mergeResult?.path ?? agentsMdPath,
140
+ bytes: mergeResult?.bytes ?? 0,
141
+ seeded: !!mergeResult?.seeded,
142
+ wave_status: state?.frontmatter?.status ?? null,
143
+ },
144
+ dedupKey: `agents-md.blackboard.set:${waveId}:${mergeResult?.bytes ?? 0}`,
145
+ }, { projectRoot, subagentId: 'parent' });
146
+ } catch {
147
+ // Observability is best-effort; never demote a successful AGENTS.md
148
+ // rewrite because the event tap had a hiccup.
149
+ }
150
+
151
+ return { ok: true };
152
+ }