@ijfw/memory-server 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -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
|
+
}
|