copperhead
Engineering

From brief.md to gerbers

The create pipeline is eight agent runs chained end to end, each one gated on evidence that it produced something real. A walk through what runs, what every stage has to prove and what happens when one of them fails.

 
$ add a USB-C power input to the key
 
read_file docs/POWER.md (48 lines)
list_nets 41 nets, no VBUS, no CC1/CC2
validate_change add-usb-c-power valid, edit tools unlocked
✓ propose: add-usb-c-power validated, edit tools unlocked
edit_file hardware/open-key.kicad_sch: USB-C receptacle, 2x 5.1k CC pulldowns
edit_file docs/POWER.md: VBUS budget, 5 V at 1.5 A
✓ edit: 2 files, 5 anchored regions
run_erc clean, 0 violations
check_drift docs match schematic
✓ verify: ERC clean, no drift
record_decision DECISIONS.md +1 · CHANGELOG.md +1
✓ remember: decision + changelog recorded
turn 1/40 · 900 in / 120 out · · drafting the power proposal
turn 2/40 · 4.2k in / 800 out · · placing the receptacle
turn 3/40 · 9.1k in / 1.5k out · · running ERC
turn 4/40 · 11.8k in / 1.9k out · · writing the decision log
done · verified erc · committed 3f2c9a1 · 17s · 12.3k tokens

Eight stages, eight commits, one command. What each of them had to prove is the rest of this post.

That run’s entire input, brief.md:

# USB-C 5V power breakout

A pass-through: USB-C in, protected 5V out. No regulation, no MCU.

- 5V from VBUS, rated 3A continuous. No PD negotiation.
- Out: 2-pin 3.5mm screw terminal and a 2x2 0.1" header.
- Power-good LED, under 1mA. Short-circuit protection.
- 30 x 20mm, two M3 holes, hand-solderable (0603 minimum).

You point copperhead at a markdown file and it hands back a design package. That is a large claim for one command, and the interesting part is not the schematic capture. It is the bookkeeping: what each stage has to prove before the next one is allowed to start, and what happens to the work when it cannot prove it.

Here is the whole path.

One file goes in

mkdir usb-c-breakout && cd usb-c-breakout

# a sentence: copperhead writes it to brief.md for you
copperhead create "a USB-C 5V power breakout, 3A, screw terminal and header, power LED"

# or write brief.md yourself, in as much detail as you like, and point at it
copperhead create --brief brief.md

That sentence is written to brief.md before anything else happens, and the --brief form is the same route from the other end: one real file on disk either way. Two later things depend on there being one: the resume command has to point at a path, and every stage stamps a sha256 of the brief into its run metadata, so a brief edited halfway through a pipeline shows up as a changed hash rather than quietly moving the target.

Then git gets set up. create will initialize a repository, set a repo-local identity, write the baseline .gitignore, make the first commit, and commit the brief. That is not a convenience. Every stage snapshots HEAD before it starts, and a failed run restores that snapshot with a hard reset and a clean, sparing only the run’s own audit trail. Committing the brief up front is what guarantees the file the resume command points at is still there afterwards.

do and sync keep the strict refusal on a dirty tree. Those run against a repository you already own, where an implicit initial commit would be a surprise rather than a courtesy.

Eight stages

Stage What it is asked for What the gate checks
Spec docs/SPEC.md, every budget also recorded as a machine-readable constraint A budgets section with real content under it
Architecture docs/SUBSYSTEMS.md, one section per subsystem with the reasoning A section heading with real prose under it
Parts docs/BOM.md, part numbers with datasheet-verifiable justification At least one row where a part was actually chosen
Schematic the .kicad_sch, built subsystem by subsystem Symbols placed, docs agreeing with them, ERC clean
Layout placement, power and critical nets routed, a draft quality note Footprints on the board, draft quality written down
Outputs gerbers, drill, DXF, STEP, SVG renders, ordering BOM Gerbers actually on disk
Firmware firmware/ with pins.h generated from the pinout Source files actually on disk
Dev plan bring-up order, test points, risks, prototype order plan A written plan rather than an empty file

Each stage is a complete agent run with its own prompt, its own turn budget, and its own commit. It is not one long conversation with eight topics in it. A stage that cannot pass its gate stops the pipeline and prints the exact command to pick up from there.

The gate is never “the file exists”

This is the part worth reading.

copperhead init scaffolds most of those markdown files before an agent writes a word. SPEC.md ships with a budgets heading and two HTML comments under it. SUBSYSTEMS.md gets auto-generated per-sheet headings full of - R1: 10k bullets pulled straight off the schematic. BOM.md arrives pre-filled with rows whose part number column reads UNVERIFIED. LAYOUT.md is scaffolded with the literal ## Draft quality heading that stage 5 is supposed to write.

So in a repository that has been through init, which is how copperhead gets pointed at an existing board, a gate that checks whether the file is there marks stages complete that have produced nothing. Every gate checks for evidence instead:

  • Spec: the budgets heading, plus at least one line beneath it that is neither blank nor an HTML comment. A filled section, not an empty placeholder.
  • Architecture: a section heading, plus a line of prose that is neither the scaffold boilerplate nor one of the auto-generated refdes bullets.
  • Parts: at least one BOM row whose part number column is not the UNVERIFIED placeholder. Somebody actually chose something.
  • Schematic: symbols exist, the BOM and pinout tables agree with them, and ERC passes.

