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.
@@ -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
- }