switchboard-cli 0.1.0-alpha.4 → 0.1.0-alpha.5
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/CHANGELOG.md +19 -0
- package/README.md +121 -65
- package/bin/switchboard.js +75 -0
- package/dist/index.cjs +14371 -0
- package/package.json +45 -8
- package/bin/switchboard.mjs +0 -49
- package/src/autoagent/boundary-check.spec.ts +0 -77
- package/src/autoagent/boundary-check.ts +0 -102
- package/src/autoagent/loop.ts +0 -327
- package/src/autoagent/results.spec.ts +0 -73
- package/src/autoagent/results.ts +0 -68
- package/src/autoagent/runner.spec.ts +0 -20
- package/src/autoagent/runner.ts +0 -92
- package/src/autoagent/types.ts +0 -64
- package/src/commands/audit-codex.ts +0 -266
- package/src/commands/autoagent.ts +0 -108
- package/src/commands/calibrate.ts +0 -70
- package/src/commands/compile.ts +0 -117
- package/src/commands/evaluate.ts +0 -103
- package/src/commands/ingest.ts +0 -250
- package/src/commands/init.ts +0 -133
- package/src/commands/packet.ts +0 -408
- package/src/commands/receipt.ts +0 -336
- package/src/commands/run-claude.ts +0 -355
- package/src/index.ts +0 -47
- package/src/lib/draft-return.ts +0 -278
- package/src/lib/drift-guard.ts +0 -105
- package/src/lib/errors.ts +0 -61
- package/src/lib/output.ts +0 -43
- package/src/lib/paths.ts +0 -125
- package/src/lib/proof.ts +0 -262
- package/src/lib/transport.ts +0 -276
- package/src/lib/yaml-io.ts +0 -62
- package/src/store/filesystem-store.ts +0 -326
package/src/commands/receipt.ts
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* switchboard receipt
|
|
3
|
-
*
|
|
4
|
-
* Builds the immutable receipt artifact from the latest ingested run.
|
|
5
|
-
* Writes both YAML and JSON under .switchboard/receipts/.
|
|
6
|
-
*
|
|
7
|
-
* CRITICAL DESIGN: This command loads the exact persisted ingest artifacts
|
|
8
|
-
* (envelope, delta, gate, reconcile, audit, next-slice) by ID from
|
|
9
|
-
* .switchboard/returns/. It does NOT re-derive them. The receipt must
|
|
10
|
-
* reflect the exact governed ingest result, not an approximation.
|
|
11
|
-
*
|
|
12
|
-
* Uses ReceiptIssuedV2 (the immutable trust artifact) and AuditedReceipt
|
|
13
|
-
* (the evaluation-lane input format) from @switchboard/core.
|
|
14
|
-
*
|
|
15
|
-
* Does not require the hosted web app or Supabase.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { writeFileSync, existsSync, readdirSync, readFileSync } from "fs";
|
|
19
|
-
import { join } from "path";
|
|
20
|
-
import type { Command } from "commander";
|
|
21
|
-
import {
|
|
22
|
-
buildReceiptIssuedV2,
|
|
23
|
-
buildAuditedReceipt,
|
|
24
|
-
formatReceiptForExport,
|
|
25
|
-
type NormalizedReturnEnvelope,
|
|
26
|
-
type CanonicalDelta,
|
|
27
|
-
type GateState,
|
|
28
|
-
type AuditResult,
|
|
29
|
-
type NextSlice,
|
|
30
|
-
type ReconcileReport,
|
|
31
|
-
type HarnessCompilationSummary,
|
|
32
|
-
type ReceiptIssuedV2,
|
|
33
|
-
type ProofManifest,
|
|
34
|
-
extractHarnessProvenance,
|
|
35
|
-
} from "@switchboard/core";
|
|
36
|
-
import { requireRepoRoot, sbDir, returnsDir, receiptsDir, packetsDir } from "../lib/paths";
|
|
37
|
-
import { readYaml } from "../lib/yaml-io";
|
|
38
|
-
import { loadPortableProofManifest } from "../lib/proof";
|
|
39
|
-
import {
|
|
40
|
-
loadContract,
|
|
41
|
-
loadHandoff,
|
|
42
|
-
loadLatestHandoff,
|
|
43
|
-
loadDispatchMeta,
|
|
44
|
-
saveReceiptV2,
|
|
45
|
-
saveReceiptJson,
|
|
46
|
-
saveAuditedReceipt,
|
|
47
|
-
} from "../store/filesystem-store";
|
|
48
|
-
import * as out from "../lib/output";
|
|
49
|
-
import { withCliErrors } from "../lib/errors";
|
|
50
|
-
|
|
51
|
-
interface IngestPointer {
|
|
52
|
-
dispatch_id: string;
|
|
53
|
-
envelope_id: string;
|
|
54
|
-
delta_id: string;
|
|
55
|
-
gate_label: string;
|
|
56
|
-
audit_id: string;
|
|
57
|
-
next_slice_id: string;
|
|
58
|
-
has_reconcile: boolean;
|
|
59
|
-
has_next_slice: boolean;
|
|
60
|
-
has_proof_manifest?: boolean;
|
|
61
|
-
proof_manifest_id?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function registerReceiptCommand(program: Command): void {
|
|
65
|
-
program
|
|
66
|
-
.command("receipt")
|
|
67
|
-
.description("Build immutable receipt from latest ingested run")
|
|
68
|
-
.option("--export <format>", "Export receipt as standalone file (json or yaml)")
|
|
69
|
-
.action(withCliErrors(async (opts: { export?: string }) => {
|
|
70
|
-
const repoRoot = requireRepoRoot();
|
|
71
|
-
|
|
72
|
-
out.heading("switchboard receipt");
|
|
73
|
-
|
|
74
|
-
// 1. Load contract
|
|
75
|
-
const contract = loadContract(repoRoot);
|
|
76
|
-
|
|
77
|
-
// 2. Load ingest pointer — tells us exactly which artifacts to load
|
|
78
|
-
const pointerPath = join(sbDir(repoRoot), "working", "latest-ingest.yaml");
|
|
79
|
-
const pointer = readYaml<IngestPointer>(pointerPath);
|
|
80
|
-
|
|
81
|
-
if (!pointer) {
|
|
82
|
-
out.fail("No ingest data found. Run `switchboard ingest` first.");
|
|
83
|
-
process.exitCode = 1;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
out.success(`Loaded ingest pointer for dispatch: ${pointer.dispatch_id}`);
|
|
88
|
-
|
|
89
|
-
// 2b. Check if Codex audit has been run for this dispatch
|
|
90
|
-
const codexAuditRan = hasCodexAuditForDispatch(repoRoot, pointer.dispatch_id);
|
|
91
|
-
if (!codexAuditRan) {
|
|
92
|
-
out.warn("No Codex audit found for this dispatch.");
|
|
93
|
-
out.warn("Run `sb audit codex` before issuing a receipt for governed projects.");
|
|
94
|
-
out.info("Proceeding without audit — receipt will reflect missing independent verification.");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 3. Load the exact persisted artifacts by ID — do NOT re-derive
|
|
98
|
-
const retDir = returnsDir(repoRoot);
|
|
99
|
-
|
|
100
|
-
const envelope = readYaml<NormalizedReturnEnvelope>(join(retDir, `${pointer.envelope_id}.yaml`));
|
|
101
|
-
if (!envelope) {
|
|
102
|
-
out.fail(`Cannot load persisted envelope: ${pointer.envelope_id}`);
|
|
103
|
-
process.exitCode = 1;
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const delta = readYaml<CanonicalDelta>(join(retDir, `${pointer.delta_id}.yaml`));
|
|
108
|
-
if (!delta) {
|
|
109
|
-
out.fail(`Cannot load persisted delta: ${pointer.delta_id}`);
|
|
110
|
-
process.exitCode = 1;
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const gate = readYaml<GateState>(join(retDir, `gate-${pointer.dispatch_id}.yaml`));
|
|
115
|
-
if (!gate) {
|
|
116
|
-
out.fail(`Cannot load persisted gate state for dispatch: ${pointer.dispatch_id}`);
|
|
117
|
-
process.exitCode = 1;
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const audit = readYaml<AuditResult>(join(retDir, `audit-${pointer.dispatch_id}.yaml`));
|
|
122
|
-
|
|
123
|
-
const nextSlice = pointer.has_next_slice
|
|
124
|
-
? readYaml<NextSlice>(join(retDir, `next-slice-${pointer.dispatch_id}.yaml`))
|
|
125
|
-
: null;
|
|
126
|
-
|
|
127
|
-
out.success(`Loaded persisted artifacts: envelope=${pointer.envelope_id}, delta=${pointer.delta_id}, gate=${pointer.gate_label}`);
|
|
128
|
-
|
|
129
|
-
// 4. Load proof manifest if available
|
|
130
|
-
const proofManifest = pointer.has_proof_manifest
|
|
131
|
-
? loadPortableProofManifest(repoRoot, pointer.dispatch_id)
|
|
132
|
-
: null;
|
|
133
|
-
|
|
134
|
-
if (proofManifest) {
|
|
135
|
-
out.success(`Loaded proof manifest: ${proofManifest.manifest_id} (${proofManifest.artifacts.length} artifact(s))`);
|
|
136
|
-
} else {
|
|
137
|
-
out.info("No proof manifest available — receipt will be honest about provenance gaps.");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 5. Load harness provenance from THIS dispatch's metadata (not "latest")
|
|
141
|
-
const dispatchMeta = loadDispatchMeta(repoRoot, pointer.dispatch_id);
|
|
142
|
-
let harnessProvenance = extractHarnessProvenance(null);
|
|
143
|
-
|
|
144
|
-
if (dispatchMeta && (dispatchMeta as any).harness_compilation_summary) {
|
|
145
|
-
harnessProvenance = extractHarnessProvenance(
|
|
146
|
-
(dispatchMeta as any).harness_compilation_summary as HarnessCompilationSummary,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 6. Build ReceiptIssuedV2 from exact persisted artifacts
|
|
151
|
-
out.info("Building ReceiptIssuedV2 from persisted ingest truth...");
|
|
152
|
-
|
|
153
|
-
const receiptV2 = buildReceiptIssuedV2({
|
|
154
|
-
envelope,
|
|
155
|
-
delta,
|
|
156
|
-
gate,
|
|
157
|
-
nextSlice,
|
|
158
|
-
projectId: contract.project_id,
|
|
159
|
-
audit,
|
|
160
|
-
proofManifest,
|
|
161
|
-
harnessProvenance: harnessProvenance.harness_ran ? harnessProvenance : null,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const v2Path = saveReceiptV2(repoRoot, receiptV2);
|
|
165
|
-
const v2JsonPath = saveReceiptJson(repoRoot, receiptV2);
|
|
166
|
-
out.fileWritten("Receipt V2 (YAML)", v2Path);
|
|
167
|
-
out.fileWritten("Receipt V2 (JSON)", v2JsonPath);
|
|
168
|
-
|
|
169
|
-
// 6. Build AuditedReceipt (V1 format for evaluation lane)
|
|
170
|
-
out.info("Building AuditedReceipt for evaluation lane...");
|
|
171
|
-
|
|
172
|
-
const auditedReceipt = buildAuditedReceipt({
|
|
173
|
-
envelope,
|
|
174
|
-
delta,
|
|
175
|
-
gate,
|
|
176
|
-
nextSlice,
|
|
177
|
-
projectId: contract.project_id,
|
|
178
|
-
audit,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const auditedPath = saveAuditedReceipt(repoRoot, auditedReceipt);
|
|
182
|
-
out.fileWritten("Audited receipt", auditedPath);
|
|
183
|
-
|
|
184
|
-
// Generate human-readable summary
|
|
185
|
-
const summaryText = generateReceiptSummary(receiptV2, contract.title, proofManifest, dispatchMeta);
|
|
186
|
-
|
|
187
|
-
// Write summary file alongside receipt
|
|
188
|
-
const summaryPath = join(receiptsDir(repoRoot), `${receiptV2.receipt_id}-summary.txt`);
|
|
189
|
-
writeFileSync(summaryPath, summaryText, "utf-8");
|
|
190
|
-
out.fileWritten("Receipt summary", summaryPath);
|
|
191
|
-
|
|
192
|
-
// Print the summary block to terminal
|
|
193
|
-
console.log("");
|
|
194
|
-
console.log(summaryText);
|
|
195
|
-
|
|
196
|
-
// Export if requested
|
|
197
|
-
if (opts.export) {
|
|
198
|
-
const format = opts.export.toLowerCase();
|
|
199
|
-
if (format !== "json" && format !== "yaml") {
|
|
200
|
-
out.fail(`Unknown export format: ${format}. Use 'json' or 'yaml'.`);
|
|
201
|
-
process.exitCode = 1;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const exported = formatReceiptForExport(receiptV2, []);
|
|
206
|
-
const exportPath = join(
|
|
207
|
-
receiptsDir(repoRoot),
|
|
208
|
-
`${receiptV2.receipt_id}-export.${format}`,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
if (format === "json") {
|
|
212
|
-
writeFileSync(exportPath, JSON.stringify(exported, null, 2), "utf-8");
|
|
213
|
-
} else {
|
|
214
|
-
const YAML = await import("yaml");
|
|
215
|
-
writeFileSync(exportPath, YAML.stringify(exported), "utf-8");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
out.fileWritten(`Exported receipt (${format})`, exportPath);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
out.bullet("Run `sb evaluate` to run independent evaluation");
|
|
222
|
-
}));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// Human-readable receipt summary
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
function generateReceiptSummary(
|
|
230
|
-
receipt: ReceiptIssuedV2,
|
|
231
|
-
projectTitle: string,
|
|
232
|
-
proofManifest: ProofManifest | null,
|
|
233
|
-
dispatchMeta: any,
|
|
234
|
-
): string {
|
|
235
|
-
const lines: string[] = [];
|
|
236
|
-
const divider = "=".repeat(60);
|
|
237
|
-
|
|
238
|
-
lines.push(divider);
|
|
239
|
-
lines.push(" SWITCHBOARD RECEIPT");
|
|
240
|
-
lines.push(divider);
|
|
241
|
-
lines.push("");
|
|
242
|
-
lines.push(` Project: ${projectTitle}`);
|
|
243
|
-
lines.push(` Receipt: ${receipt.receipt_id}`);
|
|
244
|
-
lines.push(` Dispatch: ${receipt.dispatch_id || "unknown"}`);
|
|
245
|
-
lines.push(` Surface: ${dispatchMeta?.surface ?? "unknown"}`);
|
|
246
|
-
lines.push(` Issued: ${receipt.issued_at}`);
|
|
247
|
-
lines.push("");
|
|
248
|
-
|
|
249
|
-
// Trust state — the 30-second answer
|
|
250
|
-
lines.push(` VERDICT: ${receipt.issuance_assessment.verdict.toUpperCase()}`);
|
|
251
|
-
lines.push(` Trust: ${receipt.trust_posture}`);
|
|
252
|
-
lines.push(` Gate: ${receipt.gate_label}`);
|
|
253
|
-
lines.push(` Closure: ${receipt.closure_allowed ? "allowed" : "BLOCKED"}`);
|
|
254
|
-
lines.push("");
|
|
255
|
-
|
|
256
|
-
// Claims — use top-level claim_assessments (V2 field, not inside issuance_assessment)
|
|
257
|
-
const claims = receipt.claim_assessments ?? [];
|
|
258
|
-
const claimCount = claims.length;
|
|
259
|
-
const supported = claims.filter(
|
|
260
|
-
(c: any) => c.disposition === "proven" || c.disposition === "supported"
|
|
261
|
-
).length;
|
|
262
|
-
const disputed = claims.filter(
|
|
263
|
-
(c: any) => c.disposition === "unsubstantiated"
|
|
264
|
-
).length;
|
|
265
|
-
const partial = claims.filter(
|
|
266
|
-
(c: any) => c.disposition === "partial"
|
|
267
|
-
).length;
|
|
268
|
-
|
|
269
|
-
lines.push(` Claims: ${claimCount} evaluated`);
|
|
270
|
-
if (supported > 0) lines.push(` ${supported} supported`);
|
|
271
|
-
if (partial > 0) lines.push(` ${partial} partially evidenced`);
|
|
272
|
-
if (disputed > 0) lines.push(` ${disputed} disputed`);
|
|
273
|
-
lines.push("");
|
|
274
|
-
|
|
275
|
-
// Evidence — use V2 structured arrays
|
|
276
|
-
const evidenceCount = receipt.evidence_items?.length ?? 0;
|
|
277
|
-
const verificationCount = receipt.verification_results?.length ?? 0;
|
|
278
|
-
const findingCount = receipt.findings?.length ?? 0;
|
|
279
|
-
lines.push(` Evidence: ${evidenceCount} item(s)`);
|
|
280
|
-
lines.push(` Verified: ${verificationCount} result(s)`);
|
|
281
|
-
lines.push(` Findings: ${findingCount}`);
|
|
282
|
-
lines.push("");
|
|
283
|
-
|
|
284
|
-
// Proof chain
|
|
285
|
-
if (proofManifest) {
|
|
286
|
-
const ps = proofManifest.provenance_summary;
|
|
287
|
-
lines.push(` Proof: ${proofManifest.artifacts.length} artifact(s) (${ps.captured_count} captured, ${ps.derived_count} derived)`);
|
|
288
|
-
lines.push(` Provenance: ${receipt.provenance_posture}`);
|
|
289
|
-
} else {
|
|
290
|
-
lines.push(" Proof: no proof manifest available");
|
|
291
|
-
lines.push(" Provenance: unknown");
|
|
292
|
-
}
|
|
293
|
-
lines.push("");
|
|
294
|
-
|
|
295
|
-
// Open items
|
|
296
|
-
if (receipt.residual_count > 0) {
|
|
297
|
-
lines.push(` Open items: ${receipt.residual_count} residual(s)`);
|
|
298
|
-
for (const c of (receipt.cautions_on_record ?? []).slice(0, 3)) {
|
|
299
|
-
lines.push(` - ${c.slice(0, 80)}`);
|
|
300
|
-
}
|
|
301
|
-
} else {
|
|
302
|
-
lines.push(" Open items: none");
|
|
303
|
-
}
|
|
304
|
-
lines.push("");
|
|
305
|
-
|
|
306
|
-
// Summary
|
|
307
|
-
lines.push(" Summary:");
|
|
308
|
-
lines.push(` ${receipt.summary_claimed.slice(0, 200)}`);
|
|
309
|
-
lines.push("");
|
|
310
|
-
lines.push(divider);
|
|
311
|
-
|
|
312
|
-
return lines.join("\n");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// Codex audit check — verifies independent audit was run before receipt
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
|
|
319
|
-
function hasCodexAuditForDispatch(repoRoot: string, dispatchId: string): boolean {
|
|
320
|
-
const pDir = packetsDir(repoRoot);
|
|
321
|
-
if (!existsSync(pDir)) return false;
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const files = readdirSync(pDir).filter(f => f.startsWith("codex-audit-response"));
|
|
325
|
-
for (const file of files) {
|
|
326
|
-
const content = readFileSync(join(pDir, file), "utf-8");
|
|
327
|
-
if (content.includes(dispatchId)) {
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
} catch {
|
|
332
|
-
// If we can't read the directory, assume no audit
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* switchboard run claude
|
|
3
|
-
*
|
|
4
|
-
* Dispatches a governed packet to Claude Code via the Anthropic Agent SDK.
|
|
5
|
-
* When the SDK/key is not available, fails honestly with actionable guidance.
|
|
6
|
-
*
|
|
7
|
-
* Saves dispatch metadata (handoff, dispatch receipt, prompt) into
|
|
8
|
-
* .switchboard/ so that ingest, receipt, and evaluation can reference it.
|
|
9
|
-
*
|
|
10
|
-
* Does not require the hosted web app or Supabase.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { randomBytes } from "crypto";
|
|
14
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
15
|
-
import { join } from "path";
|
|
16
|
-
import type { Command } from "commander";
|
|
17
|
-
import YAML from "yaml";
|
|
18
|
-
import {
|
|
19
|
-
compileExecutionHarness,
|
|
20
|
-
compileLoopContract,
|
|
21
|
-
compileMetaHarness,
|
|
22
|
-
augmentPromptWithHarnessContext,
|
|
23
|
-
validateDispatchPacket,
|
|
24
|
-
createDispatchReceipt,
|
|
25
|
-
commitReceipt,
|
|
26
|
-
handoffRecordSchema,
|
|
27
|
-
getSurfacePlaybook,
|
|
28
|
-
createReturnTemplate,
|
|
29
|
-
type Surface,
|
|
30
|
-
type HandoffRecord,
|
|
31
|
-
} from "@switchboard/core";
|
|
32
|
-
import { generateClaudeRuntimeProjection } from "@switchboard/projections";
|
|
33
|
-
import { buildForcedSurfaceRoute } from "./packet";
|
|
34
|
-
import { requireRepoRoot, packetsDir, returnsDir, returnTemplatePath } from "../lib/paths";
|
|
35
|
-
import { captureDispatchProof, capturePromptProof, captureTranscriptProof, loadPortableProofManifest, buildAndWriteProofManifest } from "../lib/proof";
|
|
36
|
-
import { detectClaudeTransport, executeClaudeTransport } from "../lib/transport";
|
|
37
|
-
import { generateDraftReturn, serializeDraftReturn } from "../lib/draft-return";
|
|
38
|
-
import {
|
|
39
|
-
loadContract,
|
|
40
|
-
loadCurrentState,
|
|
41
|
-
loadSpec,
|
|
42
|
-
specExists as checkSpecExists,
|
|
43
|
-
writeCurrentState,
|
|
44
|
-
saveHandoff,
|
|
45
|
-
saveDispatchMeta,
|
|
46
|
-
} from "../store/filesystem-store";
|
|
47
|
-
import * as out from "../lib/output";
|
|
48
|
-
import { withCliErrors } from "../lib/errors";
|
|
49
|
-
import { enforceDriftGuard } from "../lib/drift-guard";
|
|
50
|
-
|
|
51
|
-
// Build the dispatch prompt (same logic as packet command)
|
|
52
|
-
function buildDispatchPrompt(spec: string, loop: { objective: string; scope_in: string[]; scope_out: string[]; done_when: string[]; success_criteria: string[]; verify_with: string[]; return_when: string; blocker_escalation: string[] }): string {
|
|
53
|
-
const sections: string[] = [];
|
|
54
|
-
sections.push("# Execution Brief\n");
|
|
55
|
-
sections.push(spec.trim());
|
|
56
|
-
sections.push("\n---\n");
|
|
57
|
-
sections.push("## Objective\n");
|
|
58
|
-
sections.push(loop.objective);
|
|
59
|
-
sections.push("\n## Scope In\n");
|
|
60
|
-
sections.push(loop.scope_in.map(s => `- ${s}`).join("\n"));
|
|
61
|
-
sections.push("\n## Scope Out\n");
|
|
62
|
-
sections.push(loop.scope_out.map(s => `- ${s}`).join("\n"));
|
|
63
|
-
sections.push("\n## Done When\n");
|
|
64
|
-
sections.push(loop.done_when.map(s => `- ${s}`).join("\n"));
|
|
65
|
-
sections.push("\n## Success Criteria\n");
|
|
66
|
-
sections.push(loop.success_criteria.map(s => `- ${s}`).join("\n"));
|
|
67
|
-
sections.push("\n## Verify With\n");
|
|
68
|
-
sections.push(loop.verify_with.map(s => `- ${s}`).join("\n"));
|
|
69
|
-
sections.push("\n## Return When\n");
|
|
70
|
-
sections.push(loop.return_when);
|
|
71
|
-
sections.push("\n## Blocker Escalation\n");
|
|
72
|
-
sections.push(loop.blocker_escalation.map(s => `- ${s}`).join("\n"));
|
|
73
|
-
return sections.join("\n");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function registerRunClaudeCommand(program: Command): void {
|
|
77
|
-
program
|
|
78
|
-
.command("run")
|
|
79
|
-
.argument("<surface>", "Target surface: claude")
|
|
80
|
-
.description("Dispatch a governed packet to an execution surface")
|
|
81
|
-
.option("--dry-run", "Prepare dispatch metadata without executing")
|
|
82
|
-
.option("--force", "Proceed even if drift detected between loop.yaml and current.yaml")
|
|
83
|
-
.action(withCliErrors(async (surfaceArg: string, opts: { dryRun?: boolean; force?: boolean }) => {
|
|
84
|
-
if (surfaceArg !== "claude" && surfaceArg !== "claude-code") {
|
|
85
|
-
out.fail(`Wave 2 only supports 'claude'. Got: "${surfaceArg}"`);
|
|
86
|
-
out.info("Other surfaces: use `switchboard packet <surface>` for manual dispatch.");
|
|
87
|
-
process.exitCode = 1;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const repoRoot = requireRepoRoot();
|
|
92
|
-
const surface: Surface = "claude-code";
|
|
93
|
-
|
|
94
|
-
out.heading("switchboard run claude");
|
|
95
|
-
|
|
96
|
-
// 1. Load canonical state
|
|
97
|
-
const contract = loadContract(repoRoot);
|
|
98
|
-
const current = loadCurrentState(repoRoot);
|
|
99
|
-
const spec = loadSpec(repoRoot) ?? "";
|
|
100
|
-
|
|
101
|
-
out.success(`Loaded contract: ${contract.title}`);
|
|
102
|
-
|
|
103
|
-
// 1b. Drift guard — blocks on mismatch unless explicit --force
|
|
104
|
-
if (!enforceDriftGuard(repoRoot, current, !!opts.force)) {
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 2. Compile loop for claude-code
|
|
110
|
-
const route = buildForcedSurfaceRoute(surface, current.next_objective);
|
|
111
|
-
const loop = compileLoopContract({ contract, current, route });
|
|
112
|
-
|
|
113
|
-
// 3. Generate dispatch ID and compile harness
|
|
114
|
-
const dispatchId = `dsp-${randomBytes(8).toString("hex")}`;
|
|
115
|
-
|
|
116
|
-
const harness = compileExecutionHarness({
|
|
117
|
-
dispatch_id: dispatchId,
|
|
118
|
-
contract,
|
|
119
|
-
loop,
|
|
120
|
-
route,
|
|
121
|
-
working_directory: repoRoot,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// 4. Meta-harness (backstage)
|
|
125
|
-
const metaResult = compileMetaHarness({
|
|
126
|
-
contract,
|
|
127
|
-
loop,
|
|
128
|
-
dispatchId,
|
|
129
|
-
specMarkdown: spec,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// 5. Build prompt
|
|
133
|
-
let prompt = buildDispatchPrompt(spec, loop);
|
|
134
|
-
prompt = augmentPromptWithHarnessContext(prompt, metaResult.contextSections);
|
|
135
|
-
|
|
136
|
-
// 6. Validate packet integrity
|
|
137
|
-
const integrity = validateDispatchPacket({
|
|
138
|
-
contract,
|
|
139
|
-
loop,
|
|
140
|
-
prompt,
|
|
141
|
-
specMarkdown: spec,
|
|
142
|
-
specExists: checkSpecExists(repoRoot),
|
|
143
|
-
repoExists: contract.repo_status === "repo-present",
|
|
144
|
-
hasActiveHandoff: !!current.active_handoff_id,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (integrity.outcome === "blocked") {
|
|
148
|
-
out.fail(`Packet integrity: BLOCKED (${integrity.blocking_findings_count} blocking finding(s))`);
|
|
149
|
-
for (const f of integrity.findings.filter(f => f.severity === "blocking")) {
|
|
150
|
-
out.bullet(`${f.field}: ${f.detail}`);
|
|
151
|
-
}
|
|
152
|
-
process.exitCode = 1;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 7. Create handoff record
|
|
157
|
-
const handoff: HandoffRecord = handoffRecordSchema.parse({
|
|
158
|
-
handoff_id: dispatchId,
|
|
159
|
-
surface,
|
|
160
|
-
objective: loop.objective,
|
|
161
|
-
included_scope: loop.scope_in,
|
|
162
|
-
excluded_scope: loop.scope_out,
|
|
163
|
-
done_when: loop.done_when,
|
|
164
|
-
return_condition: loop.return_when,
|
|
165
|
-
loop,
|
|
166
|
-
readiness: { status: "ready" as const, issues: [] },
|
|
167
|
-
bundle_version: "SBX-v1" as const,
|
|
168
|
-
launched_at: new Date().toISOString(),
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
saveHandoff(repoRoot, handoff);
|
|
172
|
-
out.success(`Saved handoff: ${dispatchId}`);
|
|
173
|
-
|
|
174
|
-
// 8. Create dispatch receipt
|
|
175
|
-
const playbook = getSurfacePlaybook(surface);
|
|
176
|
-
const dispatchReceipt = commitReceipt(
|
|
177
|
-
createDispatchReceipt({
|
|
178
|
-
dispatchId,
|
|
179
|
-
contract,
|
|
180
|
-
loop,
|
|
181
|
-
prompt,
|
|
182
|
-
specMarkdown: spec,
|
|
183
|
-
surface,
|
|
184
|
-
surfaceLabel: "Claude Code",
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// 9. Capture proof artifacts at dispatch time
|
|
189
|
-
captureDispatchProof(repoRoot, dispatchId, {
|
|
190
|
-
surface,
|
|
191
|
-
prompt,
|
|
192
|
-
packetHash: dispatchReceipt.packet_hash,
|
|
193
|
-
promptHash: dispatchReceipt.prompt_hash,
|
|
194
|
-
specMarkdown: spec,
|
|
195
|
-
});
|
|
196
|
-
capturePromptProof(repoRoot, dispatchId, prompt);
|
|
197
|
-
|
|
198
|
-
// 10. Save dispatch metadata for ingest + receipt
|
|
199
|
-
// Include harness compilation summary so receipt can carry provenance
|
|
200
|
-
saveDispatchMeta(repoRoot, {
|
|
201
|
-
dispatch_id: dispatchId,
|
|
202
|
-
handoff_id: dispatchId,
|
|
203
|
-
surface,
|
|
204
|
-
prompt,
|
|
205
|
-
spec_markdown: spec,
|
|
206
|
-
dispatched_at: new Date().toISOString(),
|
|
207
|
-
dispatch_receipt: dispatchReceipt,
|
|
208
|
-
harness_compilation_summary: metaResult.summary,
|
|
209
|
-
} as any);
|
|
210
|
-
|
|
211
|
-
// 10. Update current state with active handoff
|
|
212
|
-
const updated = { ...current } as Record<string, unknown>;
|
|
213
|
-
updated["active_handoff_id"] = dispatchId;
|
|
214
|
-
updated["stage"] = "dispatched";
|
|
215
|
-
writeCurrentState(repoRoot, updated as any);
|
|
216
|
-
|
|
217
|
-
out.section("Dispatch ID", dispatchId);
|
|
218
|
-
out.section("Surface", surface);
|
|
219
|
-
out.section("Integrity", integrity.outcome);
|
|
220
|
-
|
|
221
|
-
// 11. Write dispatch-specific return template and projection
|
|
222
|
-
// These are bound to THIS dispatch ID so the operator cannot
|
|
223
|
-
// accidentally use artifacts from a different dispatch.
|
|
224
|
-
|
|
225
|
-
// Write per-dispatch return template under .switchboard/returns/
|
|
226
|
-
// This is the canonical template path. Repo-root SB_RETURN.yaml is a convenience copy.
|
|
227
|
-
const returnTemplate = createReturnTemplate(handoff);
|
|
228
|
-
const retDir = returnsDir(repoRoot);
|
|
229
|
-
if (!existsSync(retDir)) mkdirSync(retDir, { recursive: true });
|
|
230
|
-
|
|
231
|
-
const canonicalTemplatePath = returnTemplatePath(repoRoot, dispatchId);
|
|
232
|
-
const returnYamlContent = YAML.stringify(returnTemplate);
|
|
233
|
-
writeFileSync(canonicalTemplatePath, returnYamlContent, "utf-8");
|
|
234
|
-
out.fileWritten("Return template", canonicalTemplatePath);
|
|
235
|
-
|
|
236
|
-
// Write dispatch-specific -return.yaml (the file ingest looks for first).
|
|
237
|
-
// This starts as an identical copy of the template; the operator fills it
|
|
238
|
-
// in after execution, then `ingest` finds it by dispatch ID.
|
|
239
|
-
const dispatchReturnPath = join(retDir, `${dispatchId}-return.yaml`);
|
|
240
|
-
writeFileSync(dispatchReturnPath, returnYamlContent, "utf-8");
|
|
241
|
-
out.fileWritten("Return working copy", dispatchReturnPath);
|
|
242
|
-
|
|
243
|
-
// Convenience copy at repo root for easy operator access
|
|
244
|
-
const repoRootTemplatePath = join(repoRoot, "SB_RETURN.yaml");
|
|
245
|
-
writeFileSync(repoRootTemplatePath, returnYamlContent, "utf-8");
|
|
246
|
-
out.fileWritten("Return convenience copy", repoRootTemplatePath);
|
|
247
|
-
|
|
248
|
-
// Write dispatch-specific Claude projection
|
|
249
|
-
try {
|
|
250
|
-
const playbook = getSurfacePlaybook(surface);
|
|
251
|
-
const projection = generateClaudeRuntimeProjection({
|
|
252
|
-
contract,
|
|
253
|
-
current,
|
|
254
|
-
spec_markdown: spec,
|
|
255
|
-
handoff,
|
|
256
|
-
playbook,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const projDir = join(packetsDir(repoRoot), `claude-projection-${dispatchId.slice(4, 12)}`);
|
|
260
|
-
if (!existsSync(projDir)) mkdirSync(projDir, { recursive: true });
|
|
261
|
-
for (const file of projection.files) {
|
|
262
|
-
const filePath = join(projDir, file.path);
|
|
263
|
-
const fileDir = join(projDir, file.path.split("/").slice(0, -1).join("/"));
|
|
264
|
-
if (fileDir !== projDir && !existsSync(fileDir)) {
|
|
265
|
-
mkdirSync(fileDir, { recursive: true });
|
|
266
|
-
}
|
|
267
|
-
writeFileSync(filePath, file.content, "utf-8");
|
|
268
|
-
}
|
|
269
|
-
out.fileWritten("Claude projection (bound to this dispatch)", projDir);
|
|
270
|
-
} catch (e) {
|
|
271
|
-
out.warn(`Could not generate Claude projection: ${e instanceof Error ? e.message : String(e)}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (opts.dryRun) {
|
|
275
|
-
out.warn("Dry run — dispatch metadata saved but execution skipped.");
|
|
276
|
-
out.info("Use `switchboard ingest` after manual execution to close the loop.");
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 12. Attempt automatic Claude Code transport
|
|
281
|
-
const transport = detectClaudeTransport();
|
|
282
|
-
out.section("Transport", `${transport.status} — ${transport.detail}`);
|
|
283
|
-
out.section("Capability", `${transport.capability} — ${transport.capability_detail}`);
|
|
284
|
-
|
|
285
|
-
if (transport.status !== "ready") {
|
|
286
|
-
// Honest fallback — explain exactly why automatic transport isn't available
|
|
287
|
-
if (transport.status === "no-cli") {
|
|
288
|
-
out.warn("Claude Code CLI not found — falling back to manual dispatch.");
|
|
289
|
-
out.info("To enable automatic execution:");
|
|
290
|
-
out.bullet(" npm install -g @anthropic-ai/claude-code");
|
|
291
|
-
} else if (transport.status === "no-auth") {
|
|
292
|
-
out.warn("Claude Code CLI found but not authenticated.");
|
|
293
|
-
out.info("Run `claude` once to complete authentication, then retry.");
|
|
294
|
-
} else {
|
|
295
|
-
out.warn(`Transport not ready: ${transport.detail}`);
|
|
296
|
-
}
|
|
297
|
-
out.info("");
|
|
298
|
-
out.info("Dispatch metadata + return template saved. To complete the loop:");
|
|
299
|
-
out.bullet("1. Use the Claude projection just written (bound to this dispatch)");
|
|
300
|
-
out.bullet("2. Execute manually in Claude Code");
|
|
301
|
-
out.bullet("3. Fill in SB_RETURN.yaml (pre-bound to this dispatch) and run `switchboard ingest`");
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Transport ready — execute via Claude Code CLI
|
|
306
|
-
out.info("Executing governed prompt via Claude Code...");
|
|
307
|
-
const result = executeClaudeTransport(transport.cli_path!, prompt, repoRoot);
|
|
308
|
-
|
|
309
|
-
// Capture the execution transcript as proof regardless of success/failure
|
|
310
|
-
captureTranscriptProof(repoRoot, dispatchId, result.transcript, "claude-code", result.duration_ms);
|
|
311
|
-
|
|
312
|
-
// Rebuild proof manifest after transcript capture
|
|
313
|
-
const updatedManifest = buildAndWriteProofManifest(repoRoot, dispatchId);
|
|
314
|
-
|
|
315
|
-
if (result.success) {
|
|
316
|
-
out.success(`Claude Code execution completed (${result.duration_ms}ms)`);
|
|
317
|
-
} else {
|
|
318
|
-
out.warn(`Claude Code execution exited with code ${result.exit_code} (${result.duration_ms}ms)`);
|
|
319
|
-
if (result.error) {
|
|
320
|
-
out.info(`Error: ${result.error.slice(0, 200)}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
out.info(`Proof: ${updatedManifest.artifacts.length} artifact(s) captured.`);
|
|
325
|
-
|
|
326
|
-
// Generate draft return from execution evidence
|
|
327
|
-
const draftResult = generateDraftReturn({
|
|
328
|
-
handoff,
|
|
329
|
-
transcript: result.transcript,
|
|
330
|
-
proofManifest: updatedManifest,
|
|
331
|
-
repoRoot,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const draftYaml = serializeDraftReturn(draftResult);
|
|
335
|
-
|
|
336
|
-
// Write draft to dispatch-specific path and convenience copy
|
|
337
|
-
const draftPath = join(returnsDir(repoRoot), `${dispatchId}-return.yaml`);
|
|
338
|
-
writeFileSync(draftPath, draftYaml, "utf-8");
|
|
339
|
-
out.fileWritten("Draft return", draftPath);
|
|
340
|
-
|
|
341
|
-
writeFileSync(join(repoRoot, "SB_RETURN.yaml"), draftYaml, "utf-8");
|
|
342
|
-
out.fileWritten("Draft return (convenience)", "SB_RETURN.yaml");
|
|
343
|
-
|
|
344
|
-
if (draftResult.warnings.length > 0) {
|
|
345
|
-
out.warn(`${draftResult.warnings.length} warning(s):`);
|
|
346
|
-
for (const w of draftResult.warnings) {
|
|
347
|
-
out.bullet(w);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
out.info("");
|
|
352
|
-
out.info("A draft return has been generated from execution evidence.");
|
|
353
|
-
out.info("Review SB_RETURN.yaml, then run `switchboard ingest` to close the loop.");
|
|
354
|
-
}));
|
|
355
|
-
}
|