The schematic gate is three conditions because each one was learned separately. Symbols alone lets an empty capture through. Symbols plus agreement still passes on a schematic with unconnected pins, which is exactly what a run killed mid-capture leaves behind, and treating that as done means layout and outputs both run against a board that is not there.

The same check runs again after a stage reports success, and this is the check that matters most. An agent can call finish with every gate green having only planned the work: one edit to a file header, then ERC on an empty sheet, which is of course clean. So “the run ended successfully” and “the stage is done” are evaluated as two separate questions, and only the second one advances the pipeline.

Resuming is free

Because completion is read entirely off the repository, there is no pipeline state file, no checkpoint, no --resume-from. Resuming is running the same command again. Finished stages announce themselves and get skipped.

One subtlety sits underneath that. If a resumed stage’s work is sitting uncommitted, which is what you get when a run stopped on a provider usage limit halfway through, it is committed before the pipeline moves on, so that a later stage’s rollback cannot wipe it. That commit only happens when every dirty path in the tree belongs to copperhead. If your own uncommitted work is sitting next to it, the pipeline says so and leaves the whole thing alone. Sweeping an engineer’s work in progress into an agent’s commit is worse than the problem it would be solving.

Inside a single stage

Each stage runs the same loop as any other copperhead run, with the same two invariants holding.

Nothing starts without a spec. The edit tools are not in the tool list the model is shown until a change proposal has been written and passed validation. Not discouraged in the prompt, absent from the catalog. An earlier version advertised them on every turn and enforced the lock at call time instead, which saved exactly one turn and traded a structural guarantee for it. That was the wrong trade and it was reverted.

Nothing is done until the tools agree. Every edit passes through a hook on its way out. Touch a .kicad_sch and the last ERC result is discarded. Touch a .kicad_pcb and the last DRC result goes with it. When the model then tries to end the run, it gets this back:

cannot finish yet:
- ERC has not passed since the last schematic edit (run run_erc)
- open sync obligations:
  - [drift] check_drift must run clean after KiCad edits

That refusal arrives as an ordinary tool result, so the model reads it and goes back to work. It is not an error condition, it is the loop doing its job.

This is also where DRC gets enforced on the layout stage. The stage gate itself only asks whether footprints are on the board, because the board having been touched at all is what puts DRC in front of the run’s own exit. A layout stage cannot end successfully with DRC failing, so the stage gate does not need to ask twice.

The schematic stage carries extra scaffolding

An agent cannot create the KiCad files itself. write_file refuses KiCad files outright, and edit_file needs an existing file to anchor into. So immediately before stage 4, the pipeline drops an empty schematic, a blank board with a default outline, and a project file into place, and wires them into the config.

That scaffold is recreated before every attempt rather than once per stage. A failed attempt’s rollback deletes it, because at that point it is still untracked, and a retry against a missing schematic fails worse than the failure it was recovering from.

There is one more guard specific to s-expression files. After every text edit to a schematic or a board, kicad-cli is asked whether the file still loads. If it does not, the edit is reverted with the parser’s complaint attached, because a corrupted file otherwise fails every subsequent check with an opaque error and nobody can tell which edit did it. The exception is a file that was already unloadable before the edit: reverting there would deadlock incremental repair, since every partial fix gets undone unless one lucky edit happens to fix the whole file at once. So that edit is kept, and the probe output keeps the pressure on.

When a stage fails

There are two different failures here and they are handled differently.

If the run itself failed, on a rate limit with no fallback, on repair cycles running out, on the turn budget going to zero, the loop rolls the tree back to the snapshot. It stashes the touched work first, under a named stash entry you can git stash apply, so the rollback removes the work from the working tree without destroying it. If instead the run ended successfully but the stage gate says nothing usable came out of it, there is no rollback at all. Whatever the attempt did stays in the tree and the next attempt builds on it.

Either way the model is then asked, on a fresh turn with no tools available to it, whether another attempt is likely to help and what should be done differently. A retry runs the stage again with that guidance prepended to the prompt. An abort stops the pipeline for a human. Two retries by default, and the diagnosis call’s own token cost is folded into the stage’s total rather than hidden.

When the retries run out, the exact resume command gets printed, along with which stage it will pick up at.

What it costs

Every turn’s response is cached on disk against the repository. A retried stage replays what it already paid for instead of buying it twice, which is what turns “session limit reached” from a failure into a scheduled pause. Wait for the reset, run the same command, and the completed turns replay at roughly zero tokens.

At the end you get a per-stage cost table: wall clock, turns, output tokens, cache hit rate, with the slowest and the most expensive stage called out. It is written to .copperhead/runs/REPORT.md and to a JSON file next to it, so successive boards can be compared rather than remembered.

Alongside that, every stage that works on the KiCad files renders the schematic and the board to SVG into its own run directory. Every text and exit-code gate in the pipeline can be satisfied by a design that is visibly wrong, and nothing else in the run ever looks at the board. A render costs nothing and closes that gap.

What it does not prove

Worth being straight about. Stages 4 and 5 are verified against a real tool: ERC and DRC either pass or they do not, and the pipeline cannot argue with the exit code. The document stages check shape. A SPEC.md that is well formed and wrong passes stage 1, and everything downstream inherits it faithfully.

That is exactly why anything the brief did not say gets flagged ASSUMED in the spec, and why those flags are worth reading before the schematic stage rather than after layout. Correcting an assumption at stage 1 costs a minute. Correcting it at stage 6 costs the pipeline.

The full stage reference lives in the create workflow docs. The invariants underneath all of it are covered in Spec-gated in, verification-gated out.