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 { z } from "zod";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Calibration Harness Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ // --- Seed types ---
8
+
9
+ export const seedSourceSchema = z.enum(["blind", "adversarial", "replay"]);
10
+ export const complexityTargetSchema = z.enum(["simple", "structured", "governance"]);
11
+ export const returnModeSchema = z.enum(["honest", "drift"]);
12
+
13
+ export const calibrationSeedSchema = z.object({
14
+ seed_id: z.string(),
15
+ source: seedSourceSchema,
16
+ raw_idea: z.string().min(1),
17
+ complexity_target: complexityTargetSchema,
18
+ created_at: z.string(),
19
+ /** For replay seeds — the archive snapshot path it came from. */
20
+ replay_source: z.string().default(""),
21
+ /** Adversarial seeds describe their attack vector. */
22
+ attack_vector: z.string().default(""),
23
+ });
24
+
25
+ export type CalibrationSeed = z.infer<typeof calibrationSeedSchema>;
26
+
27
+ // --- Pipeline trace ---
28
+
29
+ export const pipelineOutcomeSchema = z.enum(["completed", "rejected", "error"]);
30
+
31
+ export const pipelineTraceSchema = z.object({
32
+ seed_id: z.string(),
33
+ outcome: pipelineOutcomeSchema,
34
+ rejection_point: z.string().default(""),
35
+ rejection_reason: z.string().default(""),
36
+
37
+ // Activation
38
+ complexity_score: z.number().default(0),
39
+ selected_mode: z.string().default(""),
40
+ assumptions_count: z.number().default(0),
41
+
42
+ // Routing
43
+ routed_surface: z.string().default(""),
44
+ routing_rationale: z.string().default(""),
45
+
46
+ // Return simulation
47
+ return_mode: returnModeSchema,
48
+ return_mode_sealed: z.boolean().default(true),
49
+
50
+ // Reconciliation
51
+ reconcile_findings_count: z.number().default(0),
52
+ reconcile_drift_detected: z.boolean().default(false),
53
+ reconcile_recommendation: z.string().default(""),
54
+
55
+ // Gates
56
+ gates_triggered: z.array(z.string()).default([]),
57
+ gate_label: z.string().default(""),
58
+
59
+ // Claim spread
60
+ spread_score: z.number().default(0),
61
+ spread_vector: z.record(z.number()).default({}),
62
+
63
+ // Receipt
64
+ trust_posture: z.string().default(""),
65
+ receipt_verdict: z.string().default(""),
66
+ honesty_violations: z.array(z.string()).default([]),
67
+
68
+ // Timing
69
+ duration_ms: z.number().default(0),
70
+
71
+ // Raw artifacts for Layer B
72
+ compiled_spec: z.string().default(""),
73
+ receipt_summary: z.string().default(""),
74
+ });
75
+
76
+ export type PipelineTrace = z.infer<typeof pipelineTraceSchema>;
77
+
78
+ // --- Layer A verdict ---
79
+
80
+ export const layerAVerdictSchema = z.object({
81
+ seed_id: z.string(),
82
+ verdict: z.enum(["pass", "fail", "inconclusive"]),
83
+ trust_posture: z.string(),
84
+ gate_label: z.string(),
85
+ spread_score: z.number(),
86
+ reconcile_drift: z.boolean(),
87
+ gates_triggered: z.array(z.string()),
88
+ threshold_proximity: z.record(z.object({
89
+ threshold: z.number(),
90
+ actual: z.number(),
91
+ margin: z.number(),
92
+ })).default({}),
93
+ justification: z.string(),
94
+ scored_at: z.string(),
95
+ });
96
+
97
+ export type LayerAVerdict = z.infer<typeof layerAVerdictSchema>;
98
+
99
+ // --- Layer B verdict (independent scoring) ---
100
+
101
+ export const layerBDimensionSchema = z.object({
102
+ score: z.number().min(0).max(1),
103
+ justification: z.string().min(1),
104
+ });
105
+
106
+ export const layerBVerdictSchema = z.object({
107
+ seed_id: z.string(),
108
+ verdict: z.enum(["pass", "fail", "inconclusive"]),
109
+ dimensions: z.object({
110
+ intent_fidelity: layerBDimensionSchema,
111
+ confidence_honesty: layerBDimensionSchema,
112
+ scope_discipline: layerBDimensionSchema,
113
+ execution_readiness: layerBDimensionSchema,
114
+ self_awareness: layerBDimensionSchema,
115
+ }),
116
+ composite_score: z.number().min(0).max(1),
117
+ justification: z.string(),
118
+ scored_at: z.string(),
119
+ });
120
+
121
+ export type LayerBVerdict = z.infer<typeof layerBVerdictSchema>;
122
+ export type LayerBDimension = z.infer<typeof layerBDimensionSchema>;
123
+
124
+ // --- Diff result ---
125
+
126
+ export const diffCategorySchema = z.enum([
127
+ "a_pass_b_fail", // Layer A too lenient
128
+ "a_fail_b_pass", // Layer A too strict
129
+ "both_fail_different", // Different failure models
130
+ "agreement_pass",
131
+ "agreement_fail",
132
+ ]);
133
+
134
+ export const verdictDiffSchema = z.object({
135
+ seed_id: z.string(),
136
+ category: diffCategorySchema,
137
+ layer_a_verdict: z.string(),
138
+ layer_b_verdict: z.string(),
139
+ return_mode: returnModeSchema,
140
+ reasoning: z.string(),
141
+ is_disagreement: z.boolean(),
142
+ escalated: z.boolean().default(false),
143
+ escalation_reason: z.string().default(""),
144
+ });
145
+
146
+ export type VerdictDiff = z.infer<typeof verdictDiffSchema>;
147
+
148
+ // --- Diagnosis card ---
149
+
150
+ export const fixTypeSchema = z.enum(["threshold_tuning", "new_gate", "scoring_dimension_gap"]);
151
+ export const diagnosisStatusSchema = z.enum([
152
+ "open", "fix-proposed", "fix-applied",
153
+ "verified-improved", "closed",
154
+ "regressed", "rolled-back", "escalated",
155
+ ]);
156
+
157
+ export const diagnosisCardSchema = z.object({
158
+ id: z.string(),
159
+ seed_id: z.string(),
160
+ layer_a_verdict: z.string(),
161
+ layer_b_verdict: z.string(),
162
+ return_mode: returnModeSchema,
163
+ root_cause: z.object({
164
+ gate_that_should_have_fired: z.string().default(""),
165
+ threshold_margin: z.number().default(0),
166
+ evidence_missed: z.string().default(""),
167
+ detail: z.string(),
168
+ }),
169
+ recommended_fix: z.object({
170
+ type: fixTypeSchema,
171
+ parameter: z.string().default(""),
172
+ current_value: z.number().default(0),
173
+ proposed_value: z.number().default(0),
174
+ justification: z.string(),
175
+ }),
176
+ verification_plan: z.object({
177
+ retest_this_seed: z.boolean().default(true),
178
+ retest_full_suite: z.boolean().default(true),
179
+ regression_check: z.boolean().default(true),
180
+ }),
181
+ status: diagnosisStatusSchema.default("open"),
182
+ created_at: z.string(),
183
+ });
184
+
185
+ export type DiagnosisCard = z.infer<typeof diagnosisCardSchema>;
186
+
187
+ // --- Escalation item ---
188
+
189
+ export const escalationItemSchema = z.object({
190
+ id: z.string(),
191
+ seed_id: z.string(),
192
+ diff: verdictDiffSchema,
193
+ diagnosis: diagnosisCardSchema.optional(),
194
+ human_judgment: z.enum(["agree_a", "agree_b", "neither", "pending"]).default("pending"),
195
+ human_reasoning: z.string().default(""),
196
+ reviewed_at: z.string().default(""),
197
+ });
198
+
199
+ export type EscalationItem = z.infer<typeof escalationItemSchema>;
200
+
201
+ // --- Run manifest ---
202
+
203
+ export const runManifestSchema = z.object({
204
+ run_id: z.string(),
205
+ started_at: z.string(),
206
+ completed_at: z.string().default(""),
207
+ seed_count: z.number(),
208
+ blind_count: z.number(),
209
+ adversarial_count: z.number(),
210
+ replay_count: z.number(),
211
+ trigger: z.enum(["manual", "scheduled", "retest"]).default("manual"),
212
+ baseline_snapshot: z.string().default(""),
213
+ });
214
+
215
+ export type RunManifest = z.infer<typeof runManifestSchema>;
216
+
217
+ // --- Run summary ---
218
+
219
+ export const runSummarySchema = z.object({
220
+ run_id: z.string(),
221
+ seed_count: z.number(),
222
+ gate_catch_rate: z.number(),
223
+ false_positive_rate: z.number(),
224
+ false_negative_rate: z.number(),
225
+ layer_agreement_rate: z.number(),
226
+ diagnosis_count: z.number(),
227
+ escalation_count: z.number(),
228
+ biggest_miss: z.string().default(""),
229
+ completed_at: z.string(),
230
+ });
231
+
232
+ export type RunSummary = z.infer<typeof runSummarySchema>;
233
+
234
+ // --- Baseline snapshot ---
235
+
236
+ export const baselineSnapshotSchema = z.object({
237
+ baseline_id: z.string(),
238
+ created_at: z.string(),
239
+ axis_weights: z.record(z.number()),
240
+ gate_thresholds: z.record(z.number()),
241
+ reconcile_thresholds: z.record(z.number()),
242
+ notes: z.string().default(""),
243
+ });
244
+
245
+ export type BaselineSnapshot = z.infer<typeof baselineSnapshotSchema>;
246
+
247
+ // --- Trend entry ---
248
+
249
+ export const trendEntrySchema = z.object({
250
+ run_id: z.string(),
251
+ timestamp: z.string(),
252
+ gate_catch_rate: z.number(),
253
+ false_positive_rate: z.number(),
254
+ false_negative_rate: z.number(),
255
+ layer_agreement_rate: z.number(),
256
+ pass_rate_blind: z.number(),
257
+ pass_rate_adversarial: z.number(),
258
+ pass_rate_replay: z.number(),
259
+ });
260
+
261
+ export type TrendEntry = z.infer<typeof trendEntrySchema>;
262
+
263
+ // --- Fix record ---
264
+
265
+ export const fixRecordSchema = z.object({
266
+ fix_id: z.string(),
267
+ diagnosis_id: z.string(),
268
+ baseline_before: z.string(),
269
+ parameter: z.string(),
270
+ old_value: z.number(),
271
+ new_value: z.number(),
272
+ verification_run_id: z.string().default(""),
273
+ outcome: z.enum(["pending", "improved", "regressed", "rolled-back"]).default("pending"),
274
+ applied_at: z.string(),
275
+ });
276
+
277
+ export type FixRecord = z.infer<typeof fixRecordSchema>;
278
+
279
+ // --- Calibrate CLI flags ---
280
+
281
+ export const calibrateFlagsSchema = z.object({
282
+ blind: z.number().default(5),
283
+ adversarial: z.number().default(3),
284
+ replay: z.number().default(2),
285
+ adversarialOnly: z.boolean().default(false),
286
+ replayOnly: z.boolean().default(false),
287
+ retestOpen: z.boolean().default(false),
288
+ review: z.boolean().default(false),
289
+ trends: z.boolean().default(false),
290
+ });
291
+
292
+ export type CalibrateFlags = z.infer<typeof calibrateFlagsSchema>;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "switchboard-cli",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Switchboard CLI — portable governance substrate for AI workflows",
5
+ "license": "MIT",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "bin": {
9
+ "sb": "./bin/switchboard.mjs",
10
+ "switchboard": "./bin/switchboard.mjs"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "calibration/",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": [
22
+ "switchboard",
23
+ "governance",
24
+ "ai",
25
+ "cli",
26
+ "receipts",
27
+ "trust",
28
+ "calibration",
29
+ "evaluation"
30
+ ],
31
+ "scripts": {
32
+ "build": "echo 'cli runs from source via tsx — no build step needed'",
33
+ "lint": "echo 'linted cli'",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "dev": "tsx src/index.ts"
37
+ },
38
+ "dependencies": {
39
+ "@switchboard/core": "workspace:*",
40
+ "@switchboard/projections": "workspace:*",
41
+ "commander": "^13.1.0",
42
+ "tsx": "^4.19.3",
43
+ "yaml": "^2.7.0",
44
+ "zod": "^3.24.2"
45
+ }
46
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * switchboard audit codex
3
+ *
4
+ * Generates a Codex audit packet from the latest dispatch + return + receipt.
5
+ * This is a packet-generation surface — it prepares audit context for Codex
6
+ * but does not execute the Codex transport.
7
+ *
8
+ * Uses existing @switchboard/core: shouldRequireAudit, computeAuditResult,
9
+ * formatDispatchPacket.
10
+ *
11
+ * Does not require the hosted web app or Supabase.
12
+ */
13
+
14
+ import { randomBytes } from "crypto";
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ import type { Command } from "commander";
18
+ import {
19
+ shouldRequireAudit,
20
+ computeAuditResult,
21
+ noAuditRequired,
22
+ parseStructuredReturn,
23
+ formatDispatchPacket,
24
+ type Surface,
25
+ } from "@switchboard/core";
26
+ import { requireRepoRoot, packetsDir, returnsDir, sbDir } from "../lib/paths";
27
+ import { readMarkdown, readYaml } from "../lib/yaml-io";
28
+ import {
29
+ loadContract,
30
+ loadCurrentState,
31
+ loadReceiptByDispatch,
32
+ loadLatestReceipt,
33
+ loadSpec,
34
+ writePacket,
35
+ } from "../store/filesystem-store";
36
+ import * as out from "../lib/output";
37
+ import { withCliErrors } from "../lib/errors";
38
+ import { detectCodexTransport, executeCodexTransport } from "../lib/transport";
39
+ import { captureTranscriptProof, buildAndWriteProofManifest } from "../lib/proof";
40
+
41
+ export function registerAuditCodexCommand(program: Command): void {
42
+ program
43
+ .command("audit")
44
+ .argument("<surface>", "Audit surface: codex")
45
+ .description("Generate an audit packet for Codex review")
46
+ .action(withCliErrors(async (surfaceArg: string) => {
47
+ if (surfaceArg !== "codex") {
48
+ out.fail(`Wave 2 audit only supports 'codex'. Got: "${surfaceArg}"`);
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ const repoRoot = requireRepoRoot();
54
+
55
+ out.heading("switchboard audit codex");
56
+
57
+ // 1. Load state
58
+ const contract = loadContract(repoRoot);
59
+ const current = loadCurrentState(repoRoot);
60
+ const spec = loadSpec(repoRoot) ?? "";
61
+
62
+ // 2. Anchor to latest-ingest.yaml — the governed ingest pointer.
63
+ // This ensures the audit targets the exact dispatch that was
64
+ // ingested, not a stale handoff or un-ingested dispatch.
65
+ const pointerPath = join(sbDir(repoRoot), "working", "latest-ingest.yaml");
66
+ const pointer = readYaml<{
67
+ dispatch_id?: string;
68
+ has_proof_manifest?: boolean;
69
+ proof_manifest_id?: string;
70
+ }>(pointerPath);
71
+
72
+ if (!pointer?.dispatch_id) {
73
+ out.fail("No ingested dispatch found. Run the full loop first:");
74
+ out.bullet("switchboard run claude → switchboard ingest → switchboard audit codex");
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+
79
+ const dispatchId = pointer.dispatch_id;
80
+ out.section("Auditing dispatch", dispatchId);
81
+
82
+ // 3. Load the return data for the ingested dispatch
83
+ const returnPath = join(returnsDir(repoRoot), `${dispatchId}-return.yaml`);
84
+ const rawReturn = readMarkdown(returnPath);
85
+
86
+ if (!rawReturn) {
87
+ out.fail(`No return data for dispatch ${dispatchId}. The ingest pointer exists but the return file is missing.`);
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+
92
+ const report = parseStructuredReturn(rawReturn);
93
+ out.success(`Loaded return: ${report.status}, ${report.changed_files.length} file(s)`);
94
+
95
+ // 4. Run audit check
96
+ const auditCheck = shouldRequireAudit({
97
+ report,
98
+ complexityMode: null,
99
+ taskType: "implementation-request",
100
+ });
101
+
102
+ const audit = auditCheck.required
103
+ ? computeAuditResult({ report, triggers: auditCheck.triggers, returnId: dispatchId })
104
+ : noAuditRequired(dispatchId);
105
+
106
+ out.section("Audit required", audit.audit_required ? "yes" : "no");
107
+ out.section("Outcome", audit.outcome);
108
+
109
+ if (audit.findings.length > 0) {
110
+ for (const f of audit.findings) {
111
+ const icon = f.severity === "blocker" ? "BLOCKED" : "CAUTION";
112
+ out.bullet(`[${icon}] ${f.check}: ${f.detail}`);
113
+ }
114
+ }
115
+
116
+ // 5. Load receipt for this dispatch by scanning receipt files for matching dispatch_id
117
+ let receipt = loadReceiptByDispatch(repoRoot, dispatchId);
118
+ if (!receipt) {
119
+ // Fallback to latest, but warn if it belongs to a different dispatch
120
+ receipt = loadLatestReceipt(repoRoot);
121
+ if (receipt && (receipt as any).dispatch_id !== dispatchId) {
122
+ out.warn(`Latest receipt belongs to dispatch ${(receipt as any).dispatch_id}, not ${dispatchId}. Using for context only.`);
123
+ }
124
+ }
125
+
126
+ // 6. Generate the Codex audit packet
127
+ const auditDispatchId = `codex-audit-${randomBytes(6).toString("hex")}`;
128
+ const auditPromptSections: string[] = [];
129
+
130
+ auditPromptSections.push("# Codex Audit Packet\n");
131
+ auditPromptSections.push(`**Project:** ${contract.title}`);
132
+ auditPromptSections.push(`**Dispatch:** ${dispatchId}`);
133
+ auditPromptSections.push(`**Return status:** ${report.status}`);
134
+ auditPromptSections.push(`**Objective met:** ${report.objective_met}\n`);
135
+
136
+ auditPromptSections.push("## Audit Findings\n");
137
+ if (audit.findings.length === 0) {
138
+ auditPromptSections.push("No audit findings.\n");
139
+ } else {
140
+ for (const f of audit.findings) {
141
+ auditPromptSections.push(`- **[${f.severity}]** ${f.check}: ${f.detail}`);
142
+ if (f.files_affected.length > 0) {
143
+ auditPromptSections.push(` Files: ${f.files_affected.join(", ")}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ auditPromptSections.push("\n## Changed Files\n");
149
+ for (const f of report.changed_files) {
150
+ auditPromptSections.push(`- ${f}`);
151
+ }
152
+
153
+ auditPromptSections.push("\n## Verification Runs\n");
154
+ if (report.verification_run.length === 0) {
155
+ auditPromptSections.push("None reported.\n");
156
+ } else {
157
+ for (const v of report.verification_run) {
158
+ auditPromptSections.push(`- ${v}`);
159
+ }
160
+ }
161
+
162
+ auditPromptSections.push("\n## Evidence\n");
163
+ if (report.evidence.length === 0) {
164
+ auditPromptSections.push("None reported.\n");
165
+ } else {
166
+ for (const e of report.evidence) {
167
+ auditPromptSections.push(`- ${e}`);
168
+ }
169
+ }
170
+
171
+ auditPromptSections.push("\n## Return Summary\n");
172
+ auditPromptSections.push(report.summary);
173
+
174
+ if (receipt) {
175
+ auditPromptSections.push("\n## Receipt Context\n");
176
+ auditPromptSections.push(`- Receipt ID: ${receipt.receipt_id}`);
177
+ const verdict = (receipt as any).issuance_assessment?.verdict ?? (receipt as any).trust_posture ?? "unknown";
178
+ auditPromptSections.push(`- Verdict: ${verdict}`);
179
+ auditPromptSections.push(`- Trust posture: ${(receipt as any).trust_posture ?? "unknown"}`);
180
+ auditPromptSections.push(`- Residuals: ${(receipt as any).residual_count ?? 0}`);
181
+ }
182
+
183
+ auditPromptSections.push("\n---\n");
184
+ auditPromptSections.push("## Audit Instructions\n");
185
+ auditPromptSections.push("Review the changed files against the audit findings.");
186
+ auditPromptSections.push("Verify that sensitive systems (auth, payment, migration) have adequate testing.");
187
+ auditPromptSections.push("Report any additional concerns not caught by the pattern-based audit.");
188
+
189
+ const auditContent = auditPromptSections.join("\n");
190
+
191
+ // 7. Write the audit packet
192
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
193
+ const filename = `codex-audit-${timestamp}-${auditDispatchId.slice(12)}.md`;
194
+
195
+ const packetContent = [
196
+ "---",
197
+ `dispatch_id: ${auditDispatchId}`,
198
+ `surface: codex`,
199
+ `audit_for_dispatch: ${dispatchId}`,
200
+ `generated_at: ${new Date().toISOString()}`,
201
+ "---\n",
202
+ auditContent,
203
+ ].join("\n");
204
+
205
+ const packetPath = writePacket(repoRoot, filename, packetContent);
206
+ out.fileWritten("Codex audit packet", packetPath);
207
+
208
+ out.heading("Audit packet ready");
209
+ out.section("Audit dispatch", auditDispatchId);
210
+ out.section("Systems audited", audit.systems_audited.join(", ") || "baseline only");
211
+
212
+ // 8. Attempt automatic Codex transport
213
+ const transport = detectCodexTransport();
214
+ out.section("Transport", `${transport.status} — ${transport.detail}`);
215
+
216
+ if (transport.status !== "ready") {
217
+ if (transport.status === "no-cli") {
218
+ out.warn("Codex CLI not found — falling back to manual audit.");
219
+ out.info("To enable automatic audit:");
220
+ out.bullet(" npm install -g @openai/codex");
221
+ } else if (transport.status === "cli-error") {
222
+ out.warn(`Codex CLI found but not invocable: ${transport.detail}`);
223
+ } else {
224
+ out.warn(`Transport not ready: ${transport.detail}`);
225
+ }
226
+ out.bullet("Send the audit packet to Codex for independent review");
227
+ return;
228
+ }
229
+
230
+ // Transport ready — execute via Codex CLI (read-only review)
231
+ const auditTitle = `Switchboard audit for dispatch ${dispatchId} — ${contract.title}`;
232
+ out.info("Executing audit via Codex CLI (codex review --uncommitted)...");
233
+ const result = executeCodexTransport(transport.cli_path!, auditTitle, repoRoot);
234
+
235
+ // Capture audit transcript as proof and rebuild manifest
236
+ captureTranscriptProof(repoRoot, dispatchId, result.transcript, "codex-audit", result.duration_ms);
237
+ const updatedManifest = buildAndWriteProofManifest(repoRoot, dispatchId);
238
+ out.info(`Proof manifest rebuilt: ${updatedManifest.artifacts.length} artifact(s), ${updatedManifest.provenance_summary.captured_count} captured`);
239
+
240
+ if (result.success) {
241
+ out.success(`Codex audit completed (${result.duration_ms}ms)`);
242
+
243
+ // Write the Codex response alongside the packet
244
+ const responseFilename = `codex-audit-response-${auditDispatchId.slice(12)}.md`;
245
+ writePacket(repoRoot, responseFilename, [
246
+ "---",
247
+ `audit_dispatch_id: ${auditDispatchId}`,
248
+ `source_dispatch: ${dispatchId}`,
249
+ `completed_at: ${new Date().toISOString()}`,
250
+ `duration_ms: ${result.duration_ms}`,
251
+ "---\n",
252
+ "# Codex Audit Response\n",
253
+ result.transcript,
254
+ ].join("\n"));
255
+ out.fileWritten("Codex audit response", responseFilename);
256
+ out.info("Audit transcript captured as proof artifact.");
257
+ } else {
258
+ out.warn(`Codex audit exited with code ${result.exit_code} (${result.duration_ms}ms)`);
259
+ if (result.error) {
260
+ out.info(`Error: ${result.error.slice(0, 200)}`);
261
+ }
262
+ out.info("Partial transcript captured. Review the audit packet manually.");
263
+ out.bullet("Send the audit packet to Codex for independent review");
264
+ }
265
+ }));
266
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * switchboard calibrate
3
+ *
4
+ * Runs the two-layer calibration harness against the governance pipeline.
5
+ * Layer A: internal pipeline with full trace access.
6
+ * Layer B: independent black-box scorer with zero core imports.
7
+ *
8
+ * Produces an append-only calibration ledger with seeds, traces,
9
+ * verdicts, diffs, diagnosis cards, and escalation queue.
10
+ */
11
+
12
+ import type { Command } from "commander";
13
+ import { resolve } from "path";
14
+ import { runCalibration } from "../../calibration/skill/calibrate";
15
+ import * as out from "../lib/output";
16
+ import { withCliErrors } from "../lib/errors";
17
+
18
+ export function registerCalibrateCommand(program: Command): void {
19
+ program
20
+ .command("calibrate")
21
+ .description("Run two-layer calibration harness against the governance pipeline")
22
+ .option("--blind <count>", "Number of blind seeds (default: 5)")
23
+ .option("--adversarial <count>", "Number of adversarial seeds (default: 3)")
24
+ .option("--replay <count>", "Number of replay seeds (default: 2)")
25
+ .option("--adversarial-only [count]", "Run only adversarial seeds")
26
+ .option("--replay-only", "Run only replay seeds")
27
+ .option("--retest", "Re-test seeds from open diagnosis cards")
28
+ .option("--review", "Review pending escalations")
29
+ .option("--trends", "Show calibration trends over time")
30
+ .action(withCliErrors(async (opts) => {
31
+ out.heading("switchboard calibrate");
32
+
33
+ // Build args string from commander options
34
+ const args = buildArgsString(opts);
35
+
36
+ // Use the CLI package root as the project root for calibration
37
+ // (calibration ledger lives inside the package, not the user's project)
38
+ const calibrationRoot = resolve(__dirname, "..", "..");
39
+
40
+ const result = await runCalibration(args, calibrationRoot);
41
+
42
+ console.log(result.report);
43
+
44
+ if (result.warnings.length > 0) {
45
+ console.log("");
46
+ out.heading("Self-monitoring warnings");
47
+ for (const w of result.warnings) {
48
+ out.warn(w);
49
+ }
50
+ }
51
+ }));
52
+ }
53
+
54
+ function buildArgsString(opts: Record<string, unknown>): string {
55
+ const parts: string[] = [];
56
+
57
+ if (opts["blind"]) parts.push(`--blind ${opts["blind"]}`);
58
+ if (opts["adversarial"]) parts.push(`--adversarial ${opts["adversarial"]}`);
59
+ if (opts["replay"]) parts.push(`--replay ${opts["replay"]}`);
60
+ if (opts["adversarialOnly"]) {
61
+ const count = typeof opts["adversarialOnly"] === "string" ? opts["adversarialOnly"] : "";
62
+ parts.push(`--adversarial-only ${count}`.trim());
63
+ }
64
+ if (opts["replayOnly"]) parts.push("--replay-only");
65
+ if (opts["retest"]) parts.push("--retest");
66
+ if (opts["review"]) parts.push("--review");
67
+ if (opts["trends"]) parts.push("--trends");
68
+
69
+ return parts.join(" ");
70
+ }