Skip to main content

boardforge

Generate and analyze KiCad designs from a requirements prompt: explore many candidate architectures, then turn the selected one into a routed, manufacturable board. SKiDL code is the source of truth; runs headless (KiCad only for viewing/editing results).

This project was called boardsmith until 2026-10-16; the name is taken on PyPI by an unrelated tool. An old boardsmith.toml is still read (with a warning), and old constraints.json files keep working.

Status: the physical pipeline (C1–C6: SKiDL → schematic → board → placement → routing → checks → fab outputs) and the review gate (B4) work. The detail stage's agents are a Claude Code plugin (plugin/), run end to end twice on the dual-ADC design. The explore stage is not started. Design: docsrc/design.md.

Install

boardforge needs Python 3.11 or later and these released tools:

Tool Version Needed for
KiCad 10.0 (tested with 10.0.4) everything: kicad-cli and KiCad's Python (pcbnew)
Freerouting 2.4 or later (tested with 2.4.1) routing (optional: the built-in A* router handles small boards alone)
ngspice 44 or later (tested with 44.2) the review gate's simulation checks (optional)
pdftotext (Poppler utilities) any checking that a downloaded datasheet is the right part (optional)

Install boardforge from PyPI. This also installs SKiDL 3.0.0 or later and schematizer 0.2.0 or later:

uv tool install "boardforge[analysis]"   # or: pip install "boardforge[analysis]"
boardforge doctor                        # checks KiCad 10, its Python, Freerouting, ngspice

The analysis extra adds jsonschema, which validates datasheet extractions.

boardforge doctor finds KiCad and Freerouting in their usual install locations and on PATH. Otherwise, set their paths in ~/.config/boardforge/config.toml, using the same [kicad] and [routing.freerouting] keys as a project file:

[kicad]
cli = "/opt/kicad/bin/kicad-cli"
python = "/opt/kicad/bin/python3"

[routing.freerouting]
path = "/opt/freerouting/bin/freerouting"

To design with Claude Code agents, also install the boardforge plugin (plugin/README.md).

Use

A project is a directory with a SKiDL file and a boardforge.toml:

cd test/examples/tiny
boardforge run                 # C1..C6 -> build/
boardforge run --from c4       # rerun from routing
boardforge route               # one stage: sch | board | place | route | check | fab
boardforge status
boardforge render --side both  # build/renders/*.svg/png of the current board
boardforge review              # B4: constraints.json against the design (after C1)
boardforge datasheets sync     # part-fact cache: datasheet PDFs and extractions

C3 places multi-pin parts next to what they connect to (through up to three series passives), and warns when a connector's signal reaches its first IC from more than [placement] connector_path_mm (25) away, such as an unbuffered input crossing the board. C3 also reports a routability estimate (reports/congestion.json: routing demand per 2.5 mm cell, ratsnest crossings) and, with compaction, the sizes it tried (reports/compaction.json). Each stage prints a short summary; details go to build/reports/, and build/pipeline_state.json records every stage. Outputs:

  • build/kicad/: KiCad project (labels-only schematic, board, net classes, lib tables)
  • build/fab/: Gerber zip, JLCPCB BOM and CPL
  • build/docs/: schematic PDF, 3D renders
  • build/reports/: parity, ERC, DRC, geometry checks, kicad-happy analyses

SKiDL conventions

  • Use part.fields["MPN"] = ... (and LCSC, Manufacturer, Tolerance, ...). Keyword arguments to Part() and plain attributes (part.MPN = ...) don't become fields.
  • Net classes: NetClass("Power", track_width=0.3, clearance=0.15, priority=2) and net.netclasses = power (not net.netclass, which does nothing). Keys are KiCad's .kicad_pro names; in SKiDL the larger priority wins.
  • Don't set lib_search_paths; boardforge points SKiDL at KiCad 10's libraries.
  • Expose a function that builds the circuit and name it in [design] top, or build it at import time. [design] file may also be a package directory (circuits/<name>/ with __main__.py); it runs as a module, so relative imports work. The project directory and its symbols/ are on SKiDL's library path.

boardforge.toml

See src/boardforge/config.py for every key and its default. The main ones:

[design]
file = "tiny.py"
top = "build"                  # function that builds the circuit

