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.
Files changed (142) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +47 -8
  3. package/dist/agent/context.js +2 -0
  4. package/dist/agent/context.js.map +1 -0
  5. package/dist/agent/dock-renderer.js +2 -2
  6. package/dist/agent/dock-renderer.js.map +1 -1
  7. package/dist/agent/envelope.js +105 -0
  8. package/dist/agent/envelope.js.map +1 -0
  9. package/dist/agent/filetools.js +24 -1
  10. package/dist/agent/filetools.js.map +1 -1
  11. package/dist/agent/ledger.js +24 -0
  12. package/dist/agent/ledger.js.map +1 -1
  13. package/dist/agent/loop.js +63 -72
  14. package/dist/agent/loop.js.map +1 -1
  15. package/dist/agent/prompts.js +4 -3
  16. package/dist/agent/prompts.js.map +1 -1
  17. package/dist/agent/providers/claude-code.js +17 -1
  18. package/dist/agent/providers/claude-code.js.map +1 -1
  19. package/dist/agent/providers/codex.js +84 -39
  20. package/dist/agent/providers/codex.js.map +1 -1
  21. package/dist/agent/providers/tool-protocol.js +21 -0
  22. package/dist/agent/providers/tool-protocol.js.map +1 -1
  23. package/dist/agent/recovery.js +186 -15
  24. package/dist/agent/recovery.js.map +1 -1
  25. package/dist/agent/registry.js +49 -0
  26. package/dist/agent/registry.js.map +1 -0
  27. package/dist/agent/render.js +2 -2
  28. package/dist/agent/render.js.map +1 -1
  29. package/dist/agent/theme.js +10 -5
  30. package/dist/agent/theme.js.map +1 -1
  31. package/dist/agent/tools.js +100 -586
  32. package/dist/agent/tools.js.map +1 -1
  33. package/dist/agent/transcript.js +2 -0
  34. package/dist/agent/transcript.js.map +1 -1
  35. package/dist/capabilities/define.js +35 -0
  36. package/dist/capabilities/define.js.map +1 -0
  37. package/dist/capabilities/handlers.js +744 -0
  38. package/dist/capabilities/handlers.js.map +1 -0
  39. package/dist/capabilities/helpers.js +39 -0
  40. package/dist/capabilities/helpers.js.map +1 -0
  41. package/dist/capabilities/index.js +50 -0
  42. package/dist/capabilities/index.js.map +1 -0
  43. package/dist/capabilities/skills/generate-report.js +23 -0
  44. package/dist/capabilities/skills/generate-report.js.map +1 -0
  45. package/dist/cli.js +158 -0
  46. package/dist/cli.js.map +1 -1
  47. package/dist/commands/check.js +33 -1
  48. package/dist/commands/check.js.map +1 -1
  49. package/dist/commands/create.js +181 -26
  50. package/dist/commands/create.js.map +1 -1
  51. package/dist/commands/doctor.js +83 -6
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/skill.js +109 -0
  54. package/dist/commands/skill.js.map +1 -0
  55. package/dist/commands/sync.js +3 -1
  56. package/dist/commands/sync.js.map +1 -1
  57. package/dist/config.js +19 -6
  58. package/dist/config.js.map +1 -1
  59. package/dist/kicad/bootstrap.js +24 -3
  60. package/dist/kicad/bootstrap.js.map +1 -1
  61. package/dist/kicad/cli.js +106 -18
  62. package/dist/kicad/cli.js.map +1 -1
  63. package/dist/kicad/dossier.js +207 -0
  64. package/dist/kicad/dossier.js.map +1 -0
  65. package/dist/kicad/draft/draft.js +135 -0
  66. package/dist/kicad/draft/draft.js.map +1 -0
  67. package/dist/kicad/draft/engine.js +5310 -0
  68. package/dist/kicad/draft/engine.js.map +1 -0
  69. package/dist/kicad/draft/ir.js +368 -0
  70. package/dist/kicad/draft/ir.js.map +1 -0
  71. package/dist/kicad/draft/symsource.js +504 -0
  72. package/dist/kicad/draft/symsource.js.map +1 -0
  73. package/dist/kicad/emit.js +220 -0
  74. package/dist/kicad/emit.js.map +1 -0
  75. package/dist/kicad/fab.js +13 -0
  76. package/dist/kicad/fab.js.map +1 -1
  77. package/dist/kicad/legibility.js +608 -0
  78. package/dist/kicad/legibility.js.map +1 -0
  79. package/dist/kicad/score.js +431 -0
  80. package/dist/kicad/score.js.map +1 -0
  81. package/dist/kicad/sexp.js +265 -6
  82. package/dist/kicad/sexp.js.map +1 -1
  83. package/dist/kicad/symlib.js +346 -16
  84. package/dist/kicad/symlib.js.map +1 -1
  85. package/dist/mcp/server.js +485 -0
  86. package/dist/mcp/server.js.map +1 -0
  87. package/dist/memory/bom-table.js +75 -39
  88. package/dist/memory/bom-table.js.map +1 -1
  89. package/dist/memory/scaffold.js +14 -1
  90. package/dist/memory/scaffold.js.map +1 -1
  91. package/dist/util/redact.js +6 -0
  92. package/dist/util/redact.js.map +1 -1
  93. package/package.json +13 -8
  94. package/src/agent/context.ts +35 -0
  95. package/src/agent/dock-renderer.ts +3 -2
  96. package/src/agent/envelope.ts +124 -0
  97. package/src/agent/filetools.ts +26 -1
  98. package/src/agent/ledger.ts +24 -0
  99. package/src/agent/loop.ts +73 -78
  100. package/src/agent/prompts.ts +4 -3
  101. package/src/agent/providers/claude-code.ts +22 -1
  102. package/src/agent/providers/codex.ts +91 -42
  103. package/src/agent/providers/tool-protocol.ts +22 -0
  104. package/src/agent/recovery.ts +183 -13
  105. package/src/agent/registry.ts +58 -0
  106. package/src/agent/render.ts +4 -3
  107. package/src/agent/theme.ts +15 -5
  108. package/src/agent/tools.ts +124 -628
  109. package/src/agent/transcript.ts +6 -0
  110. package/src/agent/types.ts +10 -5
  111. package/src/capabilities/define.ts +88 -0
  112. package/src/capabilities/handlers.ts +769 -0
  113. package/src/capabilities/helpers.ts +37 -0
  114. package/src/capabilities/index.ts +53 -0
  115. package/src/capabilities/skills/generate-report.ts +25 -0
  116. package/src/cli.ts +154 -0
  117. package/src/commands/check.ts +51 -1
  118. package/src/commands/create.ts +183 -21
  119. package/src/commands/doctor.ts +85 -6
  120. package/src/commands/skill.ts +127 -0
  121. package/src/commands/sync.ts +5 -3
  122. package/src/config.ts +56 -8
  123. package/src/kicad/bootstrap.ts +24 -3
  124. package/src/kicad/cli.ts +129 -18
  125. package/src/kicad/dossier.ts +217 -0
  126. package/src/kicad/draft/draft.ts +173 -0
  127. package/src/kicad/draft/engine.ts +5274 -0
  128. package/src/kicad/draft/ir.ts +416 -0
  129. package/src/kicad/draft/symsource.ts +549 -0
  130. package/src/kicad/emit.ts +300 -0
  131. package/src/kicad/fab.ts +15 -0
  132. package/src/kicad/legibility.ts +695 -0
  133. package/src/kicad/score.ts +502 -0
  134. package/src/kicad/sexp.ts +346 -6
  135. package/src/kicad/symlib.ts +364 -18
  136. package/src/mcp/server.ts +560 -0
  137. package/src/memory/bom-table.ts +85 -38
  138. package/src/memory/scaffold.ts +14 -1
  139. package/src/util/redact.ts +6 -0
  140. package/dist/memory/synap.js +0 -152
  141. package/dist/memory/synap.js.map +0 -1
  142. 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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"}
@@ -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
- const rows = [];
32
- for (const line of md.split('\n')) {
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
- if (!t.startsWith('|'))
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
- const cells = t
37
- .split('|')
38
- .slice(1, -1)
39
- .map((c) => c.trim());
40
- if (cells.every((c) => /^:?-+:?$/.test(c)))
41
- continue; // separator row
42
- rows.push({ cells });
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 rows;
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 groups) {
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