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,408 +0,0 @@
1
- /**
2
- * switchboard packet <surface>
3
- *
4
- * Compiles an execution harness from canonical state, runs the meta-harness
5
- * compile pass automatically when eligible, validates packet integrity,
6
- * and writes the formatted dispatch packet to .switchboard/packets/.
7
- *
8
- * Supports all four V1 surfaces: claude, chatgpt, cursor, codex.
9
- *
10
- * Uses the best existing output shape for each surface:
11
- * - claude-code: sdk_prompt packet (written as markdown dispatch file)
12
- * - chatgpt: sdk_prompt packet (written as markdown dispatch file)
13
- * - cursor: clipboard packet (paste-ready markdown with operating notes)
14
- * - codex: sdk_prompt packet (written as markdown dispatch file)
15
- *
16
- * Additionally generates a Claude runtime projection when targeting claude-code,
17
- * and an SBX bundle for cursor.
18
- *
19
- * Does not require the hosted web app, Supabase, or a running server.
20
- */
21
-
22
- import { randomBytes } from "crypto";
23
- import { existsSync, writeFileSync, mkdirSync } from "fs";
24
- import { join } from "path";
25
- import type { Command } from "commander";
26
- import {
27
- compileExecutionHarness,
28
- compileMetaHarness,
29
- augmentPromptWithHarnessContext,
30
- validateDispatchPacket,
31
- formatFromHarness,
32
- compileLoopContract,
33
- getSurfacePlaybook,
34
- handoffRecordSchema,
35
- routingResultSchema,
36
- type Surface,
37
- type LoopContract,
38
- type RoutingResult,
39
- type DispatchPacket,
40
- type PacketIntegrityResult,
41
- type HandoffRecord,
42
- } from "@switchboard/core";
43
- import { generateSbxBundle } from "@switchboard/projections";
44
- import { generateClaudeRuntimeProjection } from "@switchboard/projections";
45
- import { requireRepoRoot, packetsDir } from "../lib/paths";
46
- import {
47
- loadContract,
48
- loadCurrentState,
49
- loadSpec,
50
- specExists as checkSpecExists,
51
- writePacket,
52
- } from "../store/filesystem-store";
53
- import * as out from "../lib/output";
54
- import { withCliErrors } from "../lib/errors";
55
- import { enforceDriftGuard } from "../lib/drift-guard";
56
-
57
- // ---------------------------------------------------------------------------
58
- // Surface alias normalization
59
- // ---------------------------------------------------------------------------
60
-
61
- const SURFACE_ALIASES: Record<string, Surface> = {
62
- claude: "claude-code",
63
- "claude-code": "claude-code",
64
- chatgpt: "chatgpt",
65
- gpt: "chatgpt",
66
- cursor: "cursor",
67
- codex: "codex",
68
- };
69
-
70
- function resolveSurface(raw: string): Surface | null {
71
- return SURFACE_ALIASES[raw.toLowerCase()] ?? null;
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Forced-surface route builder
76
- // ---------------------------------------------------------------------------
77
-
78
- /** Surface-specific defaults for fallback, rationale, and return contract. */
79
- const SURFACE_ROUTE_DEFAULTS: Record<Surface, {
80
- fallback: Surface;
81
- rationale: string;
82
- return_condition: string;
83
- }> = {
84
- "claude-code": {
85
- fallback: "codex",
86
- rationale: "Operator-selected surface: Claude Code. Repo-native implementation with structured return.",
87
- return_condition: "Return when the requested files are changed, relevant checks are run, and the implementation status is explicit.",
88
- },
89
- chatgpt: {
90
- fallback: "claude-code",
91
- rationale: "Operator-selected surface: ChatGPT. Structured clarification or compile pass.",
92
- return_condition: "Return with one clear decision, one exact next objective, and any narrowed scope boundaries.",
93
- },
94
- cursor: {
95
- fallback: "claude-code",
96
- rationale: "Operator-selected surface: Cursor. Close-control, visual, or taste-heavy editing.",
97
- return_condition: "Return when the UI changes are coherent, the changed files are listed, and any taste-sensitive decisions are captured.",
98
- },
99
- codex: {
100
- fallback: "claude-code",
101
- rationale: "Operator-selected surface: Codex. Bounded, sandboxed implementation slice.",
102
- return_condition: "Return when the bounded code change is complete, tests pass in the sandbox, and any broader repo dependencies are listed.",
103
- },
104
- };
105
-
106
- /**
107
- * Build a complete, internally coherent route for a forced surface.
108
- * Every field is surface-appropriate — no leftover metadata from a
109
- * different surface's routing decision.
110
- */
111
- export function buildForcedSurfaceRoute(surface: Surface, objective: string): RoutingResult {
112
- const defaults = SURFACE_ROUTE_DEFAULTS[surface];
113
- return routingResultSchema.parse({
114
- primary_recommendation: surface,
115
- fallback_surface: defaults.fallback,
116
- rationale: defaults.rationale,
117
- next_objective: objective,
118
- return_condition: defaults.return_condition,
119
- });
120
- }
121
-
122
- // ---------------------------------------------------------------------------
123
- // Prompt generation from spec + loop
124
- // ---------------------------------------------------------------------------
125
-
126
- function buildDispatchPrompt(
127
- specMarkdown: string,
128
- loop: LoopContract,
129
- surface: Surface,
130
- ): string {
131
- const sections: string[] = [];
132
-
133
- sections.push("# Execution Brief\n");
134
- sections.push(specMarkdown.trim());
135
- sections.push("\n---\n");
136
- sections.push("## Objective\n");
137
- sections.push(loop.objective);
138
- sections.push("\n## Scope In\n");
139
- sections.push(loop.scope_in.map(s => `- ${s}`).join("\n"));
140
- sections.push("\n## Scope Out\n");
141
- sections.push(loop.scope_out.map(s => `- ${s}`).join("\n"));
142
- sections.push("\n## Done When\n");
143
- sections.push(loop.done_when.map(s => `- ${s}`).join("\n"));
144
- sections.push("\n## Success Criteria\n");
145
- sections.push(loop.success_criteria.map(s => `- ${s}`).join("\n"));
146
- sections.push("\n## Verify With\n");
147
- sections.push(loop.verify_with.map(s => `- ${s}`).join("\n"));
148
- sections.push("\n## Return When\n");
149
- sections.push(loop.return_when);
150
- sections.push("\n## Blocker Escalation\n");
151
- sections.push(loop.blocker_escalation.map(s => `- ${s}`).join("\n"));
152
-
153
- return sections.join("\n");
154
- }
155
-
156
- // ---------------------------------------------------------------------------
157
- // Packet file rendering
158
- // ---------------------------------------------------------------------------
159
-
160
- function renderPacketFile(packet: DispatchPacket, integrityResult: PacketIntegrityResult): string {
161
- const sections: string[] = [];
162
-
163
- sections.push("---");
164
- sections.push(`dispatch_id: ${packet.metadata.dispatch_id}`);
165
- sections.push(`surface: ${packet.surface}`);
166
- sections.push(`format: ${packet.format}`);
167
- sections.push(`integrity: ${integrityResult.outcome}`);
168
- sections.push(`generated_at: ${new Date().toISOString()}`);
169
- sections.push("---\n");
170
-
171
- if (packet.format === "clipboard") {
172
- sections.push(packet.preamble);
173
- sections.push("\n---\n");
174
- sections.push(packet.body);
175
- sections.push("\n---\n");
176
- sections.push(packet.return_instructions);
177
- } else if (packet.format === "sdk_prompt") {
178
- sections.push(`# Switchboard Dispatch — ${packet.metadata.contract_title}\n`);
179
- sections.push(`**Surface:** ${packet.surface}`);
180
- sections.push(`**Dispatch ID:** ${packet.metadata.dispatch_id}`);
181
- sections.push(`**Objective:** ${packet.metadata.objective}\n`);
182
- if (packet.working_directory) {
183
- sections.push(`**Working directory:** ${packet.working_directory}\n`);
184
- }
185
- sections.push("---\n");
186
- sections.push(packet.prompt);
187
- } else if (packet.format === "file_drop") {
188
- sections.push(packet.content);
189
- }
190
-
191
- // Integrity findings
192
- if (integrityResult.findings.length > 0) {
193
- sections.push("\n---\n");
194
- sections.push("## Packet Integrity Findings\n");
195
- for (const f of integrityResult.findings) {
196
- const icon = f.severity === "blocking" ? "BLOCKED" : "CAUTION";
197
- sections.push(`- [${icon}] ${f.field}: ${f.detail}`);
198
- }
199
- }
200
-
201
- return sections.join("\n");
202
- }
203
-
204
- // ---------------------------------------------------------------------------
205
- // Command registration
206
- // ---------------------------------------------------------------------------
207
-
208
- export function registerPacketCommand(program: Command): void {
209
- program
210
- .command("packet")
211
- .argument("<surface>", "Target surface: claude, chatgpt, cursor, codex")
212
- .description("Generate a governed dispatch packet for a target surface")
213
- .option("--force", "Generate even if integrity check has blocking findings")
214
- .action(withCliErrors(async (surfaceArg: string, opts: { force?: boolean }) => {
215
- const repoRoot = requireRepoRoot();
216
- const surface = resolveSurface(surfaceArg);
217
-
218
- if (!surface) {
219
- out.fail(`Unknown surface: "${surfaceArg}". Use: claude, chatgpt, cursor, codex`);
220
- process.exitCode = 1;
221
- return;
222
- }
223
-
224
- out.heading(`switchboard packet ${surface}`);
225
-
226
- // 1. Load canonical state
227
- const contract = loadContract(repoRoot);
228
- const current = loadCurrentState(repoRoot);
229
- const spec = loadSpec(repoRoot) ?? "";
230
-
231
- out.success(`Loaded contract: ${contract.title}`);
232
-
233
- // 1b. Drift guard: detect loop.yaml / current.yaml objective mismatch
234
- if (!enforceDriftGuard(repoRoot, current, !!opts.force)) {
235
- process.exitCode = 1;
236
- return;
237
- }
238
-
239
- // 2. Build a complete, surface-coherent route for the requested surface.
240
- // Always recompile the loop rather than reusing a cached loop.yaml,
241
- // because loop defaults (done_when, verify_with, success_criteria,
242
- // return_when, fallback_surface) are all surface-sensitive.
243
- const route = buildForcedSurfaceRoute(surface, current.next_objective);
244
-
245
- const loop = compileLoopContract({ contract, current, route });
246
- out.section("Objective", loop.objective);
247
- out.section("Surface", surface);
248
-
249
- // 3. Generate dispatch ID
250
- const dispatchId = `dsp-${randomBytes(8).toString("hex")}`;
251
-
252
- const harness = compileExecutionHarness({
253
- dispatch_id: dispatchId,
254
- contract,
255
- loop,
256
- route,
257
- working_directory: repoRoot,
258
- });
259
-
260
- out.success("Compiled execution harness");
261
- out.section("Dispatch mode", harness.dispatch_mode);
262
- out.section("Packet format", harness.packet_format);
263
-
264
- // 5. Run meta-harness compile pass (backstage, automatic)
265
- const metaResult = compileMetaHarness({
266
- contract,
267
- loop,
268
- dispatchId,
269
- specMarkdown: spec,
270
- });
271
-
272
- if (!metaResult.summary.selection.was_no_op) {
273
- out.success(`Meta-harness: +${metaResult.summary.context_chars_added} chars context`);
274
- } else {
275
- out.info(`Meta-harness: no-op (${metaResult.summary.selection.no_op_reason})`);
276
- }
277
-
278
- // 6. Build dispatch prompt (spec + loop scope + meta-harness augmentation)
279
- let prompt = buildDispatchPrompt(spec, loop, surface);
280
- prompt = augmentPromptWithHarnessContext(prompt, metaResult.contextSections);
281
-
282
- // 7. Validate packet integrity
283
- const integrity = validateDispatchPacket({
284
- contract,
285
- loop,
286
- prompt,
287
- specMarkdown: spec,
288
- specExists: checkSpecExists(repoRoot),
289
- repoExists: contract.repo_status === "repo-present",
290
- hasActiveHandoff: !!current.active_handoff_id,
291
- });
292
-
293
- if (integrity.outcome === "blocked" && !opts.force) {
294
- out.fail(`Packet integrity: BLOCKED (${integrity.blocking_findings_count} blocking finding(s))`);
295
- for (const f of integrity.findings.filter(f => f.severity === "blocking")) {
296
- out.bullet(`${f.field}: ${f.detail}`);
297
- }
298
- out.info("Fix the blocking findings or use --force to generate anyway.");
299
- process.exitCode = 1;
300
- return;
301
- }
302
-
303
- if (integrity.outcome === "ready_with_cautions") {
304
- out.warn(`Packet integrity: ready with ${integrity.caution_findings_count} caution(s)`);
305
- for (const f of integrity.findings.filter(f => f.severity === "caution")) {
306
- out.bullet(`${f.field}: ${f.detail}`);
307
- }
308
- } else if (integrity.outcome === "ready") {
309
- out.success("Packet integrity: ready");
310
- } else if (opts.force) {
311
- out.warn(`Packet integrity: BLOCKED (forced)`);
312
- }
313
-
314
- // 8. Format the packet using existing dispatch formatter
315
- const packet = formatFromHarness(harness, prompt);
316
-
317
- // 9. Write packet file
318
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
319
- const packetFilename = `${surface}-${timestamp}-${dispatchId.slice(4, 12)}.md`;
320
- const packetContent = renderPacketFile(packet, integrity);
321
- const packetPath = writePacket(repoRoot, packetFilename, packetContent);
322
-
323
- out.fileWritten("Dispatch packet", packetPath);
324
-
325
- // 10. Build a HandoffRecord for projection generators
326
- const handoff: HandoffRecord = handoffRecordSchema.parse({
327
- handoff_id: dispatchId,
328
- surface,
329
- objective: loop.objective,
330
- included_scope: loop.scope_in,
331
- excluded_scope: loop.scope_out,
332
- done_when: loop.done_when,
333
- return_condition: loop.return_when,
334
- loop,
335
- readiness: { status: "ready" as const, issues: [] },
336
- bundle_version: "SBX-v1" as const,
337
- launched_at: new Date().toISOString(),
338
- });
339
-
340
- // 11. Surface-specific extras
341
- if (surface === "claude-code") {
342
- // Generate Claude runtime projection (CLAUDE.md + rules)
343
- try {
344
- const playbook = getSurfacePlaybook(surface);
345
- const projection = generateClaudeRuntimeProjection({
346
- contract,
347
- current,
348
- spec_markdown: spec,
349
- handoff,
350
- playbook,
351
- });
352
-
353
- const projDir = join(packetsDir(repoRoot), `claude-projection-${dispatchId.slice(4, 12)}`);
354
- if (!existsSync(projDir)) mkdirSync(projDir, { recursive: true });
355
-
356
- for (const file of projection.files) {
357
- const filePath = join(projDir, file.path);
358
- const fileDir = join(projDir, file.path.split("/").slice(0, -1).join("/"));
359
- if (fileDir !== projDir && !existsSync(fileDir)) {
360
- mkdirSync(fileDir, { recursive: true });
361
- }
362
- writeFileSync(filePath, file.content, "utf-8");
363
- }
364
-
365
- out.fileWritten("Claude runtime projection", projDir);
366
- } catch (e) {
367
- out.warn(`Could not generate Claude projection: ${e instanceof Error ? e.message : String(e)}`);
368
- }
369
- }
370
-
371
- if (surface === "cursor") {
372
- // Generate SBX bundle files (paste-ready bundle structure)
373
- try {
374
- const bundle = generateSbxBundle({
375
- contract,
376
- current,
377
- spec_markdown: spec,
378
- handoff,
379
- });
380
-
381
- const bundleDir = join(packetsDir(repoRoot), `sbx-bundle-${dispatchId.slice(4, 12)}`);
382
- if (!existsSync(bundleDir)) mkdirSync(bundleDir, { recursive: true });
383
-
384
- for (const file of bundle.files) {
385
- writeFileSync(join(bundleDir, file.path), file.content, "utf-8");
386
- }
387
-
388
- out.fileWritten("SBX bundle", bundleDir);
389
- } catch (e) {
390
- out.warn(`Could not generate SBX bundle: ${e instanceof Error ? e.message : String(e)}`);
391
- }
392
- }
393
-
394
- // Summary
395
- out.heading("Packet ready");
396
- out.section("Dispatch ID", dispatchId);
397
- out.section("Surface", surface);
398
- out.section("Integrity", integrity.outcome);
399
-
400
- if (surface === "cursor") {
401
- out.bullet("Copy the packet contents into Cursor or use the SBX bundle files");
402
- } else if (surface === "claude-code") {
403
- out.bullet("Use the Claude runtime projection or paste the packet into Claude Code");
404
- } else {
405
- out.bullet("Copy the packet contents into the target surface");
406
- }
407
- }));
408
- }