copperhead 0.9.0 → 0.11.0
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/NOTICE +1 -1
- package/README.md +47 -8
- package/dist/agent/context.js +2 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/dock-renderer.js +2 -2
- package/dist/agent/dock-renderer.js.map +1 -1
- package/dist/agent/envelope.js +105 -0
- package/dist/agent/envelope.js.map +1 -0
- package/dist/agent/filetools.js +24 -1
- package/dist/agent/filetools.js.map +1 -1
- package/dist/agent/ledger.js +24 -0
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/loop.js +63 -72
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +4 -3
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/claude-code.js +17 -1
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/codex.js +84 -39
- package/dist/agent/providers/codex.js.map +1 -1
- package/dist/agent/providers/tool-protocol.js +21 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -1
- package/dist/agent/recovery.js +186 -15
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/registry.js +49 -0
- package/dist/agent/registry.js.map +1 -0
- package/dist/agent/render.js +2 -2
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/theme.js +10 -5
- package/dist/agent/theme.js.map +1 -1
- package/dist/agent/tools.js +100 -586
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js +2 -0
- package/dist/agent/transcript.js.map +1 -1
- package/dist/capabilities/define.js +35 -0
- package/dist/capabilities/define.js.map +1 -0
- package/dist/capabilities/handlers.js +744 -0
- package/dist/capabilities/handlers.js.map +1 -0
- package/dist/capabilities/helpers.js +39 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/index.js +50 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/skills/generate-report.js +23 -0
- package/dist/capabilities/skills/generate-report.js.map +1 -0
- package/dist/cli.js +158 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +33 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/create.js +181 -26
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +83 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/skill.js +109 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/config.js +19 -6
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +24 -3
- package/dist/kicad/bootstrap.js.map +1 -1
- package/dist/kicad/cli.js +106 -18
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/dossier.js +207 -0
- package/dist/kicad/dossier.js.map +1 -0
- package/dist/kicad/draft/draft.js +135 -0
- package/dist/kicad/draft/draft.js.map +1 -0
- package/dist/kicad/draft/engine.js +5310 -0
- package/dist/kicad/draft/engine.js.map +1 -0
- package/dist/kicad/draft/ir.js +368 -0
- package/dist/kicad/draft/ir.js.map +1 -0
- package/dist/kicad/draft/symsource.js +504 -0
- package/dist/kicad/draft/symsource.js.map +1 -0
- package/dist/kicad/emit.js +220 -0
- package/dist/kicad/emit.js.map +1 -0
- package/dist/kicad/fab.js +13 -0
- package/dist/kicad/fab.js.map +1 -1
- package/dist/kicad/legibility.js +608 -0
- package/dist/kicad/legibility.js.map +1 -0
- package/dist/kicad/score.js +431 -0
- package/dist/kicad/score.js.map +1 -0
- package/dist/kicad/sexp.js +265 -6
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/kicad/symlib.js +346 -16
- package/dist/kicad/symlib.js.map +1 -1
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/bom-table.js +75 -39
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/scaffold.js +14 -1
- package/dist/memory/scaffold.js.map +1 -1
- package/dist/util/redact.js +6 -0
- package/dist/util/redact.js.map +1 -1
- package/package.json +13 -8
- package/src/agent/context.ts +35 -0
- package/src/agent/dock-renderer.ts +3 -2
- package/src/agent/envelope.ts +124 -0
- package/src/agent/filetools.ts +26 -1
- package/src/agent/ledger.ts +24 -0
- package/src/agent/loop.ts +73 -78
- package/src/agent/prompts.ts +4 -3
- package/src/agent/providers/claude-code.ts +22 -1
- package/src/agent/providers/codex.ts +91 -42
- package/src/agent/providers/tool-protocol.ts +22 -0
- package/src/agent/recovery.ts +183 -13
- package/src/agent/registry.ts +58 -0
- package/src/agent/render.ts +4 -3
- package/src/agent/theme.ts +15 -5
- package/src/agent/tools.ts +124 -628
- package/src/agent/transcript.ts +6 -0
- package/src/agent/types.ts +10 -5
- package/src/capabilities/define.ts +88 -0
- package/src/capabilities/handlers.ts +769 -0
- package/src/capabilities/helpers.ts +37 -0
- package/src/capabilities/index.ts +53 -0
- package/src/capabilities/skills/generate-report.ts +25 -0
- package/src/cli.ts +154 -0
- package/src/commands/check.ts +51 -1
- package/src/commands/create.ts +183 -21
- package/src/commands/doctor.ts +85 -6
- package/src/commands/skill.ts +127 -0
- package/src/commands/sync.ts +5 -3
- package/src/config.ts +56 -8
- package/src/kicad/bootstrap.ts +24 -3
- package/src/kicad/cli.ts +129 -18
- package/src/kicad/dossier.ts +217 -0
- package/src/kicad/draft/draft.ts +173 -0
- package/src/kicad/draft/engine.ts +5274 -0
- package/src/kicad/draft/ir.ts +416 -0
- package/src/kicad/draft/symsource.ts +549 -0
- package/src/kicad/emit.ts +300 -0
- package/src/kicad/fab.ts +15 -0
- package/src/kicad/legibility.ts +695 -0
- package/src/kicad/score.ts +502 -0
- package/src/kicad/sexp.ts +346 -6
- package/src/kicad/symlib.ts +364 -18
- package/src/mcp/server.ts +560 -0
- package/src/memory/bom-table.ts +85 -38
- package/src/memory/scaffold.ts +14 -1
- package/src/util/redact.ts +6 -0
- package/dist/memory/synap.js +0 -152
- package/dist/memory/synap.js.map +0 -1
- package/src/memory/synap.ts +0 -217
package/src/commands/create.ts
CHANGED
|
@@ -3,20 +3,24 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { loadConfig, resolveCompatSettings } from '../config.js';
|
|
6
|
-
import { bootstrapKicadProject } from '../kicad/bootstrap.js';
|
|
6
|
+
import { bootstrapKicadProject, markCreateOrigin } from '../kicad/bootstrap.js';
|
|
7
7
|
import { exportSvg, runErc } from '../kicad/cli.js';
|
|
8
8
|
import { listSymbols } from '../kicad/sexp.js';
|
|
9
|
+
import { checkLegibility } from '../kicad/legibility.js';
|
|
10
|
+
import { draftSchematicToText, defaultIntentPath } from '../kicad/draft/draft.js';
|
|
9
11
|
import { isDirty, commitAll, changedFiles } from '../util/git.js';
|
|
10
12
|
import type { CompatSettings, CopperheadConfig } from '../config.js';
|
|
11
13
|
import { checkDrift } from '../memory/drift.js';
|
|
12
14
|
import { runAgentLoop, makeProvider, type BudgetExhaustedStats } from '../agent/loop.js';
|
|
13
|
-
import { diagnoseStageFailure, transcriptExcerpt, withTimeout, type StageDiagnosis } from '../agent/recovery.js';
|
|
15
|
+
import { diagnoseStageFailure, transcriptExcerpt, withTimeout, symbolAvailabilityFacts, type StageDiagnosis } from '../agent/recovery.js';
|
|
14
16
|
import type { Provider } from '../agent/types.js';
|
|
15
17
|
import type { RunMetaInput } from '../agent/runmeta.js';
|
|
16
18
|
import { fmtDuration, fmtTokens, type ProgressRenderer } from '../agent/render.js';
|
|
17
19
|
import { copper, dim, ok, stageLine, warn } from '../agent/theme.js';
|
|
18
20
|
import { openspecInit } from '../openspec/cli.js';
|
|
19
21
|
import { sweepStaleTempDirs, pruneHistoryDir } from '../util/tmp.js';
|
|
22
|
+
import { bomSymbolDossier } from '../kicad/dossier.js';
|
|
23
|
+
import { symbolSearchDirs } from '../kicad/symlib.js';
|
|
20
24
|
import { assertDiskSpace, DEFAULT_MIN_FREE_BYTES } from '../util/preflight.js';
|
|
21
25
|
import { runCheck } from './check.js';
|
|
22
26
|
import { emitCreateJlcpcbBom } from './export.js';
|
|
@@ -55,6 +59,32 @@ async function docHasHeading(repoRoot: string, rel: string, word: string): Promi
|
|
|
55
59
|
return re.test(await readFile(p, 'utf8'));
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
export async function writeBriefHash(
|
|
63
|
+
repoRoot: string,
|
|
64
|
+
docsDir: string,
|
|
65
|
+
briefMeta: { path: string; sha256: string },
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const file = path.join(repoRoot, docsDir, 'BRIEF.sha256');
|
|
68
|
+
|
|
69
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await writeFile(
|
|
73
|
+
file,
|
|
74
|
+
`brief: ${briefMeta.path}
|
|
75
|
+
sha256: ${briefMeta.sha256}
|
|
76
|
+
`,
|
|
77
|
+
{
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
flag: 'wx',
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const e = err as NodeJS.ErrnoException;
|
|
84
|
+
if (e.code !== 'EEXIST') throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
/**
|
|
59
89
|
* Returns true when a directory exists and contains at least one file
|
|
60
90
|
* matching the optional glob-style extension list (case-insensitive).
|
|
@@ -86,16 +116,23 @@ export const STAGES: Stage[] = [
|
|
|
86
116
|
const p = path.join(root, docs, 'SPEC.md');
|
|
87
117
|
if (!existsSync(p)) return false;
|
|
88
118
|
const text = await readFile(p, 'utf8');
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
const heading = /^(#{1,6})\s.*\bBudgets?\b.*$/im.exec(text);
|
|
120
|
+
if (!heading) return false;
|
|
121
|
+
// The section runs to the next heading of the SAME OR SHALLOWER depth, not to
|
|
122
|
+
// the next heading of any depth (I17): a real spec routinely splits its budgets
|
|
123
|
+
// into subsections — "## 3. Electrical budgets" followed immediately by
|
|
124
|
+
// "### 3.1 Input and rails" — which leaves the parent's own body empty and read
|
|
125
|
+
// as an unfilled placeholder. Subheadings are part of the section, not its end.
|
|
126
|
+
const depth = heading[1]!.length;
|
|
127
|
+
const afterHeadingLine = text.slice(heading.index + heading[0].length);
|
|
128
|
+
const nextSection = afterHeadingLine.search(new RegExp(`^#{1,${depth}}\\s`, 'm'));
|
|
96
129
|
const section = nextSection >= 0 ? afterHeadingLine.slice(0, nextSection) : afterHeadingLine;
|
|
130
|
+
// Strip HTML comments (single or multi-line); headings inside the section are
|
|
131
|
+
// structure, not content, so a section of nothing but subheadings still fails.
|
|
97
132
|
const cleanSection = section.replace(/<!--[\s\S]*?-->/g, '');
|
|
98
|
-
const realLines = cleanSection
|
|
133
|
+
const realLines = cleanSection
|
|
134
|
+
.split('\n')
|
|
135
|
+
.filter((l) => l.trim().length > 0 && !/^#{1,6}\s/.test(l.trim()));
|
|
99
136
|
return realLines.length > 0;
|
|
100
137
|
},
|
|
101
138
|
prompt: (brief) =>
|
|
@@ -148,7 +185,7 @@ export const STAGES: Stage[] = [
|
|
|
148
185
|
});
|
|
149
186
|
},
|
|
150
187
|
prompt: () =>
|
|
151
|
-
'Stage 3: part selection. Write docs/BOM.md with the fixed table format (| Refdes | Value | Footprint | MPN | Rationale |). Every MPN you introduce is flagged UNVERIFIED with a datasheet-verifiable justification. Check leakage/quiescent current of every part against the power budget. Run check_drift before finishing.',
|
|
188
|
+
'Stage 3: part selection. Write docs/BOM.md with the fixed table format (| Refdes | Value | Footprint | MPN | Rationale |). The Value column holds the COMPONENT VALUE and nothing else — "4.7uF", "1M", "500mAh Li-Po", "STM32F103C8T6" — because stage 4 draws it on the sheet as that part\'s Value field, where a description ("1S Li-Po cell, 500 mAh, bare leads") collides with neighbouring symbols and fails the legibility gate. Put the prose in the Rationale column instead; that is the column for it, and nothing draws it. One row per refdes: a grouped row ("SW3-SW16", "C5-C8") is not a BOM row and the schematic stage cannot match it. Every MPN you introduce is flagged UNVERIFIED with a datasheet-verifiable justification. Check leakage/quiescent current of every part against the power budget. The design must be capturable with the KiCad symbol libraries installed on THIS machine: run search_symbols for every IC, module, connector and other active part before committing it to the BOM, and if a part has no installed symbol, pick one that has — stage 4 draws only from installed symbols, and a BOM row it cannot resolve makes the whole run unwinnable. Existence is not enough: confirm the chosen symbol with symbol_pins so the pin numbers you wire in stage 4 are real. Multi-unit symbols (gate packs, dual opamps) are fine — the engine places each unit separately under the one refdes, and net endpoints use plain package pin numbers. Run check_drift before finishing.',
|
|
152
189
|
},
|
|
153
190
|
{
|
|
154
191
|
name: 'schematic',
|
|
@@ -174,10 +211,35 @@ export const STAGES: Stage[] = [
|
|
|
174
211
|
// schematic, advancing the pipeline against unverified work. Returning
|
|
175
212
|
// false here keeps the stage active so it re-runs, fixes ERC, and commits
|
|
176
213
|
// through the normal finish gate.
|
|
177
|
-
|
|
214
|
+
if (!(await runErc(p)).ok) return false;
|
|
215
|
+
// Legibility is the one stage-4 output no electrical gate sees (AC-16.22):
|
|
216
|
+
// an ERC-clean sheet with text over symbol bodies passes everything above
|
|
217
|
+
// while being unreviewable. Error-severity findings keep the stage active;
|
|
218
|
+
// advisories never block.
|
|
219
|
+
const legibility = await checkLegibility(p, {
|
|
220
|
+
docsDir: path.join(root, config.docs),
|
|
221
|
+
...(config.legibility ? { config: config.legibility } : {}),
|
|
222
|
+
});
|
|
223
|
+
if (legibility.counts.error !== 0) return false;
|
|
224
|
+
// Drafting mode: the schematic must match a re-draft of the current IR
|
|
225
|
+
// (AC-16.20) — an intent edited after the last draft_schematic call means
|
|
226
|
+
// the sheet on disk no longer reflects the design and the stage stays
|
|
227
|
+
// active until a re-draft.
|
|
228
|
+
const intentRel = defaultIntentPath(config.schematic);
|
|
229
|
+
if (existsSync(path.join(root, intentRel))) {
|
|
230
|
+
const dry = await draftSchematicToText({
|
|
231
|
+
repoRoot: root,
|
|
232
|
+
schematic: config.schematic,
|
|
233
|
+
intentPath: intentRel,
|
|
234
|
+
docsDir: config.docs,
|
|
235
|
+
});
|
|
236
|
+
if (!dry.ok) return false;
|
|
237
|
+
if (dry.text !== (await readFile(p, 'utf8'))) return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
178
240
|
},
|
|
179
241
|
prompt: () =>
|
|
180
|
-
'Stage 4: schematic. An empty KiCad project has already been scaffolded and wired into .copperhead/config.json
|
|
242
|
+
'Stage 4: schematic. An empty KiCad project has already been scaffolded and wired into .copperhead/config.json. You author INTENT, never geometry: write the netlist-intent IR and call draft_schematic — the deterministic engine computes every coordinate, wire, label, power symbol, and group box, and the sheet it draws satisfies the drafting standard by construction (captioned group boxes per SUBSYSTEMS.md subsystem, left-to-right flow, rails up and grounds down, net labels between groups, filled title block). The IR (schematic.intent.json) is JSON: {"version": 1, "parts": [{"ref", "libId", "value", "footprint", "group"}], "nets": [{"name", "pins": ["REF.PIN", …], "kind"?}], "noConnect": ["REF.PIN", …], "hints"?: {"groupOrder"?, "paper"?, "date"?}}. Build it from BOM.md (same refdes and values — validation cross-checks and refuses mismatches) and SUBSYSTEMS.md (every non-power part names one subsystem heading as its group). Use exact canonical KiCad lib_ids (e.g. Device:R) and REAL pin numbers from the library: the pin dossier below (when present) already lists every BOM part\'s installed symbol and its real pins — work from it and from symbol_pins rather than reading .kicad_sym files, and validation lists a part\'s actual pins when you name one that does not exist. Name nets as a reader expects: a bus or interface shares a prefix (I2S_BCLK, I2S_DIN, I2S_LRCLK; SPI_…; BTN_…) so the drawing colours the family together, differential pairs end in +/- or P/N, and part values carry their unit (F, H, R). Declare every deliberately unused pin in noConnect; power rails are recognized from pin types automatically (override with "kind" only when the inference is wrong — the draft report lists every net\'s resolved class). Pass the full IR as intent_json to draft_schematic; the report embeds the legibility findings and the score for the fresh sheet. To repair ANY finding (ERC, legibility, validation), fix the IR and call draft_schematic again — edit_file is refused on the drafted sheet. Text-collision findings scale with TEXT LENGTH: a net label, part value, or SUBSYSTEMS heading that is shorter draws a smaller box, so renaming a colliding net (and updating PINOUT.md) or tightening a long heading is a real repair lever; paper size and declaration order are not (placement is grid-derived). When the draft is clean run run_erc and check_drift, update PINOUT.md to match the IR\'s pin assignments, and finish.',
|
|
181
243
|
},
|
|
182
244
|
{
|
|
183
245
|
name: 'layout-draft',
|
|
@@ -259,6 +321,36 @@ async function emitJlcpcbAfterOutputs(stageName: string, opts: CreateOptions): P
|
|
|
259
321
|
if (out) opts.log(stageLine('outputs', `emitted ${out} (JLCPCB assembly BOM)`, 'ok'));
|
|
260
322
|
}
|
|
261
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The generic "contract not met" line names no defect. For the schematic stage
|
|
326
|
+
* the most common gap after the electrical gates go green is legibility, so
|
|
327
|
+
* name the finding counts by kind — the resume then starts on the actual work
|
|
328
|
+
* instead of rediscovering it.
|
|
329
|
+
*/
|
|
330
|
+
async function contractGapDetail(stageName: string, root: string, config: CopperheadConfig): Promise<string> {
|
|
331
|
+
const generic = 'the run finished but the stage completion contract is not met — no usable artifact was produced';
|
|
332
|
+
if (stageName !== 'schematic' || !config.schematic) return generic;
|
|
333
|
+
const p = path.join(root, config.schematic);
|
|
334
|
+
if (!existsSync(p)) return generic;
|
|
335
|
+
try {
|
|
336
|
+
const report = await checkLegibility(p, {
|
|
337
|
+
docsDir: path.join(root, config.docs),
|
|
338
|
+
...(config.legibility ? { config: config.legibility } : {}),
|
|
339
|
+
});
|
|
340
|
+
if (report.counts.error > 0) {
|
|
341
|
+
const byKind = new Map<string, number>();
|
|
342
|
+
for (const f of report.findings.filter((f) => f.severity === 'error')) {
|
|
343
|
+
byKind.set(f.kind, (byKind.get(f.kind) ?? 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
const counts = [...byKind].map(([k, n]) => `${k}: ${n}`).join(', ');
|
|
346
|
+
return `the schematic stage contract is not met: ${report.counts.error} error-severity legibility finding(s) remain (${counts}); resume to reconcile them`;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// fall through: an unreadable schematic already fails earlier contract steps
|
|
350
|
+
}
|
|
351
|
+
return generic;
|
|
352
|
+
}
|
|
353
|
+
|
|
262
354
|
/** Stages whose output is a KiCad file worth rendering to an image (5.4). */
|
|
263
355
|
const KICAD_STAGES = new Set(['schematic', 'layout-draft', 'outputs']);
|
|
264
356
|
|
|
@@ -278,7 +370,10 @@ function isManagedPath(f: string, config: CopperheadConfig): boolean {
|
|
|
278
370
|
f.startsWith('openspec/') ||
|
|
279
371
|
f.startsWith('outputs/') ||
|
|
280
372
|
f.startsWith('firmware/') ||
|
|
373
|
+
f.startsWith('sym-lib-cache/') ||
|
|
281
374
|
f === '.gitignore' ||
|
|
375
|
+
path.basename(f) === 'sym-lib-table' ||
|
|
376
|
+
path.basename(f) === 'schematic.intent.json' ||
|
|
282
377
|
/\.(kicad_sch|kicad_pcb|kicad_pro|kicad_prl)$/.test(f)
|
|
283
378
|
);
|
|
284
379
|
}
|
|
@@ -387,15 +482,22 @@ async function diagnose(input: {
|
|
|
387
482
|
const p = provider;
|
|
388
483
|
const excerpt = await transcriptExcerpt(input.transcriptDir);
|
|
389
484
|
return await withTimeout(
|
|
390
|
-
() =>
|
|
391
|
-
|
|
485
|
+
async () => {
|
|
486
|
+
// Fact-check symbol-availability claims before the model judges them: a
|
|
487
|
+
// refusal narrating "library not installed" is adjudicated from the
|
|
488
|
+
// machine's actual libraries, not from the narration (I15/#197). Inside
|
|
489
|
+
// the watchdog, so a wedged filesystem scan cannot outlive timeoutMs.
|
|
490
|
+
const symbolFacts = await symbolAvailabilityFacts(`${input.failure}\n${excerpt}`).catch(() => '');
|
|
491
|
+
return diagnoseStageFailure(p, {
|
|
392
492
|
stageName: input.stageName,
|
|
393
493
|
stageGoal: input.stageGoal,
|
|
394
494
|
failure: input.failure,
|
|
395
495
|
excerpt,
|
|
396
496
|
attempt: input.attempt,
|
|
397
497
|
maxAttempts: input.maxAttempts,
|
|
398
|
-
|
|
498
|
+
...(symbolFacts ? { symbolFacts } : {}),
|
|
499
|
+
});
|
|
500
|
+
},
|
|
399
501
|
input.timeoutMs,
|
|
400
502
|
() => p.close?.(),
|
|
401
503
|
);
|
|
@@ -643,7 +745,16 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
643
745
|
const brief = await readFile(path.resolve(opts.briefPath), 'utf8');
|
|
644
746
|
// Hashed from the content already in hand: a brief edited mid-pipeline shows
|
|
645
747
|
// up as a different sha256 in the next stage's metadata (AC-8.1).
|
|
646
|
-
const
|
|
748
|
+
const resolvedBrief = path.resolve(opts.briefPath);
|
|
749
|
+
const relativeBrief = path.relative(opts.repoRoot, resolvedBrief);
|
|
750
|
+
const briefPath =
|
|
751
|
+
relativeBrief.startsWith('..') || path.isAbsolute(relativeBrief)
|
|
752
|
+
? `external:${path.basename(resolvedBrief)}`
|
|
753
|
+
: relativeBrief;
|
|
754
|
+
const briefMeta = {
|
|
755
|
+
path: briefPath,
|
|
756
|
+
sha256: createHash('sha256').update(brief).digest('hex'),
|
|
757
|
+
};
|
|
647
758
|
const config = await loadConfig(opts.repoRoot);
|
|
648
759
|
// Fail fast on a nearly-full disk (4.1): a create run writes fab outputs and
|
|
649
760
|
// KiCad local history and can otherwise fill the disk mid-stage, failing with
|
|
@@ -663,6 +774,11 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
663
774
|
const pruned = await pruneHistoryDir(opts.repoRoot);
|
|
664
775
|
if (pruned) opts.log(dim(`startup: pruned ${pruned} old .history/ entrie(s) to cap local-history growth`));
|
|
665
776
|
await openspecInit(opts.repoRoot);
|
|
777
|
+
// Stamp the repo create-produced before any stage runs: the marker scopes the
|
|
778
|
+
// legibility finish gate and the fab release gate, and it must hold on
|
|
779
|
+
// resumed runs whose project predates the marker (bootstrapKicadProject
|
|
780
|
+
// no-ops on those, so it cannot be the only writer).
|
|
781
|
+
await markCreateOrigin(opts.repoRoot);
|
|
666
782
|
const completed: string[] = [];
|
|
667
783
|
const stageCosts: StageCost[] = [];
|
|
668
784
|
|
|
@@ -682,6 +798,15 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
682
798
|
opts.log(stageLine(stage.name, 'already complete (resuming past it)', 'ok'));
|
|
683
799
|
await commitResumedStage(opts, config, stage.name);
|
|
684
800
|
completed.push(stage.name);
|
|
801
|
+
if (stage.name === 'spec-seed') {
|
|
802
|
+
try {
|
|
803
|
+
await writeBriefHash(opts.repoRoot, config.docs, briefMeta);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
opts.log(
|
|
806
|
+
`warning: could not record brief provenance (${(err as Error).message})`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
685
810
|
stageCosts.push({ name: stage.name, resumed: true, wallMs: 0, turns: 0, tokensIn: 0, tokensOut: 0, cacheHits: 0 });
|
|
686
811
|
await emitJlcpcbAfterOutputs(stage.name, opts);
|
|
687
812
|
continue;
|
|
@@ -721,13 +846,38 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
721
846
|
`running${attempt > 1 ? ` (attempt ${attempt}/${config.maxStageRetries + 1})` : ''}`,
|
|
722
847
|
),
|
|
723
848
|
);
|
|
849
|
+
// The BOM freezes before this stage, so every part's real pins are
|
|
850
|
+
// computable before the first turn — recomputed per attempt, since a
|
|
851
|
+
// rolled-back retry can run against a different BOM than its
|
|
852
|
+
// predecessor. Advisory only: any failure degrades to no block.
|
|
853
|
+
let dossierBlock = '';
|
|
854
|
+
if (stage.name === 'schematic') {
|
|
855
|
+
try {
|
|
856
|
+
const bomPath = path.join(opts.repoRoot, config.docs, 'BOM.md');
|
|
857
|
+
if (existsSync(bomPath)) {
|
|
858
|
+
// Bounded: a slow or wedged library scan must delay the stage by a
|
|
859
|
+
// fixed cost at most — on timeout the stage simply runs dossier-less.
|
|
860
|
+
const dossier = await withTimeout(
|
|
861
|
+
async () => bomSymbolDossier(await readFile(bomPath, 'utf8'), await symbolSearchDirs()),
|
|
862
|
+
60_000,
|
|
863
|
+
);
|
|
864
|
+
if (dossier) {
|
|
865
|
+
dossierBlock =
|
|
866
|
+
'\n\n## Installed-symbol pin dossier (machine-verified)\nEach BOM part resolved against the KiCad libraries installed on THIS machine: the top name-match lib_id and its REAL pins (number=name/electrical-type). Confirm the match fits the BOM part; alternatives are listed. Passives (R/C/L) draw from their canonical Device symbols and are omitted. Use these pins for REF.PIN endpoints instead of reading .kicad_sym files; for any part not listed, call symbol_pins.\n' +
|
|
867
|
+
dossier;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
// the dossier is context, never a gate — the stage runs without it
|
|
872
|
+
}
|
|
873
|
+
}
|
|
724
874
|
const res = await runAgentLoop({
|
|
725
875
|
repoRoot: opts.repoRoot,
|
|
726
876
|
model: opts.model,
|
|
727
877
|
request: `create pipeline stage: ${stage.name}`,
|
|
728
878
|
stagePrompt: guidance
|
|
729
|
-
? `${basePrompt}\n\n## Recovery guidance (a previous attempt did not complete this stage — do this differently)\n${guidance}`
|
|
730
|
-
: basePrompt
|
|
879
|
+
? `${basePrompt}${dossierBlock}\n\n## Recovery guidance (a previous attempt did not complete this stage — do this differently)\n${guidance}`
|
|
880
|
+
: `${basePrompt}${dossierBlock}`,
|
|
731
881
|
interactive: opts.interactive ?? false,
|
|
732
882
|
allowDirty: true, // stages build on each other's uncommitted state within the pipeline
|
|
733
883
|
...(stageTurns !== undefined ? { maxTurns: stageTurns } : {}),
|
|
@@ -755,11 +905,14 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
755
905
|
// with the schematic stage: one header edit, ERC "clean" on an empty
|
|
756
906
|
// sheet). Advancing anyway lets every later stage run against a design
|
|
757
907
|
// that isn't there, so the completion contract is the real gate.
|
|
908
|
+
// The run's own reason rides along: the diagnosis transcript excerpt holds
|
|
909
|
+
// only assistant text and tool results, so without it a hung call or a turn
|
|
910
|
+
// stopped at the hard cap reaches the diagnosis as a bare "provider-error".
|
|
758
911
|
const failure =
|
|
759
912
|
res.outcome !== 'success'
|
|
760
|
-
? `the run ended as "${res.outcome}" (${res.exitPath})`
|
|
913
|
+
? `the run ended as "${res.outcome}" (${res.exitPath})${res.summary ? `: ${res.summary}` : ''}`
|
|
761
914
|
: !(await stage.isComplete(opts.repoRoot, config.docs))
|
|
762
|
-
?
|
|
915
|
+
? await contractGapDetail(stage.name, opts.repoRoot, config)
|
|
763
916
|
: null;
|
|
764
917
|
if (!failure) {
|
|
765
918
|
stageDone = true;
|
|
@@ -818,6 +971,15 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
818
971
|
return { ok: false, completed };
|
|
819
972
|
}
|
|
820
973
|
completed.push(stage.name);
|
|
974
|
+
if (stage.name === 'spec-seed') {
|
|
975
|
+
try {
|
|
976
|
+
await writeBriefHash(opts.repoRoot, config.docs, briefMeta);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
opts.log(
|
|
979
|
+
`warning: could not record brief provenance (${(err as Error).message})`,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
821
983
|
await renderStageArtifacts(opts, stage.name, stageTranscriptDir);
|
|
822
984
|
await emitJlcpcbAfterOutputs(stage.name, opts);
|
|
823
985
|
logCumulative(opts, stageCosts);
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { execa } from 'execa';
|
|
3
4
|
import { existsSync } from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import {
|
|
@@ -12,8 +13,9 @@ import {
|
|
|
12
13
|
type CompatSettings,
|
|
13
14
|
type CopperheadConfig,
|
|
14
15
|
} from '../config.js';
|
|
15
|
-
import { kicadCliVersion } from '../kicad/cli.js';
|
|
16
|
+
import { kicadCliVersion, MIN_KICAD_MAJOR } from '../kicad/cli.js';
|
|
16
17
|
import { redactSecrets } from '../util/redact.js';
|
|
18
|
+
import { isNotFoundError } from '../util/preflight.js';
|
|
17
19
|
|
|
18
20
|
const execFileP = promisify(execFile);
|
|
19
21
|
|
|
@@ -44,6 +46,7 @@ export interface DoctorDeps {
|
|
|
44
46
|
nodeVersion: string;
|
|
45
47
|
kicadVersion: () => Promise<string>;
|
|
46
48
|
gitVersion: () => Promise<string>;
|
|
49
|
+
openspecVersion: () => Promise<string>;
|
|
47
50
|
env: NodeJS.ProcessEnv;
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -54,6 +57,15 @@ function defaultDeps(): DoctorDeps {
|
|
|
54
57
|
// `git --version` prints "git version 2.34.1"; keep only the number, the
|
|
55
58
|
// report already labels the row "git".
|
|
56
59
|
gitVersion: async () => (await execFileP('git', ['--version'])).stdout.trim().replace(/^git version\s+/, ''),
|
|
60
|
+
/**
|
|
61
|
+
* Probes `openspec --version` via execa, the same probe shape as the real
|
|
62
|
+
* call site (src/openspec/cli.ts): execa resolves Windows .cmd/.bat shims
|
|
63
|
+
* via cross-spawn without a shell, so a missing binary still yields ENOENT
|
|
64
|
+
* (what isNotFoundError expects) on every platform, instead of a
|
|
65
|
+
* shell-reported exit 127/"not found".
|
|
66
|
+
* @returns the trimmed stdout of `openspec --version` (e.g. "1.8.0").
|
|
67
|
+
*/
|
|
68
|
+
openspecVersion: async () => (await execa('openspec', ['--version'])).stdout.trim(),
|
|
57
69
|
env: process.env,
|
|
58
70
|
};
|
|
59
71
|
}
|
|
@@ -73,15 +85,46 @@ function nodeCheck(version: string): DoctorCheck {
|
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
// MIN_KICAD_MAJOR (imported from kicad/cli.ts) enforces the minimum supported KiCad version for ERC/DRC.
|
|
89
|
+
// Behaviour note: kicadCheck() returns status: 'fail' (exit non-zero) for any
|
|
90
|
+
// kicad-cli version below this threshold, and for output that contains no
|
|
91
|
+
// parseable N.N version token. This is intentional user-visible behaviour and
|
|
92
|
+
// is NOT a no-op hint correction: users on KiCad 7 will see doctor fail where
|
|
93
|
+
// it previously succeeded. The gate mirrors the README's stated requirement and
|
|
94
|
+
// follows the same pattern as MIN_NODE_MAJOR above.
|
|
95
|
+
|
|
76
96
|
async function kicadCheck(probe: () => Promise<string>): Promise<DoctorCheck> {
|
|
77
97
|
try {
|
|
78
|
-
|
|
98
|
+
const raw = await probe();
|
|
99
|
+
// Anchored match: require a version-shaped token at the start of a line so
|
|
100
|
+
// an incidental N.N elsewhere in stdout (e.g. "OpenGL 3.2 unsupported")
|
|
101
|
+
// does not shadow the real KiCad version. The `m` flag makes ^ match
|
|
102
|
+
// line-starts within a multi-line string.
|
|
103
|
+
const match = raw.match(/(?:^|\n)\s*(?:kicad-cli\s+)?v?(\d+)\.\d+/m);
|
|
104
|
+
const major = match ? Number(match[1]) : NaN;
|
|
105
|
+
if (!Number.isFinite(major)) {
|
|
106
|
+
return {
|
|
107
|
+
name: 'kicad-cli',
|
|
108
|
+
status: 'fail',
|
|
109
|
+
detail: raw || 'could not parse version',
|
|
110
|
+
hint: `copperhead needs KiCad >= ${MIN_KICAD_MAJOR}; install or configure a compatible kicad-cli.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (major < MIN_KICAD_MAJOR) {
|
|
114
|
+
return {
|
|
115
|
+
name: 'kicad-cli',
|
|
116
|
+
status: 'fail',
|
|
117
|
+
detail: `${raw} (< ${MIN_KICAD_MAJOR})`,
|
|
118
|
+
hint: `copperhead needs KiCad >= ${MIN_KICAD_MAJOR}; upgrade KiCad (https://www.kicad.org/download/).`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { name: 'kicad-cli', status: 'ok', detail: raw };
|
|
79
122
|
} catch {
|
|
80
123
|
return {
|
|
81
124
|
name: 'kicad-cli',
|
|
82
125
|
status: 'fail',
|
|
83
126
|
detail: 'not found on PATH',
|
|
84
|
-
hint: 'install KiCad >=
|
|
127
|
+
hint: 'install KiCad >= 8 (bundles kicad-cli); ERC/DRC gates need it.',
|
|
85
128
|
};
|
|
86
129
|
}
|
|
87
130
|
}
|
|
@@ -99,6 +142,41 @@ async function gitCheck(probe: () => Promise<string>): Promise<DoctorCheck> {
|
|
|
99
142
|
}
|
|
100
143
|
}
|
|
101
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Report whether the `openspec` CLI is reachable, needed for `validate_change`
|
|
147
|
+
* and the `create` pipeline. Fails soft like `kicadCheck`/`gitCheck`: a
|
|
148
|
+
* missing or erroring probe is returned as a `fail` check, never thrown.
|
|
149
|
+
* @param probe resolves the openspec version string, or rejects if the CLI
|
|
150
|
+
* can't be run (e.g. not found on PATH).
|
|
151
|
+
* @returns an `ok` check with the version on success; a `fail` check with an
|
|
152
|
+
* install hint when `probe` rejects with a not-found error (per
|
|
153
|
+
* `isNotFoundError`), or a `fail` check with the flattened error message
|
|
154
|
+
* otherwise.
|
|
155
|
+
*/
|
|
156
|
+
async function openspecCheck(probe: () => Promise<string>): Promise<DoctorCheck> {
|
|
157
|
+
try {
|
|
158
|
+
return { name: 'openspec', status: 'ok', detail: await probe() };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (isNotFoundError(err)) {
|
|
161
|
+
return {
|
|
162
|
+
name: 'openspec',
|
|
163
|
+
status: 'fail',
|
|
164
|
+
detail: 'not found on PATH',
|
|
165
|
+
hint: 'npm i -g @fission-ai/openspec; validate_change and the create pipeline need it.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Collapse embedded newlines/whitespace from a raw shell/subprocess error:
|
|
169
|
+
// formatDoctor's column layout assumes a single-line detail, and wrapWords
|
|
170
|
+
// splits on plain spaces only.
|
|
171
|
+
const rawMessage = (err as Error).message || String(err);
|
|
172
|
+
return {
|
|
173
|
+
name: 'openspec',
|
|
174
|
+
status: 'fail',
|
|
175
|
+
detail: rawMessage.replace(/\s+/g, ' ').trim(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
102
180
|
/**
|
|
103
181
|
* Map a resolved model to the credential its provider needs, mirroring
|
|
104
182
|
* makeProvider's prefix routing (agent/loop.ts). Presence-only: it checks that a
|
|
@@ -131,9 +209,9 @@ export function checkCredential(
|
|
|
131
209
|
const settings = compat ?? { apiKeyEnv: DEFAULT_API_KEY_ENV };
|
|
132
210
|
// Display only: some endpoints embed a credential in the URL itself (a
|
|
133
211
|
// query param, userinfo) — Gemini's compat endpoint does this with
|
|
134
|
-
// ?key
|
|
135
|
-
//
|
|
136
|
-
//
|
|
212
|
+
// ?key=.... redactSecrets covers known key shapes, but a key embedded in
|
|
213
|
+
// a URL query isn't reliably one of them, so drop the query and userinfo
|
|
214
|
+
// entirely rather than pattern-matching, regardless of shape.
|
|
137
215
|
// isLocalEndpoint() below still runs against the raw settings.baseURL,
|
|
138
216
|
// never this.
|
|
139
217
|
const where = (() => {
|
|
@@ -380,6 +458,7 @@ export async function runDoctor(opts: RunDoctorOptions): Promise<DoctorReport> {
|
|
|
380
458
|
nodeCheck(deps.nodeVersion),
|
|
381
459
|
await kicadCheck(deps.kicadVersion),
|
|
382
460
|
await gitCheck(deps.gitVersion),
|
|
461
|
+
await openspecCheck(deps.openspecVersion),
|
|
383
462
|
providerCheck(opts.model, config, deps.env),
|
|
384
463
|
];
|
|
385
464
|
if (resolvedModel) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { loadConfig, resolveModel } from '../config.js';
|
|
2
|
+
import { flatten, type ToolResult } from '../agent/envelope.js';
|
|
3
|
+
import { makeProvider } from '../agent/loop.js';
|
|
4
|
+
import { dispatchToolResult, registry, type RunContext } from '../agent/tools.js';
|
|
5
|
+
import type { Provider } from '../agent/types.js';
|
|
6
|
+
import { Transcript } from '../agent/transcript.js';
|
|
7
|
+
import { ObligationsLedger } from '../agent/ledger.js';
|
|
8
|
+
|
|
9
|
+
export class SkillCliError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SkillCliError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function catalogNameFromCli(name: string): string {
|
|
17
|
+
return name.replace(/-/g, '_');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function listSkills(repoRoot: string): Promise<{ name: string; available: boolean; description: string }[]> {
|
|
21
|
+
const ctx = await minimalCtx(repoRoot, false);
|
|
22
|
+
const listed = new Set(registry.list(ctx).map((e) => e.name));
|
|
23
|
+
return registry.skills().map((s) => ({
|
|
24
|
+
name: s.name,
|
|
25
|
+
available: listed.has(s.name),
|
|
26
|
+
description: s.schema.description,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runSkill(opts: {
|
|
31
|
+
repoRoot: string;
|
|
32
|
+
name: string;
|
|
33
|
+
args?: Record<string, unknown>;
|
|
34
|
+
provider: Provider;
|
|
35
|
+
}): Promise<ToolResult> {
|
|
36
|
+
const catalogName = catalogNameFromCli(opts.name);
|
|
37
|
+
const entry = registry.get(catalogName);
|
|
38
|
+
if (!entry || entry.kind !== 'skill') throw new SkillCliError(`unknown skill "${opts.name}"`);
|
|
39
|
+
const ctx = await minimalCtx(opts.repoRoot);
|
|
40
|
+
if (!registry.list(ctx).some((e) => e.name === catalogName)) {
|
|
41
|
+
throw new SkillCliError(`skill "${opts.name}" is not available in this repo`);
|
|
42
|
+
}
|
|
43
|
+
return dispatchToolResult(ctx, entry.name, opts.args ?? {}, { provider: opts.provider });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function providerForSkillRun(repoRoot: string, modelFlag?: string): Promise<{ provider: Provider; model: string }> {
|
|
47
|
+
const config = await loadConfig(repoRoot);
|
|
48
|
+
try {
|
|
49
|
+
const { model } = resolveModel(modelFlag, config);
|
|
50
|
+
return { provider: await makeProvider(model), model };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = (err as Error).message;
|
|
53
|
+
throw new SkillCliError(
|
|
54
|
+
msg.includes('no model') || msg.includes('API_KEY') || msg.includes('configured')
|
|
55
|
+
? `${msg} — skill run needs OPENAI_API_KEY, ANTHROPIC_API_KEY, or --model for a saved-login provider (codex, claude-code, cursor).`
|
|
56
|
+
: msg,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatSkillEnvelope(result: ToolResult, json: boolean): string {
|
|
62
|
+
if (json) return JSON.stringify(result, null, 2);
|
|
63
|
+
const body = flatten(result);
|
|
64
|
+
return result.ok ? body : `error: ${body}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One skill run plus the provider's lifecycle. A saved-login provider owns a
|
|
69
|
+
* `mkdtemp` working directory (and, on some backends, an in-flight subprocess);
|
|
70
|
+
* `runAgentLoop` closes it in a `finally` because leaving it behind orphans the
|
|
71
|
+
* process and fills the disk (I8). `skill run` has no agent loop to inherit that
|
|
72
|
+
* from, so the close lives here — around the throw path too, since an unknown or
|
|
73
|
+
* unavailable skill must not leak the provider it already built. Returns the text
|
|
74
|
+
* to print and the exit code, so the CLI's only job is `console.log` + `exit`.
|
|
75
|
+
*/
|
|
76
|
+
export async function runSkillCli(opts: {
|
|
77
|
+
repoRoot: string;
|
|
78
|
+
name: string;
|
|
79
|
+
args?: Record<string, unknown>;
|
|
80
|
+
provider: Provider;
|
|
81
|
+
json: boolean;
|
|
82
|
+
warn?: (line: string) => void;
|
|
83
|
+
}): Promise<{ text: string; code: number }> {
|
|
84
|
+
try {
|
|
85
|
+
const result = await runSkill({
|
|
86
|
+
repoRoot: opts.repoRoot,
|
|
87
|
+
name: opts.name,
|
|
88
|
+
...(opts.args ? { args: opts.args } : {}),
|
|
89
|
+
provider: opts.provider,
|
|
90
|
+
});
|
|
91
|
+
return { text: formatSkillEnvelope(result, opts.json), code: result.ok ? 0 : 1 };
|
|
92
|
+
} finally {
|
|
93
|
+
try {
|
|
94
|
+
await opts.provider.close?.();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
(opts.warn ?? ((l: string) => console.error(l)))(
|
|
97
|
+
`warning: ${opts.provider.name} provider cleanup failed (${(err as Error).message})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function minimalCtx(repoRoot: string, initializeTranscript = true): Promise<RunContext> {
|
|
104
|
+
const transcript = new Transcript(repoRoot);
|
|
105
|
+
if (initializeTranscript) await transcript.init();
|
|
106
|
+
return {
|
|
107
|
+
repoRoot,
|
|
108
|
+
config: await loadConfig(repoRoot),
|
|
109
|
+
transcript,
|
|
110
|
+
ledger: new ObligationsLedger(),
|
|
111
|
+
runId: 'skill',
|
|
112
|
+
interactive: false,
|
|
113
|
+
confirm: async () => true,
|
|
114
|
+
editsUnlocked: false,
|
|
115
|
+
changeId: null,
|
|
116
|
+
proposalValidated: false,
|
|
117
|
+
filesTouched: new Set(),
|
|
118
|
+
decisions: [],
|
|
119
|
+
lastErc: null,
|
|
120
|
+
lastDrc: null,
|
|
121
|
+
lastLegibility: null,
|
|
122
|
+
lastScore: null,
|
|
123
|
+
lastDrift: null,
|
|
124
|
+
repairCycles: 0,
|
|
125
|
+
finishRequest: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { checkDrift } from '../memory/drift.js';
|
|
|
6
6
|
import { loadConstraints, checkForbiddenPins } from '../memory/constraints.js';
|
|
7
7
|
import { pinNets } from '../kicad/sexp.js';
|
|
8
8
|
import { openspecValidate } from '../openspec/cli.js';
|
|
9
|
-
import { runAgentLoop } from '../agent/loop.js';
|
|
9
|
+
import { runAgentLoop, type RunResult } from '../agent/loop.js';
|
|
10
10
|
import type { RunMetaInput } from '../agent/runmeta.js';
|
|
11
11
|
import type { ProgressRenderer } from '../agent/render.js';
|
|
12
12
|
|
|
@@ -172,7 +172,7 @@ export async function syncResolve(
|
|
|
172
172
|
model: string,
|
|
173
173
|
log: (s: string) => void,
|
|
174
174
|
extras?: { renderer?: ProgressRenderer; meta?: RunMetaInput },
|
|
175
|
-
): Promise<{ ok: boolean }> {
|
|
175
|
+
): Promise<{ ok: boolean; run: RunResult }> {
|
|
176
176
|
const reportText = formatSyncReport(report);
|
|
177
177
|
const res = await runAgentLoop({
|
|
178
178
|
repoRoot,
|
|
@@ -183,5 +183,7 @@ export async function syncResolve(
|
|
|
183
183
|
...(extras?.renderer ? { renderer: extras.renderer } : {}),
|
|
184
184
|
...(extras?.meta ? { meta: extras.meta } : {}),
|
|
185
185
|
});
|
|
186
|
-
|
|
186
|
+
// The full RunResult travels with the verdict so a non-CLI caller (the MCP
|
|
187
|
+
// server) can report the transcript path and files touched the way `do` does.
|
|
188
|
+
return { ok: res.outcome === 'success', run: res };
|
|
187
189
|
}
|