[fab]
name = "jlcpcb"
tier = "standard"              # profile tier: standard (1-2 layers) | multilayer

[board]
size = [130, 90]               # mm; omit to estimate from courtyard area (fill)
layers = 4
planes = { "In1.Cu" = "GND", "In2.Cu" = "+3V3" }
pours = ["GND"]                # outer-layer pours, stitched with vias

[rules]                        # default net class; lowered to the pad gap of a finer-pitch
track_width = 0.15             # footprint (0.4 mm QFN: 0.10), down to the fab tier minimum
clearance = 0.15

[[placement.anchor]]           # floorplan: fixed positions (mm from top-left)
value = "^LTC2291"             # regex on value (or ref = "U3"), optional sheet regex
at = [55.0, 44.0]
rot = 0
edge = "left"                  # optional: keep the offset from this edge when compacting

[[placement.anchor]]           # a connector: rotation from the footprint, mating side out
ref = "J2"
at = [12.5, 22.0]
facing = "left"
edge = "left"

[placement.compact]            # shrink the board while everything still fits, then
enabled = true                 # confirm by routing (stepping back up if it fails)
min_scale = 0.6
max_stretched = 5              # stop when 5 more passives than at full size end up
stretch_mm = 8.0               # over 8 mm from their spot (C3 summary: stretch)

[[placement.region]]           # floorplan: unanchored ICs of a sheet go in this area, near
sheet = "front_end"            # the parts they connect to; regex on sheet (and/or value);
area = [0.0, 0.0, 0.4, 1.0]    # fractions of the board (or rect = [x0, y0, x1, y1] mm)

[[placement.pin_swap]]         # FPGA/MCU pin swap; C3 writes the map, the design reads it
part = "^ICE40"
map = "fpga_pinmap.json"       # {SKiDL net: pin}; C1-C3 rerun when it changes
[[placement.pin_swap.group]]
pins = [1, 2, 3, 4, 7, 8]      # swappable pins
partner = "^LTC2291"           # the part these signals go to

[routing]
chain = ["freerouting", "astar"]
preroute = ["USB_DP", "USB_DM"]

[checks.rail_currents]         # regulator loads (A) for the thermal check, overriding
"+1V2" = 0.05                  # constraints.json rail_current; else kicad-happy guesses

Net names in the config are SKiDL names; boardforge maps them to KiCad's.

Review gate (B4)

boardforge review checks constraints.json (project root) against the design after C1 and writes build/reports/review.json: PASS / FAIL / NOT CHECKED per constraint. A HARD failure fails the gate; SOFT and ASSUMED failures, and anything not checked, warn.

{"schema": "boardforge-constraints", "version": 1, "constraints": [
  {"id": "ALIAS-A", "kind": "ac_gain", "tag": "ASSUMED", "why": "content at Nyquist 40 dB down",
   "in": "VIN_A", "out": ["AIN_AP", "AIN_AN"], "at_hz": 5e6, "ref_hz": 1e3, "max_db": -40}]}

ac_gain runs an ngspice testbench of the whole path, built from the SKiDL netlist. Op-amps and fully-differential amplifiers need a model (unmodelled ICs are open circuits):

[review]
dc = { VCM = 0.9 }             # DC volts for nets the netlist can't give
[review.models."^THS4521"]     # regex on value / MPN / symbol
type = "fda"                   # or "opamp"
gbw_hz = 93e6                  # overridden by a part-fact cache extraction
pins = { outp = 4, outn = 5 }  # roles the symbol's pin names don't give

rail_current (net, max_a: the load on the rail) is checked against the rated output current (regulator.iout_max) of the part that makes the rail, found by its power-output pin on the net or behind one series inductor or ferrite bead. The same loads feed the C5 thermal check. review also checks each part that has a datasheet extraction (see Part-fact cache), as DS:<MPN>: the voltage across its supply pins against the absolute maximum (error) and recommended range (warning), and its datasheet power pins connected. DS:coverage lists the parts without an extraction. PKG:pins checks that every symbol pin on a net has a footprint pad (and every numbered pad has a pin), and PKG:<MPN> compares the footprint with the datasheet package: pad count, pitch, thermal pad, pin numbers and body size. BOM diffs the sourced BOM ([review] bom, default sourcing/sourced_bom.csv) against the design: MPN, LCSC number, value (numerically), footprint, symbol, and the package column against the footprint's body variant.

