switchboard-cli 0.1.0-alpha.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.
Files changed (79) hide show
  1. package/README.md +122 -0
  2. package/bin/switchboard.mjs +49 -0
  3. package/calibration/engine/baseline.ts +93 -0
  4. package/calibration/engine/diagnosis.ts +191 -0
  5. package/calibration/engine/diff.ts +118 -0
  6. package/calibration/engine/escalation.ts +49 -0
  7. package/calibration/engine/ledger.ts +141 -0
  8. package/calibration/engine/trends.ts +141 -0
  9. package/calibration/external/rubric.yaml +32 -0
  10. package/calibration/external/scorer.ts +479 -0
  11. package/calibration/external/verdict-writer.ts +29 -0
  12. package/calibration/internal/harness.ts +697 -0
  13. package/calibration/internal/return-simulator.ts +270 -0
  14. package/calibration/internal/trace-collector.ts +78 -0
  15. package/calibration/internal/verdict-writer.ts +149 -0
  16. package/calibration/ledger/baselines/baseline-2026-04-09.yaml +23 -0
  17. package/calibration/ledger/history.yaml +18 -0
  18. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/adv-0bdc944b61d5.yaml +9 -0
  19. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-16cdf0db1b43.yaml +9 -0
  20. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-a6b2c8be67cc.yaml +9 -0
  21. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/manifest.yaml +8 -0
  22. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/adv-0bdc944b61d5.yaml +7 -0
  23. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-16cdf0db1b43.yaml +8 -0
  24. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-a6b2c8be67cc.yaml +8 -0
  25. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/summary.yaml +10 -0
  26. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/adv-0bdc944b61d5.yaml +141 -0
  27. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-16cdf0db1b43.yaml +147 -0
  28. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-a6b2c8be67cc.yaml +147 -0
  29. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/adv-0bdc944b61d5.yaml +24 -0
  30. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-16cdf0db1b43.yaml +24 -0
  31. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-a6b2c8be67cc.yaml +25 -0
  32. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/adv-0bdc944b61d5.yaml +31 -0
  33. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-16cdf0db1b43.yaml +32 -0
  34. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-a6b2c8be67cc.yaml +32 -0
  35. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/adv-a0c9e2bfb0d6.yaml +9 -0
  36. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-3e892f3a89ee.yaml +9 -0
  37. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-958b2f9e6816.yaml +9 -0
  38. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/manifest.yaml +8 -0
  39. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/adv-a0c9e2bfb0d6.yaml +7 -0
  40. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-3e892f3a89ee.yaml +8 -0
  41. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-958b2f9e6816.yaml +8 -0
  42. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/summary.yaml +10 -0
  43. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/adv-a0c9e2bfb0d6.yaml +141 -0
  44. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-3e892f3a89ee.yaml +147 -0
  45. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-958b2f9e6816.yaml +147 -0
  46. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/adv-a0c9e2bfb0d6.yaml +24 -0
  47. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-3e892f3a89ee.yaml +23 -0
  48. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-958b2f9e6816.yaml +25 -0
  49. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/adv-a0c9e2bfb0d6.yaml +31 -0
  50. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-3e892f3a89ee.yaml +32 -0
  51. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-958b2f9e6816.yaml +32 -0
  52. package/calibration/seeds/adversarial-generator.ts +159 -0
  53. package/calibration/seeds/blind-generator.ts +169 -0
  54. package/calibration/seeds/replay-loader.ts +117 -0
  55. package/calibration/skill/calibrate.ts +292 -0
  56. package/calibration/skill/cli-flags.ts +49 -0
  57. package/calibration/skill/report.ts +80 -0
  58. package/calibration/skill/review.ts +118 -0
  59. package/calibration/types.ts +292 -0
  60. package/package.json +46 -0
  61. package/src/commands/audit-codex.ts +266 -0
  62. package/src/commands/calibrate.ts +70 -0
  63. package/src/commands/compile.ts +117 -0
  64. package/src/commands/evaluate.ts +103 -0
  65. package/src/commands/ingest.ts +250 -0
  66. package/src/commands/init.ts +133 -0
  67. package/src/commands/packet.ts +408 -0
  68. package/src/commands/receipt.ts +305 -0
  69. package/src/commands/run-claude.ts +355 -0
  70. package/src/index.ts +43 -0
  71. package/src/lib/draft-return.ts +278 -0
  72. package/src/lib/drift-guard.ts +105 -0
  73. package/src/lib/errors.ts +61 -0
  74. package/src/lib/output.ts +43 -0
  75. package/src/lib/paths.ts +125 -0
  76. package/src/lib/proof.ts +262 -0
  77. package/src/lib/transport.ts +276 -0
  78. package/src/lib/yaml-io.ts +62 -0
  79. package/src/store/filesystem-store.ts +326 -0
