- name
- circuitcode
- description
- Use when the user wants to create, edit, or fabricate a PCB from a natural-language description — "design me a macropad", "an ESP32 soil-moisture board", "a desk air monitor", "add a second button", "make the board smaller", "fix the DRC error", "regenerate the gerbers", "how much to get 5 made" — or to tweak, re-check, or fix an existing board `.tsx` in an Autonomous Circuit board project (product.json + boards/).
# circuitcode — real boards from board programs
## Purpose
A board is **code**: `boards/main.tsx` composes golden blocks into a device,
and the pipeline compiles it to a Circuit JSON, a schematic, a PCB layout, and
a fab packet a manufacturer will actually build. The source is the truth. The
user owns the project; you write the program, run the gauntlet, look at the
pictures, and fix what is wrong.
What comes out the far end is not a design file. It is a zip a person uploads
to JLCPCB, plus a walkthrough, plus a 3D body for the printed enclosure — five
assembled boards for about $75–110, in their hands in one to two weeks.
The reason this can work at all is the block library. **You compose validated
subcircuits; you never invent a circuit from a datasheet.** That is not a
style preference — see *Non-negotiables*.
## Make it buildable and repairable — by default
Four habits, applied without being asked:
1. **Compose, don't invent.** Every IC comes in through a block from
`circuitlib.blocks`. Glue between blocks (a resistor, a capacitor, an LED, a
header, a connector) is yours to place. A chip that is not in a block does
not go on the board — say so and offer the nearest block. The one way to
make the catalog longer is `block-source`
(`$CIRCUIT_SKILLS_DIR/block-source`, which is `~/.claude/skills/block-source` unless the host sets it), which sources a passive interconnect, a
certified module, or a non-radiating integrated module that carries every
active part it needs, from the supplier with graded provenance. Run it **first
thing in the build turn**, before you write a line of board source, and
never inside the edit/build/read loop — it is one network step, taken once.
It does not loosen this rule: a part whose circuit would be yours to draw is
still refused.
2. **Decouple everything, always.** One 100nF beside every IC power pin, one
bulk cap per rail. `circuitlib.helpers.decoupling_for()` tells you the count.
Blocks bring their own; glue ICs do not exist, so this mostly means: don't
strip a block's caps to save space.
3. **Label every net.** `net.V3_3`, `net.I2C_SDA` — never an anonymous trace.
An unnamed net is a warning from the pipeline and an unreadable schematic
for the human who has to debug the board.
4. **Leave the enclosure something to hold.** Four mounting holes at the
corners on a pitch you state (27 of 32 boards built have them; two on one
edge cantilevers the rest — `dfa_mounting_collinear`), connectors on one
edge, and a board outline inside the declared envelope. Fewer holes is a
choice you make on purpose and say so. This board is going inside a
3D-printed body.
5. **One side by default; the back when the face is spoken for.** If the
user-facing parts (screen, LED ring, buttons, sensors) fill the front at
the user's size, put the MCU core on the back as one block —
`<Rp2040Core layer="bottom" pcbRotation={180} …/>` with
`doubleSidedAssembly={true}` on the board and `"assemblyTier": "standard"`
in `product.json` (economic PCBA places one side only and skips the rest
silently — `dfa_bottom_side`; standard places both at a higher price
band) — and say so in the plan with the cost. The placement ruler
scores each side (`congestion.bySide`, `pinsBySide`). The shipped router
has failed a dense two-sided 54 mm board before (2026-09-08); if it hands
back unrouted nets, that is its limit — report it, do not spend rounds.
6. **Land the debug interface.** An MCU block brings SWCLK/SWD out as nets and
terminates neither. If nothing does, the assembled board cannot be halted,
single-stepped or recovered from a bad image — every part on it correct and
the product useless. `board_plan().must_expose` names the nets; `DebugPort`
from `blocks/glue` lands them on three 2.54mm pads. **Any other row of
labelled pads is the same component under its own name** — `PadHeader`,
also from `blocks/glue`, takes `nets`, `labels`, `pitch` and `padDiameter`,
so an off-board strip connector or a bench breakout is one import rather
than a hand-copy of `DebugPort`'s internals (which is what two engineers
did on 2026-08-17 before it had a name they would look for). Put it in open board
space, not inside the MCU block: three pads inside `rp2040-core`'s own box
route the debug pair through the crystal cluster and the router comes back
with a via shorted into the QFN pad field (measured 2026-08-11).
7. **Say what routing effort the board needs.** `autorouterEffortLevel="10x"`
is the floor on every board. The same rp2040-core board is `fab.ready:
false` with five blocking KiCad findings at the default effort and
`fab.ready: true` with zero at `"5x"` — same design, only this prop changed.
**The floor was 5x until 2026-09-08, and 5x is a false economy.** A board
that fails at 5x does not simply fail: the pipeline escalates one rung and
**builds the whole thing again**, so a board needing 10x pays for two full
builds instead of one. Measured that day, same brief, two boards: one asked
for 10x up front and finished in a single attempt; the other followed this
floor at 5x, failed, escalated, and spent 35 minutes to arrive at the same
29 blocking findings twice. Starting at the top rung costs more per attempt
and less per board. It also disables the escalation, which is the point —
there is nowhere left to climb, so a failure is reported once, immediately,
instead of being re-proved at a cost of twenty minutes.
**Read the verdict, do not hand-rebuild at a higher effort to see.** If the
sidecar says routing still failed at 10x, effort is not the lever any more:
the placement is, and on a 2-layer board the first thing to check is
whether components ended up on both sides.
**The one case it still cannot reach**: findings that only KiCad can see.
The escalation decides off the circuit.json scan, and `drc_violation` —
clearance, shorting, hole-clearance — is produced by the KiCad cross-check
several stages later, so a board whose *only* blockers are DRC ones gets no
retry. That is the case where declaring `"10x"` yourself is still the move,
and it is why the fleet's RP2040 boards were rebuilt by hand.
**Since v1.8 (2026-09-11) the copper of record is Freerouting's, not the
compiler's**, whenever the toolchain has it (`build.router.engine ==
"freerouting"`). The compiler still routes first (that is the incumbent);
then the board goes to Freerouting at the rules' target and, if nets stay
open, once more at the fab floor plus a hair; routerlib patches what is
still open with Freerouting's copper as obstacles; and any net nobody
closed stays open by default. Mixing compiler copper into the result is
opt-in (`CIRCUIT_ROUTER_FILL_FROM_INCUMBENT=1`) because it was routed against
different neighbours and can create shorts. The stage keeps the whole
incumbent if connectivity would regress. `build.router.applied == false` with a `reason` is the compiler's copper, unchanged.
`CIRCUIT_ROUTER=off` pins the compiler's router for an A/B.
## Treat the device as a project
```
<project>/
├── product.json the bible: name, power, envelopeMm, layers, fab, assembly
├── parts.json the locked BOM — parts-book owns it; you never write it
├── blocks/ the golden-block library, frozen with this project
├── boards/
│ ├── main.tsx your file — the only file you write
│ └── main.circuit.json + main.board.json + main_review/ + main_fab/ generated
├── tsconfig.json, tscircuit.config.json
└── .circuit/ build cache — never edit, never read
```
### Starting a new project
The app creates the workspace; you fill it. From the skill's own templates:
```bash
SKILL="${CIRCUIT_SKILLS_DIR:-$HOME/.claude/skills}/circuitcode"
cp -R "$SKILL/templates/project_skeleton/." /abs/project/
```
`boards/main.tsx` imports blocks by relative path, and a project that owns its
own snapshot keeps building the same board after the shared library moves on.
**You do not copy them yourself — the build seeds `blocks/` from the library on
a project that has none**, and reports what it wrote. Then edit `product.json`
(name, description, power, envelope) before you write any board source.
**Never copy `blocks/` from another project.** It is the one way this has gone
wrong: on 2026-08-21, weather-badge-16 through -25 — eight boards over two and
a half days — were found holding byte-identical blocks inherited from each
other rather than from the library, and a fix that unshorted every button on
every board with one reached none of them. A build now says so, at `warning`,
whenever a board that has never been built already disagrees with the library.
Rules of the project format:
- **Device-wide facts live in `product.json` only.** Power source, envelope,
layer count, fab profile. Don't restate them in the board file; read them.
- **One file per board.** Most projects have exactly one, `boards/main.tsx`.
- **Every number comes from a table.** `circuitlib.tables` owns the electrical
law and the fab limits. Retyping `0.127` into a board file makes a number
nobody can update.
- **Never edit generated artifacts.** `.circuit.json`, `.board.json`, the SVGs,
the PNGs, the fab packet — all outputs. Editing one gets it overwritten on the
next run and desynchronises the sidecar.
- **Never edit `parts.json`.** That is `parts-book`'s file. If a part is wrong
or out of stock, hand off; don't hand-patch the lock.
### The source contract
`boards/main.tsx` default-exports a function returning one `<board>`:
```tsx
import { UsbCPower } from "../blocks/usb-c-power/usb-c-power"
import { Ldo3v3 } from "../blocks/ldo-3v3/ldo-3v3"
export default () => (
<board width="40mm" height="30mm" thickness={1.6}>
<UsbCPower pcbX={-14} pcbY={0} schX={-6} schY={0} />
<Ldo3v3 pcbX={0} pcbY={6} schX={0} schY={0} />
<hole name="H1" diameter="3.2mm" pcbX={-17} pcbY={-12} />
</board>
)
```
Four things that are not optional:
- **`thickness={1.6}` explicitly.** The toolchain default is 1.4mm; JLC's
standard stackup is 1.6mm. Leave it out and you ship the wrong board.
- **Both coordinate systems on every placed thing.** `pcbX/pcbY` put it on the
board; `schX/schY` put it on the schematic. Skip the schematic coordinates and
you get a legible board with an unreadable drawing — and you must read that
drawing later.
- **A pinned-dialect header comment** naming the tscircuit version the file
targets (copy the template's). The toolchain ships roughly seven releases a
day; the file should say what it was written against.
- **Blocks by relative import** from `../blocks/<id>/<id>`. Never an `@tsci/…`
registry import — those are mutable, unreviewed, and fetched at build time.
## The loop
**Two loops, in this order.** First the *placement* loop — edit TSX, run
`preflight` (seconds, no router), `Read` `_placement.png`, read the
`placement` score — until the parts sit where the copper can reach them; only
then `scripts/circuit` to route. Then the *repair* loop — read a copper
finding, write `edits.json`, run `scripts/circuit … --edits`, read the verdict
in thirty seconds — until the copper is clean. Do not go back to the placement
loop for a copper finding: a rebuild re-routes every net and the finding moves
somewhere else.
**The placement ruler** (`placement` in `preflight` and `fastcheck`,
`build.placement` in the sidecar, `placement_summary` finding): `ratsnestMm`
(the shortest copper that could ever connect every net), `crossings` (net
lines that cross — each is a via pair or a detour you are asking the router
to pay for), `congestion.worst` (pins in the fullest 5 mm cell), `decoupling`
(each cap to the chip pin it serves; over 3 mm is a finding an engineer raises
on sight), `crystals`, `connectorsToEdgeMm`, `longestNets`. Push these down
before routing: move the part that owns the worst number, re-run preflight,
look at the picture. Measured 2026-09-11: a board that iterated placement 62
times against overlaps and price tier alone routed to 190 vias and 789 jogs;
its ruler read 240 crossings and 10 decoupling caps over 3 mm. A placement
that scores well routes with fewer vias — that is the whole point of the
order above.
```
understand ask → inspect project → block plan → edit main.tsx
↑ ↓
└── fix ← Read _schematic.png + _pcb.png ← read the JSON verdict ← run scripts/circuit
```
`ok: true` says the pipeline ran. Only your eyes say the board is right.
## Done means orderable
**A board is finished when `fab.ready` is `true`, and at no other time.**
Dee, 2026-08-11: *"All designs generated must be ready to be sent to JLCPCB.
Perfect, no issue, board generated one shot, printed."*
That is one gate, not a scale:
| Sidecar says | What you say |
|---|---|
| `fab.ready: true` | **Done.** Here is the packet, here is what it costs, here is how to order it. |
| `fab.ready: false` | **Not done.** One line on the single thing missing, then keep working or hand back a clear blocker. |
`ok: true`, "zero blocking warnings", "the pictures look right" and "the build
is clean" are all *inputs* to that gate. None of them is the gate. A packet
with unverified gerbers (`gerberSource: "tscircuit"`, kicad-cli absent) is
`fab.ready: false` and therefore unfinished, even with zero warnings — a user
cannot send it to a fab, so it is not a board yet.
### `fab.ready: true` is a floor. Never trade it for a tidier verdict.
**Once a build reports `fab.ready: true`, no later change may lose it.** If a
fix for a non-blocking finding costs readiness, **revert the fix** and report
the finding as accepted, with its measurement, in the final response.
This is not hypothetical. On the 2026-08-11 agent eval, `macropad-6` was
`fab.ready: true` on its **first** build and `false` after five repair rounds:
the loop chased findings that were never blocking anything and gave away the
board the user had already got. `usb-blinky` on the same run went ready →
ready over three rounds, so it is not inevitable — but once is enough, because
it is the worst failure available to this skill.
The router has had the right instinct all along: stage 0b keeps an escalated
route **only when it has strictly fewer blocking warnings**. Apply the same
rule one level up.
- After every build, compare `fab.ready` with the previous build's.
- `true` → `false` is a **regression, not progress**. Undo the change that
caused it before doing anything else, and say what you reverted and why.
- A warning or an info finding is never worth an orderable board. "Zero
findings" is not the goal; **orderable** is the goal, and the finding count
is only ever evidence about it.
**Aim to earn it on build #1.** Every repair round is a defect that should have
been prevented before the first build: read the BLOCK.md files, use
`circuitlib.layout.place_board()` for the outline, placement and mounting
holes instead of guessing coordinates, and `helpers.board_plan()` for the
block set. If you find yourself fixing the same class of thing twice, the real
bug is upstream — say so, because that fix belongs in the block or the library,
not in this board.
## Plan-phase design discipline
When the app runs you in Plan mode, **write no files.** Produce an engineering
spec the user can approve or redirect:
1. **What it does** — one paragraph, in the user's terms.
2. **Block plan** — the table: capability → block → why. Call
`circuitlib.helpers.board_plan(capabilities=[...])`; report anything it
returns in `unavailable` honestly instead of inventing a circuit for it.
3. **Power budget** — source, rail tree, the arithmetic. `V5 @ 1.5A → LDO →
V3_3`; sum the block draws; state the headroom.
4. **Pin allocation** — every MCU pin you intend to use, and for what.
5. **Size and layout intent** — board outline, what sits on which edge, where
the mounting holes go, and whether it fits `product.json`'s envelope.
6. **Cost band** — `circuitlib.helpers.estimate_cost()` plus the parts total
from the lock. Quote the assembled-5x band, not a fantasy unit price.
8. **Safety verdict** — run `circuitlib.safety.safety_gate()` on the ask. If it
refuses, the plan is the refusal and the reason. Don't design around it.
### Where the numbers come from — the tables own them
| Number you need | Where it lives |
|---|---|
| Rail voltages and tolerances | `circuitlib.tables.RAILS` |
| Trace width for a current | `helpers.trace_width_for(current_a=…)` (IPC-2221) |
| Conductor spacing for a voltage | `helpers.clearance_for(volts=…)` |
| Min trace/space/drill/annular/edge | `tables.MIN_*` |
| Board thickness, layer default | `tables.BOARD_THICKNESS_MM`, `tables.DEFAULT_LAYERS` |
| Decoupling counts | `helpers.decoupling_for(power_pins=…, rails=…)` |
| I2C pull-ups, USB series/CC resistors | `tables.I2C_PULLUP_OHMS`, `tables.USB_*` |
| Assembly fees, cost bands, lead time | `tables.*_USD`, `helpers.estimate_cost()`, `helpers.fab_profile()` |
| Which block does X | `blocks.block_for(capability)`, `blocks.CAPABILITY_INDEX` |
| A real, stocked glue part | `parts.pick("resistors", resistance=4700)` — JLC Basic/Preferred mirror |
| Is this part costing us a feeder fee | `parts.cheaper_basic_part("C…")` |
| Will the regulator cook | `helpers.regulator_thermal(vin=…, vout=…, current_a=…)` |
| Is this LED resistor sane | `helpers.led_current(rail_v=…, resistance_ohms=…)` |
| What a block needs fed | `blocks.BLOCKS[id].requires` / `.provides` |
| Iteration cap | `tables.MAX_REPAIR_ITERATIONS` |
| How big a block is, and where it sits | `layout.box(block_id)` -> `(min_x, min_y, max_x, max_y)` around its origin |
| **The whole board plan** | `layout.place_board([...])` -> outline + placements + holes + its own warnings |
| Room for something that is not a block | `layout.reserve(name, w, h)` / `layout.pad_header_extent(pads)` — pass the name in with the blocks |
| Where to put a row of blocks | `layout.place_row([...])` -> `{block: (pcbX, pcbY)}` (the primitive) |
| How big the board must be | `layout.min_board_for([...], columns=n)` |
| Will anything hang off the edge | `layout.board_fits(placements, w, h)` — run it *before* building |
If a number is not in a table and not in a block, it is an **estimate** — say
so out loud rather than presenting it as measured.
## Use this skill when
The user wants a board designed, changed, checked, or made orderable.
**Do not use it for:**
GitHub에서 보기