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,117 +0,0 @@
1
- /**
2
- * switchboard compile
3
- *
4
- * Reads canonical local state (.switchboard/project/contract.yaml,
5
- * .switchboard/working/current.yaml), compiles a governed loop contract
6
- * using existing @switchboard/core logic, and writes the result to
7
- * .switchboard/working/loop.yaml.
8
- *
9
- * Also updates current.yaml stage to "activation-compiled" if it was
10
- * still in a shaping stage.
11
- *
12
- * Does not require the hosted web app, Supabase, or a running server.
13
- */
14
-
15
- import type { Command } from "commander";
16
- import {
17
- chooseRecommendedSurface,
18
- compileLoopContract,
19
- workingStateSchema,
20
- type RoutingResult,
21
- type Surface,
22
- } from "@switchboard/core";
23
- import { requireRepoRoot } from "../lib/paths";
24
- import {
25
- loadContract,
26
- loadCurrentState,
27
- loadSpec,
28
- writeCurrentState,
29
- writeLoop,
30
- } from "../store/filesystem-store";
31
- import * as out from "../lib/output";
32
- import { withCliErrors } from "../lib/errors";
33
- import { loopPath } from "../lib/paths";
34
-
35
- export function registerCompileCommand(program: Command): void {
36
- program
37
- .command("compile")
38
- .description("Compile a governed loop contract from canonical state")
39
- .option("--objective <text>", "Override the loop objective")
40
- .option("--surface <surface>", "Force a target surface (claude-code, chatgpt, cursor, codex)")
41
- .action(withCliErrors(async (opts: { objective?: string; surface?: string }) => {
42
- const repoRoot = requireRepoRoot();
43
-
44
- out.heading("switchboard compile");
45
-
46
- // 1. Load canonical state
47
- const contract = loadContract(repoRoot);
48
- const current = loadCurrentState(repoRoot);
49
- const spec = loadSpec(repoRoot);
50
-
51
- out.success(`Loaded contract: ${contract.title} (${contract.project_id})`);
52
- out.success(`Loaded state: stage=${current.stage}`);
53
-
54
- // 2. Route to surface
55
- const route: RoutingResult = chooseRecommendedSurface({
56
- repo_present: contract.repo_status === "repo-present",
57
- task: opts.objective ?? current.next_objective,
58
- });
59
-
60
- // Allow surface override
61
- if (opts.surface) {
62
- const validSurfaces = ["claude-code", "chatgpt", "cursor", "codex"];
63
- if (!validSurfaces.includes(opts.surface)) {
64
- out.fail(`Invalid surface: ${opts.surface}. Must be one of: ${validSurfaces.join(", ")}`);
65
- process.exitCode = 1;
66
- return;
67
- }
68
- (route as { primary_recommendation: Surface }).primary_recommendation = opts.surface as Surface;
69
- }
70
-
71
- out.section("Route", `${route.primary_recommendation} (fallback: ${route.fallback_surface})`);
72
- out.section("Rationale", route.rationale);
73
-
74
- // 3. Compile loop contract using existing core logic
75
- const loopInput: Parameters<typeof compileLoopContract>[0] = {
76
- contract,
77
- current,
78
- route,
79
- };
80
- if (opts.objective) loopInput.objective = opts.objective;
81
- const loop = compileLoopContract(loopInput);
82
-
83
- out.success(`Compiled loop contract`);
84
- out.section("Objective", loop.objective);
85
- out.section("Scope in", `${loop.scope_in.length} item(s)`);
86
- out.section("Scope out", `${loop.scope_out.length} item(s)`);
87
- out.section("Done when", `${loop.done_when.length} condition(s)`);
88
- out.section("Execution shape", loop.execution_shape);
89
-
90
- // 4. Write loop to filesystem
91
- writeLoop(repoRoot, loop);
92
- out.fileWritten("Loop contract", loopPath(repoRoot));
93
-
94
- // 5. Update current state — sync next_objective to the compiled loop
95
- // objective so packet/run dispatch the same thing compile produced.
96
- const shouldUpdateStage = current.stage.startsWith("activation-shaping") || current.stage === "activation-needs-narrowing";
97
- const updated = workingStateSchema.parse({
98
- ...current,
99
- stage: shouldUpdateStage ? "activation-compiled" : current.stage,
100
- current_objective: loop.objective,
101
- next_recommended_surface: route.primary_recommendation,
102
- // CRITICAL: next_objective must be the compiled loop objective,
103
- // not the router's derived objective. Otherwise packet/run diverge.
104
- next_objective: loop.objective,
105
- });
106
- writeCurrentState(repoRoot, updated);
107
- if (shouldUpdateStage) {
108
- out.success("Updated working state to activation-compiled");
109
- } else {
110
- out.success("Synced working state to compiled loop objective");
111
- }
112
-
113
- // Summary
114
- out.heading("Next steps");
115
- out.bullet(`Run \`switchboard packet ${route.primary_recommendation}\` to generate a dispatch packet`);
116
- }));
117
- }
@@ -1,103 +0,0 @@
1
- /**
2
- * switchboard evaluate
3
- *
4
- * Runs the independent evaluation lane against the latest portable
5
- * receipt. Writes evaluation output under .switchboard/evaluations/.
6
- *
7
- * Uses existing @switchboard/core evaluation logic: runEvaluation
8
- * from evaluation-lane.ts.
9
- *
10
- * Does not require the hosted web app or Supabase.
11
- */
12
-
13
- import type { Command } from "commander";
14
- import { runEvaluation } from "@switchboard/core";
15
- import { requireRepoRoot, sbDir } from "../lib/paths";
16
- import { readYaml } from "../lib/yaml-io";
17
- import {
18
- loadContract,
19
- loadLatestAuditedReceipt,
20
- saveEvaluationResult,
21
- saveEvaluationJson,
22
- } from "../store/filesystem-store";
23
- import { loadPortableProofManifest } from "../lib/proof";
24
- import * as out from "../lib/output";
25
- import { withCliErrors } from "../lib/errors";
26
- import { join } from "path";
27
-
28
- export function registerEvaluateCommand(program: Command): void {
29
- program
30
- .command("evaluate")
31
- .description("Run independent evaluation on the latest receipt")
32
- .action(withCliErrors(async () => {
33
- const repoRoot = requireRepoRoot();
34
-
35
- out.heading("switchboard evaluate");
36
-
37
- // 1. Load the audited receipt (V1 format expected by evaluation lane)
38
- const receipt = loadLatestAuditedReceipt(repoRoot);
39
-
40
- if (!receipt) {
41
- out.fail("No audited receipt found. Run `switchboard receipt` first.");
42
- process.exitCode = 1;
43
- return;
44
- }
45
-
46
- out.success(`Loaded receipt: ${receipt.receipt_id}`);
47
- out.section("Gate", receipt.gate_label);
48
- out.section("Trust posture", receipt.trust_posture);
49
-
50
- // 2. Load contract for scope context
51
- const contract = loadContract(repoRoot);
52
-
53
- // 3. Load proof manifest if available (via ingest pointer)
54
- const pointerPath = join(sbDir(repoRoot), "working", "latest-ingest.yaml");
55
- const pointer = readYaml<{ dispatch_id?: string; has_proof_manifest?: boolean }>(pointerPath);
56
-
57
- let proofManifest = null;
58
- if (pointer?.dispatch_id && pointer?.has_proof_manifest) {
59
- proofManifest = loadPortableProofManifest(repoRoot, pointer.dispatch_id);
60
- if (proofManifest) {
61
- out.success(`Loaded proof manifest: ${proofManifest.manifest_id} (${proofManifest.artifacts.length} artifact(s))`);
62
- }
63
- }
64
-
65
- if (!proofManifest) {
66
- out.info("No proof manifest — evaluation will be honest about provenance gaps.");
67
- }
68
-
69
- // 4. Run the evaluation lane with proof manifest when available
70
- const result = runEvaluation({
71
- receipt,
72
- proofManifest,
73
- trigger: "operator_requested",
74
- contracted_scope_in: contract.task_clauses.scope_in,
75
- contracted_done_when: [],
76
- });
77
-
78
- // 5. Persist
79
- const yamlPath = saveEvaluationResult(repoRoot, result);
80
- const jsonPath = saveEvaluationJson(repoRoot, result);
81
- out.fileWritten("Evaluation (YAML)", yamlPath);
82
- out.fileWritten("Evaluation (JSON)", jsonPath);
83
-
84
- // Summary
85
- out.heading("Evaluation complete");
86
- out.section("Evaluation ID", result.evaluation_id);
87
- out.section("Verdict", result.verdict);
88
- out.section("Basis", result.verdict_basis);
89
- out.section("Scope", result.evaluation_scope);
90
-
91
- if (result.violation_count > 0) {
92
- out.fail(`${result.violation_count} violation(s)`);
93
- }
94
- if (result.concern_count > 0) {
95
- out.warn(`${result.concern_count} concern(s)`);
96
- }
97
- if (result.flag_count > 0) {
98
- out.info(`${result.flag_count} flag(s)`);
99
- }
100
-
101
- out.section("Re-entry signal", `${result.reentry_signal} — ${result.reentry_basis}`);
102
- }));
103
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * switchboard ingest
3
- *
4
- * Reads a structured SB_RETURN.yaml, validates it against the active
5
- * handoff, runs the governed ingest + reconcile + gate path, and
6
- * updates .switchboard/ state.
7
- *
8
- * Uses existing @switchboard/core modules: parseStructuredReturn,
9
- * validateReturnForIngest, ingestReturnReport, normalizeReturn,
10
- * deriveCanonicalDelta, deriveGateState, deriveNextSlice.
11
- *
12
- * Does not require the hosted web app or Supabase.
13
- */
14
-
15
- import { existsSync, readdirSync } from "fs";
16
- import { join } from "path";
17
- import type { Command } from "commander";
18
- import {
19
- parseStructuredReturn,
20
- ingestReturnReport,
21
- normalizeReturn,
22
- deriveCanonicalDelta,
23
- deriveGateState,
24
- deriveNextSlice,
25
- shouldRequireAudit,
26
- computeAuditResult,
27
- noAuditRequired,
28
- } from "@switchboard/core";
29
- import { requireRepoRoot, returnsDir, returnTemplatePath } from "../lib/paths";
30
- import { readMarkdown } from "../lib/yaml-io";
31
- import { captureReturnProof, buildAndWriteProofManifest } from "../lib/proof";
32
- import {
33
- loadContract,
34
- loadCurrentState,
35
- loadHandoff,
36
- loadLatestHandoff,
37
- loadLatestDispatchMeta,
38
- loadSpec,
39
- writeCurrentState,
40
- saveReturn,
41
- saveCanonicalDelta,
42
- saveNormalizedEnvelope,
43
- saveGateState,
44
- } from "../store/filesystem-store";
45
- import * as out from "../lib/output";
46
- import { withCliErrors } from "../lib/errors";
47
-
48
- function findReturnFile(repoRoot: string, dispatchId?: string): string | null {
49
- // Priority 1: Dispatch-specific return in .switchboard/returns/
50
- if (dispatchId) {
51
- const specific = join(returnsDir(repoRoot), `${dispatchId}-return.yaml`);
52
- if (existsSync(specific)) return specific;
53
- }
54
-
55
- // Priority 2: Repo-root SB_RETURN.yaml (convenience location)
56
- const rootReturn = join(repoRoot, "SB_RETURN.yaml");
57
- if (existsSync(rootReturn)) return rootReturn;
58
-
59
- // Priority 3: Most recent return file in .switchboard/returns/
60
- const retDir = returnsDir(repoRoot);
61
- if (!existsSync(retDir)) return null;
62
-
63
- const files = readdirSync(retDir)
64
- .filter(f => f.endsWith("-return.yaml"))
65
- .sort()
66
- .reverse();
67
-
68
- return files.length > 0 ? join(retDir, files[0]!) : null;
69
- }
70
-
71
- export function registerIngestCommand(program: Command): void {
72
- program
73
- .command("ingest")
74
- .description("Ingest a structured return and update canonical state")
75
- .option("--file <path>", "Path to the return YAML file")
76
- .action(withCliErrors(async (opts: { file?: string }) => {
77
- const repoRoot = requireRepoRoot();
78
-
79
- out.heading("switchboard ingest");
80
-
81
- // 1. Load canonical state
82
- const contract = loadContract(repoRoot);
83
- const current = loadCurrentState(repoRoot);
84
- const spec = loadSpec(repoRoot) ?? "";
85
-
86
- out.success(`Loaded contract: ${contract.title}`);
87
-
88
- // 2. Resolve active handoff
89
- const activeHandoffId = current.active_handoff_id;
90
- const dispatchMeta = loadLatestDispatchMeta(repoRoot);
91
-
92
- let handoff = activeHandoffId ? loadHandoff(repoRoot, activeHandoffId) : null;
93
- if (!handoff) {
94
- handoff = loadLatestHandoff(repoRoot);
95
- }
96
-
97
- if (!handoff) {
98
- out.fail("No handoff found. Run `switchboard run claude` first to create a dispatch.");
99
- process.exitCode = 1;
100
- return;
101
- }
102
-
103
- out.success(`Loaded handoff: ${handoff.handoff_id}`);
104
-
105
- // 3. Find and read return file
106
- const returnPath = opts.file ?? findReturnFile(repoRoot, handoff.handoff_id);
107
- if (!returnPath) {
108
- out.fail("No return file found.");
109
- out.info("Place SB_RETURN.yaml in the repo root or .switchboard/returns/");
110
- out.info("Or specify: switchboard ingest --file path/to/return.yaml");
111
- process.exitCode = 1;
112
- return;
113
- }
114
-
115
- const rawYaml = readMarkdown(returnPath);
116
- if (!rawYaml) {
117
- out.fail(`Return file is empty: ${returnPath}`);
118
- process.exitCode = 1;
119
- return;
120
- }
121
-
122
- out.success(`Read return from: ${returnPath}`);
123
-
124
- // 4. Parse structured return
125
- const report = parseStructuredReturn(rawYaml);
126
- out.section("Status", report.status);
127
- out.section("Objective met", report.objective_met);
128
- out.section("Changed files", `${report.changed_files.length}`);
129
-
130
- // 5. Run governed ingest (validates, reconciles, routes)
131
- const ingestResult = ingestReturnReport({
132
- current,
133
- handoff,
134
- report,
135
- repo_present: contract.repo_status === "repo-present",
136
- contract,
137
- spec_markdown: spec,
138
- });
139
-
140
- out.success(`Ingested return: stage=${ingestResult.current.stage}`);
141
-
142
- if (ingestResult.reconcile) {
143
- out.section("Reconcile", ingestResult.reconcile.recommendation);
144
- if (ingestResult.reconcile.drift_detected) {
145
- out.warn(`Drift detected: ${ingestResult.reconcile.findings.length} finding(s)`);
146
- }
147
- }
148
-
149
- // 6. Derive gate state
150
- const gate = deriveGateState({
151
- reconcile: ingestResult.reconcile,
152
- latestReturn: report,
153
- phase: ingestResult.current.stage,
154
- });
155
-
156
- out.section("Gate", `${gate.label} — ${gate.reason}`);
157
-
158
- // 7. Normalize the return into governed envelope
159
- const auditCheck = shouldRequireAudit({
160
- report,
161
- complexityMode: null,
162
- taskType: "implementation-request",
163
- });
164
-
165
- const audit = auditCheck.required
166
- ? computeAuditResult({ report, triggers: auditCheck.triggers, returnId: handoff.handoff_id })
167
- : noAuditRequired(handoff.handoff_id);
168
-
169
- if (audit.audit_required) {
170
- out.section("Audit", `${audit.outcome} (${audit.findings.length} finding(s))`);
171
- }
172
-
173
- const envelope = normalizeReturn({
174
- report,
175
- reconcile: ingestResult.reconcile,
176
- audit,
177
- dispatch_id: handoff.handoff_id,
178
- });
179
-
180
- // 8. Derive canonical delta
181
- const delta = deriveCanonicalDelta({
182
- report,
183
- contract,
184
- gateState: gate,
185
- reconcile: ingestResult.reconcile,
186
- envelope,
187
- });
188
-
189
- out.section("Delta", `${delta.delta_id} — ${delta.summary}`);
190
-
191
- // 9. Derive next slice
192
- const nextSlice = deriveNextSlice({
193
- report,
194
- contract,
195
- gateState: gate,
196
- delta,
197
- });
198
-
199
- if (nextSlice) {
200
- out.section("Next slice", nextSlice.title);
201
- }
202
-
203
- // 10. Capture return as proof artifact + build proof manifest
204
- captureReturnProof(repoRoot, handoff.handoff_id, rawYaml);
205
- const proofManifest = buildAndWriteProofManifest(repoRoot, handoff.handoff_id);
206
- out.success(`Proof manifest: ${proofManifest.manifest_id} (${proofManifest.artifacts.length} artifact(s))`);
207
-
208
- // 11. Persist everything — receipt will load these exact artifacts by ID
209
- saveReturn(repoRoot, handoff.handoff_id, report);
210
- saveNormalizedEnvelope(repoRoot, envelope);
211
- saveCanonicalDelta(repoRoot, delta);
212
- saveGateState(repoRoot, handoff.handoff_id, gate);
213
- writeCurrentState(repoRoot, ingestResult.current);
214
-
215
- // Persist reconcile and audit so receipt consumes exact ingested truth
216
- const { writeYaml } = await import("../lib/yaml-io");
217
- const { sbDir } = await import("../lib/paths");
218
-
219
- if (ingestResult.reconcile) {
220
- writeYaml(join(returnsDir(repoRoot), `reconcile-${handoff.handoff_id}.yaml`), ingestResult.reconcile);
221
- }
222
- writeYaml(join(returnsDir(repoRoot), `audit-${handoff.handoff_id}.yaml`), audit);
223
- if (nextSlice) {
224
- writeYaml(join(returnsDir(repoRoot), `next-slice-${handoff.handoff_id}.yaml`), nextSlice);
225
- }
226
-
227
- out.success("Updated canonical state");
228
-
229
- // Summary
230
- out.heading("Ingest complete");
231
- out.section("Gate", gate.label);
232
- out.section("Stage", ingestResult.current.stage);
233
- out.bullet("Run `switchboard receipt` to build the trust artifact");
234
-
235
- // Save a pointer so receipt loads the exact persisted artifacts
236
- writeYaml(join(sbDir(repoRoot), "working", "latest-ingest.yaml"), {
237
- dispatch_id: handoff.handoff_id,
238
- envelope_id: envelope.envelope_id,
239
- delta_id: delta.delta_id,
240
- gate_label: gate.label,
241
- audit_id: audit.audit_id,
242
- next_slice_id: nextSlice?.slice_id ?? "",
243
- has_reconcile: !!ingestResult.reconcile,
244
- has_next_slice: !!nextSlice,
245
- has_proof_manifest: true,
246
- proof_manifest_id: proofManifest.manifest_id,
247
- ingested_at: new Date().toISOString(),
248
- });
249
- }));
250
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * switchboard init
3
- *
4
- * Scaffolds a minimal governed project root under .switchboard/.
5
- * Creates starter contract.yaml and current.yaml that validate against
6
- * core schemas, plus the canonical directory structure.
7
- *
8
- * Does not require Supabase, a running server, or the hosted app.
9
- */
10
-
11
- import { existsSync } from "fs";
12
- import { basename, resolve } from "path";
13
- import type { Command } from "commander";
14
- import { projectContractSchema, workingStateSchema } from "@switchboard/core";
15
- import {
16
- scaffoldDirectories,
17
- switchboardExists,
18
- writeContract,
19
- writeCurrentState,
20
- writeSpec,
21
- } from "../store/filesystem-store";
22
- import { sbDir, contractPath, currentStatePath, specPath } from "../lib/paths";
23
- import * as out from "../lib/output";
24
- import { withCliErrors } from "../lib/errors";
25
-
26
- function slugify(text: string): string {
27
- return text
28
- .toLowerCase()
29
- .replace(/[^a-z0-9]+/g, "-")
30
- .replace(/^-|-$/g, "")
31
- .slice(0, 60);
32
- }
33
-
34
- export function registerInitCommand(program: Command): void {
35
- program
36
- .command("init")
37
- .description("Scaffold a .switchboard/ governed project root")
38
- .option("--title <title>", "Project title")
39
- .option("--force", "Overwrite existing .switchboard/ files")
40
- .action(withCliErrors(async (opts: { title?: string; force?: boolean }) => {
41
- const repoRoot = resolve(process.cwd());
42
- const title = opts.title ?? basename(repoRoot);
43
- const projectId = slugify(title);
44
-
45
- // Guard: already initialized
46
- if (switchboardExists(repoRoot) && !opts.force) {
47
- out.warn(`${sbDir(repoRoot)} already exists. Use --force to overwrite.`);
48
- process.exitCode = 1;
49
- return;
50
- }
51
-
52
- out.heading("switchboard init");
53
-
54
- // 1. Create directory tree
55
- scaffoldDirectories(repoRoot);
56
- out.success("Created .switchboard/ directory structure");
57
-
58
- // 2. Write starter contract (validates against projectContractSchema)
59
- const contract = projectContractSchema.parse({
60
- project_id: projectId,
61
- title,
62
- one_liner: `${title} — [FILL IN] one-line description`,
63
- problem: "[FILL IN] the problem this project solves.",
64
- target_user: "[FILL IN] the target user for this project.",
65
- desired_v1_outcome: "[FILL IN] what V1 should deliver.",
66
- non_goals: [
67
- "No platform sprawl beyond the first bounded slice.",
68
- ],
69
- repo_status: existsSync(resolve(repoRoot, ".git"))
70
- ? "repo-present" as const
71
- : "repo-not-present" as const,
72
- task_clauses: {
73
- scope_in: [],
74
- scope_out: [],
75
- systems_expected: [],
76
- files_allowed: [],
77
- must_not_widen: [],
78
- build_constraints: [],
79
- },
80
- });
81
- writeContract(repoRoot, contract);
82
- out.fileWritten("Contract", contractPath(repoRoot));
83
-
84
- // 3. Write starter working state (validates against workingStateSchema)
85
- const current = workingStateSchema.parse({
86
- stage: "activation-shaping",
87
- current_objective: `Define and compile the first bounded slice for ${title}.`,
88
- dominant_uncertainty: "The project contract needs to be filled in before compilation.",
89
- open_questions: [
90
- "What is the narrowest first slice worth building?",
91
- ],
92
- primary_artifact_ref: ".switchboard/artifacts/SB_SPEC.md",
93
- active_handoff_id: "",
94
- last_return_status: "not-started" as const,
95
- latest_decisions: [],
96
- latest_blockers: [],
97
- next_recommended_surface: "claude-code" as const,
98
- next_objective: `Compile the first governed loop for ${title}.`,
99
- });
100
- writeCurrentState(repoRoot, current);
101
- out.fileWritten("Working state", currentStatePath(repoRoot));
102
-
103
- // 4. Write placeholder spec
104
- const placeholderSpec = [
105
- `# ${title}`,
106
- "",
107
- "## Problem",
108
- "",
109
- "[FILL IN] Describe the problem this project solves.",
110
- "",
111
- "## Target User",
112
- "",
113
- "[FILL IN] Describe the target user.",
114
- "",
115
- "## Desired V1 Outcome",
116
- "",
117
- "[FILL IN] Describe what V1 should deliver.",
118
- "",
119
- "## Non-Goals",
120
- "",
121
- ...contract.non_goals.map(ng => `- ${ng}`),
122
- "",
123
- ].join("\n");
124
- writeSpec(repoRoot, placeholderSpec);
125
- out.fileWritten("Spec placeholder", specPath(repoRoot));
126
-
127
- // Summary
128
- out.heading("Next steps");
129
- out.bullet("Edit .switchboard/project/contract.yaml with your project details");
130
- out.bullet("Edit .switchboard/artifacts/SB_SPEC.md with your canonical brief");
131
- out.bullet("Run `switchboard compile` to compile a governed loop");
132
- }));
133
- }