daftari 1.21.0 → 1.23.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 (95) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +7 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/consolidate/admit.d.ts +15 -0
  6. package/dist/consolidate/admit.d.ts.map +1 -0
  7. package/dist/consolidate/admit.js +176 -0
  8. package/dist/consolidate/admit.js.map +1 -0
  9. package/dist/consolidate/birth.d.ts +79 -0
  10. package/dist/consolidate/birth.d.ts.map +1 -0
  11. package/dist/consolidate/birth.js +315 -0
  12. package/dist/consolidate/birth.js.map +1 -0
  13. package/dist/consolidate/clocks.d.ts +15 -0
  14. package/dist/consolidate/clocks.d.ts.map +1 -0
  15. package/dist/consolidate/clocks.js +127 -0
  16. package/dist/consolidate/clocks.js.map +1 -0
  17. package/dist/consolidate/constants.d.ts +26 -0
  18. package/dist/consolidate/constants.d.ts.map +1 -0
  19. package/dist/consolidate/constants.js +83 -0
  20. package/dist/consolidate/constants.js.map +1 -0
  21. package/dist/consolidate/decorrelation.d.ts +69 -0
  22. package/dist/consolidate/decorrelation.d.ts.map +1 -0
  23. package/dist/consolidate/decorrelation.js +287 -0
  24. package/dist/consolidate/decorrelation.js.map +1 -0
  25. package/dist/consolidate/derivation-prompt.d.ts +28 -0
  26. package/dist/consolidate/derivation-prompt.d.ts.map +1 -0
  27. package/dist/consolidate/derivation-prompt.js +72 -0
  28. package/dist/consolidate/derivation-prompt.js.map +1 -0
  29. package/dist/consolidate/edge-write.d.ts +9 -0
  30. package/dist/consolidate/edge-write.d.ts.map +1 -0
  31. package/dist/consolidate/edge-write.js +59 -0
  32. package/dist/consolidate/edge-write.js.map +1 -0
  33. package/dist/consolidate/envelope.d.ts +27 -0
  34. package/dist/consolidate/envelope.d.ts.map +1 -0
  35. package/dist/consolidate/envelope.js +90 -0
  36. package/dist/consolidate/envelope.js.map +1 -0
  37. package/dist/consolidate/index.d.ts +2 -0
  38. package/dist/consolidate/index.d.ts.map +1 -0
  39. package/dist/consolidate/index.js +531 -0
  40. package/dist/consolidate/index.js.map +1 -0
  41. package/dist/consolidate/priority.d.ts +25 -0
  42. package/dist/consolidate/priority.d.ts.map +1 -0
  43. package/dist/consolidate/priority.js +103 -0
  44. package/dist/consolidate/priority.js.map +1 -0
  45. package/dist/consolidate/revision.d.ts +81 -0
  46. package/dist/consolidate/revision.d.ts.map +1 -0
  47. package/dist/consolidate/revision.js +274 -0
  48. package/dist/consolidate/revision.js.map +1 -0
  49. package/dist/consolidate/state.d.ts +10 -0
  50. package/dist/consolidate/state.d.ts.map +1 -0
  51. package/dist/consolidate/state.js +54 -0
  52. package/dist/consolidate/state.js.map +1 -0
  53. package/dist/curation/edges.d.ts +5 -0
  54. package/dist/curation/edges.d.ts.map +1 -1
  55. package/dist/curation/edges.js +82 -9
  56. package/dist/curation/edges.js.map +1 -1
  57. package/dist/curation/shadow.d.ts +30 -0
  58. package/dist/curation/shadow.d.ts.map +1 -1
  59. package/dist/curation/shadow.js +84 -26
  60. package/dist/curation/shadow.js.map +1 -1
  61. package/dist/curation/staged-actions.d.ts +2 -0
  62. package/dist/curation/staged-actions.d.ts.map +1 -1
  63. package/dist/curation/staged-actions.js +8 -0
  64. package/dist/curation/staged-actions.js.map +1 -1
  65. package/dist/curation/tension.d.ts +1 -0
  66. package/dist/curation/tension.d.ts.map +1 -1
  67. package/dist/curation/tension.js +8 -0
  68. package/dist/curation/tension.js.map +1 -1
  69. package/dist/curation/vault-docs.d.ts +2 -1
  70. package/dist/curation/vault-docs.d.ts.map +1 -1
  71. package/dist/curation/vault-docs.js +1 -0
  72. package/dist/curation/vault-docs.js.map +1 -1
  73. package/dist/eval/llm.d.ts +3 -1
  74. package/dist/eval/llm.d.ts.map +1 -1
  75. package/dist/eval/llm.js +6 -2
  76. package/dist/eval/llm.js.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/storage/index-db.d.ts +2 -0
  79. package/dist/storage/index-db.d.ts.map +1 -1
  80. package/dist/storage/index-db.js +14 -9
  81. package/dist/storage/index-db.js.map +1 -1
  82. package/dist/tools/curation.d.ts.map +1 -1
  83. package/dist/tools/curation.js +16 -2
  84. package/dist/tools/curation.js.map +1 -1
  85. package/dist/tools/edges.d.ts.map +1 -1
  86. package/dist/tools/edges.js +1 -0
  87. package/dist/tools/edges.js.map +1 -1
  88. package/dist/tools/staged-actions.d.ts.map +1 -1
  89. package/dist/tools/staged-actions.js +2 -0
  90. package/dist/tools/staged-actions.js.map +1 -1
  91. package/dist/utils/git.d.ts +1 -0
  92. package/dist/utils/git.d.ts.map +1 -1
  93. package/dist/utils/git.js +19 -0
  94. package/dist/utils/git.js.map +1 -1
  95. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ // The two-gate envelope (spec §5). PURE: callers assemble the context (I/O lives