Part-fact cache

Datasheet PDFs and their structured extractions, in kicad-happy's layout (datasheets/manifest.json, <MPN>.pdf, extracted/<MPN>.json), kept twice: in the project (datasheets/, what the analyzers and review read) and in a cache shared by all projects (~/.cache/boardforge/datasheets/). Parts are the netlist's MPN fields; ICs, diodes and crystals by default (prefixes).

boardforge datasheets status   # per part: PDF, extraction, quality, staleness, in the cache
boardforge datasheets sync     # copy from the shared cache, then find missing PDFs
boardforge datasheets add MPN FILE_OR_URL   # one PDF, after checking it names the part
boardforge datasheets check MPN...  # validate extractions, fill derived fields, score
boardforge datasheets show MPN...   # markdown summary of extractions
boardforge datasheets paths    # kicad-happy's extraction prompts, schemas, examples
boardforge datasheets publish  # copy the project's extractions to the shared cache

Before there is a netlist, the parts come from the sourced BOM CSV. A PDF is kept only if its text names the part; a scanned one needs add --force after a look by eye. sync looks for a missing PDF in the search directories (a file named after the part, OPA810.pdf for OPA810IDR), then downloads it from the symbol's datasheet URL, then LCSC. A download whose text doesn't name the part is discarded (needs pdftotext). --fresh skips the shared cache, --offline the downloads. Extraction itself is LLM work: an agent writes extracted/<MPN>.json following kicad-happy's prompts and schemas (paths), then check validates it and fills in the PDF hash, page count and quality score. publish takes only extractions that match their PDF and pass kicad-happy's schema (needs jsonschema), and keeps a shared entry unless the project's is newer and scores no lower (--force). review takes op-amp GBW and slew rate from the extractions.

[datasheets]
search = ["~/datasheets"]
cache = "~/.cache/boardforge/datasheets"   # "" = no shared cache

Layout

src/boardforge/         package
  physical.py           C1-C6 orchestration
  pcb/                  board scripts run under KiCad's Python (all pcbnew code is here)
  routers/              router interface: Freerouting (Specctra), A*
  fabs/<fab>/           fab adapters: profile.toml + adapter
  analysis/             analysis-step registry (kicad-happy analyzers by default)
  review/               B4 gate: constraints.json, SPICE path testbenches, datasheet, package and BOM checks
  datasheets.py         part-fact cache: status, sync, add, check, show, publish
plugin/                 Claude Code plugin: the detail-stage agents and skills (from skidl-skills)
  vendor/kicad_happy/   vendored kicad-happy (MIT)
test/                   pytest; test/examples/ has the tiny and dual-ADC projects
docsrc/                 documentation source (design doc)

Tests: pytest -m "not kicad" for the fast ones; pytest also runs the tiny design end to end (about a minute).

Third-party code

src/boardforge/vendor/ contains code from other projects, each under its own license. See src/boardforge/vendor/README.md. The plugin is adapted from skidl-skills (MIT): plugin/ATTRIBUTION.md.

Metadata

Release files for boardforge 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for boardforge 0.1.0
File Size Uploaded
boardforge-0.1.0.tar.gz 2.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for boardforge 0.1.0
File Interpreter ABI Platform
boardforge-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 4.1 MB

Release files / boardforge-0.1.0.tar.gz

Download URL boardforge-0.1.0.tar.gz
Size 2.0 MB
Tags Source
SHA-256 checksum
How to use checksums
e42f4d5f30fd16b42a3e3dec1db635ea1e698fdefe471580ef6bc6df7960fe07
BLAKE2b-256 checksum
How to use checksums
fd4879dcf0b58f6552a475a7f4cf87edeb08fdc1087e3c6093fac881cf719211
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / boardforge-0.1.0-py3-none-any.whl

Download URL boardforge-0.1.0-py3-none-any.whl
Size 2.1 MB
Tags Python 3
SHA-256 checksum
How to use checksums
69542ceaaf7139817ea22cd1fec180a4792dc8715a5f5da71f0a75a0fc6be557
BLAKE2b-256 checksum
How to use checksums
74f284dceb6eab5c18f6db4e74402ec2b674cdadc6c225f35725e0cc7f9870e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page