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/compile.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/evaluate.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/ingest.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -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
|
-
}
|