2
+ // in the CLI's makeAdmit, a later task). An action is admitted iff BOTH the
3
+ // invariants gate AND the trust-budget gate pass. Gated actions surface (later
4
+ // tasks); they do NOT spend (deduct-on-admit happens in the caller).
5
+ // Actions A is allowed to take that do NOT delete (never-delete invariant).
6
+ const NON_DELETING_ACTIONS = new Set([
7
+ "edge-observe",
8
+ "edge-contest",
9
+ ]);
10
+ export function evaluateEnvelope(ctx, spent) {
11
+ // --- Invariants gate (first; §5.1) ---
12
+ //
13
+ // Precondition failures (malformed context, non-finite/negative numerics) are
14
+ // folded into the invariants gate because the correct behaviour is identical:
15
+ // refuse, never spend. A separate gate variant isn't worth rippling through the
16
+ // journal for inputs that are unreachable when metrics are computed normally.
17
+ // The reason string carries the specific signal for any journal reader.
18
+ // Precondition: callers must supply exactly 2 endpoints ([from, to]).
19
+ if (ctx.endpoints.length < 2) {
20
+ return {
21
+ admit: false,
22
+ gate: "invariants",
23
+ reason: `precondition: expected 2 endpoints (from, to), got ${ctx.endpoints.length}`,
24
+ impact: ctx.impact,
25
+ };
26
+ }
27
+ // Precondition: callers must supply finite, non-negative numeric values.
28
+ // NaN/Infinity would silently corrupt the budget comparison.
29
+ if (!Number.isFinite(ctx.impact) ||
30
+ !Number.isFinite(ctx.budget) ||
31
+ !Number.isFinite(spent) ||
32
+ ctx.impact < 0 ||
33
+ ctx.budget < 0 ||
34
+ spent < 0) {
35
+ return {
36
+ admit: false,
37
+ gate: "invariants",
38
+ reason: `precondition: non-finite or negative numeric input (impact/budget/spent)`,
39
+ impact: ctx.impact,
40
+ };
41
+ }
42
+ // never-delete (defensive assert).
43
+ if (!NON_DELETING_ACTIONS.has(ctx.action)) {
44
+ return {
45
+ admit: false,
46
+ gate: "invariants",
47
+ reason: `never-delete: action '${ctx.action}' is not permitted`,
48
+ impact: ctx.impact,
49
+ };
50
+ }
51
+ // Per-endpoint checks run in spec §5.1 priority order:
52
+ // 1. provenance-required 2. premise-freshness (decayBlocking) 3. tension-respect
53
+ for (const ep of ctx.endpoints) {
54
+ if (!ep.provenanceKnown) {
55
+ return {
56
+ admit: false,
57
+ gate: "invariants",
58
+ reason: `provenance-required: ${ep.path} has unknown/broken provenance`,
59
+ impact: ctx.impact,
60
+ };
61
+ }
62
+ if (ep.decayBlocking) {
63
+ return {
64
+ admit: false,
65
+ gate: "invariants",
66
+ reason: `premise-freshness: ${ep.path} is stale/deprecated`,
67
+ impact: ctx.impact,
68
+ };
69
+ }
70
+ if (ep.hasUnresolvedTension) {
71
+ return {
72
+ admit: false,
73
+ gate: "invariants",
74
+ reason: `tension-respect: ${ep.path} has an unresolved tension`,
75
+ impact: ctx.impact,
76
+ };
77
+ }
78
+ }
79
+ // --- Trust-budget gate (§5.2; strict > matches shadow.ts would_gate) ---
80
+ if (spent + ctx.impact > ctx.budget) {
81
+ return {
82
+ admit: false,
83
+ gate: "budget",
84
+ reason: `trust-budget exhausted: spent ${spent.toFixed(3)} + I ${ctx.impact.toFixed(3)} > B0 ${ctx.budget.toFixed(3)}`,
85
+ impact: ctx.impact,
86
+ };
87
+ }
88
+ return { admit: true, gate: null, reason: "admitted", impact: ctx.impact };
89
+ }
90
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/consolidate/envelope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function runConsolidate(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/consolidate/index.ts"],"names":[],"mappings":"AA2HA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAsSpE"}
@@ -0,0 +1,531 @@
1
+ // `daftari consolidate` — the cortex consolidation loop entrypoint.
2
+ //
3
+ // Stage 1 (shipped): computes the edge due-queue + birth queue at session
4
+ // start and prints them. No LLM, no writes.
5
+ // Stage 2 (this PR): adds Component A. With `--mode=birth|revision|both`,
6
+ // the LLM re-derives queue items and emits edge_observe/edge_contest. Under
7
+ // `shadow_mode: true` the writes are journaled, not applied (calibration
8
+ // posture, spec §11.5). The auto-write tier graduates in Stage 5.
9
+ //
10
+ // Default mode is `scan` (Stage 1 behavior): no LLM, no writes — backward
11
+ // compatible. Stage 2 modes are explicit opt-in so an unsuspecting cron
12
+ // invocation doesn't start spending money.
13
+ //
14
+ // Spec: docs/superpowers/specs/2026-06-13-cortex-consolidation-loop.md
15
+ // Brief: docs/superpowers/drafts/2026-06-16-cortex-stage-2-component-a-brief.md
16
+ import { existsSync } from "node:fs";
17
+ import { posix, resolve } from "node:path";
18
+ import { listEdges } from "../curation/edges.js";
19
+ import { addTension, listTensions } from "../curation/tension.js";
20
+ import { loadDocuments } from "../curation/vault-docs.js";
21
+ import { createAnthropicClient } from "../eval/llm.js";
22
+ import { err, ok } from "../frontmatter/types.js";
23
+ import { vaultSearchRelated } from "../tools/search.js";
24
+ import { loadConfig } from "../utils/config.js";
25
+ import { changedSince, log as gitLog, isGitRepo } from "../utils/git.js";
26
+ import { makeAdmit } from "./admit.js";
27
+ import { appendBirthTrace, birthOne, } from "./birth.js";
28
+ import { birthQueue, decayBackstopDue, eventDue } from "./clocks.js";
29
+ import { CONSOLIDATE_AGENT, CONSOLIDATE_DEFAULT_BUDGET, CONSOLIDATE_DEFAULT_MODEL, CONSOLIDATE_PANEL_SIZE, estimateCostUSD, isModelPriced, } from "./constants.js";
30
+ import { formatDecorrelationReport, loadFixture, runDecorrelation } from "./decorrelation.js";
31
+ import { makeContest, makeObserve } from "./edge-write.js";
32
+ import { prioritize } from "./priority.js";
33
+ import { appendRevisionTrace, revisionPanel, } from "./revision.js";
34
+ import { readConsolidateState, writeConsolidateState } from "./state.js";
35
+ const HELP = `daftari consolidate — cortex loop scheduler + Component A (shadow-only).
36
+
37
+ Usage:
38
+ daftari consolidate [--vault <path>] [--budget <n>] [--mode <m>]
39
+ [--max-panels <n>] [--max-births <n>] [--model <id>]
40
+ daftari consolidate --report decorrelation --fixture <path> [--model <id>]
41
+
42
+ Modes (--mode, default 'scan'):
43
+ scan Stage 1: emit the edge due-queue + birth queue. No LLM, no writes.
44
+ birth Stage 2: re-derive each unprocessed doc's top-K neighbors and
45
+ edge_observe the survivors as k=0 candidates. (Requires
46
+ ANTHROPIC_API_KEY.)
47
+ revision Stage 2: cast an M-vote panel on each due edge, emit edge_observe
48
+ (survives) or edge_contest (fails) per vote.
49
+ both birth then revision (the full pass).
50
+
51
+ Stage 2 modes call the LLM and emit edge writes. Under shadow_mode:true those
52
+ writes go to .daftari/shadow-actions.jsonl, not .daftari/edges.jsonl. With
53
+ shadow_mode off, edge_observe/contest land for real — but Stage 2 ships with
54
+ NO auto-write graduation, so even off-shadow only the auto-write tier from
55
+ spec §4.2 lands (link / confidence-down / tension_log / contest revoke).
56
+
57
+ What it does:
58
+ Computes three clocks (event / decay / backstop) over the derives_from edge
59
+ store + git history at session start, ranks the due work into four slices
60
+ under a compute budget, then (mode != scan) runs Component A on those
61
+ items. The trace files (.daftari/{birth,revision}-trace.jsonl) are the
62
+ decorrelation report's input (brief item 8).
63
+
64
+ Reports:
65
+ --report decorrelation --fixture <path>
66
+ Run the shared foundational-ordering prompt against a ground-truth-labeled
67
+ fixture (brief item 8). Reports per-axis + majority accuracy, lift
68
+ (informational — the axes are identical under the deterministic prompt),
69
+ inter-axis agreement, error correlation. PASS/FAIL gates on ACCURACY: exits
70
+ 6 if majority accuracy < CONSOLIDATE_DIRECTION_MIN_ACCURACY (the foundational
71
+ prompt did not recover direction well enough — fix the elicitation/fixture).
72
+ Requires ANTHROPIC_API_KEY. A real fixture lives at
73
+ tests/fixtures/decorrelation-fixture-v2.json.
74
+
75
+ Exit codes:
76
+ 0 — ran cleanly
77
+ 2 — config error (no vault, bad flags, missing ANTHROPIC_API_KEY in LLM mode)
78
+ 3 — runtime error (edge store / vault I/O)
79
+ 4 — ran, but backstop-overdue work was left unserved (cron-alertable)
80
+ 5 — ran, but one or more trace writes failed (recall-evaluation data lost)
81
+ 6 — decorrelation report ran, accuracy below the gate (fix elicitation/fixture)
82
+ 7 — ran, but the event-clock baseline was unreachable (gap skipped, re-baselined to HEAD)
83
+ `;
84
+ const MS_PER_DAY = 86_400_000;
85
+ function flag(argv, name) {
86
+ const i = argv.indexOf(`--${name}`);
87
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
88
+ }
89
+ // Canonicalize a vault-relative path so the three joins (edge paths ↔ git-diff
90
+ // paths ↔ loaded-doc paths) key consistently. The edge store stores caller
91
+ // paths with only .trim() applied; an alias like `a/../a/x.md` or `./x.md`
92
+ // could otherwise silently miss the byPremise/birth join.
93
+ function canon(p) {
94
+ return posix.normalize(p).replace(/^\.\//, "");
95
+ }
96
+ const VALID_MODES = new Set(["scan", "birth", "revision", "both"]);
97
+ export async function runConsolidate(argv) {
98
+ if (argv.includes("--help") || argv.includes("-h")) {
99
+ process.stdout.write(HELP);
100
+ return 0;
101
+ }
102
+ // --report=decorrelation is an alternate entry point: no vault scan, no
103
+ // queue, no Component A — just runs the v1 axes against the fixture and
104
+ // emits the analysis. Branches early so the main consolidate flow stays
105
+ // unchanged.
106
+ const reportName = flag(argv, "report");
107
+ if (reportName === "decorrelation") {
108
+ return runDecorrelationReportCli(argv);
109
+ }
110
+ if (reportName !== undefined) {
111
+ process.stderr.write(`consolidate: --report must be 'decorrelation', got ${reportName}\n`);
112
+ return 2;
113
+ }
114
+ try {
115
+ const vaultRoot = resolve(flag(argv, "vault") ?? process.cwd());
116
+ if (!existsSync(vaultRoot)) {
117
+ process.stderr.write(`consolidate: no vault at ${vaultRoot}\n`);
118
+ return 2;
119
+ }
120
+ const budget = Number(flag(argv, "budget") ?? CONSOLIDATE_DEFAULT_BUDGET);
121
+ if (!Number.isFinite(budget) || budget <= 0) {
122
+ process.stderr.write("consolidate: --budget must be a number > 0\n");
123
+ return 2;
124
+ }
125
+ const modeStr = (flag(argv, "mode") ?? "scan").toLowerCase();
126
+ if (!VALID_MODES.has(modeStr)) {
127
+ process.stderr.write(`consolidate: --mode must be one of scan|birth|revision|both, got ${modeStr}\n`);
128
+ return 2;
129
+ }
130
+ const mode = modeStr;
131
+ const maxBirths = parseOptionalInt(flag(argv, "max-births"));
132
+ if (maxBirths.error) {
133
+ process.stderr.write(`consolidate: ${maxBirths.error}\n`);
134
+ return 2;
135
+ }
136
+ const maxPanels = parseOptionalInt(flag(argv, "max-panels"));
137
+ if (maxPanels.error) {
138
+ process.stderr.write(`consolidate: ${maxPanels.error}\n`);
139
+ return 2;
140
+ }
141
+ const model = flag(argv, "model") ?? CONSOLIDATE_DEFAULT_MODEL;
142
+ const now = new Date();
143
+ const state = readConsolidateState(vaultRoot);
144
+ const edgesRes = await listEdges(vaultRoot, {}, now);
145
+ if (!edgesRes.ok) {
146
+ process.stderr.write(`consolidate: ${edgesRes.error.message}\n`);
147
+ return 3;
148
+ }
149
+ const edges = edgesRes.value.map((e) => ({
150
+ ...e,
151
+ fromPath: canon(e.fromPath),
152
+ toPath: canon(e.toPath),
153
+ }));
154
+ const edgeByKey = new Map();
155
+ for (const e of edges)
156
+ edgeByKey.set(`${e.fromPath}\n${e.toPath}`, e);
157
+ const docsRes = await loadDocuments(vaultRoot);
158
+ if (!docsRes.ok) {
159
+ process.stderr.write(`consolidate: ${docsRes.error.message}\n`);
160
+ return 3;
161
+ }
162
+ const docs = docsRes.value.map((d) => ({ relPath: canon(d.path), content: d.content }));
163
+ const docByPath = new Map();
164
+ for (const d of docs)
165
+ docByPath.set(d.relPath, d);
166
+ const birthProcessed = {};
167
+ for (const [k, v] of Object.entries(state.birthProcessed))
168
+ birthProcessed[canon(k)] = v;
169
+ const inRepo = await isGitRepo(vaultRoot);
170
+ let eventEdges = [];
171
+ let staleBaseline = false;
172
+ if (state.lastConsolidationCommit && inRepo) {
173
+ const changed = await changedSince(vaultRoot, state.lastConsolidationCommit);
174
+ if (changed.ok)
175
+ eventEdges = eventDue(changed.value.map(canon), edges);
176
+ else {
177
+ // The stored baseline commit is unreachable (rebase / force-push / GC /
178
+ // re-clone). The event clock can't run for the gap — it re-baselines to
179
+ // HEAD below (the decay+backstop clock will still re-derive those edges
180
+ // within the cap), but the gap is NOT event-examined, so we surface a
181
+ // non-zero exit (7) rather than silently exit 0 as if all was covered.
182
+ staleBaseline = true;
183
+ process.stderr.write(`consolidate: stale baseline ${state.lastConsolidationCommit} — event clock skipped, ` +
184
+ `re-baselining to HEAD (gap relies on the backstop clock; exit 7)\n`);
185
+ }
186
+ }
187
+ const decayEdges = decayBackstopDue(edges, now);
188
+ const birth = birthQueue(docs, birthProcessed);
189
+ const ages = {};
190
+ for (const e of edges) {
191
+ ages[e.fromPath] = (now.getTime() - new Date(e.lastRederived).getTime()) / MS_PER_DAY;
192
+ }
193
+ const { queue, backstopOverdueRemaining } = prioritize({
194
+ edgeDue: [...eventEdges, ...decayEdges],
195
+ birth,
196
+ budget,
197
+ ages,
198
+ });
199
+ const edgeItems = queue.filter((q) => q.kind === "edge");
200
+ const birthItems = queue.filter((q) => q.kind === "birth");
201
+ // --- report header (mode-independent) -------------------------------------
202
+ let report = `consolidate @ ${vaultRoot}\n`;
203
+ report += ` edges: ${edges.length} | docs: ${docs.length} | budget: ${budget} | mode: ${mode}\n`;
204
+ report += ` edge due-queue (${edgeItems.length}):\n`;
205
+ for (const q of edgeItems) {
206
+ if (q.kind === "edge")
207
+ report += ` [${q.slice}/${q.reason}] ${q.fromPath} ← ${q.toPath}\n`;
208
+ }
209
+ report += ` birth queue (${birthItems.length}):\n`;
210
+ for (const q of birthItems) {
211
+ if (q.kind === "birth")
212
+ report += ` [birth] ${q.path}\n`;
213
+ }
214
+ report += ` backstop-overdue remaining: ${backstopOverdueRemaining}\n`;
215
+ // --- Stage 2: Component A dispatch ---------------------------------------
216
+ const stage2 = emptyStage2();
217
+ if (mode !== "scan") {
218
+ const cfg = await loadConfig(vaultRoot);
219
+ if (!cfg.ok) {
220
+ process.stderr.write(`consolidate: ${cfg.error.message}\n`);
221
+ return 2;
222
+ }
223
+ // ANTHROPIC_API_KEY check before constructing the client — fail-fast with
224
+ // a clear message rather than the SDK's terse internal throw.
225
+ if (!process.env.ANTHROPIC_API_KEY) {
226
+ process.stderr.write("consolidate: ANTHROPIC_API_KEY env var is required for --mode != scan\n");
227
+ return 2;
228
+ }
229
+ let llm;
230
+ try {
231
+ llm = createAnthropicClient();
232
+ }
233
+ catch (e) {
234
+ process.stderr.write(`consolidate: ${e instanceof Error ? e.message : String(e)}\n`);
235
+ return 2;
236
+ }
237
+ stage2.shadowMode = cfg.value.shadowMode;
238
+ const observe = makeObserve({
239
+ vaultRoot,
240
+ shadowMode: cfg.value.shadowMode,
241
+ });
242
+ const contest = makeContest({
243
+ vaultRoot,
244
+ shadowMode: cfg.value.shadowMode,
245
+ });
246
+ // The real envelope-owned admit (Stage 3 Task 7): assembles the
247
+ // EnvelopeCtx from in-process data, owns the §3.7 per-session spend
248
+ // scalar (deduct-on-admit), and journals every decision. Construction
249
+ // does I/O (loadDocuments / staged actions / tensions) and can fail.
250
+ //
251
+ // FAIL CLOSED: if construction fails the envelope cannot evaluate the
252
+ // gate, so Component A MUST NOT run — letting birth/revision write
253
+ // ungated would be exactly the auto-write-on-incomplete-info the envelope
254
+ // exists to prevent. We surface the error and abort with a runtime exit.
255
+ // Reuse the docs index.ts already loaded above (docsRes.value) — makeAdmit
256
+ // derives its reverse maps / per-endpoint metadata / docCount from this
257
+ // same LoadedDoc[], so no second vault walk happens.
258
+ const admitRes = await makeAdmit({
259
+ vaultRoot,
260
+ principal: CONSOLIDATE_AGENT,
261
+ docs: docsRes.value,
262
+ });
263
+ if (!admitRes.ok) {
264
+ process.stderr.write(`consolidate: envelope init failed: ${admitRes.error.message} — Component A skipped (fail-closed)\n`);
265
+ return 3;
266
+ }
267
+ const admit = admitRes.value.admit;
268
+ if (mode === "birth" || mode === "both") {
269
+ const cap = maxBirths.value ?? Number.POSITIVE_INFINITY;
270
+ const birthOnly = birthItems
271
+ .filter((q) => q.kind === "birth")
272
+ .slice(0, Math.min(birthItems.length, cap));
273
+ await runBirthLoop(birthOnly, docByPath, admit, observe, llm, vaultRoot, model, stage2, birthProcessed);
274
+ }
275
+ if (mode === "revision" || mode === "both") {
276
+ const cap = maxPanels.value ?? Number.POSITIVE_INFINITY;
277
+ const edgeOnly = edgeItems
278
+ .filter((q) => q.kind === "edge")
279
+ .slice(0, Math.min(edgeItems.length, cap));
280
+ await runRevisionLoop(edgeOnly, edgeByKey, docByPath, admit, observe, contest, llm, vaultRoot, model, stage2);
281
+ }
282
+ // Surface journal-write failures the admit closure counted across the loops.
283
+ // A failed journal write loses a calibration row but does not change any gate
284
+ // verdict (mirrors traceWriteFailures — counted, not thrown).
285
+ stage2.journalWriteFailures = admitRes.value.journalFailures();
286
+ report += ` Component A (${mode}):\n`;
287
+ report += ` births_processed: ${stage2.birthsProcessed}\n`;
288
+ report += ` panels_cast: ${stage2.panelsCast} | votes_cast: ${stage2.votesCast}\n`;
289
+ report += ` observed: ${stage2.observedTotal} | contested: ${stage2.contestedTotal} | gated: ${stage2.gated}\n`;
290
+ report += ` llm_calls: ${stage2.llmCalls} | tokens: ${stage2.inputTokens}/${stage2.outputTokens} in/out\n`;
291
+ report += ` est_cost_usd: ${estimateCostUSD(model, stage2.inputTokens, stage2.outputTokens).toFixed(4)} (${model})${isModelPriced(model) ? "" : " [pricing_fallback: haiku — model unpriced]"}\n`;
292
+ if (stage2.traceWriteFailures > 0) {
293
+ report += ` trace_write_failures: ${stage2.traceWriteFailures} — recall@K evaluator input lost (exit 5)\n`;
294
+ }
295
+ if (stage2.journalWriteFailures > 0) {
296
+ report += ` journal_write_failures: ${stage2.journalWriteFailures} — calibration rows lost\n`;
297
+ }
298
+ // The envelope journals every decision (admit/gate) to shadow-actions.jsonl
299
+ // regardless of shadow mode (makeAdmit owns this). In shadow mode the edge
300
+ // STORE write is additionally suppressed (makeObserve); off-shadow, admitted
301
+ // edges also land in edges.jsonl.
302
+ if (stage2.shadowMode) {
303
+ report += ` shadow_mode: true — envelope decisions journaled to shadow-actions.jsonl, edge store untouched\n`;
304
+ }
305
+ else {
306
+ report += ` shadow_mode: false — envelope decisions journaled to shadow-actions.jsonl; admitted edges also written to the store\n`;
307
+ }
308
+ }
309
+ process.stdout.write(report);
310
+ // --- persist state -------------------------------------------------------
311
+ // Always persist birthProcessed (Stage 2 progress is independent of git).
312
+ // Only advance the event-clock baseline (lastConsolidationCommit) when we
313
+ // have a HEAD; in a non-git dir we keep the prior value (likely null) so
314
+ // the event clock stays in the nil-path on every run.
315
+ let newCommit = state.lastConsolidationCommit;
316
+ if (inRepo) {
317
+ const head = await gitLog(vaultRoot, { limit: 1 });
318
+ if (head.ok && head.value[0])
319
+ newCommit = head.value[0].hash;
320
+ }
321
+ const wrote = writeConsolidateState(vaultRoot, {
322
+ ...state,
323
+ lastConsolidationCommit: newCommit,
324
+ birthProcessed,
325
+ });
326
+ if (!wrote.ok)
327
+ process.stderr.write(`consolidate: ${wrote.error.message}\n`);
328
+ // --- exit code hierarchy --------------------------------------------------
329
+ // 5 (trace lost) > 4 (backstop unserved) > 7 (event-clock baseline lost) > 0.
330
+ // Trace loss silently corrupts the calibration data flow (most serious);
331
+ // backstop-overdue and a lost baseline are cron-alertable warnings the next
332
+ // session / a human can resolve.
333
+ //
334
+ // journalWriteFailures is deliberately NOT in this hierarchy: a lost
335
+ // calibration (shadow-actions) row never changes a gate verdict and is
336
+ // already surfaced in the report above. It is reported-only by design, not
337
+ // forgotten — the exit code is reserved for the data flows a human/cron must
338
+ // act on.
339
+ if (stage2.traceWriteFailures > 0)
340
+ return 5;
341
+ if (backstopOverdueRemaining > 0)
342
+ return 4;
343
+ return staleBaseline ? 7 : 0;
344
+ }
345
+ catch (e) {
346
+ process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
347
+ return 2;
348
+ }
349
+ }
350
+ // --- helpers -----------------------------------------------------------------
351
+ function parseOptionalInt(raw) {
352
+ if (raw === undefined)
353
+ return { value: null };
354
+ const n = Number(raw);
355
+ if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
356
+ return { value: null, error: `expected a non-negative integer, got ${JSON.stringify(raw)}` };
357
+ }
358
+ return { value: n };
359
+ }
360
+ function emptyStage2() {
361
+ return {
362
+ birthsProcessed: 0,
363
+ panelsCast: 0,
364
+ votesCast: 0,
365
+ observedTotal: 0,
366
+ contestedTotal: 0,
367
+ gated: 0,
368
+ llmCalls: 0,
369
+ inputTokens: 0,
370
+ outputTokens: 0,
371
+ traceWriteFailures: 0,
372
+ journalWriteFailures: 0,
373
+ shadowMode: false,
374
+ };
375
+ }
376
+ async function runBirthLoop(birthItems, docByPath, admit, observe, llm, vaultRoot, model, stage2, birthProcessed) {
377
+ const searchNeighbors = async (docPath, k) => {
378
+ const r = await vaultSearchRelated(vaultRoot, { path: docPath, limit: k });
379
+ if (!r.ok)
380
+ return r;
381
+ return ok(r.value.hits.map((h) => h.path));
382
+ };
383
+ // Neighbor content is already in process (the docs map) — no disk read.
384
+ const loadNeighborContent = async (path) => {
385
+ const d = docByPath.get(canon(path));
386
+ return d ? ok(d.content) : err(new Error(`neighbor not in docs map: ${path}`));
387
+ };
388
+ // Direction-pending tensions are authored under the loop principal. Dedup on
389
+ // the canonical title (mirrors vaultEdgeContest): a doc re-births on every
390
+ // edit, so without this an unresolved direction-pending pair would re-append
391
+ // an identical tension on each re-birth and flood the advisory log.
392
+ const recordTension = async (input) => {
393
+ const existing = await listTensions(vaultRoot);
394
+ if (existing.ok && existing.value.some((t) => t.title === input.title && !t.resolved)) {
395
+ return ok(undefined); // already open for this pair — don't stack a duplicate
396
+ }
397
+ return addTension(vaultRoot, input);
398
+ };
399
+ const recordBirthTrace = (row) => appendBirthTrace(vaultRoot, row);
400
+ const opts = {
401
+ vaultRoot,
402
+ agent: CONSOLIDATE_AGENT,
403
+ axis: "forward",
404
+ budgetRemaining: Number.POSITIVE_INFINITY,
405
+ model,
406
+ };
407
+ for (const item of birthItems) {
408
+ const docPath = canon(item.path);
409
+ const doc = docByPath.get(docPath);
410
+ if (!doc)
411
+ continue; // queue → docs is reconstructible; a missing doc is non-fatal
412
+ const out = await birthOne(doc, {
413
+ llm,
414
+ searchNeighbors,
415
+ loadNeighborContent,
416
+ admit,
417
+ observe,
418
+ recordTension,
419
+ recordBirthTrace,
420
+ }, opts);
421
+ if (!out.ok) {
422
+ process.stderr.write(`consolidate: birth failed for ${docPath}: ${out.error.message}\n`);
423
+ continue;
424
+ }
425
+ accumulateBirth(stage2, out.value);
426
+ // Only mark the doc processed if its trace landed. A failed trace write
427
+ // (exit 5) must NOT be permanent: birthProcessed is keyed on content-hash, so
428
+ // marking it here would stop the doc ever re-birthing and the recall@K trace
429
+ // row — the whole reason birth mode exists — would be lost unrecoverably.
430
+ if (out.value.traceWritten)
431
+ birthProcessed[docPath] = out.value.contentHash;
432
+ }
433
+ }
434
+ function accumulateBirth(stage2, out) {
435
+ stage2.birthsProcessed++;
436
+ stage2.observedTotal += out.observations.length;
437
+ stage2.gated += out.gatedCount;
438
+ stage2.llmCalls += out.llmCalls;
439
+ stage2.inputTokens += out.inputTokens;
440
+ stage2.outputTokens += out.outputTokens;
441
+ if (!out.traceWritten)
442
+ stage2.traceWriteFailures++;
443
+ }
444
+ async function runRevisionLoop(edgeItems, edgeByKey, docByPath, admit, observe, contest, llm, vaultRoot, model, stage2) {
445
+ const loadDoc = async (path) => {
446
+ const d = docByPath.get(canon(path));
447
+ if (!d) {
448
+ return { ok: false, error: new Error(`doc not in load set: ${path}`) };
449
+ }
450
+ return ok({ path: d.relPath, content: d.content });
451
+ };
452
+ const recordRevisionTrace = (row) => appendRevisionTrace(vaultRoot, row);
453
+ const opts = {
454
+ vaultRoot,
455
+ agent: CONSOLIDATE_AGENT,
456
+ panelSize: CONSOLIDATE_PANEL_SIZE,
457
+ budgetRemaining: Number.POSITIVE_INFINITY,
458
+ model,
459
+ };
460
+ for (const item of edgeItems) {
461
+ const key = `${canon(item.fromPath)}\n${canon(item.toPath)}`;
462
+ const edge = edgeByKey.get(key);
463
+ if (!edge)
464
+ continue;
465
+ // Defensive: the clocks already exclude direction-symmetric edges from the
466
+ // due-list, but the revision panel votes on a FIXED from→to direction and
467
+ // has no symmetric guard of its own. Keep the invariant next to the consumer
468
+ // so a future due-path (e.g. the deferred TTL clock) can't feed a pending
469
+ // edge into a directional verdict.
470
+ if (edge.directionVerdict === "symmetric")
471
+ continue;
472
+ const out = await revisionPanel(edge, { llm, loadDoc, admit, observe, contest, recordRevisionTrace }, opts);
473
+ if (!out.ok) {
474
+ process.stderr.write(`consolidate: revision failed for ${key.replace("\n", "←")}: ${out.error.message}\n`);
475
+ continue;
476
+ }
477
+ accumulateRevision(stage2, out.value);
478
+ }
479
+ }
480
+ function accumulateRevision(stage2, out) {
481
+ stage2.panelsCast++;
482
+ stage2.votesCast += out.votes.length;
483
+ stage2.observedTotal += out.observedCount;
484
+ stage2.contestedTotal += out.contestedCount;
485
+ if (out.decision === "gated")
486
+ stage2.gated++;
487
+ stage2.llmCalls += out.llmCalls;
488
+ stage2.inputTokens += out.inputTokens;
489
+ stage2.outputTokens += out.outputTokens;
490
+ if (!out.traceWritten)
491
+ stage2.traceWriteFailures++;
492
+ }
493
+ // --- --report=decorrelation ---------------------------------------------------
494
+ async function runDecorrelationReportCli(argv) {
495
+ const fixturePath = flag(argv, "fixture");
496
+ if (!fixturePath) {
497
+ process.stderr.write("consolidate --report decorrelation: --fixture <path> is required\n");
498
+ return 2;
499
+ }
500
+ if (!process.env.ANTHROPIC_API_KEY) {
501
+ process.stderr.write("consolidate --report decorrelation: ANTHROPIC_API_KEY env var is required\n");
502
+ return 2;
503
+ }
504
+ const fixtureRes = loadFixture(fixturePath);
505
+ if (!fixtureRes.ok) {
506
+ process.stderr.write(`consolidate: ${fixtureRes.error.message}\n`);
507
+ return 2;
508
+ }
509
+ if (fixtureRes.value.edges.length === 0) {
510
+ process.stderr.write("consolidate: fixture is empty\n");
511
+ return 2;
512
+ }
513
+ let llm;
514
+ try {
515
+ llm = createAnthropicClient();
516
+ }
517
+ catch (e) {
518
+ process.stderr.write(`consolidate: ${e instanceof Error ? e.message : String(e)}\n`);
519
+ return 2;
520
+ }
521
+ const model = flag(argv, "model") ?? CONSOLIDATE_DEFAULT_MODEL;
522
+ const reportRes = await runDecorrelation(fixtureRes.value, { llm }, { model, fixtureSource: fixturePath });
523
+ if (!reportRes.ok) {
524
+ process.stderr.write(`consolidate: ${reportRes.error.message}\n`);
525
+ return 3;
526
+ }
527
+ process.stdout.write(formatDecorrelationReport(reportRes.value));
528
+ // Kill condition fires → exit 6 so CI / a calibration wrapper can branch.
529
+ return reportRes.value.passes ? 0 : 6;
530
+ }
531
+ //# sourceMappingURL=index.js.map