dual-brain 4.6.0 → 4.7.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.
- package/CLAUDE.md +35 -130
- package/README.md +34 -179
- package/hooks/control-panel.mjs +379 -8
- package/hooks/cost-logger.mjs +11 -53
- package/hooks/cost-report.mjs +126 -65
- package/hooks/decision-ledger.mjs +3 -53
- package/hooks/dual-brain-review.mjs +25 -261
- package/hooks/dual-brain-think.mjs +37 -300
- package/hooks/enforce-tier.mjs +93 -265
- package/hooks/failure-detector.mjs +1 -3
- package/hooks/gpt-work-dispatcher.mjs +153 -12
- package/hooks/health-check.mjs +25 -17
- package/hooks/quality-gate.mjs +11 -6
- package/hooks/risk-classifier.mjs +2 -135
- package/hooks/session-report.mjs +71 -41
- package/hooks/summary-checkpoint.mjs +8 -35
- package/hooks/test-orchestrator.mjs +31 -2080
- package/install.mjs +616 -1564
- package/orchestrator.json +96 -73
- package/package.json +2 -7
- package/hooks/agent-chains.mjs +0 -369
- package/hooks/agent-templates.mjs +0 -441
- package/hooks/atomic-write.mjs +0 -109
- package/hooks/config-validator.mjs +0 -156
- package/hooks/confirmation-policy.mjs +0 -167
- package/hooks/error-channel.mjs +0 -68
- package/hooks/ship-captain.mjs +0 -1176
- package/hooks/ship-gate.mjs +0 -971
package/hooks/cost-report.mjs
CHANGED
|
@@ -9,22 +9,50 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Reads:
|
|
11
11
|
* .claude/hooks/usage.jsonl — tool call log written by cost-logger.mjs
|
|
12
|
-
*
|
|
13
|
-
* Reports token-weighted activity scores (0-100), not dollar estimates.
|
|
12
|
+
* .claude/orchestrator.json — cost rates per model
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
17
16
|
import { dirname, join } from "path";
|
|
18
17
|
import { fileURLToPath } from "url";
|
|
19
|
-
import {
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
20
19
|
|
|
21
20
|
// ---------------------------------------------------------------------------
|
|
22
21
|
// Paths
|
|
23
22
|
// ---------------------------------------------------------------------------
|
|
24
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
24
|
const WORKSPACE = join(__dirname, "..", ".."); // workspace root
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const CONFIG_FILE = join(__dirname, "..", "orchestrator.json");
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Load orchestrator config
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function loadConfig() {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a flat map: { "haiku": { input_per_mtok, output_per_mtok, tier }, … }
|
|
40
|
+
* from orchestrator.json's subscriptions block.
|
|
41
|
+
*/
|
|
42
|
+
function buildRateMap(config) {
|
|
43
|
+
const rates = {};
|
|
44
|
+
if (!config?.subscriptions) return rates;
|
|
45
|
+
for (const provider of Object.values(config.subscriptions)) {
|
|
46
|
+
for (const [modelKey, data] of Object.entries(provider.models || {})) {
|
|
47
|
+
rates[modelKey] = {
|
|
48
|
+
tier: data.tier,
|
|
49
|
+
input_per_mtok: data.input_per_mtok,
|
|
50
|
+
output_per_mtok: data.output_per_mtok,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return rates;
|
|
55
|
+
}
|
|
28
56
|
|
|
29
57
|
// ---------------------------------------------------------------------------
|
|
30
58
|
// Load & parse usage log
|
|
@@ -55,20 +83,41 @@ function loadUsage() {
|
|
|
55
83
|
// Cost estimation
|
|
56
84
|
// ---------------------------------------------------------------------------
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Very rough token estimate per tool call.
|
|
88
|
+
* Without actual token counts from the session files, we use a conservative
|
|
89
|
+
* heuristic based on typical Claude Code usage patterns.
|
|
90
|
+
*/
|
|
91
|
+
const TOKEN_HEURISTICS = {
|
|
92
|
+
// { input_tok, output_tok }
|
|
93
|
+
search: { input: 2_000, output: 500 },
|
|
94
|
+
execute: { input: 4_000, output: 1_500 },
|
|
95
|
+
think: { input: 8_000, output: 3_000 },
|
|
96
|
+
};
|
|
65
97
|
|
|
66
|
-
function
|
|
98
|
+
function estimateCost(tier, model, rateMap, record = {}) {
|
|
99
|
+
const heuristic = TOKEN_HEURISTICS[tier] || TOKEN_HEURISTICS.execute;
|
|
100
|
+
// Use actual tokens if logged, otherwise fall back to heuristics
|
|
67
101
|
const hasActual = record.input_tokens != null && record.output_tokens != null;
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
const inputTok = hasActual ? record.input_tokens : heuristic.input;
|
|
103
|
+
const outputTok = hasActual ? record.output_tokens : heuristic.output;
|
|
104
|
+
const rate = rateMap[model] || rateMap["main-session"];
|
|
105
|
+
if (!rate) {
|
|
106
|
+
// Fallback: use tier-matched rate from whatever model we know about
|
|
107
|
+
// "main-session" and "unknown" map to think-tier (Opus) since that's the session model
|
|
108
|
+
const fallbackTier = (model === "main-session" || model === "unknown") ? "think" : tier;
|
|
109
|
+
const tierRate = Object.values(rateMap).find((r) => r.tier === fallbackTier)
|
|
110
|
+
|| Object.values(rateMap).find((r) => r.tier === tier);
|
|
111
|
+
if (!tierRate) return 0;
|
|
112
|
+
return (
|
|
113
|
+
(inputTok / 1_000_000) * tierRate.input_per_mtok +
|
|
114
|
+
(outputTok / 1_000_000) * tierRate.output_per_mtok
|
|
115
|
+
);
|
|
70
116
|
}
|
|
71
|
-
return
|
|
117
|
+
return (
|
|
118
|
+
(inputTok / 1_000_000) * rate.input_per_mtok +
|
|
119
|
+
(outputTok / 1_000_000) * rate.output_per_mtok
|
|
120
|
+
);
|
|
72
121
|
}
|
|
73
122
|
|
|
74
123
|
// ---------------------------------------------------------------------------
|
|
@@ -77,10 +126,17 @@ function computeActivity(tier, record = {}) {
|
|
|
77
126
|
function gitFallbackSummary() {
|
|
78
127
|
try {
|
|
79
128
|
const today = new Date().toISOString().slice(0, 10);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
const proc = spawnSync("git", [
|
|
130
|
+
"-C", WORKSPACE,
|
|
131
|
+
"log", "--oneline",
|
|
132
|
+
`--since=${today} 00:00`,
|
|
133
|
+
`--until=${today} 23:59`,
|
|
134
|
+
], {
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
137
|
+
timeout: 10_000,
|
|
138
|
+
});
|
|
139
|
+
const log = proc.status === 0 ? (proc.stdout || "").trim() : "";
|
|
84
140
|
const commits = log ? log.split("\n").length : 0;
|
|
85
141
|
return commits;
|
|
86
142
|
} catch {
|
|
@@ -97,35 +153,29 @@ function todayPrefix() {
|
|
|
97
153
|
}
|
|
98
154
|
|
|
99
155
|
/**
|
|
100
|
-
* Aggregate records into { [tier]: { model, calls,
|
|
156
|
+
* Aggregate records into { [tier]: { model, calls, cost } }
|
|
101
157
|
* where model is the most-seen model for that tier.
|
|
102
158
|
*/
|
|
103
|
-
function aggregate(records, datePrefix = null) {
|
|
159
|
+
function aggregate(records, rateMap, datePrefix = null) {
|
|
104
160
|
const filtered = datePrefix
|
|
105
161
|
? records.filter((r) => r.timestamp?.startsWith(datePrefix))
|
|
106
162
|
: records;
|
|
107
163
|
|
|
108
|
-
// tier → { calls
|
|
164
|
+
// tier → { calls: number, costSum: number, modelCounts: { model: count } }
|
|
109
165
|
const buckets = {};
|
|
110
166
|
|
|
111
167
|
for (const record of filtered) {
|
|
112
168
|
const tier = record.tier || "execute";
|
|
113
169
|
const model = record.model || "unknown";
|
|
114
170
|
if (!buckets[tier]) {
|
|
115
|
-
buckets[tier] = { calls: 0,
|
|
171
|
+
buckets[tier] = { calls: 0, costSum: 0, modelCounts: {} };
|
|
116
172
|
}
|
|
117
173
|
buckets[tier].calls += 1;
|
|
118
|
-
|
|
119
|
-
buckets[tier].activityRaw += raw;
|
|
174
|
+
buckets[tier].costSum += estimateCost(tier, model, rateMap, record);
|
|
120
175
|
buckets[tier].modelCounts[model] = (buckets[tier].modelCounts[model] || 0) + 1;
|
|
121
|
-
if (record.input_tokens != null && record.output_tokens != null)
|
|
122
|
-
buckets[tier].actualCount += 1;
|
|
123
|
-
}
|
|
176
|
+
if (record.input_tokens != null && record.output_tokens != null) buckets[tier].actualCount = (buckets[tier].actualCount || 0) + 1;
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
// Compute total raw for percentage breakdown
|
|
127
|
-
const totalRaw = Object.values(buckets).reduce((s, b) => s + b.activityRaw, 0);
|
|
128
|
-
|
|
129
179
|
// Resolve dominant model per tier
|
|
130
180
|
const result = {};
|
|
131
181
|
for (const [tier, data] of Object.entries(buckets)) {
|
|
@@ -133,23 +183,24 @@ function aggregate(records, datePrefix = null) {
|
|
|
133
183
|
result[tier] = {
|
|
134
184
|
model: dominantModel,
|
|
135
185
|
calls: data.calls,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
actualCount: data.actualCount,
|
|
186
|
+
cost: data.costSum,
|
|
187
|
+
actualCount: data.actualCount || 0,
|
|
139
188
|
};
|
|
140
189
|
}
|
|
141
190
|
return result;
|
|
142
191
|
}
|
|
143
192
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Opus all-in cost (for savings calculation)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
function allOpusCost(records, rateMap, datePrefix = null) {
|
|
197
|
+
const filtered = datePrefix
|
|
198
|
+
? records.filter((r) => r.timestamp?.startsWith(datePrefix))
|
|
199
|
+
: records;
|
|
200
|
+
|
|
201
|
+
return filtered.reduce((sum, record) => {
|
|
202
|
+
return sum + estimateCost("think", "opus", rateMap, record);
|
|
203
|
+
}, 0);
|
|
153
204
|
}
|
|
154
205
|
|
|
155
206
|
// ---------------------------------------------------------------------------
|
|
@@ -166,6 +217,10 @@ const TIER_LABELS = {
|
|
|
166
217
|
think: "Think ",
|
|
167
218
|
};
|
|
168
219
|
|
|
220
|
+
function fmt$(n) {
|
|
221
|
+
return "$" + n.toFixed(2);
|
|
222
|
+
}
|
|
223
|
+
|
|
169
224
|
function pad(str, len, align = "left") {
|
|
170
225
|
str = String(str);
|
|
171
226
|
if (str.length >= len) return str.slice(0, len);
|
|
@@ -173,37 +228,39 @@ function pad(str, len, align = "left") {
|
|
|
173
228
|
return align === "right" ? spaces + str : str + spaces;
|
|
174
229
|
}
|
|
175
230
|
|
|
176
|
-
function renderTable(title, aggregated, records = []) {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
231
|
+
function renderTable(title, aggregated, allOpus, records = []) {
|
|
232
|
+
const totalCost = Object.values(aggregated).reduce((s, v) => s + v.cost, 0);
|
|
233
|
+
const savings = allOpus - totalCost;
|
|
234
|
+
const savingsPct = allOpus > 0 ? Math.round((savings / allOpus) * 100) : 0;
|
|
180
235
|
|
|
181
236
|
const line = (s) => `║ ${pad(s, W - 2)} ║`;
|
|
182
|
-
const border = (l, r) => l + "═".repeat(W) + r;
|
|
237
|
+
const border = (l, r, m) => l + "═".repeat(W) + r;
|
|
183
238
|
const sep = () => "╠" + "═".repeat(W) + "╣";
|
|
184
239
|
|
|
185
240
|
const rows = TIER_ORDER
|
|
186
241
|
.filter((t) => aggregated[t])
|
|
187
242
|
.map((t) => {
|
|
188
|
-
const { model, calls,
|
|
243
|
+
const { model, calls, cost } = aggregated[t];
|
|
189
244
|
const tierLbl = pad(TIER_LABELS[t] || t, 8);
|
|
190
245
|
const modelLbl = pad(model, 10);
|
|
191
246
|
const callsLbl = pad(String(calls), 5, "right");
|
|
192
|
-
const
|
|
193
|
-
return line(`${tierLbl} │ ${modelLbl} │ ${callsLbl} │ ${
|
|
247
|
+
const costLbl = pad(fmt$(cost), 12, "right");
|
|
248
|
+
return line(`${tierLbl} │ ${modelLbl} │ ${callsLbl} │ ${costLbl}`);
|
|
194
249
|
});
|
|
195
250
|
|
|
196
|
-
const header = line(`Tier │ Model │ Calls │
|
|
251
|
+
const header = line(`Tier │ Model │ Calls │ Est. Cost `);
|
|
197
252
|
const hline = line(`─────────┼────────────┼───────┼────────────`);
|
|
198
253
|
|
|
199
254
|
const totalCalls = Object.values(aggregated).reduce((s, v) => s + v.calls, 0);
|
|
200
255
|
const actualCalls = Object.values(aggregated).reduce((s, v) => s + (v.actualCount || 0), 0);
|
|
201
|
-
const
|
|
202
|
-
actualCalls === totalCalls ? 'actual
|
|
203
|
-
`
|
|
256
|
+
const confidence = actualCalls === 0 ? 'low (heuristic only)' :
|
|
257
|
+
actualCalls === totalCalls ? 'high (actual tokens)' :
|
|
258
|
+
`medium (${Math.round(actualCalls/totalCalls*100)}% actual)`;
|
|
204
259
|
|
|
205
260
|
// Data quality stats
|
|
261
|
+
const totalRecords = Object.values(aggregated).reduce((s, v) => s + v.calls, 0);
|
|
206
262
|
const unknownModels = records.filter(r => !r.model || r.model === 'unknown').length;
|
|
263
|
+
const v2Records = records.filter(r => r.schema_version >= 2).length;
|
|
207
264
|
const errorRecords = records.filter(r => r.status === 'error').length;
|
|
208
265
|
|
|
209
266
|
const lines = [
|
|
@@ -214,15 +271,15 @@ function renderTable(title, aggregated, records = []) {
|
|
|
214
271
|
hline,
|
|
215
272
|
...rows,
|
|
216
273
|
sep(),
|
|
217
|
-
line(`
|
|
218
|
-
line(`
|
|
219
|
-
line(`
|
|
274
|
+
line(`Total estimated: ${fmt$(totalCost)}`),
|
|
275
|
+
line(`Savings vs all-Opus: ~${fmt$(Math.max(0, savings))} (${savingsPct}%)`),
|
|
276
|
+
line(`Confidence: ${confidence}`),
|
|
220
277
|
border("╚", "╝"),
|
|
221
278
|
];
|
|
222
279
|
|
|
223
280
|
if (unknownModels > 0 || errorRecords > 0) {
|
|
224
281
|
lines.splice(-1, 0,
|
|
225
|
-
line(`Unknown models: ${unknownModels}/${
|
|
282
|
+
line(`Unknown models: ${unknownModels}/${totalRecords} entries`),
|
|
226
283
|
line(`Errors: ${errorRecords} tool calls failed`),
|
|
227
284
|
);
|
|
228
285
|
}
|
|
@@ -235,7 +292,7 @@ function renderEmpty() {
|
|
|
235
292
|
const ln = (s) => `║ ${pad(s, W - 2)} ║`;
|
|
236
293
|
return [
|
|
237
294
|
border("╔", "╗"),
|
|
238
|
-
ln("
|
|
295
|
+
ln("Activity & Cost Estimate"),
|
|
239
296
|
border("╠", "╣"),
|
|
240
297
|
ln("No usage data yet."),
|
|
241
298
|
ln(""),
|
|
@@ -253,6 +310,8 @@ function main() {
|
|
|
253
310
|
const args = process.argv.slice(2);
|
|
254
311
|
const showAll = args.includes("--all");
|
|
255
312
|
|
|
313
|
+
const config = loadConfig();
|
|
314
|
+
const rateMap = buildRateMap(config);
|
|
256
315
|
const records = loadUsage();
|
|
257
316
|
|
|
258
317
|
if (records.length === 0) {
|
|
@@ -269,12 +328,13 @@ function main() {
|
|
|
269
328
|
|
|
270
329
|
if (!showAll) {
|
|
271
330
|
// Today's report
|
|
272
|
-
const todayAgg = aggregate(records, today);
|
|
331
|
+
const todayAgg = aggregate(records, rateMap, today);
|
|
332
|
+
const todayOpus = allOpusCost(records, rateMap, today);
|
|
273
333
|
const todayRecords = records.filter(r => r.timestamp?.startsWith(today));
|
|
274
334
|
const hasTodayData = Object.keys(todayAgg).length > 0;
|
|
275
335
|
|
|
276
336
|
if (hasTodayData) {
|
|
277
|
-
console.log(renderTable("
|
|
337
|
+
console.log(renderTable("Activity & Cost Estimate — Today", todayAgg, todayOpus, todayRecords));
|
|
278
338
|
} else {
|
|
279
339
|
console.log(" No activity recorded for today yet.");
|
|
280
340
|
}
|
|
@@ -283,8 +343,9 @@ function main() {
|
|
|
283
343
|
}
|
|
284
344
|
|
|
285
345
|
// All-time report
|
|
286
|
-
const allAgg = aggregate(records);
|
|
287
|
-
|
|
346
|
+
const allAgg = aggregate(records, rateMap);
|
|
347
|
+
const allOpus = allOpusCost(records, rateMap);
|
|
348
|
+
console.log(renderTable("Activity & Cost Estimate — All Time", allAgg, allOpus, records));
|
|
288
349
|
}
|
|
289
350
|
|
|
290
351
|
main();
|
|
@@ -23,7 +23,6 @@ import { appendFileSync, existsSync, readFileSync } from 'fs';
|
|
|
23
23
|
import { dirname, join } from 'path';
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
import { randomBytes } from 'crypto';
|
|
26
|
-
import { logHookError } from './error-channel.mjs';
|
|
27
26
|
|
|
28
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
28
|
const LEDGER_FILE = join(__dirname, 'decision-ledger.jsonl');
|
|
@@ -63,7 +62,7 @@ function recordDecision(decision = {}) {
|
|
|
63
62
|
|
|
64
63
|
try {
|
|
65
64
|
appendFileSync(LEDGER_FILE, entry + '\n');
|
|
66
|
-
} catch
|
|
65
|
+
} catch {}
|
|
67
66
|
|
|
68
67
|
return id;
|
|
69
68
|
}
|
|
@@ -98,7 +97,7 @@ function recordOutcome(decisionId, outcome = {}) {
|
|
|
98
97
|
|
|
99
98
|
try {
|
|
100
99
|
appendFileSync(LEDGER_FILE, entry + '\n');
|
|
101
|
-
} catch
|
|
100
|
+
} catch {}
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
function loadLedger() {
|
|
@@ -205,55 +204,6 @@ function getInsights(opts = {}) {
|
|
|
205
204
|
};
|
|
206
205
|
}
|
|
207
206
|
|
|
208
|
-
/**
|
|
209
|
-
* getOutcomeStats — lightweight aggregation for the routing hot path.
|
|
210
|
-
*
|
|
211
|
-
* Returns success rates by tier and provider over the last 24 hours,
|
|
212
|
-
* plus flags for any tier with < 50% success (with ≥ 5 outcomes).
|
|
213
|
-
*/
|
|
214
|
-
function getOutcomeStats() {
|
|
215
|
-
const { decisions, outcomes } = loadLedger();
|
|
216
|
-
const merged = mergeDecisionsWithOutcomes(decisions, outcomes);
|
|
217
|
-
|
|
218
|
-
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
219
|
-
const recent = merged.filter(d => d.outcome && d.timestamp >= cutoff);
|
|
220
|
-
|
|
221
|
-
const byTier = {};
|
|
222
|
-
const byProvider = {};
|
|
223
|
-
|
|
224
|
-
for (const d of recent) {
|
|
225
|
-
// Tier stats
|
|
226
|
-
const t = d.tier || 'execute';
|
|
227
|
-
if (!byTier[t]) byTier[t] = { total: 0, success: 0 };
|
|
228
|
-
byTier[t].total++;
|
|
229
|
-
if (d.outcome.success) byTier[t].success++;
|
|
230
|
-
|
|
231
|
-
// Provider stats
|
|
232
|
-
const p = d.provider || 'claude';
|
|
233
|
-
if (!byProvider[p]) byProvider[p] = { total: 0, success: 0 };
|
|
234
|
-
byProvider[p].total++;
|
|
235
|
-
if (d.outcome.success) byProvider[p].success++;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Flag underperforming tiers (< 50% success with ≥ 5 outcomes)
|
|
239
|
-
const underperforming = [];
|
|
240
|
-
for (const [tier, stats] of Object.entries(byTier)) {
|
|
241
|
-
if (stats.total >= 5) {
|
|
242
|
-
const rate = Math.round((stats.success / stats.total) * 100);
|
|
243
|
-
if (rate < 50) {
|
|
244
|
-
underperforming.push({ tier, rate, total: stats.total });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
by_tier: byTier,
|
|
251
|
-
by_provider: byProvider,
|
|
252
|
-
total_outcomes: recent.length,
|
|
253
|
-
underperforming,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
207
|
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
258
208
|
|
|
259
209
|
function printInsights() {
|
|
@@ -346,4 +296,4 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
346
296
|
}
|
|
347
297
|
}
|
|
348
298
|
|
|
349
|
-
export { recordDecision, recordOutcome, getInsights,
|
|
299
|
+
export { recordDecision, recordOutcome, getInsights, loadLedger };
|