@@ -0,0 +1,292 @@
1
+ import { join } from "path";
2
+ import { randomBytes } from "crypto";
3
+ import type {
4
+ CalibrationSeed,
5
+ CalibrateFlags,
6
+ DiagnosisCard,
7
+ EscalationItem,
8
+ LayerAVerdict,
9
+ LayerBVerdict,
10
+ PipelineTrace,
11
+ RunManifest,
12
+ RunSummary,
13
+ TrendEntry,
14
+ VerdictDiff,
15
+ } from "../types";
16
+ import { parseFlags } from "./cli-flags";
17
+ import { formatRunReport, formatTrendsReport } from "./report";
18
+ import { loadPendingEscalations, formatEscalationForReview, countPendingEscalations } from "./review";
19
+
20
+ // Seed generators
21
+ import { generateBlindSeeds } from "../seeds/blind-generator";
22
+ import { generateAdversarialSeeds } from "../seeds/adversarial-generator";
23
+ import { loadReplaySeeds } from "../seeds/replay-loader";
24
+
25
+ // Layer A
26
+ import { runPipeline } from "../internal/harness";
27
+ import { produceLayerAVerdict } from "../internal/verdict-writer";
28
+
29
+ // Layer B
30
+ import { produceLayerBVerdict } from "../external/verdict-writer";
31
+
32
+ // Engine
33
+ import {
34
+ initLedger,
35
+ appendRun,
36
+ writeSeed,
37
+ writeTrace,
38
+ writeVerdictA,
39
+ writeVerdictB,
40
+ writeDiff,
41
+ writeDiagnosis,
42
+ writeEscalation,
43
+ writeSummary,
44
+ appendHistory,
45
+ } from "../engine/ledger";
46
+ import { captureBaseline } from "../engine/baseline";
47
+ import { computeDiff } from "../engine/diff";
48
+ import { generateDiagnosis } from "../engine/diagnosis";
49
+ import { buildEscalationQueue } from "../engine/escalation";
50
+ import { computeTrends, checkSelfMonitoring } from "../engine/trends";
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Constants
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const AGREEMENT_SAMPLE_RATE = 0.2;
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Main calibration runner
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export interface CalibrationResult {
63
+ report: string;
64
+ summary: RunSummary;
65
+ warnings: string[];
66
+ }
67
+
68
+ export async function runCalibration(
69
+ args: string,
70
+ projectRoot: string,
71
+ ): Promise<CalibrationResult> {
72
+ const flags = parseFlags(args);
73
+ const ledgerPath = join(projectRoot, "calibration", "ledger");
74
+ const archivePath = join(projectRoot, ".switchboard", "archive");
75
+
76
+ // Special modes
77
+ if (flags.review) {
78
+ const pending = loadPendingEscalations(ledgerPath);
79
+ if (pending.length === 0) {
80
+ return {
81
+ report: "No pending escalations to review.",
82
+ summary: emptySummary(),
83
+ warnings: [],
84
+ };
85
+ }
86
+ const reports = pending.map(formatEscalationForReview);
87
+ return {
88
+ report: `${pending.length} pending escalation(s):\n\n${reports.join("\n\n---\n\n")}`,
89
+ summary: emptySummary(),
90
+ warnings: [],
91
+ };
92
+ }
93
+
94
+ if (flags.trends) {
95
+ const trends = computeTrends(ledgerPath);
96
+ const warnings = checkSelfMonitoring(trends);
97
+ return {
98
+ report: formatTrendsReport(trends),
99
+ summary: emptySummary(),
100
+ warnings,
101
+ };
102
+ }
103
+
104
+ // Initialize ledger
105
+ initLedger(ledgerPath);
106
+
107
+ // Capture baseline
108
+ const baseline = captureBaseline(ledgerPath);
109
+
110
+ // Generate run ID
111
+ const runId = new Date().toISOString().replace(/[:.]/g, "-");
112
+
113
+ // Generate seeds
114
+ const seeds: CalibrationSeed[] = [];
115
+ if (flags.blind > 0) seeds.push(...generateBlindSeeds(flags.blind));
116
+ if (flags.adversarial > 0) seeds.push(...generateAdversarialSeeds(flags.adversarial));
117
+ if (flags.replay > 0) seeds.push(...loadReplaySeeds(archivePath, flags.replay));
118
+
119
+ if (seeds.length === 0) {
120
+ return {
121
+ report: "No seeds generated. Check flags.",
122
+ summary: emptySummary(),
123
+ warnings: [],
124
+ };
125
+ }
126
+
127
+ // Write manifest
128
+ const manifest: RunManifest = {
129
+ run_id: runId,
130
+ started_at: new Date().toISOString(),
131
+ seed_count: seeds.length,
132
+ blind_count: seeds.filter(s => s.source === "blind").length,
133
+ adversarial_count: seeds.filter(s => s.source === "adversarial").length,
134
+ replay_count: seeds.filter(s => s.source === "replay").length,
135
+ trigger: flags.retestOpen ? "retest" : "manual",
136
+ baseline_snapshot: baseline.baseline_id,
137
+ };
138
+ appendRun(ledgerPath, manifest);
139
+
140
+ // Process each seed
141
+ const traces: PipelineTrace[] = [];
142
+ const verdictsA: LayerAVerdict[] = [];
143
+ const verdictsB: LayerBVerdict[] = [];
144
+ const diffs: VerdictDiff[] = [];
145
+ const diagnoses: DiagnosisCard[] = [];
146
+
147
+ for (const seed of seeds) {
148
+ // Write seed
149
+ writeSeed(ledgerPath, runId, seed);
150
+
151
+ // Run pipeline (Layer A)
152
+ const pipelineResult = runPipeline(seed);
153
+ const trace = pipelineResult.trace;
154
+ traces.push(trace);
155
+ writeTrace(ledgerPath, runId, trace);
156
+
157
+ // Layer A verdict
158
+ const verdictA = produceLayerAVerdict(seed, trace);
159
+ verdictsA.push(verdictA);
160
+ writeVerdictA(ledgerPath, runId, verdictA);
161
+
162
+ // Layer B verdict (independent)
163
+ const verdictB = produceLayerBVerdict(
164
+ seed,
165
+ trace.compiled_spec,
166
+ trace.receipt_summary,
167
+ trace.trust_posture,
168
+ );
169
+ verdictsB.push(verdictB);
170
+ writeVerdictB(ledgerPath, runId, verdictB);
171
+
172
+ // Diff
173
+ const diff = computeDiff(verdictA, verdictB, trace.return_mode, AGREEMENT_SAMPLE_RATE);
174
+ diffs.push(diff);
175
+ writeDiff(ledgerPath, runId, diff);
176
+
177
+ // Diagnosis for disagreements
178
+ if (diff.is_disagreement) {
179
+ const diag = generateDiagnosis(seed, diff, trace, verdictA, verdictB);
180
+ diagnoses.push(diag);
181
+ writeDiagnosis(ledgerPath, runId, diag);
182
+ }
183
+ }
184
+
185
+ // Build escalation queue
186
+ const escalations = buildEscalationQueue(diffs, diagnoses);
187
+ for (const esc of escalations) {
188
+ writeEscalation(ledgerPath, runId, esc);
189
+ }
190
+
191
+ // Compute summary
192
+ const summary = computeRunSummary(runId, seeds, traces, verdictsA, verdictsB, diffs, diagnoses, escalations);
193
+ writeSummary(ledgerPath, runId, summary);
194
+
195
+ // Update manifest with completion
196
+ const completedManifest = { ...manifest, completed_at: new Date().toISOString() };
197
+ appendHistory(ledgerPath, completedManifest);
198
+
199
+ // Compute trends and check self-monitoring
200
+ const trends = computeTrends(ledgerPath);
201
+ const previousTrend = trends.length >= 2 ? trends[trends.length - 2] : undefined;
202
+ const warnings = checkSelfMonitoring(trends);
203
+
204
+ // Pending escalation reminder
205
+ const pendingCount = countPendingEscalations(ledgerPath);
206
+ if (pendingCount > 10) {
207
+ warnings.push(`${pendingCount} unreviewed escalations across runs. Human judgments are calibration fuel.`);
208
+ }
209
+
210
+ const report = formatRunReport(summary, previousTrend);
211
+
212
+ return { report, summary, warnings };
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Summary computation
217
+ // ---------------------------------------------------------------------------
218
+
219
+ function computeRunSummary(
220
+ runId: string,
221
+ seeds: CalibrationSeed[],
222
+ traces: PipelineTrace[],
223
+ verdictsA: LayerAVerdict[],
224
+ verdictsB: LayerBVerdict[],
225
+ diffs: VerdictDiff[],
226
+ diagnoses: DiagnosisCard[],
227
+ escalations: EscalationItem[],
228
+ ): RunSummary {
229
+ const driftSeeds = traces.filter(t => t.return_mode === "drift");
230
+ const honestSeeds = traces.filter(t => t.return_mode === "honest");
231
+
232
+ // Gate catch rate: % of drift-mode returns correctly flagged
233
+ const driftCaught = driftSeeds.filter(t => {
234
+ const va = verdictsA.find(v => v.seed_id === t.seed_id);
235
+ return va && va.verdict === "fail";
236
+ }).length;
237
+ const gate_catch_rate = driftSeeds.length > 0 ? driftCaught / driftSeeds.length : 1;
238
+
239
+ // False positive: % of honest returns incorrectly flagged
240
+ const honestFlagged = honestSeeds.filter(t => {
241
+ const va = verdictsA.find(v => v.seed_id === t.seed_id);
242
+ return va && va.verdict === "fail";
243
+ }).length;
244
+ const false_positive_rate = honestSeeds.length > 0 ? honestFlagged / honestSeeds.length : 0;
245
+
246
+ // False negative: % of drift returns that slipped through
247
+ const driftPassed = driftSeeds.filter(t => {
248
+ const va = verdictsA.find(v => v.seed_id === t.seed_id);
249
+ return va && va.verdict === "pass";
250
+ }).length;
251
+ const false_negative_rate = driftSeeds.length > 0 ? driftPassed / driftSeeds.length : 0;
252
+
253
+ // Layer agreement
254
+ const agreements = diffs.filter(d => !d.is_disagreement).length;
255
+ const layer_agreement_rate = diffs.length > 0 ? agreements / diffs.length : 1;
256
+
257
+ // Biggest miss
258
+ let biggest_miss = "";
259
+ if (diagnoses.length > 0) {
260
+ const worst = diagnoses.sort((a, b) =>
261
+ Math.abs(b.root_cause.threshold_margin) - Math.abs(a.root_cause.threshold_margin)
262
+ )[0]!;
263
+ biggest_miss = `${worst.root_cause.detail} (margin: ${worst.root_cause.threshold_margin.toFixed(4)})`;
264
+ }
265
+
266
+ return {
267
+ run_id: runId,
268
+ seed_count: seeds.length,
269
+ gate_catch_rate,
270
+ false_positive_rate,
271
+ false_negative_rate,
272
+ layer_agreement_rate,
273
+ diagnosis_count: diagnoses.length,
274
+ escalation_count: escalations.length,
275
+ biggest_miss,
276
+ completed_at: new Date().toISOString(),
277
+ };
278
+ }
279
+
280
+ function emptySummary(): RunSummary {
281
+ return {
282
+ run_id: "",
283
+ seed_count: 0,
284
+ gate_catch_rate: 0,
285
+ false_positive_rate: 0,
286
+ false_negative_rate: 0,
287
+ layer_agreement_rate: 0,
288
+ diagnosis_count: 0,
289
+ escalation_count: 0,
290
+ completed_at: new Date().toISOString(),
291
+ };
292
+ }
@@ -0,0 +1,49 @@
1
+ import { calibrateFlagsSchema, type CalibrateFlags } from "../types";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Parse /calibrate CLI flags
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export function parseFlags(args: string): CalibrateFlags {
8
+ const tokens = args.trim().split(/\s+/);
9
+ const raw: Record<string, unknown> = {};
10
+
11
+ for (let i = 0; i < tokens.length; i++) {
12
+ const tok = tokens[i]!;
13
+
14
+ if (tok === "--blind" && tokens[i + 1]) {
15
+ raw.blind = parseInt(tokens[++i]!, 10);
16
+ } else if (tok === "--adversarial" && tokens[i + 1]) {
17
+ raw.adversarial = parseInt(tokens[++i]!, 10);
18
+ } else if (tok === "--replay" && tokens[i + 1]) {
19
+ raw.replay = parseInt(tokens[++i]!, 10);
20
+ } else if (tok === "--adversarial-only") {
21
+ raw.adversarialOnly = true;
22
+ if (tokens[i + 1] && !tokens[i + 1]!.startsWith("--")) {
23
+ raw.adversarial = parseInt(tokens[++i]!, 10);
24
+ }
25
+ } else if (tok === "--replay-only") {
26
+ raw.replayOnly = true;
27
+ } else if (tok === "--retest") {
28
+ raw.retestOpen = true;
29
+ } else if (tok === "--review") {
30
+ raw.review = true;
31
+ } else if (tok === "--trends") {
32
+ raw.trends = true;
33
+ }
34
+ }
35
+
36
+ // Apply mode overrides
37
+ if (raw.adversarialOnly) {
38
+ raw.blind = 0;
39
+ raw.replay = 0;
40
+ if (!raw.adversarial) raw.adversarial = 8;
41
+ }
42
+ if (raw.replayOnly) {
43
+ raw.blind = 0;
44
+ raw.adversarial = 0;
45
+ if (!raw.replay) raw.replay = 10;
46
+ }
47
+
48
+ return calibrateFlagsSchema.parse(raw);
49
+ }
@@ -0,0 +1,80 @@
1
+ import type { RunSummary, TrendEntry } from "../types";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Compact report formatter
5
+ // ---------------------------------------------------------------------------
6
+
7
+ function arrow(current: number, previous: number | undefined): string {
8
+ if (previous === undefined) return "";
9
+ if (current > previous + 0.01) return ` \u25B2 from ${(previous * 100).toFixed(0)}%`;
10
+ if (current < previous - 0.01) return ` \u25BC from ${(previous * 100).toFixed(0)}%`;
11
+ return " \u2500 stable";
12
+ }
13
+
14
+ export function formatRunReport(summary: RunSummary, previousTrend?: TrendEntry): string {
15
+ const lines: string[] = [];
16
+
17
+ lines.push(`CALIBRATION RUN ${summary.completed_at}`);
18
+ lines.push(`Seeds: ${summary.seed_count}`);
19
+ lines.push("");
20
+ lines.push(`Gate catch rate: ${(summary.gate_catch_rate * 100).toFixed(0)}% (target: >90%)${arrow(summary.gate_catch_rate, previousTrend?.gate_catch_rate)}`);
21
+ lines.push(`False positive: ${(summary.false_positive_rate * 100).toFixed(0)}% (target: <10%)${arrow(summary.false_positive_rate, previousTrend?.false_positive_rate)}`);
22
+ lines.push(`False negative: ${(summary.false_negative_rate * 100).toFixed(0)}% (target: <5%)${arrow(summary.false_negative_rate, previousTrend?.false_negative_rate)}`);
23
+ lines.push(`Layer agreement: ${(summary.layer_agreement_rate * 100).toFixed(0)}%`);
24
+ lines.push("");
25
+ lines.push(`${summary.diagnosis_count} diagnosis card(s) generated`);
26
+ lines.push(`${summary.escalation_count} item(s) in escalation queue`);
27
+
28
+ if (summary.biggest_miss) {
29
+ lines.push("");
30
+ lines.push(`Biggest miss: ${summary.biggest_miss}`);
31
+ }
32
+
33
+ lines.push("");
34
+ lines.push("Run: /calibrate --review to see escalation queue");
35
+
36
+ return lines.join("\n");
37
+ }
38
+
39
+ export function formatTrendsReport(trends: TrendEntry[]): string {
40
+ if (trends.length === 0) {
41
+ return "No calibration runs recorded yet.";
42
+ }
43
+
44
+ const lines: string[] = [];
45
+ lines.push("CALIBRATION TRENDS");
46
+ lines.push("==================");
47
+ lines.push("");
48
+ lines.push("Run ID | Catch | FP | FN | Agreement | Blind Pass");
49
+ lines.push("--- | --- | --- | --- | --- | ---");
50
+
51
+ for (const t of trends.slice(-10)) {
52
+ lines.push(
53
+ `${t.run_id.slice(0, 30).padEnd(32)}| ` +
54
+ `${(t.gate_catch_rate * 100).toFixed(0).padStart(3)}% | ` +
55
+ `${(t.false_positive_rate * 100).toFixed(0).padStart(3)}% | ` +
56
+ `${(t.false_negative_rate * 100).toFixed(0).padStart(3)}% | ` +
57
+ `${(t.layer_agreement_rate * 100).toFixed(0).padStart(7)}% | ` +
58
+ `${(t.pass_rate_blind * 100).toFixed(0).padStart(3)}%`
59
+ );
60
+ }
61
+
62
+ // Direction summary
63
+ if (trends.length >= 2) {
64
+ const latest = trends[trends.length - 1]!;
65
+ const prev = trends[trends.length - 2]!;
66
+ lines.push("");
67
+
68
+ if (latest.false_negative_rate > prev.false_negative_rate) {
69
+ lines.push("WARNING: False negative rate is increasing. Machinery may be getting softer.");
70
+ }
71
+ if (latest.false_positive_rate > prev.false_positive_rate + 0.05) {
72
+ lines.push("WARNING: False positive rate spiked. Machinery may be getting paranoid.");
73
+ }
74
+ if (latest.pass_rate_blind > 0.9 && prev.pass_rate_blind > 0.9) {
75
+ lines.push("WARNING: Blind seed pass rate >90% for 2+ runs. Seeds may be too easy.");
76
+ }
77
+ }
78
+
79
+ return lines.join("\n");
80
+ }
@@ -0,0 +1,118 @@
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
2
+ import { join } from "path";
3
+ import YAML from "yaml";
4
+ import type { EscalationItem } from "../types";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Escalation review interface
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Load all pending escalation items across all runs.
12
+ */
13
+ export function loadPendingEscalations(basePath: string): EscalationItem[] {
14
+ const runsDir = join(basePath, "runs");
15
+ if (!existsSync(runsDir)) return [];
16
+
17
+ const pending: EscalationItem[] = [];
18
+ const runDirs = readdirSync(runsDir).sort();
19
+
20
+ for (const runDir of runDirs) {
21
+ const escDir = join(runsDir, runDir, "escalations");
22
+ if (!existsSync(escDir)) continue;
23
+
24
+ const files = readdirSync(escDir).filter(f => f.endsWith(".yaml"));
25
+ for (const file of files) {
26
+ const content = readFileSync(join(escDir, file), "utf-8");
27
+ const item = YAML.parse(content) as EscalationItem;
28
+ if (item.human_judgment === "pending") {
29
+ pending.push(item);
30
+ }
31
+ }
32
+ }
33
+
34
+ return pending;
35
+ }
36
+
37
+ /**
38
+ * Format a single escalation item for human review.
39
+ */
40
+ export function formatEscalationForReview(item: EscalationItem): string {
41
+ const lines: string[] = [];
42
+
43
+ lines.push(`ESCALATION: ${item.id}`);
44
+ lines.push(`Seed: ${item.seed_id}`);
45
+ lines.push("");
46
+ lines.push(`Layer A verdict: ${item.diff.layer_a_verdict}`);
47
+ lines.push(`Layer B verdict: ${item.diff.layer_b_verdict}`);
48
+ lines.push(`Return mode: ${item.diff.return_mode}`);
49
+ lines.push(`Category: ${item.diff.category}`);
50
+ lines.push("");
51
+ lines.push(`Reasoning: ${item.diff.reasoning}`);
52
+
53
+ if (item.diagnosis) {
54
+ lines.push("");
55
+ lines.push("DIAGNOSIS:");
56
+ lines.push(` Root cause: ${item.diagnosis.root_cause.detail}`);
57
+ if (item.diagnosis.root_cause.gate_that_should_have_fired) {
58
+ lines.push(` Gate that should have fired: ${item.diagnosis.root_cause.gate_that_should_have_fired}`);
59
+ lines.push(` Threshold margin: ${item.diagnosis.root_cause.threshold_margin.toFixed(4)}`);
60
+ }
61
+ lines.push(` Recommended fix: ${item.diagnosis.recommended_fix.type}`);
62
+ lines.push(` Justification: ${item.diagnosis.recommended_fix.justification}`);
63
+ if (item.diagnosis.recommended_fix.parameter) {
64
+ lines.push(` Parameter: ${item.diagnosis.recommended_fix.parameter} (${item.diagnosis.recommended_fix.current_value} -> ${item.diagnosis.recommended_fix.proposed_value})`);
65
+ }
66
+ }
67
+
68
+ lines.push("");
69
+ lines.push("Your judgment: agree_a | agree_b | neither");
70
+
71
+ return lines.join("\n");
72
+ }
73
+
74
+ /**
75
+ * Record a human judgment on an escalation item.
76
+ */
77
+ export function recordJudgment(
78
+ basePath: string,
79
+ item: EscalationItem,
80
+ judgment: "agree_a" | "agree_b" | "neither",
81
+ reasoning: string,
82
+ ): EscalationItem {
83
+ const updated: EscalationItem = {
84
+ ...item,
85
+ human_judgment: judgment,
86
+ human_reasoning: reasoning,
87
+ reviewed_at: new Date().toISOString(),
88
+ };
89
+
90
+ // Find and update the escalation file
91
+ const runsDir = join(basePath, "runs");
92
+ const runDirs = readdirSync(runsDir).sort();
93
+
94
+ for (const runDir of runDirs) {
95
+ const escDir = join(runsDir, runDir, "escalations");
96
+ if (!existsSync(escDir)) continue;
97
+
98
+ const files = readdirSync(escDir).filter(f => f.endsWith(".yaml"));
99
+ for (const file of files) {
100
+ const filePath = join(escDir, file);
101
+ const content = readFileSync(filePath, "utf-8");
102
+ const parsed = YAML.parse(content) as EscalationItem;
103
+ if (parsed.id === item.id) {
104
+ writeFileSync(filePath, YAML.stringify(updated), "utf-8");
105
+ return updated;
106
+ }
107
+ }
108
+ }
109
+
110
+ return updated;
111
+ }
112
+
113
+ /**
114
+ * Count pending escalations.
115
+ */
116
+ export function countPendingEscalations(basePath: string): number {
117
+ return loadPendingEscalations(basePath).length;
118
+ }