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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `copperhead mcp` — a stdio MCP server exposing the gated pipeline to MCP
|
|
3
|
+
* hosts as five opaque, outcome-level tools (design D1).
|
|
4
|
+
*
|
|
5
|
+
* The security boundary is tool granularity, not prompt wording. Hosts get
|
|
6
|
+
* `check`, `do`, `sync` and `init` as whole-pipeline invocations, plus the
|
|
7
|
+
* read-only `doctor` probe, and nothing finer: no file-edit tool, no raw KiCad
|
|
8
|
+
* tool, no way to drive one step of the loop. There is therefore no sequence of MCP calls that skips the spec gate or
|
|
9
|
+
* the verification gate, because every mutating path runs the same loop the CLI
|
|
10
|
+
* runs. Safety rails are inherited rather than restated — this module is a
|
|
11
|
+
* transport adapter over the existing command entry points, and deliberately
|
|
12
|
+
* owns no policy of its own.
|
|
13
|
+
*
|
|
14
|
+
* Two rails are enforced here because they are properties of the transport
|
|
15
|
+
* rather than of the pipeline: stdout carries only the JSON-RPC stream (every
|
|
16
|
+
* human-readable byte goes to stderr, or the protocol corrupts), and mutating
|
|
17
|
+
* calls are serialized per repo *within this server process*. That lock is
|
|
18
|
+
* in-memory, so it does not interlock two separately spawned servers or a
|
|
19
|
+
* concurrent CLI run — those are held apart by the loop's own dirty-tree
|
|
20
|
+
* preflight, not by this.
|
|
21
|
+
*/
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { existsSync } from 'node:fs';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { loadConfig, resolveModel } from '../config.js';
|
|
28
|
+
import { runCheck } from '../commands/check.js';
|
|
29
|
+
import { syncVerify, syncResolve, formatSyncReport } from '../commands/sync.js';
|
|
30
|
+
import { runInit, InitError } from '../memory/scaffold.js';
|
|
31
|
+
import { SandboxError } from '../util/paths.js';
|
|
32
|
+
import { runDoctor } from '../commands/doctor.js';
|
|
33
|
+
import { runAgentLoop } from '../agent/loop.js';
|
|
34
|
+
import { kicadCliVersion } from '../kicad/cli.js';
|
|
35
|
+
import { seal } from '../agent/envelope.js';
|
|
36
|
+
import { plainRenderer } from '../agent/render.js';
|
|
37
|
+
/** The copperhead package version, for run self-description (AC-8.1). This is
|
|
38
|
+
* deliberately not MCP_PROTOCOL_VERSION: run metadata records which copperhead
|
|
39
|
+
* produced a commit, and the transport's own version would be a false answer. */
|
|
40
|
+
const { version: COPPERHEAD_VERSION } = createRequire(import.meta.url)('../../package.json');
|
|
41
|
+
/**
|
|
42
|
+
* Unstable by declaration (design D7). The `0.` major is load-bearing: it is
|
|
43
|
+
* the signal to hosts that tool names, input schemas and result shapes may
|
|
44
|
+
* change in any release, and it is what defers a registry listing until the
|
|
45
|
+
* stabilization criteria in the proposal are met. A test asserts the `0.`, so
|
|
46
|
+
* removing the experimental status is a deliberate edit rather than a drift.
|
|
47
|
+
*/
|
|
48
|
+
export const MCP_PROTOCOL_VERSION = '0.1.0';
|
|
49
|
+
/** The entire surface. Anything not on this list is not reachable over MCP. */
|
|
50
|
+
export const PIPELINE_TOOL_NAMES = [
|
|
51
|
+
'copperhead_check',
|
|
52
|
+
'copperhead_do',
|
|
53
|
+
'copperhead_sync',
|
|
54
|
+
'copperhead_init',
|
|
55
|
+
'copperhead_doctor',
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Per-tool input schema versions, bumped when a tool's inputs change shape.
|
|
59
|
+
* Separate from MCP_PROTOCOL_VERSION so one tool changing does not imply the
|
|
60
|
+
* whole surface did.
|
|
61
|
+
*/
|
|
62
|
+
export const TOOL_SCHEMA_VERSIONS = {
|
|
63
|
+
copperhead_check: 1,
|
|
64
|
+
copperhead_do: 1,
|
|
65
|
+
copperhead_sync: 1,
|
|
66
|
+
copperhead_init: 1,
|
|
67
|
+
copperhead_doctor: 1,
|
|
68
|
+
};
|
|
69
|
+
/** Human-readable output goes to stderr; stdout belongs to JSON-RPC alone. */
|
|
70
|
+
const note = (line) => {
|
|
71
|
+
process.stderr.write(`${line}\n`);
|
|
72
|
+
};
|
|
73
|
+
/** A sealed failure envelope. `seal` redacts, so a key can never ride out. */
|
|
74
|
+
export function failure(kind, message) {
|
|
75
|
+
return seal({ ok: false, summary: message, error: { kind, message } });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the model for an LLM-backed tool, converting the two ways model
|
|
79
|
+
* resolution can fail into typed errors a host agent can act on: no credential
|
|
80
|
+
* at all, and two credentials with nothing to choose between them. Neither is
|
|
81
|
+
* an exception — a host that cannot run `do` should be told why in a result.
|
|
82
|
+
*/
|
|
83
|
+
async function resolveModelOrFail(repoRoot) {
|
|
84
|
+
try {
|
|
85
|
+
const config = await loadConfig(repoRoot);
|
|
86
|
+
const { model, source } = resolveModel(undefined, config);
|
|
87
|
+
return { model, source };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// `unavailable` rather than `validation`: the call was well-formed, the
|
|
91
|
+
// environment just cannot serve it.
|
|
92
|
+
return { error: failure('unavailable', err.message) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** kicad-cli is a hard precondition for every tool, exactly as in the CLI. */
|
|
96
|
+
async function requireKicad() {
|
|
97
|
+
try {
|
|
98
|
+
return { version: await kicadCliVersion() };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
error: failure('unavailable', `kicad-cli is not available: ${err.message}. Install KiCad 9+ and ensure kicad-cli is on PATH.`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Mutating tools are serialized per repo. Rejecting rather than queueing is
|
|
108
|
+
* deliberate: a `do` run can take minutes, and a host blocked on an invisible
|
|
109
|
+
* queue looks hung, while a typed busy error is something an agent can relay
|
|
110
|
+
* and retry. Concurrent `check` calls are unrestricted — they mutate nothing.
|
|
111
|
+
*/
|
|
112
|
+
export class RepoLocks {
|
|
113
|
+
busy = new Set();
|
|
114
|
+
tryAcquire(repoRoot) {
|
|
115
|
+
if (this.busy.has(repoRoot))
|
|
116
|
+
return false;
|
|
117
|
+
this.busy.add(repoRoot);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
release(repoRoot) {
|
|
121
|
+
this.busy.delete(repoRoot);
|
|
122
|
+
}
|
|
123
|
+
isBusy(repoRoot) {
|
|
124
|
+
return this.busy.has(repoRoot);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A ProgressRenderer that mirrors loop progress onto MCP progress
|
|
129
|
+
* notifications. `progress` is a monotonically rising count of observed events
|
|
130
|
+
* rather than a percentage: the loop cannot know how many turns a run will take,
|
|
131
|
+
* and inventing a denominator would be a worse lie than an open-ended counter.
|
|
132
|
+
*/
|
|
133
|
+
export function mcpRenderer(sink) {
|
|
134
|
+
let ticks = 0;
|
|
135
|
+
const emit = (message) => {
|
|
136
|
+
ticks += 1;
|
|
137
|
+
sink({ message, progress: ticks });
|
|
138
|
+
};
|
|
139
|
+
const base = plainRenderer((line) => {
|
|
140
|
+
note(line);
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
log: (line) => {
|
|
144
|
+
base.log(line);
|
|
145
|
+
},
|
|
146
|
+
turnStart: (turn, maxTurns, tokensIn, tokensOut) => {
|
|
147
|
+
base.turnStart(turn, maxTurns, tokensIn, tokensOut);
|
|
148
|
+
emit(`turn ${turn}/${maxTurns}`);
|
|
149
|
+
},
|
|
150
|
+
toolResult: (name, firstLine, ok, viewHint) => {
|
|
151
|
+
base.toolResult(name, firstLine, ok, viewHint);
|
|
152
|
+
emit(`${name}: ${firstLine}`);
|
|
153
|
+
},
|
|
154
|
+
status: (text) => {
|
|
155
|
+
base.status(text);
|
|
156
|
+
},
|
|
157
|
+
heartbeat: (info) => {
|
|
158
|
+
base.heartbeat(info);
|
|
159
|
+
emit('working');
|
|
160
|
+
},
|
|
161
|
+
finish: (line) => {
|
|
162
|
+
base.finish(line);
|
|
163
|
+
emit(line);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Map a finished run onto the status vocabulary hosts see. `rolled_back` is the
|
|
169
|
+
* honest default for every non-success exit path that is not a refusal: the
|
|
170
|
+
* loop restores its git snapshot on the way out, so "the run failed" and "the
|
|
171
|
+
* tree is back where it started" are the same fact.
|
|
172
|
+
*/
|
|
173
|
+
export function runStatus(res, dryRun) {
|
|
174
|
+
if (res.outcome === 'refused')
|
|
175
|
+
return 'refused';
|
|
176
|
+
if (res.outcome === 'success')
|
|
177
|
+
return dryRun ? 'dry_run' : 'committed';
|
|
178
|
+
return 'rolled_back';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Build the server and register the tools. Exported separately from
|
|
182
|
+
* `startMcpServer` so tests can drive the tool surface without a transport.
|
|
183
|
+
*/
|
|
184
|
+
export function createMcpServer(opts) {
|
|
185
|
+
const { repoRoot } = opts;
|
|
186
|
+
const locks = new RepoLocks();
|
|
187
|
+
const server = new McpServer({ name: 'copperhead', version: MCP_PROTOCOL_VERSION }, {
|
|
188
|
+
instructions: 'EXPERIMENTAL, UNSTABLE SURFACE: tool names, inputs and result shapes may change in any release. ' +
|
|
189
|
+
'Use these tools to change or verify a KiCad project instead of editing .kicad_sch / .kicad_pcb files ' +
|
|
190
|
+
'directly. Every mutation runs a spec-gated, verified, rollback-on-failure pipeline; editing the files ' +
|
|
191
|
+
'yourself bypasses all of it.',
|
|
192
|
+
});
|
|
193
|
+
const toMcp = (result) => ({
|
|
194
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
195
|
+
...(result.ok ? {} : { isError: true }),
|
|
196
|
+
});
|
|
197
|
+
server.registerTool('copperhead_check', {
|
|
198
|
+
title: 'Verify the KiCad project',
|
|
199
|
+
description: 'Run ERC, DRC, doc-drift and spec validation on the project. Makes no model call and no network call, ' +
|
|
200
|
+
'and changes nothing. Safe to call at any time, including concurrently.',
|
|
201
|
+
inputSchema: {},
|
|
202
|
+
_meta: { schemaVersion: TOOL_SCHEMA_VERSIONS.copperhead_check },
|
|
203
|
+
}, async () => {
|
|
204
|
+
const kicad = await requireKicad();
|
|
205
|
+
if ('error' in kicad)
|
|
206
|
+
return toMcp(kicad.error);
|
|
207
|
+
try {
|
|
208
|
+
const res = await runCheck(repoRoot, (s) => {
|
|
209
|
+
note(s);
|
|
210
|
+
});
|
|
211
|
+
return toMcp(seal({
|
|
212
|
+
ok: res.ok,
|
|
213
|
+
summary: res.ok ? 'check passed' : 'check found violations',
|
|
214
|
+
viewHint: 'diagnostic',
|
|
215
|
+
data: res,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
return toMcp(failure('exception', err.message));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
server.registerTool('copperhead_doctor', {
|
|
223
|
+
title: 'Check that this host can run copperhead',
|
|
224
|
+
description: 'Probe the environment copperhead needs: node, kicad-cli, git, openspec, and whether a model credential ' +
|
|
225
|
+
'resolves. Makes no model call and no network call, and changes nothing. Call this first when another ' +
|
|
226
|
+
'tool reports that something is unavailable.',
|
|
227
|
+
inputSchema: {},
|
|
228
|
+
_meta: { schemaVersion: TOOL_SCHEMA_VERSIONS.copperhead_doctor },
|
|
229
|
+
}, async () => {
|
|
230
|
+
// Deliberately no requireKicad() preflight. A missing kicad-cli is the
|
|
231
|
+
// single most likely thing a host needs told about, and gating this tool
|
|
232
|
+
// on it would make the diagnostic fail in exactly the case it exists for;
|
|
233
|
+
// runDoctor probes kicad-cli and reports it as a failed check instead.
|
|
234
|
+
try {
|
|
235
|
+
const report = await runDoctor({ repoRoot });
|
|
236
|
+
const failed = report.checks.filter((c) => c.status === 'fail');
|
|
237
|
+
return toMcp(seal({
|
|
238
|
+
ok: report.ok,
|
|
239
|
+
summary: report.ok
|
|
240
|
+
? 'environment is ready'
|
|
241
|
+
: `${failed.length} check(s) failed: ${failed.map((c) => c.name).join(', ')}`,
|
|
242
|
+
viewHint: 'diagnostic',
|
|
243
|
+
data: report,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return toMcp(failure('exception', err.message));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
server.registerTool('copperhead_init', {
|
|
251
|
+
title: 'Scaffold design docs from the schematic',
|
|
252
|
+
description: 'Generate the docs/ memory scaffold from an existing schematic. Also installs a git pre-commit hook ' +
|
|
253
|
+
'that runs copperhead check before each commit. Idempotent, and refuses rather than overwriting docs ' +
|
|
254
|
+
'a human has hand-edited.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
path: z.string().optional().describe('where to look for KiCad files, relative to the repo root'),
|
|
257
|
+
},
|
|
258
|
+
_meta: { schemaVersion: TOOL_SCHEMA_VERSIONS.copperhead_init },
|
|
259
|
+
}, async ({ path: searchPath }) => {
|
|
260
|
+
const kicad = await requireKicad();
|
|
261
|
+
if ('error' in kicad)
|
|
262
|
+
return toMcp(kicad.error);
|
|
263
|
+
if (!locks.tryAcquire(repoRoot)) {
|
|
264
|
+
return toMcp(failure('unavailable', 'another copperhead run is in progress for this repo; retry shortly'));
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const res = await runInit({
|
|
268
|
+
repoRoot,
|
|
269
|
+
...(searchPath ? { searchPath } : {}),
|
|
270
|
+
force: false,
|
|
271
|
+
installHooks: true,
|
|
272
|
+
});
|
|
273
|
+
const refused = res.refused.length > 0;
|
|
274
|
+
return toMcp(seal({
|
|
275
|
+
ok: !refused,
|
|
276
|
+
summary: refused
|
|
277
|
+
? `init refused ${res.refused.length} hand-edited doc(s)`
|
|
278
|
+
: `init wrote ${res.created.length} file(s)`,
|
|
279
|
+
viewHint: 'mutation',
|
|
280
|
+
data: res,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
// A missing schematic is the user's situation, not a crash: report it
|
|
285
|
+
// as a validation failure so the host can ask for a path.
|
|
286
|
+
// A missing schematic and a path that escapes the repo are both the
|
|
287
|
+
// caller's situation, not a crash: report them as validation failures
|
|
288
|
+
// so the host can correct the input rather than retrying blindly.
|
|
289
|
+
const kind = err instanceof InitError || err instanceof SandboxError ? 'validation' : 'exception';
|
|
290
|
+
return toMcp(failure(kind, err.message));
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
locks.release(repoRoot);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
server.registerTool('copperhead_do', {
|
|
297
|
+
title: 'Make a verified change to the project',
|
|
298
|
+
description: 'Run the full gated pipeline for a change request: propose, spec-gate, edit, verify with ERC/DRC, repair, ' +
|
|
299
|
+
'and commit — or roll back to the pre-run state if verification cannot be satisfied. This is the only way ' +
|
|
300
|
+
'to change the project. Long-running; progress is streamed. Requires an API key in the environment.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
request: z.string().min(1).describe('the change request, in natural language'),
|
|
303
|
+
dry_run: z.boolean().optional().describe('propose the change and write nothing'),
|
|
304
|
+
},
|
|
305
|
+
_meta: { schemaVersion: TOOL_SCHEMA_VERSIONS.copperhead_do },
|
|
306
|
+
}, async ({ request, dry_run: dryRun }, extra) => {
|
|
307
|
+
const kicad = await requireKicad();
|
|
308
|
+
if ('error' in kicad)
|
|
309
|
+
return toMcp(kicad.error);
|
|
310
|
+
const resolved = await resolveModelOrFail(repoRoot);
|
|
311
|
+
if ('error' in resolved)
|
|
312
|
+
return toMcp(resolved.error);
|
|
313
|
+
if (!locks.tryAcquire(repoRoot)) {
|
|
314
|
+
return toMcp(failure('unavailable', 'another copperhead run is in progress for this repo; retry shortly'));
|
|
315
|
+
}
|
|
316
|
+
const progressToken = extra?._meta?.progressToken;
|
|
317
|
+
const sink = (update) => {
|
|
318
|
+
if (progressToken === undefined)
|
|
319
|
+
return;
|
|
320
|
+
void extra
|
|
321
|
+
?.sendNotification({
|
|
322
|
+
method: 'notifications/progress',
|
|
323
|
+
params: { progressToken, progress: update.progress, message: update.message },
|
|
324
|
+
})
|
|
325
|
+
.catch(() => {
|
|
326
|
+
// A host that stopped listening must not fail the run.
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
try {
|
|
330
|
+
const res = await runAgentLoop({
|
|
331
|
+
repoRoot,
|
|
332
|
+
request,
|
|
333
|
+
model: resolved.model,
|
|
334
|
+
// allowDirty is deliberately not exposed: it is a safety rail, and a
|
|
335
|
+
// tool input that switches a rail off is a rail a host can bypass.
|
|
336
|
+
allowDirty: false,
|
|
337
|
+
dryRun: dryRun ?? false,
|
|
338
|
+
// No confirm callback and no budget-extension prompt: there is no
|
|
339
|
+
// human on this transport, so the loop must fail rather than block.
|
|
340
|
+
interactive: false,
|
|
341
|
+
renderer: mcpRenderer(sink),
|
|
342
|
+
meta: {
|
|
343
|
+
command: 'do',
|
|
344
|
+
modelSource: resolved.source,
|
|
345
|
+
version: COPPERHEAD_VERSION,
|
|
346
|
+
kicadCliVersion: kicad.version,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const status = runStatus(res, dryRun ?? false);
|
|
350
|
+
return toMcp(seal({
|
|
351
|
+
// A rollback is a successful tool call whose result says the run
|
|
352
|
+
// did not land (design D6). Only `refused` is reported as not-ok,
|
|
353
|
+
// because that is the pipeline declining rather than failing.
|
|
354
|
+
ok: status !== 'refused',
|
|
355
|
+
summary: `${status}: ${res.summary}`,
|
|
356
|
+
viewHint: 'mutation',
|
|
357
|
+
data: {
|
|
358
|
+
status,
|
|
359
|
+
commit: res.commit,
|
|
360
|
+
filesTouched: res.filesTouched,
|
|
361
|
+
transcriptDir: res.transcriptDir,
|
|
362
|
+
exitPath: res.exitPath,
|
|
363
|
+
stats: res.stats,
|
|
364
|
+
},
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
return toMcp(failure('exception', err.message));
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
locks.release(repoRoot);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
server.registerTool('copperhead_sync', {
|
|
375
|
+
title: 'Verify design-state consistency, and optionally resolve drift',
|
|
376
|
+
description: 'Run the deterministic consistency check across docs, constraints and the KiCad files. With resolve=true, ' +
|
|
377
|
+
'additionally run the gated loop to fix the drift it found. Requirement violations are always reported and ' +
|
|
378
|
+
'never auto-resolved — those are for a human. resolve=true requires an API key in the environment.',
|
|
379
|
+
inputSchema: {
|
|
380
|
+
resolve: z.boolean().optional().describe('run the LLM resolve phase for resolvable drift'),
|
|
381
|
+
},
|
|
382
|
+
_meta: { schemaVersion: TOOL_SCHEMA_VERSIONS.copperhead_sync },
|
|
383
|
+
}, async ({ resolve }, extra) => {
|
|
384
|
+
const kicad = await requireKicad();
|
|
385
|
+
if ('error' in kicad)
|
|
386
|
+
return toMcp(kicad.error);
|
|
387
|
+
let report;
|
|
388
|
+
try {
|
|
389
|
+
report = await syncVerify(repoRoot);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
return toMcp(failure('exception', err.message));
|
|
393
|
+
}
|
|
394
|
+
const verdict = (summary, ok = true) => seal({ ok, summary, viewHint: resolve ? 'mutation' : 'diagnostic', data: report });
|
|
395
|
+
if (!resolve)
|
|
396
|
+
return toMcp(verdict(formatSyncReport(report)));
|
|
397
|
+
// Truth precedence (design D14): a requirement violation is never
|
|
398
|
+
// silently resolved, so the resolve phase does not start when one exists.
|
|
399
|
+
if (report.violations.length) {
|
|
400
|
+
return toMcp(verdict(`${report.violations.length} requirement violation(s) found; these are never auto-resolved. ` +
|
|
401
|
+
`Resolve them by hand or change the requirement.\n\n${formatSyncReport(report)}`, false));
|
|
402
|
+
}
|
|
403
|
+
if (!report.resolvable.length)
|
|
404
|
+
return toMcp(verdict('design state is consistent; nothing to resolve'));
|
|
405
|
+
const resolved = await resolveModelOrFail(repoRoot);
|
|
406
|
+
if ('error' in resolved)
|
|
407
|
+
return toMcp(resolved.error);
|
|
408
|
+
if (!locks.tryAcquire(repoRoot)) {
|
|
409
|
+
return toMcp(failure('unavailable', 'another copperhead run is in progress for this repo; retry shortly'));
|
|
410
|
+
}
|
|
411
|
+
// The report above was computed before the lock was held, so another run
|
|
412
|
+
// may have rewritten the tree in between. Re-verify now that nothing else
|
|
413
|
+
// can move, rather than asking the loop to fix drift that is already gone.
|
|
414
|
+
const fresh = await syncVerify(repoRoot);
|
|
415
|
+
if (fresh.violations.length || !fresh.resolvable.length) {
|
|
416
|
+
locks.release(repoRoot);
|
|
417
|
+
return toMcp(seal({
|
|
418
|
+
ok: !fresh.violations.length,
|
|
419
|
+
summary: fresh.violations.length
|
|
420
|
+
? `${fresh.violations.length} requirement violation(s) found; these are never auto-resolved.`
|
|
421
|
+
: 'design state is consistent; nothing to resolve',
|
|
422
|
+
viewHint: 'diagnostic',
|
|
423
|
+
data: fresh,
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
const progressToken = extra?._meta?.progressToken;
|
|
427
|
+
const sink = (update) => {
|
|
428
|
+
if (progressToken === undefined)
|
|
429
|
+
return;
|
|
430
|
+
void extra
|
|
431
|
+
?.sendNotification({
|
|
432
|
+
method: 'notifications/progress',
|
|
433
|
+
params: { progressToken, progress: update.progress, message: update.message },
|
|
434
|
+
})
|
|
435
|
+
.catch(() => {
|
|
436
|
+
// A host that stopped listening must not fail the run.
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
try {
|
|
440
|
+
const res = await syncResolve(repoRoot, fresh, resolved.model, (s) => {
|
|
441
|
+
note(s);
|
|
442
|
+
}, {
|
|
443
|
+
renderer: mcpRenderer(sink),
|
|
444
|
+
meta: {
|
|
445
|
+
command: 'sync',
|
|
446
|
+
modelSource: resolved.source,
|
|
447
|
+
version: COPPERHEAD_VERSION,
|
|
448
|
+
kicadCliVersion: kicad.version,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
return toMcp(seal({
|
|
452
|
+
ok: res.ok,
|
|
453
|
+
summary: res.ok ? 'drift resolved and verified' : `resolve did not land: ${res.run.summary}`,
|
|
454
|
+
viewHint: 'mutation',
|
|
455
|
+
data: {
|
|
456
|
+
resolved: res.ok,
|
|
457
|
+
report: fresh,
|
|
458
|
+
commit: res.run.commit,
|
|
459
|
+
filesTouched: res.run.filesTouched,
|
|
460
|
+
transcriptDir: res.run.transcriptDir,
|
|
461
|
+
exitPath: res.run.exitPath,
|
|
462
|
+
},
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
return toMcp(failure('exception', err.message));
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
locks.release(repoRoot);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
return server;
|
|
473
|
+
}
|
|
474
|
+
/** Start the server on stdio and serve until the host closes the transport. */
|
|
475
|
+
export async function startMcpServer(opts) {
|
|
476
|
+
if (!existsSync(opts.repoRoot)) {
|
|
477
|
+
throw new Error(`repo not found: ${opts.repoRoot}`);
|
|
478
|
+
}
|
|
479
|
+
const server = createMcpServer(opts);
|
|
480
|
+
note(`copperhead mcp ${MCP_PROTOCOL_VERSION} (EXPERIMENTAL — unstable surface: tool names, inputs and results may ` +
|
|
481
|
+
`change in any release)`);
|
|
482
|
+
note(`repo: ${opts.repoRoot}`);
|
|
483
|
+
await server.connect(new StdioServerTransport());
|
|
484
|
+
}
|
|
485
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
package/dist/memory/bom-table.js
CHANGED
|
@@ -28,20 +28,83 @@ function isSeparatorRow(cells) {
|
|
|
28
28
|
* whatever cells they split into — this function never throws.
|
|
29
29
|
*/
|
|
30
30
|
export function parseMarkdownTables(md) {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
return (scanTableGroups(md)
|
|
32
|
+
// A group opened by an un-piped line with no delimiter row is not a table:
|
|
33
|
+
// GFM requires the delimiter row for one to exist at all. Without this,
|
|
34
|
+
// ordinary prose such as `Second-source options: Yageo | Vishay | KOA`
|
|
35
|
+
// under a BOM table becomes a part in `export bom`, which `check` cannot
|
|
36
|
+
// catch because `parseCanonicalTables` drops the header-less group.
|
|
37
|
+
.filter((g) => g[0].piped || g.some((l) => l.separator))
|
|
38
|
+
.flat()
|
|
39
|
+
.filter((l) => !l.separator)
|
|
40
|
+
.map((l) => ({ cells: l.cells })));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Group a document's table lines, one group per table. Both readers below go
|
|
44
|
+
* through this so they cannot disagree about which documents are readable:
|
|
45
|
+
* `check` accepting a BOM.md that `export bom` reads as empty is how a clean
|
|
46
|
+
* check turns into a header-only ordering file.
|
|
47
|
+
*
|
|
48
|
+
* Lines inside a fenced code block are skipped. Docs legitimately show the
|
|
49
|
+
* table format as an example, and an example row is not a part or a pin.
|
|
50
|
+
*/
|
|
51
|
+
function scanTableGroups(md) {
|
|
52
|
+
const groups = [];
|
|
53
|
+
let current = null;
|
|
54
|
+
let width = 0; // column count of the open group, set by its first line
|
|
55
|
+
// The open fence's delimiter, or null outside one. A fence closes only on the
|
|
56
|
+
// same character, at least as long (CommonMark). Toggling on any fence-like
|
|
57
|
+
// line would let a shorter or different delimiter *inside* a block end it
|
|
58
|
+
// early, and the example rows after it would be read as parts or pins.
|
|
59
|
+
let fence = null;
|
|
60
|
+
// Split on either line ending: a CRLF document would otherwise leave a
|
|
61
|
+
// trailing \r on every line, and \r is a line terminator that `.` does not
|
|
62
|
+
// match, so the fence patterns below would never fire on a CRLF file.
|
|
63
|
+
for (const line of md.split(/\r?\n/)) {
|
|
33
64
|
const t = line.trim();
|
|
34
|
-
|
|
65
|
+
// Matched against the raw line: CommonMark allows a fence up to three
|
|
66
|
+
// spaces of indentation, and four or more makes it content rather than a
|
|
67
|
+
// fence. Trimming first would let an indented delimiter inside a block
|
|
68
|
+
// read as a closer.
|
|
69
|
+
const delim = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
70
|
+
if (delim) {
|
|
71
|
+
const marker = delim[1];
|
|
72
|
+
const char = marker[0];
|
|
73
|
+
if (!fence) {
|
|
74
|
+
fence = { char, length: marker.length };
|
|
75
|
+
current = null;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// A closing fence carries no info string; anything else stays content.
|
|
79
|
+
if (char === fence.char && marker.length >= fence.length && !delim[2].trim()) {
|
|
80
|
+
fence = null;
|
|
81
|
+
current = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (fence || !t.includes('|')) {
|
|
86
|
+
current = null; // a blank, prose, or fenced line terminates the table
|
|
35
87
|
continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
}
|
|
89
|
+
const cells = splitRow(t);
|
|
90
|
+
// Outer pipes make a line unambiguously a table row. Without them, a
|
|
91
|
+
// pipe-bearing prose line (`Legend: A | B`) is indistinguishable from a
|
|
92
|
+
// row by shape alone, so the column count decides: matching the open
|
|
93
|
+
// table's width keeps it as a row, a mismatch ends the table there rather
|
|
94
|
+
// than reading the prose as a part/pin. The line still opens a new group,
|
|
95
|
+
// in case it is itself the header of an un-piped table.
|
|
96
|
+
if (current && !t.startsWith('|') && cells.length !== width)
|
|
97
|
+
current = null;
|
|
98
|
+
if (!current) {
|
|
99
|
+
current = [];
|
|
100
|
+
width = cells.length;
|
|
101
|
+
groups.push(current);
|
|
102
|
+
}
|
|
103
|
+
// The separator row stays in the group so the header can be located
|
|
104
|
+
// relative to it; it is dropped by the readers above and below.
|
|
105
|
+
current.push({ cells, separator: isSeparatorRow(cells), piped: t.startsWith('|') });
|
|
43
106
|
}
|
|
44
|
-
return
|
|
107
|
+
return groups;
|
|
45
108
|
}
|
|
46
109
|
/** True for a table's header row. BOM.md and PINOUT.md both lead with a
|
|
47
110
|
* Refdes or Pin column, so one check covers both doc types. */
|
|
@@ -77,35 +140,8 @@ export function parseCanonicalRows(md) {
|
|
|
77
140
|
* it loops on finish forever. Resolving by header name fixes that.
|
|
78
141
|
*/
|
|
79
142
|
export function parseCanonicalTables(md) {
|
|
80
|
-
const groups = [];
|
|
81
|
-
let current = null;
|
|
82
|
-
let width = 0; // column count of the open group, set by its first line
|
|
83
|
-
for (const line of md.split('\n')) {
|
|
84
|
-
const t = line.trim();
|
|
85
|
-
if (!t.includes('|')) {
|
|
86
|
-
current = null; // a blank or prose line terminates the current table
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const cells = splitRow(t);
|
|
90
|
-
// Outer pipes make a line unambiguously a table row. Without them, a
|
|
91
|
-
// pipe-bearing prose line (`Legend: A | B`) is indistinguishable from a
|
|
92
|
-
// row by shape alone, so the column count decides: matching the open
|
|
93
|
-
// table's width keeps it as a row, a mismatch ends the table there rather
|
|
94
|
-
// than reading the prose as a part/pin. The line still opens a new group,
|
|
95
|
-
// in case it is itself the header of an un-piped table.
|
|
96
|
-
if (current && !t.startsWith('|') && cells.length !== width)
|
|
97
|
-
current = null;
|
|
98
|
-
if (!current) {
|
|
99
|
-
current = [];
|
|
100
|
-
width = cells.length;
|
|
101
|
-
groups.push(current);
|
|
102
|
-
}
|
|
103
|
-
// The separator row stays in the group so the header can be located
|
|
104
|
-
// relative to it; it is dropped from the rows returned below.
|
|
105
|
-
current.push({ cells, separator: isSeparatorRow(cells) });
|
|
106
|
-
}
|
|
107
143
|
const tables = [];
|
|
108
|
-
for (const g of
|
|
144
|
+
for (const g of scanTableGroups(md)) {
|
|
109
145
|
// The header is the row directly above the separator. Falling back to the
|
|
110
146
|
// first row keeps a table that omits the separator working, and anchoring
|
|
111
147
|
// on the separator means a stray pipe-bearing prose line immediately above
|