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/index.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Switchboard CLI — repo-native governance substrate.
|
|
5
|
-
*
|
|
6
|
-
* Portable-first: reads .switchboard/ canonical state, compiles governed
|
|
7
|
-
* dispatch packets, and writes them to disk. No hosted app required.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Command } from "commander";
|
|
11
|
-
import { registerInitCommand } from "./commands/init";
|
|
12
|
-
import { registerCompileCommand } from "./commands/compile";
|
|
13
|
-
import { registerPacketCommand } from "./commands/packet";
|
|
14
|
-
import { registerRunClaudeCommand } from "./commands/run-claude";
|
|
15
|
-
import { registerIngestCommand } from "./commands/ingest";
|
|
16
|
-
import { registerReceiptCommand } from "./commands/receipt";
|
|
17
|
-
import { registerEvaluateCommand } from "./commands/evaluate";
|
|
18
|
-
import { registerAuditCodexCommand } from "./commands/audit-codex";
|
|
19
|
-
import { registerCalibrateCommand } from "./commands/calibrate";
|
|
20
|
-
import { registerAutoAgentCommand } from "./commands/autoagent";
|
|
21
|
-
|
|
22
|
-
const program = new Command();
|
|
23
|
-
|
|
24
|
-
program
|
|
25
|
-
.name("sb")
|
|
26
|
-
.description("Switchboard CLI — portable governance substrate for AI workflows")
|
|
27
|
-
.version("0.1.0-alpha.1");
|
|
28
|
-
|
|
29
|
-
// Wave 1: compile + packet
|
|
30
|
-
registerInitCommand(program);
|
|
31
|
-
registerCompileCommand(program);
|
|
32
|
-
registerPacketCommand(program);
|
|
33
|
-
|
|
34
|
-
// Wave 2: run + ingest + receipt + evaluate + audit
|
|
35
|
-
registerRunClaudeCommand(program);
|
|
36
|
-
registerIngestCommand(program);
|
|
37
|
-
registerReceiptCommand(program);
|
|
38
|
-
registerEvaluateCommand(program);
|
|
39
|
-
registerAuditCodexCommand(program);
|
|
40
|
-
|
|
41
|
-
// Wave 3: calibration
|
|
42
|
-
registerCalibrateCommand(program);
|
|
43
|
-
|
|
44
|
-
// Wave 4: autoagent
|
|
45
|
-
registerAutoAgentCommand(program);
|
|
46
|
-
|
|
47
|
-
program.parse();
|
package/src/lib/draft-return.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Draft Return Generator — produces a structured SB_RETURN.yaml draft
|
|
3
|
-
* from execution evidence, proof artifacts, and dispatch context.
|
|
4
|
-
*
|
|
5
|
-
* This is NOT auto-ingest. The draft is:
|
|
6
|
-
* - Explicitly marked as operator-review-required
|
|
7
|
-
* - Conservative: never claims objective_met=yes without explicit evidence
|
|
8
|
-
* - Provenance-labeled: every populated field cites its source
|
|
9
|
-
* - Honest about gaps: where evidence is missing, says so plainly
|
|
10
|
-
*
|
|
11
|
-
* The operator opens the draft, reviews it, edits as needed, then submits
|
|
12
|
-
* through the existing ingest path.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
16
|
-
import { join, relative } from "path";
|
|
17
|
-
import { execSync } from "child_process";
|
|
18
|
-
import type { HandoffRecord, ReturnReport } from "@switchboard/core";
|
|
19
|
-
import type { ProofManifest } from "@switchboard/core";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Types
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export interface DraftReturnInput {
|
|
26
|
-
handoff: HandoffRecord;
|
|
27
|
-
transcript: string | null;
|
|
28
|
-
proofManifest: ProofManifest | null;
|
|
29
|
-
repoRoot: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface DraftReturnResult {
|
|
33
|
-
draft: ReturnReport;
|
|
34
|
-
warnings: string[];
|
|
35
|
-
evidence_sources: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Transcript signal extraction (deterministic, no LLM)
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
interface TranscriptSignals {
|
|
43
|
-
mentions_success: boolean;
|
|
44
|
-
mentions_failure: boolean;
|
|
45
|
-
mentions_error: boolean;
|
|
46
|
-
mentions_test: boolean;
|
|
47
|
-
mentions_blocked: boolean;
|
|
48
|
-
file_mentions: string[];
|
|
49
|
-
verification_mentions: string[];
|
|
50
|
-
summary_hint: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function extractTranscriptSignals(transcript: string): TranscriptSignals {
|
|
54
|
-
const lower = transcript.toLowerCase();
|
|
55
|
-
|
|
56
|
-
const mentions_success = /\b(completed?|succeed(ed)?|success(ful(ly)?)?|done|finished|implemented|created|built)\b/.test(lower);
|
|
57
|
-
const mentions_failure = /\b(fail(ed|ure)?|could not|unable to|cannot|error(ed)?)\b/.test(lower);
|
|
58
|
-
const mentions_error = /\b(error|exception|traceback|panic|crash)\b/.test(lower);
|
|
59
|
-
const mentions_test = /\b(test(s|ing)?|spec|vitest|jest|mocha|pass(ed|ing)?|assert)\b/.test(lower);
|
|
60
|
-
const mentions_blocked = /\b(block(ed)?|permission denied|not allowed|unauthorized|timeout)\b/.test(lower);
|
|
61
|
-
|
|
62
|
-
// Extract file-like mentions (paths with extensions)
|
|
63
|
-
const fileRegex = /[\w./-]+\.(ts|js|tsx|jsx|json|yaml|yml|md|css|html|py|go|rs|sh)\b/g;
|
|
64
|
-
const file_mentions = [...new Set(transcript.match(fileRegex) ?? [])].slice(0, 20);
|
|
65
|
-
|
|
66
|
-
// Extract verification-like mentions
|
|
67
|
-
const verification_mentions: string[] = [];
|
|
68
|
-
if (mentions_test) {
|
|
69
|
-
const testLines = transcript.split("\n").filter(l =>
|
|
70
|
-
/(pass|fail|test|spec|✓|✗|PASS|FAIL)/.test(l)
|
|
71
|
-
).slice(0, 5);
|
|
72
|
-
verification_mentions.push(...testLines.map(l => l.trim().slice(0, 120)));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Build summary hint
|
|
76
|
-
let summary_hint: string;
|
|
77
|
-
if (mentions_blocked) {
|
|
78
|
-
summary_hint = "Execution was blocked or encountered permission issues.";
|
|
79
|
-
} else if (mentions_failure && !mentions_success) {
|
|
80
|
-
summary_hint = "Execution encountered failures.";
|
|
81
|
-
} else if (mentions_success && !mentions_failure) {
|
|
82
|
-
summary_hint = "Transcript suggests successful completion (needs operator verification).";
|
|
83
|
-
} else if (mentions_success && mentions_failure) {
|
|
84
|
-
summary_hint = "Transcript shows mixed results — some success, some failures.";
|
|
85
|
-
} else {
|
|
86
|
-
summary_hint = "Transcript did not contain clear success or failure signals.";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
mentions_success,
|
|
91
|
-
mentions_failure,
|
|
92
|
-
mentions_error,
|
|
93
|
-
mentions_test,
|
|
94
|
-
mentions_blocked,
|
|
95
|
-
file_mentions,
|
|
96
|
-
verification_mentions,
|
|
97
|
-
summary_hint,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Git-based changed files detection
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
function detectChangedFiles(repoRoot: string): string[] {
|
|
106
|
-
try {
|
|
107
|
-
// git status --porcelain captures staged, unstaged, AND untracked files
|
|
108
|
-
const output = execSync("git status --porcelain 2>/dev/null || true", {
|
|
109
|
-
cwd: repoRoot,
|
|
110
|
-
encoding: "utf-8",
|
|
111
|
-
timeout: 5000,
|
|
112
|
-
});
|
|
113
|
-
// Porcelain format: "XY filename" — extract filenames, skip .switchboard/ internals
|
|
114
|
-
return output
|
|
115
|
-
.split("\n")
|
|
116
|
-
.map(l => l.slice(3).trim())
|
|
117
|
-
.filter(f => f && !f.startsWith(".switchboard/"))
|
|
118
|
-
.slice(0, 50);
|
|
119
|
-
} catch {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Main generator
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
export function generateDraftReturn(input: DraftReturnInput): DraftReturnResult {
|
|
129
|
-
const { handoff, transcript, proofManifest, repoRoot } = input;
|
|
130
|
-
const warnings: string[] = [];
|
|
131
|
-
const evidence_sources: string[] = [];
|
|
132
|
-
|
|
133
|
-
// Start from the template defaults
|
|
134
|
-
const draft: ReturnReport = {
|
|
135
|
-
handoff_id: handoff.handoff_id,
|
|
136
|
-
surface_used: handoff.surface,
|
|
137
|
-
objective_attempted: handoff.loop.objective,
|
|
138
|
-
// Conservative defaults — NEVER auto-promote to "yes"
|
|
139
|
-
status: "partial",
|
|
140
|
-
objective_met: "partial",
|
|
141
|
-
done_when_met: "partial",
|
|
142
|
-
summary: "",
|
|
143
|
-
changed_files: [],
|
|
144
|
-
decisions_made: [],
|
|
145
|
-
blockers: [],
|
|
146
|
-
escalations: [],
|
|
147
|
-
evidence: [],
|
|
148
|
-
success_criteria_met: [],
|
|
149
|
-
verification_run: [],
|
|
150
|
-
artifacts_created: [],
|
|
151
|
-
recommended_next_step: "Review this draft, verify claims, then run switchboard ingest.",
|
|
152
|
-
packet_id: "",
|
|
153
|
-
packet_hash: "",
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// --- Populate from transcript signals ---
|
|
157
|
-
let signals: TranscriptSignals | null = null;
|
|
158
|
-
|
|
159
|
-
if (transcript && transcript.trim().length > 0) {
|
|
160
|
-
signals = extractTranscriptSignals(transcript);
|
|
161
|
-
evidence_sources.push("execution-transcript");
|
|
162
|
-
|
|
163
|
-
// Status from transcript signals
|
|
164
|
-
if (signals.mentions_blocked) {
|
|
165
|
-
draft.status = "blocked";
|
|
166
|
-
draft.blockers.push("Execution was blocked (transcript indicates permission or access issues).");
|
|
167
|
-
warnings.push("Transcript suggests execution was blocked. Check for permission issues.");
|
|
168
|
-
} else if (signals.mentions_failure && !signals.mentions_success) {
|
|
169
|
-
draft.status = "partial";
|
|
170
|
-
warnings.push("Transcript suggests failures occurred. Review before claiming completion.");
|
|
171
|
-
}
|
|
172
|
-
// Never auto-set status to "done" — that's for the operator
|
|
173
|
-
|
|
174
|
-
// Evidence from transcript
|
|
175
|
-
if (signals.mentions_success) {
|
|
176
|
-
draft.evidence.push(`[transcript] Execution transcript suggests completion (needs operator verification).`);
|
|
177
|
-
}
|
|
178
|
-
if (signals.mentions_error) {
|
|
179
|
-
draft.evidence.push(`[transcript] Errors detected in execution transcript — review required.`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Verification from transcript
|
|
183
|
-
for (const v of signals.verification_mentions) {
|
|
184
|
-
draft.verification_run.push(`[transcript] ${v}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// File mentions from transcript
|
|
188
|
-
for (const f of signals.file_mentions) {
|
|
189
|
-
if (!draft.changed_files.includes(f)) {
|
|
190
|
-
draft.evidence.push(`[transcript] File referenced in transcript: ${f}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Summary from transcript signals
|
|
195
|
-
draft.summary = `DRAFT — ${signals.summary_hint} Operator review required before ingest.`;
|
|
196
|
-
} else {
|
|
197
|
-
draft.summary = "DRAFT — no execution transcript available. Fill in manually from execution results.";
|
|
198
|
-
warnings.push("No execution transcript found. Draft is a skeleton only.");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// --- Populate changed files from git ---
|
|
202
|
-
const gitChangedFiles = detectChangedFiles(repoRoot);
|
|
203
|
-
if (gitChangedFiles.length > 0) {
|
|
204
|
-
draft.changed_files = gitChangedFiles;
|
|
205
|
-
evidence_sources.push("git-diff");
|
|
206
|
-
draft.evidence.push(`[git-status] ${gitChangedFiles.length} file(s) detected (staged + unstaged + untracked, excluding .switchboard/).`);
|
|
207
|
-
} else {
|
|
208
|
-
warnings.push("No changed files detected via git. Fill in changed_files manually if execution modified files.");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// --- Populate from proof manifest ---
|
|
212
|
-
if (proofManifest) {
|
|
213
|
-
evidence_sources.push("proof-manifest");
|
|
214
|
-
draft.evidence.push(`[proof] ${proofManifest.artifacts.length} proof artifact(s) captured (${proofManifest.provenance_summary.captured_count} captured, ${proofManifest.provenance_summary.derived_count} derived).`);
|
|
215
|
-
for (const artifact of proofManifest.artifacts) {
|
|
216
|
-
draft.artifacts_created.push(`[proof:${artifact.kind}] ${artifact.filename} (${artifact.size_bytes} bytes)`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// --- Do NOT pre-fill success_criteria_met ---
|
|
221
|
-
// Downstream accounting (gate, delta, normalizer) treats any row in
|
|
222
|
-
// success_criteria_met as real criteria met. Pre-filling with
|
|
223
|
-
// "[needs confirmation]" placeholders would silently inflate confidence
|
|
224
|
-
// if the operator ingests without editing. Leave empty; add a warning
|
|
225
|
-
// so the operator knows to fill them in.
|
|
226
|
-
if (handoff.loop.done_when.length > 0) {
|
|
227
|
-
warnings.push(`${handoff.loop.done_when.length} done-when criteria must be confirmed manually: ${handoff.loop.done_when.map(c => c.slice(0, 50)).join("; ")}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// --- Ensure schema validity ---
|
|
231
|
-
// objective_met stays "partial" — we never auto-promote
|
|
232
|
-
// done_when_met stays "partial" — we never auto-promote
|
|
233
|
-
// If draft has evidence + verification, note it's still partial pending review
|
|
234
|
-
if (draft.evidence.length > 0 && draft.verification_run.length > 0) {
|
|
235
|
-
warnings.push("Draft has evidence and verification data from transcript. Promote objective_met/done_when_met to 'yes' only after manual review.");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Final safety: ensure summary is non-empty
|
|
239
|
-
if (!draft.summary) {
|
|
240
|
-
draft.summary = "DRAFT — generated from execution evidence. Operator review required.";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { draft, warnings, evidence_sources };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
// YAML serialization with draft header
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
|
|
250
|
-
export function serializeDraftReturn(result: DraftReturnResult): string {
|
|
251
|
-
const YAML = require("yaml") as typeof import("yaml");
|
|
252
|
-
|
|
253
|
-
const header = [
|
|
254
|
-
"# ============================================================",
|
|
255
|
-
"# DRAFT RETURN — REVIEW REQUIRED BEFORE INGEST",
|
|
256
|
-
"# ============================================================",
|
|
257
|
-
"#",
|
|
258
|
-
"# This return was auto-generated from execution evidence.",
|
|
259
|
-
"# Fields marked [transcript], [git], [proof] cite their source.",
|
|
260
|
-
"# Fields marked [needs confirmation] require operator verification.",
|
|
261
|
-
"#",
|
|
262
|
-
`# Evidence sources: ${result.evidence_sources.join(", ") || "none"}`,
|
|
263
|
-
`# Warnings: ${result.warnings.length}`,
|
|
264
|
-
];
|
|
265
|
-
|
|
266
|
-
for (const w of result.warnings) {
|
|
267
|
-
header.push(`# - ${w}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
header.push(
|
|
271
|
-
"#",
|
|
272
|
-
"# Review this file, then run: switchboard ingest",
|
|
273
|
-
"# ============================================================",
|
|
274
|
-
"",
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
return header.join("\n") + YAML.stringify(result.draft);
|
|
278
|
-
}
|
package/src/lib/drift-guard.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drift guard — detects meaningful disagreement between the compiled
|
|
3
|
-
* loop objective and current.next_objective.
|
|
4
|
-
*
|
|
5
|
-
* Fails or warns honestly before packet/run proceeds with a mismatched
|
|
6
|
-
* objective. This prevents the operator from unknowingly dispatching
|
|
7
|
-
* against a stale or overridden objective.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync } from "fs";
|
|
11
|
-
import type { LoopContract, WorkingState } from "@switchboard/core";
|
|
12
|
-
import { loopPath } from "./paths";
|
|
13
|
-
import { readYaml } from "./yaml-io";
|
|
14
|
-
import * as out from "./output";
|
|
15
|
-
|
|
16
|
-
function compact(text: string): string {
|
|
17
|
-
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DriftCheckResult {
|
|
21
|
-
drifted: boolean;
|
|
22
|
-
loopObjective: string | null;
|
|
23
|
-
currentObjective: string;
|
|
24
|
-
reason: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Check if the compiled loop objective meaningfully disagrees with
|
|
29
|
-
* current.next_objective.
|
|
30
|
-
*
|
|
31
|
-
* Returns { drifted: false } when:
|
|
32
|
-
* - No loop.yaml exists (packet/run will compile one fresh)
|
|
33
|
-
* - The objectives match after whitespace normalization
|
|
34
|
-
*
|
|
35
|
-
* Returns { drifted: true } when:
|
|
36
|
-
* - loop.yaml exists AND its objective differs from current.next_objective
|
|
37
|
-
*/
|
|
38
|
-
export function checkObjectiveDrift(repoRoot: string, current: WorkingState): DriftCheckResult {
|
|
39
|
-
const loopFile = loopPath(repoRoot);
|
|
40
|
-
|
|
41
|
-
if (!existsSync(loopFile)) {
|
|
42
|
-
return {
|
|
43
|
-
drifted: false,
|
|
44
|
-
loopObjective: null,
|
|
45
|
-
currentObjective: current.next_objective,
|
|
46
|
-
reason: "No compiled loop.yaml — will compile fresh.",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const loop = readYaml<LoopContract>(loopFile);
|
|
51
|
-
if (!loop || !loop.objective) {
|
|
52
|
-
return {
|
|
53
|
-
drifted: false,
|
|
54
|
-
loopObjective: null,
|
|
55
|
-
currentObjective: current.next_objective,
|
|
56
|
-
reason: "loop.yaml exists but has no objective.",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const loopNorm = compact(loop.objective);
|
|
61
|
-
const currentNorm = compact(current.next_objective);
|
|
62
|
-
|
|
63
|
-
if (loopNorm === currentNorm) {
|
|
64
|
-
return {
|
|
65
|
-
drifted: false,
|
|
66
|
-
loopObjective: loop.objective,
|
|
67
|
-
currentObjective: current.next_objective,
|
|
68
|
-
reason: "Objectives match.",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
drifted: true,
|
|
74
|
-
loopObjective: loop.objective,
|
|
75
|
-
currentObjective: current.next_objective,
|
|
76
|
-
reason: "Compiled loop objective differs from current.next_objective.",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Enforce the drift guard. Returns true if safe to proceed, false if blocked.
|
|
82
|
-
* When --force is set, warns but proceeds.
|
|
83
|
-
*/
|
|
84
|
-
export function enforceDriftGuard(
|
|
85
|
-
repoRoot: string,
|
|
86
|
-
current: WorkingState,
|
|
87
|
-
force: boolean,
|
|
88
|
-
): boolean {
|
|
89
|
-
const check = checkObjectiveDrift(repoRoot, current);
|
|
90
|
-
|
|
91
|
-
if (!check.drifted) return true;
|
|
92
|
-
|
|
93
|
-
out.warn("Objective drift detected between loop.yaml and current.yaml");
|
|
94
|
-
out.section("Loop objective", check.loopObjective ?? "(none)");
|
|
95
|
-
out.section("Current objective", check.currentObjective);
|
|
96
|
-
|
|
97
|
-
if (force) {
|
|
98
|
-
out.warn("Proceeding with current.next_objective (--force).");
|
|
99
|
-
out.info("The compiled loop.yaml will be ignored; a fresh loop will be compiled.");
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
out.fail("Run `switchboard compile` to re-sync, or use --force to override.");
|
|
104
|
-
return false;
|
|
105
|
-
}
|
package/src/lib/errors.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI error handling — converts thrown errors into operator-facing messages.
|
|
3
|
-
*
|
|
4
|
-
* Wraps command action handlers so stack traces never leak to the user
|
|
5
|
-
* unless NODE_DEBUG=switchboard is set.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as out from "./output";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Wrap an async command action so errors produce clean CLI output
|
|
12
|
-
* instead of raw stack traces.
|
|
13
|
-
*/
|
|
14
|
-
export function withCliErrors<T extends unknown[]>(
|
|
15
|
-
fn: (...args: T) => Promise<void>,
|
|
16
|
-
): (...args: T) => Promise<void> {
|
|
17
|
-
return async (...args: T) => {
|
|
18
|
-
try {
|
|
19
|
-
await fn(...args);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
-
|
|
23
|
-
// Known error patterns → helpful operator messages
|
|
24
|
-
if (message.includes("Required file not found")) {
|
|
25
|
-
out.fail(message);
|
|
26
|
-
out.info("Run `switchboard init` to create the .switchboard/ directory first.");
|
|
27
|
-
} else if (message.includes("Could not find a Switchboard project root")) {
|
|
28
|
-
out.fail(message);
|
|
29
|
-
} else if (err && (err as any).issues && Array.isArray((err as any).issues)) {
|
|
30
|
-
// ZodError — extract human-readable validation messages
|
|
31
|
-
const issues = (err as any).issues as Array<{ path: string[]; message: string }>;
|
|
32
|
-
const summary = issues
|
|
33
|
-
.slice(0, 5)
|
|
34
|
-
.map(i => ` ${i.path.join(".")}: ${i.message}`)
|
|
35
|
-
.join("\n");
|
|
36
|
-
out.fail("Invalid YAML data — validation failed:");
|
|
37
|
-
console.error(summary);
|
|
38
|
-
if (issues.length > 5) {
|
|
39
|
-
out.info(` ... and ${issues.length - 5} more issue(s)`);
|
|
40
|
-
}
|
|
41
|
-
out.info("Check that the return file matches the expected SB_RETURN.yaml schema.");
|
|
42
|
-
} else if (message.includes("Return objective does not match")) {
|
|
43
|
-
out.fail(message);
|
|
44
|
-
out.info("The return's objective_attempted must exactly match the handoff objective.");
|
|
45
|
-
out.info("Check .switchboard/handoffs/ for the expected objective.");
|
|
46
|
-
} else if (message.includes("no active handoff")) {
|
|
47
|
-
out.fail(message);
|
|
48
|
-
out.info("Run `switchboard run claude` first to create a dispatch.");
|
|
49
|
-
} else {
|
|
50
|
-
out.fail(message);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Show stack trace only in debug mode
|
|
54
|
-
if (process.env["NODE_DEBUG"]?.includes("switchboard") && err instanceof Error) {
|
|
55
|
-
console.error(err.stack);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
package/src/lib/output.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI output helpers — structured console output with consistent formatting.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const RESET = "\x1b[0m";
|
|
6
|
-
const BOLD = "\x1b[1m";
|
|
7
|
-
const DIM = "\x1b[2m";
|
|
8
|
-
const GREEN = "\x1b[32m";
|
|
9
|
-
const YELLOW = "\x1b[33m";
|
|
10
|
-
const RED = "\x1b[31m";
|
|
11
|
-
const CYAN = "\x1b[36m";
|
|
12
|
-
|
|
13
|
-
export function heading(text: string): void {
|
|
14
|
-
console.log(`\n${BOLD}${CYAN}${text}${RESET}\n`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function success(text: string): void {
|
|
18
|
-
console.log(`${GREEN}✓${RESET} ${text}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function warn(text: string): void {
|
|
22
|
-
console.log(`${YELLOW}⚠${RESET} ${text}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function fail(text: string): void {
|
|
26
|
-
console.log(`${RED}✗${RESET} ${text}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function info(text: string): void {
|
|
30
|
-
console.log(`${DIM} ${text}${RESET}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function bullet(text: string): void {
|
|
34
|
-
console.log(` • ${text}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function fileWritten(label: string, path: string): void {
|
|
38
|
-
console.log(`${GREEN}✓${RESET} ${label}: ${DIM}${path}${RESET}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function section(label: string, value: string): void {
|
|
42
|
-
console.log(` ${BOLD}${label}:${RESET} ${value}`);
|
|
43
|
-
}
|
package/src/lib/paths.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path resolution helpers for Switchboard CLI.
|
|
3
|
-
*
|
|
4
|
-
* Discovers the repo root (by walking up to find .git or .switchboard/)
|
|
5
|
-
* and resolves all canonical .switchboard/ paths from there.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readdirSync, statSync } from "fs";
|
|
9
|
-
import { dirname, join, resolve } from "path";
|
|
10
|
-
|
|
11
|
-
const SWITCHBOARD_DIR = ".switchboard";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Walk up from `startDir` looking for a directory that contains
|
|
15
|
-
* `.switchboard/` or `.git/`. Returns the first match, or null.
|
|
16
|
-
*/
|
|
17
|
-
export function findRepoRoot(startDir: string = process.cwd()): string | null {
|
|
18
|
-
let dir = resolve(startDir);
|
|
19
|
-
const root = dirname(dir) === dir ? dir : undefined; // filesystem root guard
|
|
20
|
-
|
|
21
|
-
while (true) {
|
|
22
|
-
if (existsSync(join(dir, SWITCHBOARD_DIR)) || existsSync(join(dir, ".git"))) {
|
|
23
|
-
return dir;
|
|
24
|
-
}
|
|
25
|
-
const parent = dirname(dir);
|
|
26
|
-
if (parent === dir) return null; // hit filesystem root
|
|
27
|
-
dir = parent;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve repo root or throw with a helpful message.
|
|
33
|
-
*/
|
|
34
|
-
export function requireRepoRoot(startDir?: string): string {
|
|
35
|
-
const root = findRepoRoot(startDir);
|
|
36
|
-
if (!root) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
"Could not find a Switchboard project root.\n" +
|
|
39
|
-
"Run `switchboard init` to create a .switchboard/ directory, or run from inside a git repo.",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return root;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Canonical path helpers
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
export function sbDir(repoRoot: string): string {
|
|
50
|
-
return join(repoRoot, SWITCHBOARD_DIR);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function contractPath(repoRoot: string): string {
|
|
54
|
-
return join(sbDir(repoRoot), "project", "contract.yaml");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function currentStatePath(repoRoot: string): string {
|
|
58
|
-
return join(sbDir(repoRoot), "working", "current.yaml");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function loopPath(repoRoot: string): string {
|
|
62
|
-
return join(sbDir(repoRoot), "working", "loop.yaml");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function specPath(repoRoot: string): string {
|
|
66
|
-
return join(sbDir(repoRoot), "artifacts", "SB_SPEC.md");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function packetsDir(repoRoot: string): string {
|
|
70
|
-
return join(sbDir(repoRoot), "packets");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function handoffsDir(repoRoot: string): string {
|
|
74
|
-
return join(sbDir(repoRoot), "handoffs");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function receiptsDir(repoRoot: string): string {
|
|
78
|
-
return join(sbDir(repoRoot), "receipts");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function evaluationsDir(repoRoot: string): string {
|
|
82
|
-
return join(sbDir(repoRoot), "evaluations");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function returnsDir(repoRoot: string): string {
|
|
86
|
-
return join(sbDir(repoRoot), "returns");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function dispatchMetaPath(repoRoot: string, dispatchId: string): string {
|
|
90
|
-
return join(sbDir(repoRoot), "working", `dispatch-${dispatchId}.yaml`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function returnTemplatePath(repoRoot: string, dispatchId: string): string {
|
|
94
|
-
return join(returnsDir(repoRoot), `${dispatchId}-template.yaml`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function proofDir(repoRoot: string, dispatchId: string): string {
|
|
98
|
-
return join(sbDir(repoRoot), "proof", dispatchId);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function proofManifestPath(repoRoot: string, dispatchId: string): string {
|
|
102
|
-
return join(proofDir(repoRoot, dispatchId), "manifest.yaml");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Latest-file lookup
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Find the most recently modified YAML file in a directory.
|
|
111
|
-
* Returns the full path, or null if the dir is empty / doesn't exist.
|
|
112
|
-
*/
|
|
113
|
-
export function latestYamlIn(dirPath: string): string | null {
|
|
114
|
-
if (!existsSync(dirPath)) return null;
|
|
115
|
-
|
|
116
|
-
const files = readdirSync(dirPath)
|
|
117
|
-
.filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
118
|
-
.map(f => {
|
|
119
|
-
const full = join(dirPath, f);
|
|
120
|
-
return { path: full, mtime: statSync(full).mtimeMs };
|
|
121
|
-
})
|
|
122
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
123
|
-
|
|
124
|
-
return files[0]?.path ?? null;
|
|
125
|
-
}
|