akcli is the command-line entry point of akcli. It reads
Altium binary .SchDoc/.SchLib/.PcbDoc/.PcbLib and KiCad
.kicad_sch/.kicad_sym/.kicad_pcb/.kicad_mod, runs checks (including design-intent and
design-contract assertions), diffs revisions, verifies schematic ↔ PCB equivalence, audits and
repairs project library workspaces, gates manufacturing against versioned fab profiles, draws KiCad
schematics with a before/after net-connectivity diff, and provides 60 standards-cited engineering
calculators (akcli calc) — a pure-stdlib toolchain purpose-built for KiCad, with read-only
Altium import as the on-ramp.
This reference is the contract for the CLI surface. It tracks the subcommands and flags defined in
src/akcli/cli.py.
akcli [GLOBAL FLAGS] <subcommand> [ARGS...] # doc-noqa (usage synopsis, not a runnable line)
Convention: stdout carries data (parsed JSON/text results); stderr carries logs and
diagnostics. This keeps akcli ... --json | jq clean.
| Flag | Effect |
|---|---|
--version |
Print package version and protocol_version, then exit. |
-h, --help |
Show help for akcli or a subcommand, then exit. |
-C, --config PATH |
Use this akcli.toml instead of walk-up discovery from the input file's directory. |
-v, -vv |
Increase log verbosity (to stderr). -v info, -vv debug-level logs. |
-q, --quiet |
Suppress non-error logs on stderr. |
--json |
Emit machine-readable JSON on stdout (carries schema_version). |
--no-color |
Disable ANSI color in text output. |
--debug |
Show full Python tracebacks instead of structured ERROR: CODE messages. |
Parse an Altium or KiCad schematic/PCB/library into the normalized model and print it.
- Input:
.SchDoc,.SchLib,.PcbDoc,.PcbLib,.PrjPcb,.kicad_sch,.kicad_sym,.kicad_pcb,.kicad_mod. - Fail-loud format detection. The extension picks the reader; a bare OLE2 container with an
unknown extension is classified by its storage layout (
Board6→ PcbDoc,Library→ PcbLib,FileHeader→ SchDoc/SchLib) — never assumed to be a schematic. An unrecognized container exits5instead of silently returning an empty model. The--jsonmetadata carriesdetected_format,detection_method(extension|content) andobject_counts. EMPTY_IMPORT: a non-empty source that normalizes to zero objects prints anEMPTY_IMPORTwarning (the classic silent-failure signature);--strictturns it into exit1.- A
.PcbLibdecodes each footprint storage into theLibrarymodel'sfootprints(FootprintDef/FootprintPad: pad number/position/size/drill/shape/rotation, NPTH vs plated); undecoded primitives (silkscreen graphics, text, 3D bodies) surface asUNSUPPORTED_PRIMITIVEwarnings, never dropped silently..kicad_mod/.prettyread into the same model. - A KiCad root sheet recurses into its
(sheet ...)children (paths relative to the parent file, cycle- and depth-guarded); every sheet instance contributes its components under the designator from the matching(instances (path ...))entry. - An Altium root recurses into sheet symbols the same way (RECORD 15/16/32/33; ports pair
with their own sheet entry — Altium Automatic scope). A
.PrjPcbreads the project's top sheet and honorsPowerPortNamesTakePriority; if it lists more than one top-level sheet, only the first is read and a warning names the ignored ones (a schematic that cannot be scanned for sheet symbols also warns instead of being skipped silently). - Nothing is dropped silently. Every
.SchDocRECORD id the reader does not model is counted and reported once per read aswarning: altium: skipped 5 unsupported record(s): 26 (Bus) ×3, 37 (BusEntry) ×2(stderr, and in--jsonunderwarnings); counts are ordered most-frequent-first and unnamed ids print bare. Buses (RECORD 26 / bus entry 37) are not read — when they appear in that histogram the netlist is missing their connectivity. A RECORD-41 parameter that lands nowhere (owned by an unsupported record, e.g. a parameter set) is counted too, even though 41 itself is a supported id. - Altium footprints are model-type-aware. A component links several models through the same
record; only
ModelType=PCBLIB(or an absentModelType) is a footprint, and among those theIsCurrent=Tone wins. ASIM/SI/PCB3DLibmodel name never reachesfootprint, so a part with no footprint model reads as missing (andcheckraisesBOM_MISSING_FOOTPRINT) instead of importing a SPICE model name that looks like a footprint. Comment="=<ParamName>"is an indirection, not a literal: it resolves (case-insensitively) against that component's own parameters. An unresolvable reference keeps the literal string and warns:warning: altium: 1 unresolved Comment parameter reference(s) (imported literally): R1 '=Value'.- Document parameters (
Title,Revision,DocumentNumber,Date, …) — the ownerless RECORD-41s that fill the title block — read intotitle_blockinread --json(and in--summarywhen non-empty). For a hierarchical design this is the root sheet's. - A
.PcbDocdecodes both the ASCII sections (nets/components/classes/rules) and the binary copper sectionsTracks6/Vias6/Arcs6/Pads6intotracks/vias/arcs/pads(mils, Altium's native +Y-up frame);Fills6/Regions6/Texts6/Polygons6are skipped. - A
.kicad_pcbdecodes footprints, pad-level net bindings (both(net N "name")and KiCad 10(net "name")dialects, absolute pad positions with the footprint rotation folded in), tracks, vias (through/blind/micro), zones, and the board setup (board: copper layers, thickness, setup values, Edge.Cuts outline bbox). KiCad geometry stays in its native mm, +Y-down frame (board.units == "mm"), unlike the Altium PCB reader's mils — checksource_format. --jsonprints the fullSchematic/Pcb/Libraryexport withschema_version;--mdprints a human Markdown summary.--summaryprints counts + metadata only, never the full object arrays — the context-budget escape hatch for big boards.--summary --jsonemits{schema_version, source, format, counts, metadata[, warnings]}; drill down afterwards withakcli nets --match,akcli component --match, or a fullread --json.--match GLOB/--limit Nfilter and cap the listed objects (components/symbols/footprints; case-sensitive fnmatch). The--jsonmetadata carriestotal/matched/returned/truncated.
Extract the netlist (net → pin membership) using the shared netbuild engine.
- With
NAME, print just that net; a miss prints a notice (with a did-you-mean hint) to stderr and exits8(QUERY_MISS);--jsonadditionally emits{"found": false, "query": NAME, "kind": "net", "source": FILE}on stdout so an agent can distinguish "net absent" from "file absent" (exit4) without parsing stderr. --jsonshares ONE envelope + net shape withakcli nets:{schema_version, source, ..., nets: [{name, stable_id, members: ["REF.PIN", ...][, aliases]}]}(the list branch adds thetotal/matched/returned/truncatedthrottle meta; theNAMEbranch adds per-net diagnostics —is_named,source_names,confidence,merge_reasons). One parser covers both commands.- Pin-level queries (
REF.PIN, split on the first dot; unknown pins exit8with a did-you-mean plus the valid pin list):--at REF.PIN— reverse lookup: the net this pin is on (net: nullwhen unconnected).--same A.PIN B.PIN— binary connectivity:{"same": true|false}+ the common net, and each pin's net name; always exits0when both pins exist (the answer IS the payload).--why REF.PIN— attachment diagnosis for "why is this pin (not) connected": pin tip world coords, attachment status (at_junction/at_wire_end/midspan_touch/label_on_tip), the nearest wire end / label / other pin with mil distances, anoff_grid_50milflag, and actionablehints(built from the same quantized helpersnetbuilduses, so the verdict never disagrees with the net engine). Geometry needs a.kicad_sch; other formats answer net membership only.
- Buses (imported KiCad designs): the engine models real bus semantics
arbitrated against
kicad-cli. A(bus_entry)conducts between its two ends (two wires ending on its two ends are one net even with no bus); an entry end attaches to a wire only at a wire endpoint or junction (a bare mid-span touch floats, same as a pin) but to a bus anywhere along a segment; the bus member a rip joins is chosen by the wire-side label (an unlabeled rip stays unconnected);NAME[a..b]vector expansion is inclusive at both ends in either order (K[3..0]→ K3…K0); local bus labels are sheet-scoped while global bus labels merge member nets across sheets. Soakcli net/diffon a bus-carried KiCad design reports the bus-carried nets correctly. A(bus_alias ...)declaration is deliberately ignored for netlisting — ground-truth tested againstkicad-cli10.0.4, an alias-labeled bus is member-less (identical to no alias), and a vector-looking alias name still expands as the vector — soakclimatcheskicad-clihere by design. - Performance:
netbuilduses anO(log n + k)orthogonal-segment index, so connectivity on large ladder/bus sheets is near-linear (a ~5100-segment sheet builds in a fraction of a second); the semantics are byte-identical to the prior brute-force scan.
Print every net → sorted members, one line per net (MID: C1.1, R1.2, R2.1); unnamed nets
render as <unnamed net_...>. --json emits
{source, total, matched, returned, truncated, nets: [{name, stable_id, members}]}.
--match GLOBfilters by net name (case-sensitivefnmatch, e.g.--match 'VDD*');--limit Ncaps the listing. A cut listing is visible in-band: the JSON envelope carriestotal/matched/returned/truncated, and text mode prints anote:to stderr — a filtered result can never be mistaken for the whole netlist.--intent-snapshot OUT.jsonadditionally writes the netlist as a design-intent JSON document ('-'= stdout) thatakcli check --intentconsumes — the snapshot → edit → assert workflow. Named nets only by default;--include-unnamedalso captures unnamed nets keyed by theirstable_id.- A snapshot round-trips:
akcli check <sch> --intent <snapshot>on the unchanged schematic reports zero findings.
Without REF: list components — compact rows (designator, library reference, value, pin count),
filterable with --match GLOB (on the designator) and --limit N; --json wraps the rows in the
same total/matched/returned/truncated envelope as nets.
With REF: that component's pin → net table. A missing REF prints a notice (with a
did-you-mean hint) to stderr and exits 8 (QUERY_MISS); --json additionally emits
{"found": false, "query": REF, "kind": "component", "source": FILE} on stdout.
akcli pins <lib_id> [--at X Y] [--rotation {0,90,180,270}] [--mirror {none,x,y}] [--symbols PATH ...]
Op-list authoring helper: resolve a symbol (Device:R, Timer:NE555P, ...) from the same
sources the writer uses (repeatable --symbols, config [paths] .kicad_sym entries) and print
every pin's number, name, electrical type, owning unit, and world coordinate for the given
placement:
$ akcli pins Device:R --at 2000 1000 --symbols .../Device.kicad_sym
Device:R @(2000,1000) rot=0 mirror=none
pin name type unit x_mil y_mil
1 passive 1 2000 850
2 passive 1 2000 1150
It mirrors the writer's geometry.pin_world, so a printed coordinate is byte-for-byte where
draw will place that pin — the exact point wires, labels, and power ports must land on. An
unresolvable lib_id is SYMBOL_NOT_FOUND (exit 6). --json emits the table as objects.
Without --symbols, a standard nickname is resolved through the project and per-user global
sym-lib-table. Those tables address KiCad's bundled libraries as ${KICAD10_SYMBOL_DIR}/...
— variables KiCad compiles in rather than exporting (a clean install stores them as null
in kicad_common.json), so akcli derives their default from the located kicad-cli install
(macOS <KiCad.app>/Contents/SharedSupport/symbols, Linux /usr/share/kicad/symbols, Windows
<install>\share\kicad\symbols; footprints/3D models likewise). An environment value always
wins: export KICAD10_SYMBOL_DIR=/path/to/symbols overrides the derivation, and is the fix
when KiCad is installed somewhere akcli cannot infer. akcli doctor reports whether this
resolution actually works here (the symbols check).
akcli bbox <lib_id> [--at X Y] [--rotation {0,90,180,270}] [--mirror {none,x,y}] [--symbols PATH ...]
The spacing-planning sibling of pins: per unit, the drawn-body box and the full box
(body UNION pin tips) in world mils for a hypothetical placement, plus width/height and the
documented 400-mil spacing convention. Pin-only symbols (power stubs) fall back to the pin box
and say so (pin_only). Same transform chain as the writer, so the boxes can never disagree
with where draw puts the part. --json emits the boxes as objects.
Functional-group inspection + visual frames. List mode reports every group recovered from the
hidden Group symbol property (written by grouped ops — see the groups envelope in
docs/op-list-authoring.md): members, world bounding box, and whether its
frame is present. --frame (dry-run unless --apply) draws one border rectangle + title per
group through the standard draw pipeline (.bak, verify gate, journal, undo); frames carry a
stable key, so a re-run after parts move replaces the stale border in place.
Run the design checks (ERC-lite + power + BOM hygiene + nets + pairs + layout + document integrity) and print findings.
-
-C/--configsupplies rails, MCU designator, the schematic grid,[[erc_waiver]]entries, and the checker-agnostic[[waiver]]table (below). -
--erc/--power/--bom/--nets/--pairs/--layout/--integrity/--intent/--contract/--libsyncselect check families (default:erc+power+bom+nets+pairs+layout+integrity;layoutandintegrityonly run on.kicad_sch;[check] pairs = falseoptspairsout of the default set;intent,contractandlibsyncare opt-in only — they never run by default). -
--integrityruns the write gate's document checks read-only over any.kicad_sch—DANGLING_ENDPOINT,DANGLING_BUS_ENTRY,DUPLICATE_UUID,DUPLICATE_DESIGNATOR,UNRESOLVED_LIB_ID,INVALID_INSTANCES_PATH,POWER_PORT_STACKED(two net-naming power symbols on one coordinate — their rails merge invisibly),SHEET_FILE_MISSING(a(sheet …)whose child file is not next to it),NO_CONNECT_CONFLICT(WARNING — reported by the same pass but, alone among these, does not refuse a write) — so imported or hand-edited files get the same gatedraw --applyenforces post-write. A wire end on another wire's mid-span counts as connected only with a(junction)there — eeschema's rule, and the oneakcli netsapplies, so the two never disagree. -
--pairsis name-level continuity:PAIR_INCOMPLETE(WARNING — a_P/_DP/_H/+net whose_N/_DN/_L/-partner does not exist; deliberately asymmetric, a lone_N/_Lis the active-low convention and never fires),PAIR_PIN_MISMATCH(NOTE — both sides exist with different pin counts), andBUS_GAP(a numbered familyD0..D7with internal index holes; WARNING for families ≥ 4 members, NOTE below; families need not start at 0).[check] pair_suffixes = [["_P","_N"], ...]replaces the suffix table andbus_min_familyraises the family-size threshold; geometric diff-pair skew stays inreview analyze(EMC family). -
[check] group_clearance = N(mils,0= off) makes--layoutrequire that much channel between every pair of functional groups' extents (LAYOUT_GROUP_CLEARANCE, advisory) — the lint side ofarrange --groups --group-gap N. -
--netsis connectivity hygiene:NET_SINGLE_PIN(a floating label or a power port driving nothing),NET_OFF_GRID(pins off the configured grid — wires that touch on screen without ever connecting), and, on.kicad_schinputs, the attachment near-miss lintsNET_PIN_MIDSPAN_TOUCH(a pin tip on a wire's mid-span with no junction — connected in Altium's dialect, NOT in eeschema),NET_LABEL_UNATTACHED(a label anchored on neither a pin tip nor a wire), andNET_WIRE_CORNER_ON_PIN(an L-wire corner landing on a pin tip — the classic accidental-short trap). The grid comes from config[project] grid(bare number = mils, or"50mil"/"1.27mm"/"0.5mm"strings; default 50 mil), compared in exact integer nanometres. -
--layoutis a geometric-overlap lint: estimated bounding boxes for symbol bodies and label text, reporting symbols drawn over each other, labels running over a body or pin field, label-label overlaps, texts stacked on one anchor, plusLAYOUT_POWER_ON_PIN(aPWR_FLAGanchored on another symbol's pin tip — it belongs mid-wire, see theplace_pwr_flagmacro — or any power symbol whose body lands on the part. A rail port anchored at a pin tip with its body extending away is the blessed label-on-pin pattern and is not flagged),LAYOUT_WIRE_THROUGH_SYMBOL(a wire routed through a symbol body), andLAYOUT_LABEL_OVER_WIRE(NOTE: label text crossing an unrelated wire). ERC can never see these — a schematic can be electrically perfect and visually unreadable. -
--ercadditionally reportsERC_UNPLACED_UNIT(WARNING,.kicad_schonly): a multi-unit part with units never placed (e.g. gate B of a dual comparator) — terminate them with theterminate_unused_unitmacro. Waiver rule token:unplaced_unit. -
--ercalso runs the pin-type conflict matrix (ERC_PIN_CONFLICT, waiver tokenpin_conflict): the high-signal cells of KiCad's default matrix — open-collector/open-emitter/tri-state pins mixed with push-pulloutput/power_outdrivers on one net (WARNING; softer cells NOTE) — and undriven POWER_IN (ERC_POWER_IN_UNDRIVEN, waiver tokenpower_in_undriven): a supply pin on a net that is neither a name-recognized rail nor driven by anypower_outpin. Both are gated behind the type-confidence demotion like every type-based rule, so a mostly-Passive import degrades to NOTEs instead of emitting garbage. -
--ercalso reportsNC_PIN_CONNECTED(WARNING, waiver tokennc_pin_connected): a pin carrying a no-connect marker that is nevertheless a member of a ≥2-pin net — the marker and the wire/label contradict each other (the classic accident: an edit wires up a pin that still carries a stale marker from an earlier revision). Works on any input format and sees label-only connections, because it rides on the inferred net membership; on.kicad_schthe wire-based subset of the same conflict is also caught pre-netbuild by--integrity'sNO_CONNECT_CONFLICT— the overlap is intentional defense-in-depth, not a duplicate bug. -
The type-confidence demotion is per NET, not per board. A type-based finding keeps its full severity when every member pin of the net in question carries a real electrical type, even on a board whose overall confidence is below the floor — the demotion exists because an untyped pin might be the driver the rule cannot see, and on a fully-typed net that excuse is gone. A net that still has untyped pins on a low-confidence board is demoted to a NOTE as before, and says so in the message. (The header's passive-pin ratio still reports the board-wide picture.)
-
--powerreportsPOWER_RAIL_NO_SOURCE(WARNING): a recognized rail with ≥2 consumers that nothing appears to feed. "Feed" is read generously so the rule only fires on a rail nobody wired — anypower_out/outputpin, aPWR_FLAG, a connector/battery/test-point designator (J/P/X/XP/CN/BT/TP/G), a configured[[rail]](a declaration of intent), an entry-style rail name (VBUS/VBAT/VIN/DCIN… — an off-board source is idiomatically drawn as a bare label), or any of those on a net the rail reaches through series parts (a rail behind a fuse/ferrite/diode inherits its upstream source) silences it. Ground is never crossed while searching, and DNP parts neither source a rail nor carry a source through. -
Presence-style checks ignore DNP parts: a Do-Not-Populate bypass cap does not satisfy
POWER_NO_DECOUPLING(nor a DNP pull-up/load-cap/fuse/TVS the matchingreviewrule). The BOM checks are where DNP parts still count. -
--intent FILEasserts a design-intent JSON file (written byakcli nets --intent-snapshot, or by hand) against the built netlist. Bare--intent(no value) uses the project's standing contract fromakcli.toml[paths] intent— the "which intent applies to this board" question answered by config instead of convention:{"protocol_version": 1, "mode": "exact", "nets": {"SWCLK": ["U1.4", "J2.2"], "GND": {"members": ["R*.2", "U?.1"], "mode": "subset"}}}modeis"exact"(the matched net must contain exactly the listed pins) or"subset"(containment only). Members are"REF.PIN"split on the FIRST dot, so pin names with dots parse ("U1.P0.25"= pinP0.25). Intent nets are matched to actual nets by pin membership, never by display name, so renames can't fake a pass. Findings (all ERROR):INTENT_PIN_UNKNOWN,INTENT_NET_NOT_FOUND,INTENT_MISSING_MEMBER,INTENT_EXTRA_MEMBER(exact mode only),INTENT_NETS_SHORTED(two intent nets landed on ONE actual net). A malformed file isBAD_CONFIG(exit2); a wrongprotocol_versionisPROTOCOL_MISMATCH(exit6); a missing file exits4.- Per-net mode override (additive;
protocol_versionstays1): a net value may be an object{"members": [...], "mode": "exact"|"subset"}that overrides the documentmodefor that one net; the classic plain-list form keeps using the document mode. An unknown key or a badmodein the object form isBAD_CONFIG. - Wildcard members: the REF part of a member may be an
fnmatchpattern ("R*.2","U?.1","[UJ]1.3") — the pin part stays literal. A wildcard is satisfied when at least one actual pin matches both the REF pattern and the pin; zero matches raiseINTENT_MISSING_MEMBERnaming the pattern (neverINTENT_PIN_UNKNOWN, which is literal-member-only). Inexactmode wildcards are ignored when computingINTENT_EXTRA_MEMBER— only literal members are subtracted, so a pin present solely via a wildcard match still surfaces as extra (a wildcard asserts existence, not a closed set).
- Per-net mode override (additive;
-
--contract FILEasserts a design-contract TOML file against the built netlist — topology and semantic rules ERC cannot express, each carrying datasheetevidence. Rule kinds per[[contract]]:require/forbid(a pin on / off a named net),require_same_net/forbid_same_net(pin-pair topology),component+value(exact, space/case-insensitive, or a list of accepted values), andnc(a pin must not join a multi-pin net). Pin specsREF.PINresolve the pin number first, then the pin name (U1.FB2). A rule withwaived = true(plusowner/reason/optionalexpires) is an approved exception. The three outcomes are never conflated:CONTRACT_PASS(info),CONTRACT_FAIL(at the rule'sseverity, default error), andCONTRACT_WAIVED(note) — an expired exception raisesCONTRACT_EXCEPTION_EXPIRED(warning) instead of silently passing. A malformed file isBAD_CONFIG(exit2), a too-newprotocol_versionisPROTOCOL_MISMATCH(exit6). Intent snapshots do net-membership regression; contracts express policy — the two compose. See docs/design-integrity.md. -
--libsync [--symbols DIR ...]checks the freshness of the embeddedlib_symbolscache (.kicad_schonly). With--symbolssources it compares pin signatures (number/name/type/position/unit) against the fresh libraries and warnsLIB_EMBED_STALEon drift (graphics-only drift is deliberately silent); without sources it falls back to an old-format heuristic and notesLIB_EMBED_OLD_FORMAT(pre-20231120 document version or symbols missingexclude_from_sim), pointing atakcli relink-symbols. -
Lint-style exit:
--fail-on {info,note,warning,error,never}sets the minimum (post-waiver) finding severity that exits non-zero. Defaultwarningreproduces the historical behavior (exit1iff any finding ≥ WARNING);neveralways exits0.--exit-zerois a deprecated alias for--fail-on never(still works). The same pair is accepted by every findings-emitting command —diff,pinmap,library audit,library lint-symbols,fab check, andgate— one exit policy to learn, not one per command. -
[[waiver]]config (checker-agnostic, applied centrally before both rendering and the exit decision — independent of the ERC-only[[erc_waiver]]). Each entry:code(required;fnmatchglob like"ERC_*"or"BOM_*"), optionalrefs(string or list offnmatchglobs matched any-vs-any against a finding's refs; omit → all findings of that code), optionalseverity(off|note|info, defaultoff:offdrops the finding,note/infodemote it), optionalreason(free text — always supply one). An unknown key, missingcode, or badseverityisBAD_CONFIG(exit2). The metadata header always printsconfig-waived: N (M demoted)(json/sarif metadata keyconfig_waived), so a waiver-cleaned run is never mistaken for intrinsically clean. -
--format text|json|sarif|junit—sarifemits SARIF 2.1.0 for GitHub code scanning (stablepartialFingerprints, schematic path as the artifact URI);junitemits JUnit XML for CI test reporters (WARNING+ findings become failed testcases). Exit semantics are unchanged by the format. -
Structured finding positions (present only when the checker located the finding — positionless findings keep their exact prior shape): text output appends a trailing
@ (x,y)clause (mils, model frame — top-left origin, +Y down; ints render without.0). JSON adds"pos": [x_mil, y_mil]and"anchors": [{"kind","id"[,"pos"]}](kind∈ component|pin|net|label; e.g. an overlap carries two component anchors). SARIF addslogicalLocations+properties.akcli.{pos,anchors}onto the result;partialFingerprintsdeliberately exclude pos/anchors so alert identity never churns when a part moves.
Engineering design review: advisory findings with explicit confidence and evidence. Unlike
check, review findings never fail the build on their own — they exit 0 unless you opt in with
--fail-on. See docs/review-rules.md for the full rule table.
akcli review analyze <sch> [--profile fast|standard|deep] [--detector NAME] [--pcb FILE] [--gerbers DIR] [--facts DIR] [-o FILE] [--fail-on info|note|warning|error|critical|never]— runs the review detectors over a schematic (--profilepicks the detector families;--detectorrepeatable to run only named detectors, overriding--profile;--pcb/--gerbersfeed the pcb/cross and gerber-package checks;--factspoints at a datasheet facts dir, auto-discovered as<sch dir>/datasheetswhen it holdsextracted/;--outalso writes the findings JSON envelope). Advisory by default (exit0);--fail-onturns on the same lint-style exit1ascheck.akcli review facts <add|verify|lookup> ...— the datasheet facts store: audited, PDF-pinned numbers that turn heuristic findingsdatasheet_backed.facts add MPN --pdf FILE [--dir DIR] [--method manual|pdftotext|llm] [--set KEY=VAL@pN ...]binds a fact to its source PDF by sha256;facts verify [MPN] [--dir DIR] [--exit-zero]audits the store (schema, PDF staleness, page bounds, quotes);facts lookup MPN [KEY] [--dir DIR]prints one MPN's audited facts.--dirdefaults to the project-local./datasheets— the store is a committed team asset (deliverable-class, SPEC §10): review it in PRs and it survives handoff.akcli review report <findings.json> [--format text|json|sarif|junit|markdown]— re-renders a findings file (fromanalyze --out) in another format.akcli review explain <CODE>— prints one review rule: what it checks, the formula, and its provenance.akcli review propose <findings.json> [-o FILE]— turns findings into declarative candidate changes (op-list / contract / sim drafts); never touches design files.akcli review testbench <sch> [--findings FILE] [--deck-only] [-o DIR] [--timeout S] [--exit-zero]— auto-generates and runs subcircuit SPICE testbenches from quantitative findings (RC corners, divider ratios) with ngspice verdicts. Reuses a findings file with--findings, or runsreview analyzein-process (standard profile) otherwise.--deck-onlyemits the decks without running ngspice (exit0); with--deck-only,--out DIRwrites<fingerprint>_<kind>.deckfiles there.akcli review diff <old.json> <new.json> [--fail-on-new](alias:review drift) — compares two findings files (fingerprint-aligned drift);--fail-on-newexits1when the later run adds findings. This is findings-drift — the net-level revision diff isakcli diff.akcli review tree <sch>— prints the schematic's power tree (rails, regulators, consumers).akcli review validate <candidates.json> <sch> [--facts DIR] [-o FILE]— gates LLM deep-review candidates: schema / anchors / datasheet evidence / masquerade checks; failures are quarantined with reasons.
Diff two schematic revisions. Nets are matched by membership (not display name); components by
UniqueID, then (value, footprint, pin-count) signature, then refdes. --json validates against
schemas/diff.schema.json (summary counts, rename map, per-component and per-net changes with
match method + confidence); net members are "REF.PIN" strings — the same encoding as
net/nets/verify.
--bom appends the per-component BOM delta: added/removed parts, value edits, order-id edits
and assembly-class flips (fitted↔dnp) — the "what does purchasing see" answer; cost deltas come
from diffing jlc bom --lock lockfiles.
akcli arrange <target.kicad_sch> [--apply] [--groups [FILE]] [--frames] [--grid MIL] [--margin MIL] [--group-gap MIL] [--page-width MIL] [--row-width MIL] [--allow-open] [--note TEXT] [--symbols PATH ...]
Resolve symbol overlaps by nudging free components — parts with no wire
endpoint or label anchor on any pin (moving anchored parts would strand their
connectivity, which is exactly what check --nets flags). Greedy first-fit in
reading order; anchored overlaps are reported for manual fixing (exit 1).
Dry-run by default; --apply writes through the draw pipeline (.bak +
connectivity re-verify), so akcli undo reverts an arrange. --symbols
supplies extra symbol sources for the write pipeline (same semantics as
draw --symbols). --allow-open writes even when a KiCad GUI lock file is
present (File>Revert in KiCad afterwards); --note records a free-form
intent note in the workspace journal.
With --groups each functional block relocates into its own shelf-packed
region as rigid, net-preserving bundles (moves carry labels/wires; power
satellites ride their host). --groups FILE takes a TOML/JSON
{group: [refdes, ...]} map; bare --groups derives the map from the
sheet's hidden Group properties — the file itself is the module map.
Net preservation is enforced at apply time: the moves are dry-applied to
a temp copy and the before/after netlists must be equivalent, otherwise the
write is REFUSED (exit 6) with the split/merge lines on stderr — a sheet
wired across group boundaries cannot move rigidly (connect groups with
label-on-pin nets instead). --allow-net-changes is the explicit override.
The moves themselves run in two phases through a staging band (uniform
shift below the sheet, then per-bundle placement), so carried labels can
never be captured by a later mover whose original pins overlap an earlier
mover's slot; at genuinely coincident pin tips the writer leaves one label
behind per staying pin.
--propose-labels OUT.json turns a refusal into a repair draft: every
named net touching ≥ 2 grouped components gets an add_net_label on each
member pin (connectivity becomes name-backed and move-invariant), wire
clusters those labels make redundant are deleted (delete_object — stretched
cross-bundle wires are the failure mode, not a feature), and stranded #
satellites (PWR_FLAG, mid-wire ports) are re-seated onto a pin of their net.
The draft never touches the file: akcli plan/draw it, then re-run the
arrange. Unnamed multi-bundle nets are reported — they need a human-chosen
name first. Proven end-to-end on a real 88-part/10-group board
(tests/test_corpus_pod.py).
--frames redraws each group's border + title after packing (keyed uuids
replace stale frames; one undo reverts the arrange together with its
frames).
Group blocks stack straight down the page by default, --group-gap apart
(default 1200 mil); --row-width MIL wraps a group's internal shelf past
this width (default 4000). --page-width MIL switches to 2D packing: blocks go
side by side left→right, wrap past the page width, and --group-gap is
guaranteed on both axes — functional neighbours adjacent with a routing
channel between every pair. Project policy pins these in akcli.toml so
every session packs the same way (flags still override):
[arrange]
group_gap = 1000 # channel between group blocks, both axes (mil)
page_width = 20000 # pack side by side, wrap past this width
group_margin = 200 # clearance between bundles inside a group
row_width = 4000 # wrap a group's internal shelf past this widthThe matching lint: [check] group_clearance = 1000 makes check --layout
flag any pair of groups whose extents sit closer than that channel
(LAYOUT_GROUP_CLEARANCE, advisory) — so a later manual move that squeezes
the gap is caught even when nothing overlaps. The dry-run/--json report
now carries each block's at/size (mil) for programmatic verification.
Two modes, dispatched by the second file's type:
- Schematic ↔ schematic (
verify a.SchDoc b.kicad_sch) — net-equivalence proof on top of the diff engine, the one-command answer to "did the conversion keep the circuit?". PASS (exit0) iff the component set matches and every net's pin membership is identical; nets that merely changed display name are listed but do not fail (conversions rename unnamed nets). Component value/footprint drift is reported as a note —--strictturns it into a failure. - Schematic ↔ PCB (
verify board.kicad_sch board.kicad_pcb, second file.kicad_pcb) — compares refdes presence, footprint/value assignment, and the pad-level net partition (net names are untrusted; what must hold is that pins joined on the schematic are joined on the board and vice versa). Findings, each located to the designator/pad:SCHPCB_MISSING_ON_PCB/SCHPCB_EXTRA_ON_PCB,SCHPCB_FOOTPRINT_MISMATCH,SCHPCB_PAD_MISSING,SCHPCB_NET_SPLIT(one schematic net across >1 board nets),SCHPCB_NET_MERGE(one board net shorting >1 schematic nets),SCHPCB_UNNETTED_PAD.#PWR/#FLGpseudo-components are excluded;--strictalso fails on value mismatches. PASS (exit0) iff no ERROR-level finding.
--json carries the verdict (equivalent, mode) plus the findings/diff report.
--exit-zero always exits 0 regardless of the PASS/FAIL verdict (report mode).
Bootstrap a minimal blank .kicad_sch (root (uuid ...), (paper ...), and an
optional title block) that akcli draw/plan can immediately append to — the
first-class replacement for hand-seeding a skeleton file. --paper sets the
sheet size (A4|A3|…, default A4); --title fills the title-block title;
--force overwrites an existing file (otherwise a present target is refused).
--json = {created, target, paper, title, status}. Keep the created file for
the whole session — its root uuid is the namespace for every deterministic op
UUID, so regenerating it breaks idempotent re-runs. When no .kicad_pro sits
next to the target (and --json was not passed), a stderr hint points at
akcli project init — the sheet is written either way.
akcli project new [DIR] [--name NAME] [--paper SIZE] [--title T] [--no-pcb] [--no-lib-tables] [--no-gitignore] [--sheets A,B] [--dry-run] [--force] [--example NAME]
Scaffold a complete KiCad project from scratch: <name>.kicad_pro,
<name>.kicad_sch (the root, akcli new's document with the same root
(uuid ...) the project file's sheets[0][0] points at), <name>.kicad_pcb,
an empty sym-lib-table/fp-lib-table, and a KiCad .gitignore — the set
akcli new alone left missing, so opening the result in KiCad finds a project
instead of a repair prompt. DIR defaults to .; --name defaults to the
directory name. --paper/--title are the same as akcli new. --no-pcb,
--no-lib-tables, --no-gitignore each skip one file group; --sheets A,B also writes blank child sheets A.kicad_sch/B.kicad_sch (not yet
wired into the root — instantiate them with an add_sheet op). Refuses a
directory that already has any of these files (PROJECT_EXISTS, exit 2)
unless --force overwrites (previous bytes backed up under
.akcli/backups/); a directory with none of them yet is unaffected either
way. An existing .gitignore is the one exception — it is user-managed
content and is reported kept, never rewritten, not even under --force.
--dry-run always exits 0, printing the plan (including any CONFLICT
lines) without writing anything.
--example NAME also drops a worked op-list into DIR/ops.json — one of the
examples packaged with this install (akcli project new --help lists what is
actually available; the roster comes from the packaged akcli.examples
directory, never a hand-kept constant). It lands as a kind: "ops" file in
the plan/JSON output alongside the KiCad files, subject to the same
created/kept/conflict rules as everything else; text mode prints a
note: line on stderr showing the exact akcli draw invocation to actually
draw it (--sheets' child-sheet note follows the same shape). The
op-list itself is the same JSON tests/test_examples.py applies to a blank
sheet in CI and diffs against expected_nets.json — it is not a toy, it is
the canonical example corpus's shipping copy.
akcli project new demo --title "Demo board"
akcli project new demo --sheets amp,psu --dry-run
akcli project new demo --example voltage-divider--json = {command, dry_run, applied, status, mode, directory, name, project_file, root_sch, root_uuid, files:[{path, name, kind, action}], created, kept, overwritten, conflicts} (status ∈ applied|unchanged|
dry-run; action ∈ created|kept|overwritten|conflict; kind ∈
pro|sch|sheet|pcb|sym-lib-table|fp-lib-table|ops|gitignore,
which is also the order files are reported and committed in; every path —
directory, project_file, root_sch, files[].path — is ABSOLUTE and
as_posix()).
The blank .kicad_pcb is a container, not a layout — zero footprints,
zero tracks, no board outline; akcli writes no PCB content (a SPEC
non-goal), it only makes sure pcbnew finds a board instead of a prompt. It
is written in KiCad 10's board-file format (version 20241229): KiCad 8
cannot open it (repair-prompts or refuses), while the schematic stays
KiCad 8's own .kicad_sch format and opens fine everywhere. Pass --no-pcb
to skip the board entirely on a KiCad 8 workflow.
akcli project init [DIR] [--name NAME] [--root FILE] [--paper SIZE] [--title T] [--no-pcb] [--no-lib-tables] [--no-gitignore] [--sheets A,B] [--dry-run]
Complete a directory that already has some KiCad files, adding only what
is missing — purely additive, so it is safe on a real design and idempotent
(a second run reports status: unchanged, zero actions). Existing files are
always kept, never rewritten, even a .kicad_pro already present (though
its JSON is validated — a truncated one is PROJECT_FILE_INVALID, exit
3); there is deliberately no --force here. The root schematic is
whichever .kicad_sch is already in DIR (or the tree root when several
are present and exactly one resolves as root; --root FILE names it
explicitly otherwise, else PROJECT_ROOT_AMBIGUOUS) — its existing root
(uuid ...) is adopted into the .kicad_pro this writes, so the project
binds to the design you already have rather than orphaning it. DIR with no
.kicad_sch at all is PROJECT_FILE_MISSING (exit 4) — that is project new's job. Every other flag (--paper, --title, --no-pcb,
--no-lib-tables, --no-gitignore, --sheets, --dry-run) matches
project new, and only applies to files this run actually creates —
except --name, which here defaults to the existing .kicad_pro's stem (or
the root schematic's) before falling back to the directory name.
akcli project init demo
akcli project init demo --json--json shape matches project new's (command reads "project init",
mode: "init"). --dry-run always exits 0 without writing.
What the .kicad_pro actually says — read-only, no write flags. DIR
accepts a directory (the first .kicad_pro in it, sorted) or a .kicad_pro
path directly, default .; a bare akcli project with no subcommand is
not the same as info — it exits 2 with an error naming the four
subcommands. No project file found is
PROJECT_FILE_MISSING (exit 4) — the message points at project init.
Exit 0 on any project it can find, even a malformed one: a .kicad_pro
that fails to parse degrades into ok: false / error: "..." rather than
raising, because "what is this project" should not itself require a clean
project to answer — that judgment belongs to project check.
akcli project info demo
akcli project info demo --json--json = {project_file, name, ok, error, root_sheet:{uuid, file}, sheet_tree:[...], schematics:[...], boards:[...], lib_tables:{sym, fp}, net_classes:[...], text_variables:{...}, pinned_libs:{symbol:[...], footprint:[...]}} — root_sheet.uuid is
sheets[0][0] from the .kicad_pro (the schematic KiCad actually opens);
sheet_tree is the same whole-tree walk akcli check/draw use
(project.tree_files), not just the files the project JSON happens to name;
schematics/boards are every *.kicad_sch/*.kicad_pcb in the project
directory, tree membership or not; root_sheet.file is null when the
schematic the project names is not on disk, and error is null unless
ok is false. Every path is ABSOLUTE and as_posix().
Lint-style findings over the same project material info reports — "is
anything WRONG with it" rather than "what is this". Shares the one
--fail-on/--exit-zero exit policy every findings command uses (default
warning; never/--exit-zero always exits 0). A freshly scaffolded
project reports NOTEs at most — none of them at or above the default
threshold — so the gate is calibrated against the exact thing this command
family produces: project new demo && cd demo && akcli project check reports
zero findings and exits 0. (Opting out with --no-pcb/--no-gitignore, or
init-ing into a directory whose existing .gitignore does not cover akcli
churn, leaves PROJECT_PCB_MISSING/PROJECT_STATE_UNIGNORED NOTEs — still
exit 0.)
akcli project check demo
akcli project check demo --json --fail-on error--json = {project_file, name, findings:[{code, severity, message}]}
(project_file absolute, as_posix()). Findings are emitted in check order,
not sorted by severity (severities per the shared report.Severity vocabulary
— see the finding-code table below): a malformed .kicad_pro, a
root-sheet binding that is missing or points at the wrong design, a Sheetfile
referencing a child that is not on disk (reuses SHEET_FILE_MISSING from the
op-list vocabulary), a missing .kicad_pcb, every library audit finding
over the same project (unless the caller — release preflight's project
gate — already runs library-audit separately), and derived-state hygiene
(.akcli/, *-backups, *.kicad_prl, ~*.lck not covered by any
.gitignore).
| Code | Severity | Meaning |
|---|---|---|
PROJECT_FILE_MALFORMED |
warning | The .kicad_pro did not parse (truncated, not JSON, not a JSON object). Not an error: every akcli read path still works without it — KiCad will offer to repair it on open, but the design itself is intact. |
PROJECT_META_FILENAME_STALE |
note | meta.filename no longer matches the file's own name — the project was renamed on disk. Cosmetic; KiCad rewrites it on next save. |
PROJECT_ROOT_SHEET_MISSING |
error | The project names no root sheet, or the schematic its sheets[0] binds to is not on disk — opening it loses the design. |
PROJECT_ROOT_SHEET_MISMATCH |
error | The project's sheets[0] uuid does not match the root schematic's own (uuid ...) — the project points at a different design than the file on disk (e.g. a regenerated root schematic). |
SHEET_FILE_MISSING |
error | A Sheetfile in the tree names a child that does not exist (reused from the op-list vocabulary — same code, same meaning). |
PROJECT_PCB_MISSING |
note | No <name>.kicad_pcb — pcbnew will offer to create one. project init writes a blank board container; akcli never writes PCB content, so --no-pcb is a supported, permanently-note-level choice. |
PROJECT_STATE_UNIGNORED |
note | .akcli/, *-backups, *.kicad_prl, or ~*.lck are not covered by any reachable .gitignore — KiCad/akcli churn will show up as untracked changes. |
Plus every library audit finding (FOOTPRINT_LIB_UNREGISTERED,
FOOTPRINT_MISSING, SYMBOL_LIB_UNREGISTERED/SYMBOL_MISSING,
LIB_URI_UNRESOLVED/LIB_PATH_MISSING, MODEL_MISSING/MODEL_NOT_PORTABLE,
FOOTPRINT_LEGACY_FORMAT) over the project's tables — see akcli library audit above.
akcli undo <target.kicad_sch> [--apply] [--steps N] [--list] [--run RUN_ID] [--allow-open] [--note TEXT]
Restore the target from the rotated draw backups that akcli draw --apply
writes under the workspace's .akcli/backups/ (<name>.bak newest, then
.bak2, .bak3 — up to 3). Legacy pre-0.12 stacks beside the file are still
found when .akcli/backups/ holds none for the target.
Dry-run by default (prints the part/net delta that restoring would cause);
--apply swaps, so undo twice is a redo.
--listprints the backup stack (level, path, size, mtime — newest first, contiguous from.bak) and exits;--json={target, depth, backups:[…]}.--steps Nwalks back N snapshots at once (default 1), leaving the stack so a single subsequentundoredoes the last step; N clamps to the available stack.--steps 0is a usage error (exit2);--stepswith no backup exits4.--run RUN_ID(0.20) restores every file of a multi-sheet draw transaction together, all-or-nothing, from.akcli/backups/runs/<RUN_ID>/(therun_idis in the draw's--jsonpayload, its status line, and the journal). Three-way swap per file, so the same command again is a redo. Refuses (exit 6) when any file changed since the transaction — a swap would silently discard that later edit — and exits4for an unknown RUN_ID. A childadd_sheet {"create": true}wrote is never deleted by this restore (a rollback restores CONTENT, never unlinks): it reverts to the blank stub it started as, and replaying the op-list onto that stub reproduces the applied bytes exactly.--allow-openswaps even when a KiCad GUI lock file is present (File>Revert in KiCad afterwards);--noterecords a free-form intent note in the workspace journal.- Exit
4when no backup exists.
Emit the MCU pin → net table (MCU chosen by mcu_designator in config, or --mcu REF).
--expected PATHcross-checks against an external expected pin→signal table (CSV or JSON). The schematic is authoritative; the expected table is advisory.--jsonemits the standard findings envelope, validated againstschemas/pinmap.schema.json(eachPINMAPfinding is one pin→net row;PINMAP_*codes report expected-table mismatches).
Extract an expected pin→signal table from a Zephyr devicetree source/overlay
(gpios = <&gpioN pin ...> phandles and Nordic NRF_PSEL(...) pinctrl) or from a
markdown pinout table (--key-header/--value-header pick columns explicitly).
Emits the JSON object pinmap --expected consumes; -o writes it to a file. Output is
always JSON regardless of the global --json flag (the one deliberate exception to the
"--json switches machine mode" contract — the product IS the machine format).
Exits 1 when nothing was extracted (an empty table would make pinmap --expected vacuously pass), 2 on an unsupported input type, 4 when the
file is missing. The schematic stays authoritative — this table is advisory.
Offline engineering calculators (60): E-series snapping and 2–4-resistor
combination search (IEC 60063:2015), voltage dividers and LED resistors,
LM317/FB regulator networks with worst-case corners (TI SLVS044Y), IPC-2221B
track width ↔ current ↔ temperature rise and Table 6-1 clearance, via
R/thermal/ampacity/L/C (Johnson & Graham 1993), differential pairs
(IPC-2141A), Onderdonk/Preece fusing, ASTM B258 AWG,
microstrip/stripline/coax impedance (Hammerstad–Jensen 1980, Cohn 1954),
PI/TEE/bridged-TEE attenuators, L/PI matching networks (Pozar §5.1),
buck/boost/flyback power stages (TI SLVA477B/372C, Erickson ch. 6), LDO
dissipation, MOSFET gate drive (TI SLUA618A), current-sense shunts, NE555
(TI SLFS022I), op-amp gain pairs, comparator hysteresis (TI SLVA954),
Sallen–Key filters (TI SLOA024B), ADC resolution/settling, I²C pull-up window
(NXP UM10204), RS-485 fail-safe bias (TIA-485-A), CAN split termination
(ISO 11898-2), crystal load caps (ST AN2867), junction thermal (JESD51), TVS
selection (IEC 61000-4-5 surge), fuse derating (IEC 60127 R10), NTC inrush,
battery life (battery and the datasheet-mAh battery-life), LDO headroom
go/no-go (ldo-headroom), open-drain-aware comparator thresholds
(comparator-hysteresis), diode envelope-detector RC validity
(envelope-detector), unit conversions (dBm/W/Vrms, mil/mm, oz/µm), resistor
color/SMD/EIA-96 markings (IEC 60062:2016), galvanic compatibility
(MIL-STD-889C).
- Inputs take engineering notation (
4k7,100n,2M2); every result prints its formal reference.--jsonreturns{calc, inputs, results{value,unit,note}, reference};--mdrenders a markdown table. - Input-suffix rule (already-milli units): a parameter whose declared unit
is itself milli-denominated (
battery-life'scapacityin mAh,i_avgin mA) takes a bare number in that unit —capacity=2500means 2500 mAh. A trailing engineeringmthere is rejected (ERROR: capacity is already in mAh — write capacity=2500) rather than silently applying a compounding 1000× milli. The generic length unitm(meters) is unaffected —width=5mstill means 5 mm via the milli prefix. battery-life's defaultderatingis 0.8 (aligned withbattery, so the two give identical hours for the same capacity/current); override withderating=for a chemistry/load-specific figure.capacity=2500 i_avg=10→ 200 h / 8.33 d.--ops FILE(design-type calculators:vdivider-design,regulator-design,led,i2c-pullup,crystal-caps,hysteresis-design,sallen-key,attenuator) additionally emits a schema-valid op-list with the computed E-series values filled in (-= stdout) — edit coordinates, thenakcli plan. Four of the eight emit a macro op (vdivider-design→place_divider,led→place_led_indicator,i2c-pullup→place_pullup,crystal-caps→place_crystal); the rest emitplace_componentops.batch <file|->runs{"jobs": [{"calc": ..., "params": {...}}, ...]}and emits an array of result envelopes; exits1if any job failed (each failure carries anerrorfield),0when all succeed.listgroups all calculators;info <name>shows parameters, defaults and the citation. Both honour--json(infoemits one entry in the same shapelistuses, plus its ownname). Bad name/params exit2, as does a non-finite (nan/inf) or degenerate input the formula cannot evaluate.- Numerics are cross-checked in the test suite against KiCad's
pcb_calculator (independent reimplementation — no GPL code) and published
datasheet/handbook values. IPC-2152 is deliberately NOT included: it is
chart-based licensed measurement data with no public closed form — this
tool refuses to fake it (
tracktemp/trackwidthuse the conservative IPC-2221 fit instead). akcli view calclaunches a local web UI for all calculators (localhost only): auto-compute forms with live notation parsing, physical-style SVG illustrations, ⌘K palette, session log, shareable URLs, op-list export.
Export the schematic's netlist for other EDA tools. Default --format protel (an
Altium-importable .NET); kicad emits a legacy eeschema netlist; csv flat net,ref,pin rows.
Writes stdout unless -o is given. Deterministically sorted; unnamed nets are named by their
membership-derived stable_id, so re-exports diff cleanly. --json wraps the rendered netlist
in a {schema_version, source, format, content} envelope (the content string is byte-identical
to the plain output); for a structured net-by-net document use akcli net --json.
--format kicademits the sections KiCad's ownsch export netlistwrites, including the UUID identity a re-import needs:(design)(source/tool/sheet table), per-component(sheetpath (names ..) (tstamps ..))+(tstamps <symbol-uuid>),(libparts)(fields + typed pins per distinctlib_id) and(fields)for extra properties (LCSC/MPN/Datasheet). KiCad 6+ keys the footprint↔symbol association by that UUID path, so a refdes-only netlist loses every association on a renumber.(date "")is deliberately empty: a wall-clock date would break the same-input-bytes → same-output-bytes rule. Net names are eeschema's, not akcli's bare ones: a sheet-scoped name (local or hierarchical label) is qualified with its sheet path —/GNDon the root sheet,/Amp/SIGone level down,/Amp/Deep/SIGtwo — while a global label or power port is written bare (VOUT,GND). Every net also carries(class "Default"). pcbnew keys board nets by NAME, so a netlist that disagrees with eeschema renames the whole board the next time it is updated from the schematic. Only--format kicadqualifies names;protelandcsvkeep the bare akcli names, andakcli net/netsare unaffected.tests/test_e2e_netlist.pyarbitrates the result against a realkicad-cliexport (committed ground truth intests/fixtures/kicad/, re-derived live wherever KiCad is installed) — component set, net→member partition, per-ref UUID path and net names must match. Note KiCad omits#-prefixed virtual symbols (power ports,PWR_FLAG) from its netlist while akcli keeps them: they are how the net engine names nets. Nets with no designed name are the one deliberate divergence: KiCad inventsNet-(R1-Pad2)/unconnected-(R2-Pad2)from its own traversal order, which a normalized model cannot reproduce, so akcli keeps the membership-derivedstable_id.
Validate an op-list against protocol_version and schemas/ops.schema.json, resolve it against the
target .kicad_sch (symbols from repeatable --symbols sources and the target's inline cache), and
print what would change. Never writes. Includes the Net changes block (below);
--no-net-diff skips it. --render OUT.svg renders the WOULD-BE sheet from the same temp
dry-apply the net diff uses (coordinate-grid overlay included) — look before you --apply;
a refused op-list honestly skips the render, and a renderer failure is a warning that never
changes the verdict. The --json payload carries preview: {path, bytes} (or null).
akcli draw <target.kicad_sch> --ops FILE [--symbols PATH ...] [--apply] [--no-net-diff] [--root FILE] [--flat] [--strict-nets] [--allow-open] [--no-erc] [--render OUT.svg]
Execute an op-list against a KiCad .kicad_sch. The vocabulary is 22 ops + 10 macros (see
schemas/ops.schema.json), including delete_component (with cascade) / delete_object /
move_component / rename_net, hierarchical add_sheet (below), and multi-unit placement via
place_component's optional "unit" field.
-
Default is a dry run (no file written): prints per-op results and the connectivity verification.
--dry-runstates it explicitly, and wins over--applyif both are given (a warning goes to stderr) — a defensively added flag can never cause a write. -
--applyperforms the write via the atomic snapshot → temp → verify-on-temp →os.replacepipeline, writing a rotated<target>.bakcopy under.akcli/backups/. The write is rejected (exit6) if any op errors or the connectivity verifier finds an ERROR (e.g.DANGLING_ENDPOINT,DANGLING_BUS_ENTRY— a bus entry end landing on neither a bus nor a wire). -
Net changes (both
plananddraw, dry-run and apply): the op-list is dry-applied to a temp copy placed next to the target (so a hierarchical root still resolves its child sheets) and the before/after netlists are diffed by pin membership (never by name). The block prints one deterministic line per change, most severe first:Net changes: ! SPLIT THR (4 pins) -> THR(2) + <unnamed@R7.2>(2) ! MERGE MID + +3V3 -> MID ~ VTH: +U1.7 (5->6 pins) = RENAME MID -> VOUT (3 pins) + NEW BALL1_N (5) - GONE SENSE1 (3)(none)when connectivity is unchanged; the block is suppressed (not shown as "(none)") when the dry-apply itself fails.--no-net-diffskips the computation. If the diff cannot be computed at all (unreadable target, missing child sheet, ...) aWARNING: net diff unavailable: ...line goes to stderr — never a silent skip. -
Sheet-tree scope (0.20). When the target is a child sheet of a hierarchy, the net diff is evaluated over the whole sheet tree, not the child alone (a child read standalone is a truncated graph — its global labels lose every member the other sheets contribute). The root is auto-discovered by scanning the project directory for the file that instantiates the target;
--root FILEnames it explicitly (a root whose tree does not contain the target isPROJECT_ROOT_MISMATCH, exit2), and when several roots instantiate the target the run refuses to guess (PROJECT_ROOT_AMBIGUOUS— under--strict-netsthe write fails closed; advisory runs degrade tonet diff unavailable).--flatrestores the single-file view explicitly. Tree-scoped runs stage the tree under.akcli/stage/(cleaned up afterwards) and stampproject_root+evaluation_scope: "tree"into the--jsonpayload — both fields are absent on the flat path, whose output is byte-identical to 0.19. -
Multi-sheet op-lists (0.20). The envelope's
sheetsmap ({"amp": {"file": "amp.kicad_sch"}}) + per-op"sheet"tags route ops onto declared child files, so ONE op-list edits the root and its children in one atomic transaction: every dirty file is staged and verified before the first write, the pre-write bytes of every file are copied to.akcli/backups/runs/<run_id>/, and a partial commit rolls back automatically (TXN_ROLLBACK_FAILEDonly when even the rollback fails — the backup set is intact andakcli undo <target> --run <run_id> --applyrestores it). The--jsonpayload carriesrun_id+ per-filefiles[]on these runs (absent on single-file runs). Asheetsentry naming a file nobody creates isSHEET_UNKNOWN(remediation suggestscreate: trueon the referencingadd_sheet, which lets the same transaction write it). A sheet instantiated more than once refuses a scalar designator (INSTANCE_REFS_REQUIRED) — passinstance_designatorsmaps; see docs/op-list-authoring.md. -
--strict-nets(with--apply): refuse to write (exit6, evidence lines on stderr) when the diff shows a split or merge touching a named net on either side — splits/merges of unnamed nets are ordinary wiring edits and pass. It forces the diff even under--no-net-diff, and it fails closed: when the diff cannot be computed, the write is refused (REFUSED: --strict-nets: net diff unavailable (...)) rather than waved through. -
GUI-open guard.
--applyrefuses withTARGET_LOCKED(exit6) when KiCad's~<name>.lcklock file is present — a write under an open GUI is a losing race (the GUI's later save overwrites it from memory).--allow-openis explicit risk acceptance, and a successful apply under an open GUI prints aFile>Revertreminder on stderr. (Same guard onarrange/undo --apply, and on therelink-symbols/library repairapply paths.) -
A final status line states the outcome unambiguously:
status: dry-run — nothing written (re-run with --apply),status: APPLIED — wrote board.kicad_sch (backup .akcli/backups/board.kicad_sch.bak; akcli undo reverts), orREFUSED: --strict-nets: net split/merge touches a named net; nothing written. -
After a successful
--apply, an advisorykicad-cliERC runs when that binary is installed (never fatal);--no-ercskips it honestly (logged at-v) — akcli's own connectivity gate always runs regardless. -
--jsonpayloads validate againstschemas/draw-result.schema.json(schema_version/applied/status/ops/connectivity, plus"net_diff": {"equivalent", "risk", "lines"}ornullwhen unavailable). -
add_sheet(hierarchical authoring, KiCad only —altium: false): emits a(sheet …)node withSheetname/Sheetfile, deterministic uuids, and edge-computed sheet pins. Op shape:{op:"add_sheet", name, file, at:[x,y], size:[w,h], pins?:[{name, type, side, offset_mil}]}(mils;at= top-left corner).type∈ input|output|bidirectional|tri_state|passive;side∈ left|right|top|bottom. Wires attach to a sheet pin by coordinate (at+offset_milalong the side, grid-snapped) — there is noSheet.Pinwire endpoint. A cross-sheet net is a parent sheet-pin paired with the child's same-name hierarchical label. The referenced child.kicad_schmust ALREADY exist next to the parent — runakcli new <child>.kicad_schfirst, or the op is refused withSHEET_FILE_MISSING(exit6, remediation now also suggestscreate: true) — UNLESS the op itself sets optional"create": true(bool, defaultfalse), which has this SAME draw write a blank child stub (same shapeakcli newwrites) before any op runs, inside the same transaction; an existing child is never touched. A stub is enough; it does not have to be finished. The same code reports a root whose child later went missing, since a dangling(sheet …)makes everynets/check/renderon the design fail. Note:create: trueputs even an otherwise single-file op-list on the multi-sheet transaction path (below) — no rotated<name>.bak, recover withakcli undo <root> --run <run_id> --apply.
(also available as akcli library relink — the library-maintenance family it belongs to)
Re-embed stale lib_symbols cache entries from fresh .kicad_sym libraries — the fix for
KiCad's lib_symbol_mismatch ERC noise on files carrying old embedded symbols. For each embedded
Nick:Name entry it resolves <libdir>/<Nick>.kicad_sym (--libs takes dirs or .kicad_sym
files, repeatable; default: the KiCad.app SharedSupport symbols dir when it exists) and
classifies it up-to-date, replace, or missing-lib (comparison is whitespace-insensitive,
so formatting alone never triggers a replace):
$ akcli relink-symbols board.kicad_sch
replace Device:R [/Applications/KiCad/.../symbols/Device.kicad_sym]
replace power:GND [/Applications/KiCad/.../symbols/power.kicad_sym]
status: dry-run — 2 replacement(s) pending; re-run with --apply
- Dry-run by default.
--applysplices the fresh blocks in, then a safety gate re-reads both versions and requires identical net membership — a moved pin in the new library refuses the write withVERIFY_FAILED(exit6) and leaves the file untouched. The spliced document must also pass the same connectivity gatedraw --applyruns, compared as a delta: a finding the splice INTRODUCES refuses the write, while findings the file already carried do not (relink is not the tool that repairs a broken import). On pass it writes.akcli/backups/<name>.bakand replaces atomically. --only power,Devicerestricts to the listed library nicknames (or full lib_ids).- Exit
6when any entry ismissing-lib(scope with--onlyto silence intentionally unavailable nicks);--jsonlists the actions (minus the raw symbol text).
akcli ops <list | template OP [--required-only] | validate FILE | from-intent DESIGN [-o FILE] [--symbols PATH ...]>
Op-list authoring kit: list prints the 22-op vocabulary with required fields
and per-executor support, plus the 10 macro ops (connect_and_label,
place_pwr_flag, terminate_unused_unit, place_divider, place_decoupling,
place_pullup, place_led_indicator, place_rc_filter, place_crystal,
place_array)
that expand to core ops before validation; template emits a fill-in JSON
op-list skeleton for any op or macro (guide:
docs/op-list-authoring.md). A mistyped op or
calculator name gets a did-you-mean suggestion.
validate <oplist.json>runs the target-free structural validation — envelope, per-op fields, macro expansion; exactly the checksplan/drawrun before touching a target. Exit0valid /6on any problem;--jsonemits{protocol_version, valid, ops_sha256, op_count, errors}. The plugin's PreToolUse hook runs this before anydraw --applyand blocks on failure; the hook also warns (never blocks) when the workspace journal shows no priorplan/dry-run for that exact op-list.from-intent <design.json>generates a complete op-list from a design document: thecheck --intentpin→net table plus acomponentsmap and optionallayouthints (schemas/design-intent.schema.json) — the pin→net table is the authoring interface, the tool owns the geometry. Emits only the ordinary vocabulary (place_component+ label-on-pinadd_net_label+add_no_connectfor every unlisted pin — opt out with the design doc'slayout.no_connect_unlisted: false; the otherlayoutknobs areorigin,columns,margin_miland per-componentgroup) into a uniform 50-mil-grid cell layout, so the output flows through the unchangedplan/drawpipeline;-o FILEwrites it (default stdout),--symbolsadds symbol sources using the same resolution chain asplan/draw. Multi-unit symbols place ALL units in adjacent cells (unreferenced units all-no-connect). Deterministic: identical design content → identical output bytes. Refusals exit6withDESIGN_UNSUPPORTED(pin addressed by NAME — the message names the correct number — wildcard members, a REF missing fromcomponents, one pin in two nets, coincident stacked pins on different nets, bus-syntax net names, multi-unit parts with common unit-0 pins); an unknown lib_id is the usualSYMBOL_NOT_FOUND. The SAME design file then asserts the drawn sheet:
akcli ops from-intent design.json -o ops.json --symbols MyLib.kicad_sym
akcli draw board.kicad_sch --ops ops.json --symbols MyLib.kicad_sym --apply
akcli check board.kicad_sch --intent design.jsonONE local dashboard server for both pages (127.0.0.1 only, zero dependencies,
HTML bundled in the package). Default port 8765, auto-incrementing when
busy. / is the hub — the entry page the browser opens on launch, with
one card per dashboard (the live card shows the watched file, step count and
latest ERC state in real time; C/L jump straight in).
Bare akcli view needs no file: it serves the hub with /live idle.
view <sch.kicad_sch> is shorthand for view live <sch>; view live with
no path watches the single .kicad_sch in the current directory (none or
several → explicit usage error, exit 2); view calc serves /calc alone.
/calc— the calculator bench: home launcher + grouped sidebar with fuzzy filter, ⌘K command palette, forms that auto-compute as you type with live engineering-notation parse hints (defaults shown in typed-back notation, e.g.35u), results with formal references, change chips vs the previous run, click-to-copy values, copy as markdown/JSON/CLI, diagram captions annotated with the computed values, a session log, shareable URL hashes, theme-aware physical-style SVG illustrations, and one-click op-list export for the mappable design calculators (GET /api/ops, the web twin ofcalc --ops)./live— watches the schematic; every on-disk change (e.g. anakcli draw --apply) exports every sheet's SVG viakicad-cliand appends a step to the timeline immediately; KiCad's JSON ERC back-fills the step seconds later (badgeERC…while pending). Updates are pushed over Server-Sent Events (/live/events), with a slow poll as fallback. The dashboard: inline SVG with zoom/pan/crop, sheet tabs for hierarchical designs, per-step ERC badges plus a violation panel that marks NEW findings vs the previous step (with click-to-locate markers on the sheet), diff mode (previous step ghosted in red), timeline replay, PNG export, parts-trend sparkline, a note box that annotates the next step (POST /live/note, the UI twin ofnote.txt), and a clear-timeline action. A live offline-lint overlay (toolbarlintbutton / keyG) draws dashed-square markers at each finding's position fromGET /api/findings— a fast, offline nets+geom+layout lint of the watched sheet (neverkicad-cli/network, mtime-cached), converting the finding's mil position into the SVG's mm frame (MIL_TO_MM = 0.0254); markers are click-to-zoom and suppressed on multi-sheet views (positions are root-frame only). When the BOM panel runs its networked?check=1pass, each priced line with an LCSC id gains a datasheet link (resolved viaparts.datasheet.resolve; direct PDFs vs page-links get distinct glyphs) — per-line failure-tolerant and absent on the offline BOM. Steps live in a per-run temp dir;--state-dir DIRpersists the timeline across runs and--max-steps Nbounds it (default 500 — oldest SVGs are deleted;0= unlimited). Needskicad-cli(KiCad 8+) onPATH, in the macOS app bundle, or viaKICAD_CLI=.AUTO_REVERT=1asks an open KiCad editor to File→Revert after each step (macOS only).
JLCPCB/LCSC part search (jlc search <query> [--limit N] [--category NAME] —
--category keeps only results whose category equals NAME, case-insensitive
exact match: a value/package query like 0402 47K otherwise mixes NTC
thermistors from Sensors into a resistor search; the filter is client-side
with over-fetch, so --limit applies to the filtered list), single-part
inspection (jlc show <C-number> [--easyeda] — one part's catalog record;
--easyeda also queries EasyEDA for metadata + 3D-model availability), BOM
purchasability check (jlc bom <sch>... — stock/tier-price/est-cost
per BOM line via LCSC/MPN parameters; several schematics merge into ONE cart with per-board
breakdowns and tier pricing at the merged quantity; --qty N evaluates at build quantity).
Components carry their assembly class everywhere (fitted / dnp / external / no-part — from
KiCad dnp/in_bom attributes, Sourcing parameters and structural refdes classes, tunable in
akcli.toml [bom]): dnp/external/no-part lines are never looked up or priced, coverage counts
fitted parts only, and every explicit C-number is reverse-verified against the schematic's
value + package (on disagreement the line is marked verified: "mismatch" and its note carries
BOM_LCSC_MISMATCH — a BOM-line state, not a check finding; a mistyped id is a wrong reel,
not a formality). LCSC_ALT parameters supply second sources that take over automatically when the
primary is at risk. Flags: --suggest/--fix
find and write catalog replacements for missing/dead part ids — matches are graded by
NORMALIZED value equality (never substring), --fix writes only
high-confidence matches, --fix-all also writes low-confidence ones; --alternates proposes
second sources for at-risk lines and notes Basic swaps for Extended passives; --csv OUT.csv
exports a JLC-EDA-template BOM CSV (every class kept, annotated in a Note column; '-' =
stdout); --md [OUT.md] renders a Markdown report; --lock OUT.json freezes the check as a
lockfile and --against-lock LOCK.json reports drift (price/stock/EOL/id changes, exit 1);
--offline answers from the HTTP cache only (misses degrade to unverified, bannered);
--min-stock N flags lines with stock below N (default 1, or akcli.toml [bom].min_stock);
--exit-zero always exits 0 (report mode), datasheet resolution/download
(jlc datasheet <C-number|MPN|sch> [--fetch] [--force] [--resolve-mpn] [--exit-zero] [-o DIR]
— resolves szlcsc PDF links via the EasyEDA record, %PDF-magic-verified downloads, whole-BOM
batch mode; --force re-downloads even when the file already exists; --resolve-mpn
exact-matches MPN-only BOM lines against the catalog (same policy as jlc bom) to pin a
C-number before resolving; files cache under AKCLI_DATASHEET_DIR or
~/.cache/akcli/datasheets/), and library conversion (jlc add C<num> [--3d] [--footprint-lib NICKNAME] [--3d-path relative|absolute|'${VAR}'] → KiCad symbol + footprint +
optional STEP). --footprint-lib sets both the output directory and the fp-lib-table nickname
written into the symbol's Footprint field — pass the nickname your project registers, or KiCad
reports "footprint not found" (the field previously hardcoded footprint:). --3d-path picks the
3D-model reference policy — relative (portable, resolves only next to the library), absolute
(always resolves on this machine, not portable), or a ${VAR} prefix — and prints the trade-off.
-o DIR overrides the output directory (default <[paths].parts_dir or ./akcli-parts>/<C-number>/);
--lib-name names the KiCad symbol library (default akcli); --force overwrites existing
artifacts; --place --designator U1 --at X Y also emits a ready-to-draw place_component
op-list.
This is the only networked subcommand family. Network failures exit 7
(ERROR: NETWORK: ...); transient errors are retried with backoff, and a stale cached response is
served with a warning when retries are exhausted. See docs/jlc.md for the full reference.
akcli sim <sch> [--sim FILE] [--deck-only] [-o PATH] [--gnd NET] [--wave OUT.csv] [--sweep NAME=v1,v2,...] [--timeout S] [--exit-zero]
Simulate a schematic with libngspice and assert on the results. akcli sim renders the
schematic to a SPICE deck (net → node mapping, component → device via the model-resolution ladder,
sim.json stimuli → V/I/B sources), runs it in an isolated child subprocess (crash- and
timeout-safe), reads back the .meas measurements, and compares each against a bound declared in
sim.json (gt/lt/ge/le/approx+tol, engineering notation accepted; a lower + an upper
bound in one entry forms a two-sided range). --deck-only emits the deck and exits 0 without
ngspice (the engine-free plan/review mode — write it with --out, or --json for
{deck_sha, deck, warnings, unmodeled}); otherwise --sim FILE is required. --gnd NET names the
net that becomes SPICE node 0 (default GND). --wave OUT.csv dumps the simulated vectors as a
tidy CSV (one time column + one column per options.wave_vectors entry). --sweep NAME=v1,v2,... re-runs the assert pass across a Cartesian corner matrix (component-value override
R21=2.2k,3.3k or temp=0,25,60; repeatable, ≤64 corners; engine-only) and prints a per-corner
verdict table, exiting 1 if any corner fails. --timeout S kills the engine after S seconds
(default 60). --exit-zero reports without failing. Text output is an engine/deck-sha header, an
always-printed measured-value table, then findings; --json returns
{deck_sha, engine, measured, findings, ok}. The engine is discovered via AKCLI_NGSPICE (a path,
or off to disable) → macOS KiCad bundle → find_library → Linux sonames → Windows KiCad; when
none loads, run mode prints ERROR: NGSPICE_MISSING and exits 7. New in this cycle, the deck
builder auto-appends .option rshunt=1e12 when it detects a floating node (SIM_FLOATING_NODE) and
warns SIM_UNDRIVEN_RAIL for a power-named net with no source. See docs/sim.md for the
full reference (sim.json format, the model-resolution ladder, fit_diode, and the M/MEG deck
gotchas).
akcli sim fit-diode --point V@I [--rs-point V@I] [--cjo F] [--n-prior N] [--name MODEL] [--apply SCH --designator REF [--write]]
Fit a SPICE diode .model from datasheet forward-voltage points (0.37@20m = 0.37 V @ 20 mA;
--point repeatable). A single point plus the ideality prior (--n-prior, default 1.05 Schottky)
solves IS directly — the honest default over eyeballed curve fits; --rs-point adds series
resistance and --cjo a junction capacitance. Prints the .model card + Sim.Params, or --json
for {name, model_card, sim_params, params, note}. --apply SCH --designator REF plans a native
set_component_parameters write stamping Sim.Device/Sim.Params onto that component (a dry
run printing the op-list unless --write, which commits through the KiCad writer with a rotated
.bak). This closes the datasheet → model loop with jlc datasheet.
The one query-family command that writes a file by default (<input>.svg next to the
input) — agents that only want the SVG content should pass -o - for stdout.
Render a schematic to SVG with the pure-stdlib renderer — the same normalized model every check runs
on, so an Altium .SchDoc renders as readily as a .kicad_sch. Connectivity-true, with
faithful symbol artwork on KiCad sources: wires, buses, bus entries, junctions, labels
(scope-colored), No-ERC marks, pin tips, and component bodies drawn from the sheet's embedded
lib_symbols graphics (rectangles/polylines/circles/arcs, pin stubs, pin names/numbers unless the
symbol hides them) through the same transform chain the net engine uses — so artwork can never
disagree with connectivity. Components the artwork path cannot draw (Altium sources, multi-unit
parts, symbols missing from lib_symbols) fall back to the synthesized pin-box body with
refdes/value. Hierarchical designs render one titled block per sheet.
Deterministic output (same input bytes → same SVG bytes). -o - writes to stdout; default
output is <input>.svg next to the input. --json emits
{render_version, source, output, components, wires, labels, junctions, bytes}. The
visual-feedback channel after a draw --apply: render, then look at what you placed.
--grid overlays world-mil gridlines (major every 500 mil), coordinate captions and an origin
cross — the numbers on the image are exactly the numbers op-lists use (plan/draw --render
previews always include it).
Generate the pinout book — a human-readable Markdown design document composed from the
normalized model (works on .kicad_sch and imported Altium .SchDoc alike):
- Pin tables per IC/connector (default refs
U*,J*,CN*,P*; override with a comma-separated--refsglob list): pin number, name, electrical type, and the net each pin actually landed on — the as-drawn pinout a reviewer checks against the datasheet. - Power rails — the
review treeanalysis as a table (rail, voltage, regulator, consumers, decoupling count). - BOM — real in-BOM components grouped by value/footprint/symbol, refs natural-sorted.
Deterministic output (same input bytes → same Markdown bytes; no timestamps), so books diff
cleanly in review. -o FILE writes the file (stdout stays clean); --json emits the same
content structured ({schema_version, source, refs, component_count, net_count, components: [{ref, lib_id, value, footprint, dnp, pins: [...]}, …], rails, bom} — schemas/doc.schema.json). The design-review entry point: /circuit-draw output plus akcli doc is what a
human reads without opening an EDA tool.
Query the workspace write journal. Every write-path command (plan, draw, arrange,
undo, relink-symbols) appends one JSONL entry to <dir>/.akcli/journal.jsonl — timestamp,
command, target, status (dry-run/applied/refused), the op-list sha256, op count, the
net-diff verdict, and the backup name (a multi-sheet transaction also records run_id and the
dirty files it touched — feed run_id to akcli undo <target> --run <run_id>) — so a later
invocation (or a harness hook) can answer
"was there a plan for this op-list before the --apply" and "what did the last session do here"
without re-deriving state. Write commands accept --note TEXT to record why the edit was
made next to what was done — design intent for the next session (see
agent-state.md). PATH is the workspace directory
or an edited file (which filters to that file); --cmd filters by command; --json emits
{journal_version, journal, returned, entries}. Journaling never fails the parent command
(failures degrade to a stderr note:), is size-capped with one rotation
(journal.jsonl.1), and AKCLI_JOURNAL=off disables it. Add .akcli/ to your project's
.gitignore.
The self-describing surface manifest — the single root document an agent reads to drive the
tool blind. --json emits every subcommand + flag (introspected from the live parser, so it can
never drift from reality), the frozen exit-code and error-code tables, the op-list vocabulary
(core/sugar/macros + per-executor support, plus a constraints block — the rotation enum, the
orthogonal-wire-only rule, grid_mil, and the hierarchy mode hierarchy: "hierarchical_write_v2"
(sheet_reuse: true) with a hierarchy_note spelling out the sheets map / per-op sheet
multi-file transaction and the per-instance reference rule for a reused sheet — and an honest
altium_write_wired: false flag: Altium is import-only, so the informational "altium" op column
is NOT a write target), the calculator registry, the packaged JSON Schemas with their version fields, and
the tool conventions (stdout=data/stderr=logs, dry-run default, version-stamp policy, and the
--json error-envelope contract). Without --json, a compact human summary. Static surface only —
environment probing (which optional tools are actually installed here) is akcli doctor.
Slicing (0.20). The full manifest is ~96 KB (mostly commands), while an agent usually
needs one section or one command's flags. --section NAME (one of exit_codes, error_codes,
global_flags, commands, ops, calculators, schemas, conventions) prints that section
alone in a stamped envelope; --command VERB prints one command's entry (implies
--section commands; an unknown VERB reports found: false and exits 8, the query-miss
convention). A slice is machine-facing by definition, so it is JSON with or without --json;
the no-flag document keeps its exact historical shape.
akcli capabilities --section exit_codes
akcli capabilities --command drawPrint a packaged JSON Schema byte-for-byte — the fourteen schemas ship inside the wheel
(mirrored from the repo's canonical schemas/, byte-identity CI-gated), and this command makes
them reachable through the CLI: an orchestrator that pip install-ed akcli gets
ops.schema.json for client-side validation without digging into site-packages. Names accept
the short form (ops, findings, netlist, draw-result, ops.capabilities, ...) or the
full filename; an unknown name fails at parse time with the full list (USAGE envelope under
--json). The output IS the schema file's exact text — already JSON, already carrying its own
version field — so --json changes nothing.
akcli schema ops
akcli schema findings.schema.jsonOne-shot environment report. Probes — the same way the features themselves discover them —
python (>= 3.11), the akcli install (version + mode), packaged schemas,
kicad-cli (KICAD_CLI env → PATH → known install locations), ngspice
(AKCLI_NGSPICE honored; KiCad's bundled libngspice found automatically), symbols,
config discovery, and workspace hygiene (legacy beside-the-file backup stacks, leftover
KiCad ~*.lck lock files, an un-gitignored .akcli/, and — new — a directory holding
.kicad_sch but no .kicad_pro (KiCad opens that as a project-less file; the hint points at
akcli project init .) — advisory, never --require-gatable).
--network additionally probes the jlc endpoint (doctor is offline by default).
The symbols check walks the whole path akcli pins Device:R / place_component take —
project + global sym-lib-table → ${KICAD*_SYMBOL_DIR} expansion → .kicad_sym load →
symbol lookup — and prints the resolved library file on success. It is the probe for the
"everything is SYMBOL_NOT_FOUND" failure mode: KiCad's default tables are written against
path variables KiCad never exports, so if akcli cannot infer your install the hint is
export KICAD10_SYMBOL_DIR=/path/to/symbols (match whichever KICAD*_SYMBOL_DIR your
sym-lib-table uses).
Every missing item prints a remediation hint; only python is a hard requirement.
--require kicad-cli,ngspice,... turns the report into a CI gate: exit 1 when a named
capability is missing (unknown capability names exit 2). --json emits
{checks: {name: {ok, detail, hint?}}, required, ok}. The akcli-setup skill drives this
command for setup/repair flows.
Project library workspace: the sym/fp-lib-table, the schematics, the registered libraries and their 3D models as one auditable object — plus authoring and linting the symbols themselves. See docs/design-integrity.md.
akcli library audit [<project>] [--sch FILE ...] [--fail-on SEV] [--exit-zero]— cross-checks schematics ↔sym-lib-table/fp-lib-table↔ library contents ↔ 3D models (project dir or.kicad_pro; default.). Findings:FOOTPRINT_LIB_UNREGISTERED(a Footprint nickname the fp-lib-table does not register — the "cannot find footprint" trap),FOOTPRINT_MISSING,SYMBOL_LIB_UNREGISTERED/SYMBOL_MISSING,LIB_URI_UNRESOLVED/LIB_PATH_MISSING,MODEL_MISSING/MODEL_NOT_PORTABLE,FOOTPRINT_LEGACY_FORMAT(a pre-v6 footprint that parses via API but is invisible to the KiCad GUI). Only the project tables are consulted; lint-style exit (1on ≥ WARNING).akcli library repair [<project>] [--rename-footprint-lib OLD=NEW] [--3d-path absolute|'${VAR}'] [--apply]— productizes the two historically hand-sed-ed fixes as a plan: rewrite Footprint nicknamesOLD:* → NEW:*(lossless S-expression edit of registered.kicad_sym+ schematics) and rewrite bare-relative 3D-model paths. Dry-run by default;--applywrites atomically with a.bakand re-audits.akcli library import-altium <part.PcbLib> [-o DIR] [--courtyard MM] [--apply]— converts an Altium.PcbLibinto a.prettylibrary, pads verbatim (never recomputed). Transformations are declared, not silent (filesystem-safe renames; an optional pad-bbox--courtyard); on--applyit writes the.kicad_modfiles plus aprovenance.json(source SHA-256, converter version, options, every warning). Dry-run by default.akcli library check-lock [<project>]— is any KiCad file open in the GUI? Scans the project tree for documents whose~<name>.lckGUI lock file is present. akcli's own write ops already refuse a locked target (TARGET_LOCKED), but hand scripts andseddo not — this exposes the same probe so an external flow can gate on it (akcli library check-lock . && ./relayout.sh). Exit6(TARGET_LOCKED) when ANY file is locked,0when the tree is writable;--jsonlists each locked file with its lock path.akcli library relink <target.kicad_sch> [--libs DIR ...] [--only NICKS] [--apply]— the library-maintenance home of top-levelrelink-symbols(same handler and flags; the old name stays for compatibility).
Generate a single-unit .kicad_sym from a pin table — you supply what a datasheet gives up
reliably (number, name, electrical type, which body edge), akcli computes what hand-authoring gets
wrong (coordinates). Input shape: schemas/symbol-pins.schema.json.
akcli library new-symbol pins.json # dry-run: print the placement
akcli library new-symbol pins.json -o MyLib.kicad_sym --apply
akcli library new-symbol pins.json --name ALT_NAME --jsonsideandtypeare required per pin and never guessed. A guessed side is a wrong pin position; a defaultedunspecifiedtype is a pin KiCad's ERC pin-conflict matrix exempts from every rule.- Layout: pins on the 100-mil grid, standard 100-mil pin length. Within one side, pins are
ordered by their
group's first appearance in the array, then by array order — so the output is stable and related pins stay together. The body rectangle is sized from the per-side pin counts and the pin-name text widths;body.width_milmay only widen it (shrinking below the computed minimum would push top/bottom pins onto the left/right pin columns). - Deterministic: identical input yields identical bytes.
- Stacked pins:
stack_withis the ONLY way to put two pins on one coordinate, and the target must be on the same side with the same name and the same electrical type (KiCad's stacked-pin convention). Chains are refused so a coordinate has exactly one owner. - Write gate (deliberate exception to "geometry findings are advisory"): if the generated symbol
trips
SYMLIB_PIN_COINCIDENT, the command refuses and writes nothing — exit6(VERIFY_FAILED), with or without--apply. Two pins on one point is a connectivity defect: KiCad merges them into one node in every placement of the part, and ERC cannot report it because the symbol is what told ERC they are the same node. - Spec surface: required top-level keys
protocol_version,name,pins; optionalreference_prefix(defaultU) andbody.width_mil. Per pin:number,name,type,side(required), plus optionalgroup,hidden(an invisible pin) andstack_with. - Not supported yet: multi-unit symbols. A spec carrying
units/unit_count/part_count, or a pin carryingunit/unit_number/owner_part_id, is refused with an explicit "not supported yet" rather than a generic unknown-key error. A wrongprotocol_versionisPROTOCOL_MISMATCH(exit6). Other refusals (allBAD_CONFIG, exit2): duplicate pin numbers, an unknowntype(the message lists the whole vocabulary), astack_withname/type/side mismatch, and an empty pin table. - Dry-run by default;
--applywrites atomically, leaves a.bakwhen replacing, and refusesTARGET_LOCKEDif KiCad holds the target open. Output path defaults to<symbol name>.kicad_sym(under[paths].parts_dirwhen configured).
Run the same geometry checks over a library akcli did not write — an imported, converted or
hand-authored .kicad_sym. This is where the incident behind new-symbol would have been caught.
akcli library lint-symbols MyLib.kicad_sym
akcli library lint-symbols MyLib.kicad_sym --json --fail-on errorFindings: SYMLIB_PIN_COINCIDENT (error), SYMLIB_PIN_TYPE_UNSPECIFIED (note),
SYMLIB_PIN_OFF_GRID (warning) — see the finding-code table below. Multi-unit symbols are evaluated
per unit, so four gates drawn on one body never produce a false positive. Lint-style exit (1 on
≥ --fail-on, default warning); here the findings are advisory — only new-symbol gates a write.
Manufacturing policy against a versioned fab profile (TOML carrying mandatory [source] urls —
policy is source-controlled, never a builtin constant).
akcli fab check <board.kicad_pcb> --profile FILE [--order FILE] [--fail-on SEV] [--exit-zero]— checks the deep-read board: via geometry vs the free-process envelope (FAB_VIA_PAID_PROCESS,FAB_VIA_TENTED_TOO_BIG,FAB_VIA_MIN_MARGIN), via-in-pad (FAB_VIA_IN_PAD, with registeredthermal_viaexceptions →FAB_VIA_IN_PAD_EXCEPTION; an expired one →FAB_EXCEPTION_EXPIRED), blind/buried bans (FAB_VIA_TYPE_FORBIDDEN), stackup drift (FAB_STACKUP_MISMATCH), and cost thresholds (FAB_COST_*: board size/area, drill density, fine multilayer traces).--ordervalidates the declared order manifest (delivery format, finish, via covering, … — pricing inputs never guessed from the PCB):ORDER_INCOMPLETE,ORDER_REVIEW_REQUIRED(ENIG/panel/ multi-design),ORDER_PROFILE_CONFLICT. Lint-style exit.akcli fab explain <CODE> [--profile FILE]— prints the rule behind a finding code, the fix direction, and (with--profile) the profile's evidence sources.
akcli release preflight --sch FILE [--pcb FILE] [--intent FILE] [--contract FILE] [--fab-profile FILE] [--order FILE] [--sim FILE] [--gerbers DIR] [--review-policy FILE] [-o FILE] [--allow-dirty]
Run every applicable gate and emit a traceable release manifest. Gates:
check (ERC/power/BOM/nets), intent, contract, library-audit, project (needs a
.kicad_pro beside --sch; runs project check's findings with library-audit
suppressed there, since library-audit above already covers it — one finding reported
twice reads as two problems; no project file is a skip, not a silent pass), sch-pcb
(needs --pcb),
fab (needs --pcb + --fab-profile), order, sim (needs --sim sim.json — quantitative
SPICE assertions; a requested sim gate that cannot run FAILS rather than skips, so a PASS
manifest never ships because the machine lacked libngspice), review (needs --review-policy),
gerber (needs --gerbers), and git (clean worktree). A gate with no input
is skipped with a reason, never silently green. --out writes the manifest JSON: input
SHA-256s, the akcli version, the git revision and dirtiness, and each gate's findings. A dirty
worktree fails the git gate (RELEASE_DIRTY_WORKTREE, ERROR) unless --allow-dirty (which
records the fact). Exit 0 when every gate is pass or skipped (a skipped gate is a recorded
absence of evidence, not a blocker); any single fail exits 1.
The iteration-weight counterpart to release preflight: four gates re-run by hand after every
GUI edit round during a design campaign, with none of the release manifest/input-hashing/git-state
machinery. Not the same thing as the repo's own CI-green gate (.github/scripts/ ci_green_gate.sh, see docs/releasing.md) — that one checks GitHub Actions status on a tagged
release commit; this one is a local, offline, per-edit check bundle.
Gates:
check— the full defaultakcli check PATHset (erc/power/bom/nets, plus layout/integrity for a.kicad_sch, plus pairs unless[check].pairs = false) — a wider set thanrelease preflight's fixed erc/power/bom/nets four-tuple.intent—--intent FILE, falling back to[paths].intent(the same config keycheck --intentfalls back to). Skipped with a reason when absent.sch-pcb—--pcb FILE, falling back to[paths].pcb. Skipped with a reason when absent.bom-lock— BOM lockfile drift (--lock FILE, falling back to[paths].bom_lock), the samediff_lock()calljlc bom --against-lockmakes; each drift is aBOM_LOCK_DRIFTfinding (WARNING). Offline (HTTP-cache-only) by default — an every-edit loop must never touch the network on its own;--liveopts one run into a fresh catalog fetch. A cache miss degrades a line tounverifiedrather than failing the gate outright; an unverified count is always shown, so a cold cache never reads as a fully-verified PASS. Skipped with a reason when no lockfile is given.
PATH is optional: it falls back to [paths].schematic — the first command that actually reads
that documented config key. No path anywhere is a USAGE error naming the missing key. Exit is
worst-of across the four gates (0 when every gate passed or was skipped, 1 otherwise); the
shared --fail-on/--exit-zero flags re-tune that threshold without changing what each gate
reports. --json emits {schema_version, result, gates: [...]} — the same per-gate shape as
release preflight, minus the release manifest's inputs/git/tool-hashing fields.
akcli gate board.kicad_sch --pcb board.kicad_pcb --intent intent.json --lock bom.lock.json
| Code | Meaning |
|---|---|
0 |
Success / no findings. |
1 |
Check findings present (lint-style; tune with check --fail-on, never always exits 0). |
2 |
Usage / argument / config error (incl. BAD_CONFIG, PATH_OUTSIDE_ROOT). |
3 |
Parse error (corrupt OLE2 or S-expression, incl. a bad numeric field: KICAD_SEXPR_BAD_NUMBER, or bytes that are not UTF-8 at all: KICAD_BAD_ENCODING) — also the last-resort code for an unexpected internal exception (INTERNAL). |
4 |
The path is not a usable file: missing (FILE_NOT_FOUND), or present but unreadable — a directory passed where a file was expected, no read permission, an IO error (FILE_UNREADABLE). |
5 |
Unsupported format (incl. ALTIUM_UNSUPPORTED features). |
6 |
Op-list or verify failure (incl. --strict-nets refusal, relink-symbols / library repair gate refusal, PROTOCOL_MISMATCH, GUI-lock TARGET_LOCKED, sim deck-build refusals). |
7 |
Required external tool missing or network failure (KICAD_CLI_MISSING/KICAD_CLI_TIMEOUT, jlc ERROR: NETWORK, BINFETCH_*, sim's NGSPICE_MISSING/NGSPICE_FAILED, reserved BRIDGE_TIMEOUT). |
8 |
Query miss: the file parsed fine but the named entity does not exist in it (net <file> NAME, component <file> REF; --json emits {"found": false, ...}). |
Without --debug, failures print a single structured line, e.g.:
ERROR: ALTIUM_FAT_CYCLE: FAT chain revisits sector 42 (cycle); aborting
- With
--json, a failing command that would otherwise leave stdout empty instead emits a JSON error envelope on stdout:{"schema_version": "1", "akcli_version": "<version>", "error": {"code", "message", "exit", "remediation"}}—akcli_versionnames the build that failed, andremediationis an actionable next step from the frozenerrors.REMEDIATIONtable (covering every error code); the plainERROR:line still goes to stderr for humans. The envelope is skipped when the handler already wrote data on stdout, so a failure after partial output never yields two JSON documents. - argparse failures use the same envelope. A wrong flag, a bad
choicesvalue, or an unknown command emits{"error": {"code": "USAGE", "message", "exit": 2, "remediation"}}under--json(the argparse text, including the fullchoose fromlist, rides inmessage); plain-text behavior is unchanged.USAGEandFILE_NOT_FOUNDare envelope-level pseudo-codes named after the exit category, not members of theERROR_CODESregistry below. - Invoking
akcli --jsonwith no subcommand emits the sameUSAGEenvelope (exit2); the help text still goes to stderr. There is no non-zero exit that leaves stdout empty under--json. INTERNALmeans unknown, and only that. Failures with an identifiable cause are classified before the last-resort handler sees them: theOSErrorfamily (directory-as-file, unreadable file, IO error) →FILE_UNREADABLEexit4, undecodable bytes → exit3. Only a genuinely unexpected exception still yieldsINTERNAL(exit3) plus a traceback on stderr;--debugre-raises instead.- JSON output is RFC 8259. Non-finite floats (
NaN,Infinity) are never emitted — producers refuse them at the input (e.g.calc rc r=nanis aUSAGEerror, as is a degenerater=0 c=0that would divide by zero), so an agent's parser never fails on the document. -C/--configis strict. An explicit-Cnaming a file that does not exist, is a directory, or fails to parse isBAD_CONFIG(exit2) — it is never silently ignored. Walk-up discovery finding noakcli.tomlstays silent, as before.
Error codes (frozen registry in src/akcli/errors.py), grouped by the exit code they
surface as:
| Exit | Error codes |
|---|---|
3 (parse) |
ALTIUM_BAD_MAGIC, ALTIUM_FAT_CYCLE, ALTIUM_OOB_SECTOR, ALTIUM_BAD_SECTOR_SHIFT, ALTIUM_ALLOC_GUARD, ALTIUM_MALFORMED, KICAD_SEXPR_DEPTH, KICAD_SEXPR_UNTERMINATED, KICAD_SEXPR_TOOBIG, KICAD_SEXPR_BAD_NUMBER, KICAD_BAD_ENCODING, INTERNAL, PROJECT_FILE_INVALID |
4 (unusable path) |
FILE_UNREADABLE, PROJECT_FILE_MISSING |
5 (unsupported) |
ALTIUM_UNSUPPORTED |
6 (op-list/verify) |
SYMBOL_NOT_FOUND, BAD_ANGLE, NON_ORTHOGONAL_WIRE, OFF_GRID, OVERLAP, VERIFY_FAILED, OP_UNSUPPORTED, HIERARCHICAL_UNSUPPORTED, PROJECT_ROOT_AMBIGUOUS, SHEET_UNKNOWN, INSTANCE_REFS_REQUIRED, TXN_ROLLBACK_FAILED, PROTOCOL_MISMATCH, TARGET_LOCKED, GROUP_UNKNOWN, GROUP_NO_ORIGIN, POWER_PORT_STACKED, SHEET_FILE_MISSING, DESIGN_UNSUPPORTED, SIM_NO_GROUND, SIM_NODE_COLLISION, SIM_NODE_COUNT_MISMATCH, SIM_VALUE_INVALID, BRIDGE_BUSY (reserved, unraised) |
2 (usage/config) |
PATH_OUTSIDE_ROOT, BAD_CONFIG, SIM_OP_ASSERT_UNSUPPORTED, PROJECT_ROOT_MISMATCH, PROJECT_EXISTS |
7 (tool/network) |
KICAD_CLI_TIMEOUT, KICAD_CLI_MISSING, NGSPICE_MISSING, NGSPICE_FAILED, NETWORK (jlc), BINFETCH_DOWNLOAD, BINFETCH_CHECKSUM, BRIDGE_TIMEOUT (reserved, unraised) |
Two related but distinct vocabularies:
- Per-op results (
plan/drawoutput) carry anerror_codefrom the same registry; a crashing op handler is contained and reported as error codeINTERNALinstead of a traceback. - Finding codes (
checkoutput; these drive exit1, not the table above) now also includeNET_PIN_MIDSPAN_TOUCH,NET_LABEL_UNATTACHED,NET_WIRE_CORNER_ON_PIN,LAYOUT_POWER_ON_PIN,LAYOUT_WIRE_THROUGH_SYMBOL,LAYOUT_LABEL_OVER_WIRE,ERC_UNPLACED_UNIT,INTENT_PIN_UNKNOWN,INTENT_NET_NOT_FOUND,INTENT_MISSING_MEMBER,INTENT_EXTRA_MEMBER,INTENT_NETS_SHORTED,LIB_EMBED_STALE,LIB_EMBED_OLD_FORMAT,POWER_RAIL_NO_SOURCE,REVIEW_FACTS_UNMATCHED, the write-gate document findings thatcheck --integrityalso runs read-only (DANGLING_ENDPOINT,DANGLING_BUS_ENTRY,DUPLICATE_UUID,DUPLICATE_DESIGNATOR,UNRESOLVED_LIB_ID,INVALID_INSTANCES_PATH,POWER_PORT_STACKED,SHEET_FILE_MISSING) plusNO_CONNECT_CONFLICT— reported by the same pass at WARNING, so it surfaces in--integritybut does not by itself refuse a write — plus the design-integrity familiesCONTRACT_*(check --contract),SCHPCB_*(verify sch board.kicad_pcb), thelibrary auditcodes (FOOTPRINT_LIB_UNREGISTERED,FOOTPRINT_MISSING,MODEL_MISSING,FOOTPRINT_LEGACY_FORMAT, …), thelibrary lint-symbolscodes (SYMLIB_PIN_COINCIDENT,SYMLIB_PIN_TYPE_UNSPECIFIED,SYMLIB_PIN_OFF_GRID),FAB_*/ORDER_*(fab check), and theproject checkcodes (PROJECT_FILE_MALFORMED,PROJECT_META_FILENAME_STALE,PROJECT_ROOT_SHEET_MISSING,PROJECT_ROOT_SHEET_MISMATCH,PROJECT_PCB_MISSING,PROJECT_STATE_UNIGNORED, plus reusedSHEET_FILE_MISSINGandlibrary audit's own codes — see the finding-code table above).
| code | severity | what it means |
|---|---|---|
SYMLIB_PIN_COINCIDENT |
error | Two pins of one symbol unit share a connection point, with differing name or electrical type. KiCad treats a symbol's pin connection point as a node, so every placement of the part joins those pins into one net — and ERC reports nothing, because the symbol is what told it they are the same node. The one exemption is KiCad's stacked-pin convention: same name and same type. Multi-unit parts are evaluated per unit (_0_* pins count as belonging to every unit), so four gates drawn on one body never trip it. |
SYMLIB_PIN_TYPE_UNSPECIFIED |
note | Pins carrying the unspecified (or free) electrical type. KiCad's native ERC decides pin conflicts from a type-vs-type matrix in which unspecified conflicts with nothing: those pins are exempt from every driver/input rule, and the volume of exempt pins an untyped symbol contributes is what buries the genuine conflicts elsewhere in the report. |
SYMLIB_PIN_OFF_GRID |
warning | A pin connects off the 100-mil grid, so a wire drawn on the editor grid can never land on it: the pin looks wired and behaves as floating. |
SYMLIB_PIN_COINCIDENT is the one geometry-shaped finding akcli treats as
connectivity, not advice — see the library new-symbol write gate below.
akcli read main.SchDoc --json | jq '.components | length'
akcli net board.kicad_sch --json > netlist.json
akcli check main.SchDoc -C akcli.toml # exit 1 if findings
akcli diff v1.SchDoc v2.SchDoc
akcli pinmap main.SchDoc -C akcli.toml --expected pins.csv
akcli export main.SchDoc --format protel -o board.net
akcli nets board.kicad_sch --intent-snapshot intent.json # snapshot the netlist you meant
akcli plan board.kicad_sch --ops ops.json --symbols Device.kicad_sym
akcli draw board.kicad_sch --ops ops.json --symbols Device.kicad_sym --apply --strict-nets
akcli check board.kicad_sch --intent intent.json # assert the intent held
akcli relink-symbols board.kicad_sch --apply # refresh embedded lib_symbols
akcli sim board.kicad_sch --deck-only # emit the SPICE deck (no ngspice)
akcli sim board.kicad_sch --sim board.sim.json # run + assert, exit 1 on failure
akcli sim board.kicad_sch --sim board.sim.json --sweep temp=0,25,60 # corner matrix
akcli sim fit-diode --point 0.37@20m --n-prior 1.05 --name DBAT # datasheet -> .model