app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← Commit history
1.0.62: standard aliases for 16 of the 1.0.61 verbs, the standard-decision test, battery coverage for all 78 new verbs, live_create_unsupported, stale lock files cleared, release script syncs bridge.json, README verbs and the standard's coverage
22 files changed
+1606−138
CONTRIBUTING.md+8README.md+251−16bridge.json+194−100package.json+1rust/Cargo.lock+3−3rust/Cargo.toml+1−1rust/README.md+21−1rust/crates/kicad-bridge/src/catalog.rs+107−3rust/crates/kicad-bridge/src/kicad_only.txt+156rust/crates/kicad-bridge/src/standard_names.txt+202rust/crates/kicad-bridge/src/verbs.rs+4rust/crates/kicad-bridge/src/verbs_board.rs+7−6rust/crates/kicad-core/src/ipc/board_live.rs+75−4rust/crates/kicad-core/src/pcb.rs+29−2skills/kicad-bridge-contributing/SKILL.md+7skills/kicad-bridge-dev/SKILL.md+20skills/kicad-bridge-publish/SKILL.md+10skills/kicad-bridge-test/run_verb_tests.py+297−1skills/kicad-schematic-and-board-edits/SKILL.md+58tools/build_native_release.py+42−1tools/readme_verbs.py+83tools/sync_standard_names.py+30CONTRIBUTING.md+8@@ -83,3 +83,11 @@ Prefer `adom-wiki branch create <repo> <branch> --from <hash>`, followed by `rep Review every deletion, module and verb registration, brace and `cfg(test)` boundary. Keep Rust formatting consistent without unrelated whole-file reformatting. Run a production `cargo check --workspace --bins` and the combined workspace tests with required features: tests alone can miss production functions accidentally nested inside test modules. Preserve tests for capabilities merged since the original checkout. Each PR must state its exact base hash, dependencies, scope, preserved capabilities, test commands/results and native acceptance still needed. IPC deletion visibility, refill Undo steps, final revisions and rendering require native validation; source tests are not evidence of those behaviors. Maintainer merge, release publication and deployed acceptance remain separate. Do not replace a shared runtime to test a PR.++## Verb names and the EDA Bridge Standard++New verbs are named from the [EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard) (or carry its name+as an alias), are listed in `bridge.json`, and get a verb battery entry. `cargo test --workspace` enforces the first+two (`every_verb_has_a_standard_decision`, `bridge_json_declares_exactly_the_catalog`); the battery enforces the+third. See `rust/README.md`, "Name verbs from the EDA Bridge Standard".+
README.md+251−16@@ -255,27 +255,262 @@ viewer. This is how every 3D shot above exists. ## Verb reference -117 verbs as of 1.0.33 (prefix `kicad_`); the groups below name the core set, not every verb. `kicad_describe` is the-live catalog and the source of truth, with per-verb hints,-related verbs, and pitfalls.+<!-- verbs:start (tools/readme_verbs.py writes this) --> -**Windows & UI**: `launch`, `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,-`open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`,-`close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`, `demo`+Every verb of this build (211, `kicad_describe` is the live catalog with inputs, hints and pitfalls). A name marked *standard* is the EDA Bridge Standard's name for the verb after the arrow. -**Libraries & parts**: `install_library`, `install_symbol`, `install_footprint`, `install_plugin`,-`place_footprint`, `adom_library_status`+**Board model and reads (EDA Bridge Standard)** (12) -**Board model (EDA Bridge Standard)**: `routing_state {"model":"standard"}`, `layer_stack`, `pour_state`,-`bodies`, `netlist`, `board_summary`, `list_nets`, `list_designators`, `copper_layers`, `layers_in_use`,-`component_data`, `component_pins`+| Verb | What it does |+|---|---|+| `kicad_board_summary` | One-screen summary of the board: parts by side, nets, pads, copper layers and those actually in use, the outline's size and area, unconnected and the revision. |+| `kicad_bodies` | Every part's 3D body as a box: its footprint rectangle on the board and its height range above the surface it sits on, read from the 3D model files the footprints name. For height checks, enclosure fit and placement review. |+| `kicad_component_data` | Placement data for one part or all of them: footprint, value, side, position, rotation, the size of its pads' extent, pad count and the nets on its pads. |+| `kicad_component_pins` | Every pad of one part with its net, position, layer, size, rotation and hole. |+| `kicad_copper_layers` | The board's copper layers top to bottom by KiCad's names (F.Cu, In1.Cu.., B.Cu). |+| `kicad_layer_stack` | The board's stack-up: copper layers top to bottom with copper thickness and the dielectric under each (thickness, Dk, material), the masks and the board thickness. For impedance, current capacity and fab quotes. |+| `kicad_layers_in_use` | Which copper layers actually carry copper (tracks, arcs, vias, pads, pours), densest first, and which are empty. |+| `kicad_list_designators` | Every part designator on the board, in natural order (R2 before R10). |+| `kicad_list_nets` | Every net on the board with its pad count. |+| `kicad_netlist` | The compiled netlist in the EDA Bridge Standard's shape: every net with its pins as Ref.Pin across every sheet (hierarchy resolved); or one net; or compare with the nets you expect. parts:true adds each part's pins with their electrical types. |+| `kicad_pour_state` | Every copper pour (zone) on every layer with the copper KiCad actually poured: filled area per zone and per layer, islands, and the zones that poured nothing. |+| `kicad_routing_state` | Inspect the live editor: pads, copper, revision and KiCad-measured net connectivity. | -**Checks & export**: `run_drc`, `run_erc`, `lint_board`, `lint_schematic`, `lint_library`,-`format_upgrade`, `export_gerber`, `export_pdf`, `export_svg`, `export_step`, `export_bom_csv`,-`export_molecule`+**Schematic** (37) -**Detect, install & meta**: `list_versions`, `readiness`, `describe`, `diagnostics`, `status`,-`enable_software_opengl`, `check_for_updates`, `upgrade`, `uninstall`, `bridge_status`, `bridge_call`+| Verb | What it does |+|---|---|+| `kicad_design_review` | Schematic design review from the netlist: ICs without a decoupling capacitor on each power-pin net, I2C lines without pull-ups, floating or undriven reset and enable pins, LEDs without a series resistor, regulators without input or output capacitors, rails without bulk capacitance, unannotated or duplicate references. Each finding has severity, refs, nets, why and how to fix. |+| `kicad_render_schematic` | Render a schematic sheet to PNG (kicad-cli sch export svg, rasterised in the bridge), and keep visual baselines: baseline:"set" stores the image under <project>/.adom/baselines/, baseline:"compare" reports the pixel drift percentage, the changed region and a diff image. |+| `kicad_sch_annotate` | Number every X? reference on a .kicad_sch the way eeschema's Annotate does: the first free number per prefix across the project's sheets, in position order, units of one multi-unit part sharing a designator (edits the file on disk). |+| `kicad_sch_batch` | Apply a list of schematic edit operations to one .kicad_sch in ONE read and ONE atomic write: all or nothing, one ERC before and after, dryRun reports the plan. |+| `kicad_sch_bus` | Draw a bus polyline on a .kicad_sch (one bus segment per pair of points), optionally with a bus label such as D[0..7] at its first point (edits the file on disk). |+| `kicad_sch_bus_entry` | Add a 45-degree bus entry (the short diagonal between a member wire and a bus) at a point (edits the file on disk). |+| `kicad_sch_check` | Connectivity and layout check of a sheet from the file: dangling wire ends, floating labels, unconnected non-passive pins, nets that reach only one pin, distinct net names shorted together, no-connect flags on wired pins, and with overlaps:true symbol, label and text collisions (symbol body and pin extents). Each finding has a location and a fix. |+| `kicad_sch_components` | List the placed symbols of a .kicad_sch: reference, value, footprint, lib id, every unit with position, rotation and mirror, and every field. |+| `kicad_sch_connect_pins` | Wire two pins by Ref.Pin with a horizontal and vertical (L-shaped) route, adding junction dots where the route makes a T with existing wires (edits the file on disk). |+| `kicad_sch_connect_to_bus` | Fan a set of pins onto a bus: per pin a wire to just short of the bus, a 45-degree bus entry onto it, and a member label (D0, D1 ...) on the wire (edits the file on disk). |+| `kicad_sch_connect_to_net` | Connect one or many pins to a named net: a short stub wire away from the body plus a net label (local, global, hierarchical) or a power symbol at its end (edits the file on disk). |+| `kicad_sch_create` | Create a blank KiCad 10 schematic (.kicad_sch) with a paper size and title block; optionally add it to an existing project as a hierarchical sheet of the root, or make it the root of a new project. |+| `kicad_sch_delete` | Delete any top-level item (symbol, wire, label, junction, sheet ...) from a .kicad_sch by uuid, every unit of a symbol by reference, or labels by name (edits the file on disk). |+| `kicad_sch_delete_wire` | Delete wires (or bus segments) by uuid, by their two endpoints, or every wire through a point, and prune junction dots that no longer join three or more items (edits the file on disk). |+| `kicad_sch_edit_label` | Edit net labels on a .kicad_sch: rotate (with the justify KiCad pairs with the angle), move to a point or by a delta, rename, or move every label of a net by (dx, dy) (edits the file on disk). |+| `kicad_sch_fix_connectivity` | Snap near-miss wire ends onto the pin, label, sheet pin or wire end they almost touch (within a tolerance). dryRun (the default) lists the moves; dryRun:false writes them. |+| `kicad_sch_hierarchy` | The sheet tree of a design, recursively: each sheet's name, file, uuid, instance path, page, position, size and pins; renumber:true reassigns page numbers depth-first (root 1). |+| `kicad_sch_items` | List the wiring items of a .kicad_sch with uuids and coordinates: wires, buses, bus entries, labels (local, global, hierarchical), power symbols, junctions, no-connects, texts, sheets. |+| `kicad_sch_junction` | Add junction dots at one or many points, or remove them by uuid or point (edits the file on disk). |+| `kicad_sch_label` | Add a net label (local, global, hierarchical) or a power symbol on a .kicad_sch (edits the file on disk). |+| `kicad_sch_move` | Move a placed symbol (by reference) so its anchor lands on a new point or by a delta; every unit of a multi-unit part and every field move with it, and stretchWires keeps the attached wires on the pins (edits the file on disk). |+| `kicad_sch_move_region` | Move everything inside a box by (dx, dy): symbols by their anchor, wire ends inside the box (wires crossing the edge stretch), labels, junctions, no-connects, texts, bus entries, sheets (edits the file on disk). |+| `kicad_sch_no_connect` | Put no-connect flags on pins of a .kicad_sch (an intentionally open pin: a spare GPIO, an unused connector pin) so ERC stops reporting them, or remove flags (edits the file on disk). |+| `kicad_sch_page` | Read or set a schematic's page: paper size (A5 to A0, A to E, USLetter, USLegal, USLedger, custom mm), orientation and title block (title, date, rev, company, comments 1 to 9). |+| `kicad_sch_pins` | Every pin's absolute (x, y) on a .kicad_sch for the given references (rotation and mirror applied), with number, name, electrical type, outward direction, and the wires, labels and net touching it. |+| `kicad_sch_place_symbol` | Place a library symbol on a .kicad_sch (edits the file on disk; the lib_symbols entry is copied from the library when the sheet lacks it). |+| `kicad_sch_replace_symbol` | Change the library symbol of a placed part (lib id), keeping its units, fields and position; the new lib_symbols entry is copied from the library (edits the file on disk). |+| `kicad_sch_reset_fields` | Put a symbol's Reference and Value (or the named fields) back at the library symbol's offsets, angle and justify, through the unit's rotation and mirror (edits the file on disk). |+| `kicad_sch_rotate` | Rotate a placed symbol to an absolute angle (0, 90, 180, 270); every unit turns by the same delta about its own anchor and the fields turn with it (edits the file on disk). |+| `kicad_sch_set_property` | Set or add a field on a placed symbol by reference: Value, Footprint, Datasheet, a custom field, or Reference itself (edits the file on disk). |+| `kicad_sch_sheet` | Hierarchical sheets on a page: add (creates the child file when missing and links it), edit (name, file, size, position), move, delete (the child file is kept), duplicate (a new child file with fresh uuids and free references). |+| `kicad_sch_sheet_pins` | A sheet's pins: list, add, edit (rename, shape, move along an edge), delete; import:true adds a pin for every hierarchical label in the child not yet on the sheet; validate:true reports labels without pins, pins without labels and shape mismatches. |+| `kicad_sch_split_wire` | Split the wire passing through a point into two wires there, and put a junction dot at the cut (edits the file on disk). |+| `kicad_sch_text` | Add free text (a note, a title, a design comment) to a .kicad_sch with size, angle, justify, bold and italic (edits the file on disk). |+| `kicad_sch_trace_point` | Everything electrically connected at a point on a sheet: the wires, labels, pins, junctions, sheet pins and no-connect flags there, and for each net they belong to the whole net (pins, labels, power symbols, wires) with its name. |+| `kicad_sch_update_from_library` | Re-embed the lib_symbols definitions of a .kicad_sch from their libraries (all, or the listed lib ids) and report which changed (edits the file on disk). |+| `kicad_sch_wire` | Draw a wire polyline on a .kicad_sch (one wire segment per pair of points; edits the file on disk). |++**Symbols, footprints and libraries** (27)++| Verb | What it does |+|---|---|+| `kicad_adom_library_heal` | Reconcile a SPLIT Adom library: merge the orphaned file's symbols into the registered one (default), repoint the row, or prune the symbols KiCad refuses. Human-chosen; dryRun previews. |+| `kicad_adom_library_status` | Report Adom library install state across KiCad versions. |+| `kicad_export_part` | One-off single-part library exports: a .kicad_sym with ONLY that symbol, a .pretty with ONLY that footprint, the bare 3D model file. The reverse of install. |+| `kicad_fp_create` | Write a new footprint (.kicad_mod, KiCad 10) into a .pretty folder from a pad list: courtyard generated with a margin, fab outline, silkscreen box clear of the pads with a pin-1 dot, reference and value text, optional 3D model. |+| `kicad_fp_edit` | Edit a .kicad_mod in place: edit, delete, add or renumber pads; append, replace or delete graphics on a layer (line, arc, rect, circle, poly); set description, tags and attributes; replace, append or delete 3D models (path, offset, scale, rotate). Everything not edited stays byte-identical. |+| `kicad_fp_info` | Full details of one footprint: pads (number, type, shape, position, size, drill, layers), layers used, courtyard box, graphics per layer, attributes, properties and 3D models. |+| `kicad_install_footprint` | Write a .kicad_mod into the global Adom.pretty footprint library, register it in fp-lib-table, optionally install and wire a 3D model. |+| `kicad_install_library` | Register a symbol/footprint library in the user's sym-/fp-lib-table (footprint installs also carry a bundle's 3D models). |+| `kicad_install_library_bundle` | Install an Adom wiki Cloud Library bundle zip (symbols + footprints + 3D models) in one call. |+| `kicad_install_plugin` | Retired: the native bridge installs no Python plugin into KiCad. Returns success with nothing to do so old scripts keep working. |+| `kicad_install_symbol` | Merge the symbol(s) of a .kicad_sym into the single global Adom library (Adom.kicad_sym), creating and registering it on first use. |+| `kicad_lint_library` | Lint a symbol/footprint library via kicad-cli. |+| `kicad_list_footprints` | What footprints actually exist, per library: the fp-lib-table (URIs expanded, stock libraries included) plus unregistered .pretty folders in the user dir. |+| `kicad_list_symbols` | Names in a registered symbol library, with KiCad's own inventory. |+| `kicad_make_part_project` | Generate a small real project (pro/sch/pcb) AROUND an installed Adom-library part, so the project/schematic/2D-board/3D-board surfaces have something true to show for one component. |+| `kicad_model_check` | Read-only 3D model diagnostics for a footprint or a whole board: which (model ...) references exist, where KiCad looks, whether each file exists and holds geometry. |+| `kicad_pcm_add_repository` | Register a PCM repository in kicad.json (pcm.repositories), the way Plugin and Content Manager > Manage > add URL does. |+| `kicad_pcm_install` | Install or update a package from a KiCad PCM repository on disk, exactly as the Plugin and Content Manager would (no dialog). |+| `kicad_pcm_list` | Packages KiCad's PCM considers installed on this machine, each with updateAvailable from its own repository (read-only). |+| `kicad_pcm_remove_repository` | Drop a PCM repository from kicad.json pcm.repositories. Installed packages stay installed. |+| `kicad_pcm_uninstall` | Remove a PCM-installed package: its 3rdparty directories and its record. |+| `kicad_rescan_libraries` | Make newly installed parts visible ONCE: reload every open editor's library tree in place (seconds, nothing closed), then verify each listed footprint by loading it in the Footprint Editor. force:true is the process restart, the last resort. |+| `kicad_search_library` | Search symbols and footprints across every registered library (project and global sym-lib-table / fp-lib-table, stock and PCM libraries included) by name, keyword and description; ranked hits with lib ids. |+| `kicad_sym_create` | Append a symbol to a .kicad_sym (KiCad 10) from a pin list: rectangle body sized to the pins and names, pins on the 2.54 mm grid by side, Reference/Value/Footprint/Datasheet/Description/keywords and custom fields, multi-unit when pins name units. |+| `kicad_sym_delete` | Delete one symbol from a .kicad_sym; refuses when other symbols extend it unless force:true. |+| `kicad_sym_info` | Full details of one symbol: every pin (number, name, electrical type, shape, position, length, unit, hidden), unit and body-style counts, fields, power flag, extends (resolved). |+| `kicad_uninstall_library` | Remove a library from the user's sym-/fp-lib-table: the inverse of kicad_install_library. Optionally deletes the copy the install made. |++**Placement and board edits** (31)++| Verb | What it does |+|---|---|+| `kicad_add_footprint` | Add a library footprint at x, y, rotation and side with a reference and value, a row or grid of them (count, pitch), or a copy of a placed one (duplicateOf); optional pad nets. |+| `kicad_add_mounting_hole` | Add a mounting hole footprint at x, y: NPTH by default, or plated with a pad (and a net); KiCad's MountingHole library footprint when one matches the drill, else a generated one. |+| `kicad_align_footprints` | Align footprints on a common left, right, top, bottom or centre line, or distribute them evenly, by courtyard box or anchor, through kicad_move_footprint's checked move. |+| `kicad_board_outline` | Draw the board outline on Edge.Cuts: a rectangle, a rounded rectangle with true arcs, or a polygon; replace:true removes the old outline first. |+| `kicad_board_pads` | Every pad in BOARD coordinates with its net: the input a router needs. |+| `kicad_check_clearance` | Measure the gap between footprints, courtyard to courtyard or pad copper to pad copper, for the listed refs or the whole board, and report every pair under a threshold, closest first. |+| `kicad_cross_probe` | From a schematic symbol (reference, uuid or sheet path) to the board footprint linked to it, and back, by the footprint's symbol path link. |+| `kicad_delete_footprint` | Delete footprints by reference, as one native Undo step live or one atomic write to a closed file. |+| `kicad_delete_graphics` | Delete board graphics (lines, rects, circles, arcs, polygons, curves, texts, text boxes, dimensions) by uuid, or every one on a layer and/or of a type. |+| `kicad_edit_footprint` | Edit placed footprints: value, reference, any field (add, change, remove), DNP, exclude from BOM or position files and other attributes, and the lock. |+| `kicad_flip_footprint` | Move footprints to the other side of the board, mirrored about their anchors: KiCad's own FlipItems live on 10.0.6+, a full client-side mirror otherwise (and on a closed file). |+| `kicad_footprint_graphics` | List a placed footprint's graphics (silkscreen, fab, courtyard lines, rects, circles, arcs, polygons, texts) and edit one by uuid: vertices, width, layer, fill, or delete it. |+| `kicad_footprint_models` | List, add, replace or remove the 3D model entries (path, offset, scale, rotation, visibility) of a placed footprint. |+| `kicad_get_selection` | What is selected in the PCB editor right now: ids, types, footprint references, nets and positions. |+| `kicad_import_svg` | Import an SVG logo or artwork as filled polygons on a silkscreen, copper, mask or user layer, at a position and size; curves flattened, holes kept. |+| `kicad_move_footprint` | Move one footprint, or a batch, live in the PCB editor as ONE native Undo step, after checking the revision, courtyard collisions and the board outline. |+| `kicad_place_decoupling` | Plan positions for decoupling capacitors beside an IC's power pins (each cap just outside the courtyard, power pad facing its pin), dryRun by default; apply moves them through kicad_move_footprint's live commit as one Undo step. |+| `kicad_placement_state` | Every footprint with its courtyard box, side, pose, pads and nets, the board outline, the nets each footprint touches and a ratsnest estimate: the input an AI needs to place a board. |+| `kicad_placement_validate` | Check the placement: courtyard overlaps between every footprint pair, footprints outside the outline or parked, the ratsnest total, and KiCad DRC filtered to placement-class violations. |+| `kicad_save_board` | Save the board open in the PCB editor (File > Save through the IPC API) and confirm the file on disk matches the editor. |+| `kicad_score_placement` | Score a placement 0 to 100 with named deductions: courtyard overlaps, parts off the board, connectors away from the edge, decoupling caps far from their IC's power pin, and ratsnest length. |+| `kicad_select_clear` | Clear the live PCB editor's selection. |+| `kicad_select_items` | Set the PCB editor's selection to items by id and/or footprints by reference: replace (default), add or remove. |+| `kicad_select_net` | Select every copper item of one net on the live board (pours, tracks, vias, pads) so the editor highlights the net as a whole; no cursor, no hotkey. |+| `kicad_set_layer_count` | Set the copper layer count (2, 4, 6 or 8) of a closed board: the layers table and, when the board has one, the physical stack-up (board thickness kept). |+| `kicad_silk_fields_batch` | Update native footprint reference/value field geometry by stable field UUID. |+| `kicad_silk_graphics_batch` | Create/update/remove native silkscreen lines, borders, arcs and Bezier leaders by stable ID. |+| `kicad_silk_text_batch` | Create, update and remove standalone silkscreen text by stable UUID with native Undo and DRC preflight. |+| `kicad_text_bounds` | Measure candidate silkscreen text using native KiCad text metrics without editing. |+| `kicad_update_footprints_from_library` | Compare placed footprints with their current library definition and, with apply:true, replace the ones that differ, keeping position, side, reference, value, fields, pad nets and the schematic link. |+| `kicad_update_pcb_from_schematic` | Compare the schematic's netlist with the board (missing, extra and changed footprints, changed values, pad net changes) and, on a closed board file, apply it: add missing footprints from the libraries in a row below the board, update values and links, assign pad nets, mark extras. |++**Routing, copper and rules** (22)++| Verb | What it does |+|---|---|+| `kicad_add_track` | Write one copper segment to a CLOSED board file. |+| `kicad_add_via` | Drop one via so a route can change layer. |+| `kicad_add_zone` | Add copper zones (pours) live as one Undo step: DRC preflight with the zone filled, then created and filled through the IPC API. |+| `kicad_apply_zone_plan` | Reconcile a complete explicit-owned zone plan as one native Undo step after full-candidate DRC. |+| `kicad_autoroute` | Route a board with the engine the USER picked: "ai" (recommended) returns the plan request you drive with kicad_route_net; "freerouting" runs Freerouting headless (installed on demand) and lands its copper live as one undo step per net, or into the closed file. |+| `kicad_board_rules` | Read or write the project's board constraints (Board Setup > Constraints: min clearance, track width, via, annular ring, hole sizes and spacing, copper to edge, silk, text) and the predefined track widths, via sizes and diff pairs in the .kicad_pro. |+| `kicad_copy_routing` | Copy the tracks and vias inside a source box to a target offset, optionally remapping nets by pairing footprints (refMap or a reference pattern), live with DRC preflight as ONE Undo step. |+| `kicad_custom_rules` | List, add, replace, remove and validate the named rules in the project's .kicad_dru (per-layer width or clearance, netclass pairs, any KiCad constraint). |+| `kicad_freerouting` | The Freerouting engine's lifecycle: status (disk facts only), install (88 MB download with its own Java, no elevation, only when asked), uninstall (removes the bridge's Freerouting folder and nothing else). |+| `kicad_list_design_rules` | List the fabs / processes / layer counts on adom/pcb-design-rules. |+| `kicad_modify_track` | Change the width, layer or endpoints of existing tracks by id, live in the PCB editor as ONE Undo step after a KiCad DRC preflight. |+| `kicad_netclasses` | Read every netclass in the project (.kicad_pro) with its clearance, widths, via and diff pair sizes, its patterns and the board nets they match; create or update a class, assign nets by pattern, remove a class. |+| `kicad_plan_fanout` | Plan the fanout of a BGA or QFN/QFP: detect the pad pitch, pick dogbone or via-in-pad per ball, the escape layer, surface stubs for the outer ring, a via grid for an exposed pad, returned as kicad_route_net-ready calls. |+| `kicad_refill_zones` | Refill every copper zone (or the listed ones when KiCad accepts a list) in the open PCB editor, wait for the fill to settle, and report the filled copper per zone and layer. |+| `kicad_remove_route` | Remove specific unlocked copper item IDs live as one Undo step. |+| `kicad_remove_zone` | Remove zones (pours or keepouts) by id live as one Undo step. |+| `kicad_route` | Lay a whole path for one net in a single write, vias included. |+| `kicad_route_diff_pair` | Route a differential pair as two coupled parallel paths offset from one centerline (gap, width, layer, vias placed as pairs), each half through kicad_route_net's planner, DRC preflight, ONE Undo step, with the length skew reported. |+| `kicad_route_net` | Commit a waypoint path or branching paths for ONE net live with native Undo and DRC preflight. |+| `kicad_routing_validate` | Run KiCad DRC against the current unsaved live board snapshot. |+| `kicad_set_design_rules` | Stage a fab's .kicad_dru next to a board and run DRC with it. |+| `kicad_zone_state` | Read every zone on the live board with KiCad's filled copper area per layer and the coverage. |++**Checks, exports and manufacturing** (22)++| Verb | What it does |+|---|---|+| `kicad_errors` | Every KiCad error/warning DIALOG on screen, with its exact text (read-only, background). |+| `kicad_estimate_cost` | The board's numbers in the shape a quote tool needs (size, layers, thickness, min track/space/drill, holes, SMD/THT parts and joints, unique parts) plus a rough JLCPCB-style price table with every assumption stated. |+| `kicad_export_bom_csv` | Export a BOM CSV from a schematic via kicad-cli. |+| `kicad_export_dxf` | Export board layers to DXF via kicad-cli, one .dxf per layer (mechanical outline, panel drawings, laser stencils). |+| `kicad_export_fab_package` | One-call fab package: Gerbers and drill, a BOM grouped from the board (value + footprint + MPN), the CPL, a README listing every file, all zipped. fab:"jlcpcb" writes JLCPCB's BOM and CPL columns (LCSC part field included). |+| `kicad_export_gencad` | Export the board as GenCAD 1.4 via kicad-cli (test-fixture and some assembly houses' CAM input). |+| `kicad_export_gerber` | Export Gerbers AND Excellon drill files (plated and non-plated separate, with drill maps) from a board via kicad-cli: a complete fab set in one call. |+| `kicad_export_ipc2581` | Export the board as IPC-2581 (one XML with copper, drill, stackup, netlist and BOM) via kicad-cli. |+| `kicad_export_ipc356` | Export the IPC-D-356 netlist (bare-board electrical test) via kicad-cli. |+| `kicad_export_molecule` | One-shot molecule export pack: STEP + silk renders + sch/pro discovery for the Adom molecule pipeline. |+| `kicad_export_odb` | Export the board as ODB++ via kicad-cli (zip by default). |+| `kicad_export_pdf` | Plot a board or schematic to PDF via kicad-cli. |+| `kicad_export_pos` | Export the pick-and-place (component position / CPL) file from a board via kicad-cli, optionally reformatted for a fab's assembly service. |+| `kicad_export_step` | Export a STEP model of the board via kicad-cli. |+| `kicad_export_svg` | Plot a board or schematic to SVG via kicad-cli. |+| `kicad_format_upgrade` | Upgrade a file to the current KiCad file format. |+| `kicad_lint_board` | Lint a .kicad_pcb (DRC pre-check) via kicad-cli. |+| `kicad_lint_schematic` | Lint a .kicad_sch (ERC pre-check) via kicad-cli. |+| `kicad_render_board` | Render a 3D image (PNG/JPG) of a board from a side via kicad-cli pcb render, headless, no KiCad window. |+| `kicad_run_drc` | Run kicad-cli pcb drc on a board and return the parsed JSON report (violations, unconnected, counts by severity). |+| `kicad_run_erc` | Run kicad-cli sch erc on a schematic and return the parsed JSON report. |+| `kicad_validate_for_fab` | Manufacturability pre-flight: closed outline, layer count, min track, space, drill and ring vs a fab profile, copper near the edge, silkscreen on pads, vias in pads, acid traps, footprints without 3D models, fiducials (assembly), plus KiCad DRC errors and unconnected items. Pass or fail with findings. |++**Projects** (4)++| Verb | What it does |+|---|---|+| `kicad_project_create` | Create a new KiCad 10 project folder: <name>.kicad_pro, a blank .kicad_sch and a blank .kicad_pcb (2 to 32 copper layers), optional title and text variables; a fab rules name is recorded to stage later. |+| `kicad_project_info` | A project at a glance: name, files with sizes and dates, the schematic sheet tree, the board (layers, footprints, nets, size), text variables, net classes, last modified, and KiCad lock files. |+| `kicad_project_rename` | Rename a project: the .kicad_pro, root .kicad_sch, .kicad_pcb, .kicad_prl (and .kicad_dru, backups folder), with the references inside updated (project file entries, every schematic's instance headers, the board's sheet references and title). Refused while KiCad has the project open. |+| `kicad_project_snapshot` | Timestamped copy of the project's files into <project>/snapshots/<stamp>[-label]/, plus schematic and board PDFs made with kicad-cli. |++**Windows, editors, install and the bridge** (56)++| Verb | What it does |+|---|---|+| `kicad_3d_view` | Drive the 3D Viewer without the mouse: zoom, rotate, view orientation, flip, or a scripted tour; menu commands posted to the viewer in the background. |+| `kicad_3d_viewer_settings` | Read or set the 3D viewer's model-visibility toggles (SMD, through-hole, virtual, not-in-POS, DNP) that can hide every chip body. |+| `kicad_analyze_connections` | Connectivity overview: suspected-unconnected pins, power nets, highest-fanout nets/components (read-only). |+| `kicad_bridge_call` | Retired: there is no in-KiCad plugin to call. Use the verb that does the job directly. |+| `kicad_bridge_status` | Retired: the in-KiCad Python plugin (reverse bridge) is not part of the native bridge. Reports that, plus what replaced each of its jobs. |+| `kicad_check_for_updates` | Read-only: installed KiCad vs latest stable. |+| `kicad_click` | Click at coordinates inside a KiCad window. |+| `kicad_close` | Close KiCad (editors first, then the project manager). |+| `kicad_close_3d_viewer` | Close the 3D viewer window. |+| `kicad_close_footprint_editor` | Close the Footprint Editor window. |+| `kicad_close_symbol_editor` | Close the Symbol Editor window. |+| `kicad_demo` | Six-beat KiCad showcase tour: staged, or a background job with structured progress (percent/ETA), an ab demo-panel remote, captions, narration, per-beat verification and a kicad_state final payload. |+| `kicad_describe` | List every verb with input, example, hint, related verbs and pitfalls. |+| `kicad_diagnostics` | Environment the bridge runs in: user, paths, env vars that matter, KiCad config dir. |+| `kicad_dismiss_dialogs` | Programmatically expire KiCad dialogs (auto-clears the benign OpenGL/software-render notice; {all:true} clears every dialog). |+| `kicad_enable_software_opengl` | LAST-RESORT fallback: deploy Mesa llvmpipe software OpenGL into KiCad's bin so it renders on a GPU-less host (Hyper-V/RDP/headless). SLOW (CPU). Never suggest proactively. |+| `kicad_extract_netlist` | Extract the full netlist from a schematic: every component and every net with its nodes (read-only, via kicad-cli). |+| `kicad_find_connections` | Everything one component connects to, pin by pin, with peer pins per net (read-only). |+| `kicad_fix_keyboard` | Reset stuck modifier keys on a KiCad window. |+| `kicad_get_settings` | Read the bridge's user-visible settings (overlay badges, ...). |+| `kicad_ipc_api` | Read or flip KiCad's IPC API server switch (Preferences > Plugins) that live routing needs. |+| `kicad_launch` | Cold-start KiCad (project manager) when nothing is running. |+| `kicad_list_versions` | Every KiCad install found (registry and standard folders) with bin dir, kicad-cli path and version. |+| `kicad_log_tail` | New KiCad log output since a byte offset: what KiCad said about ITSELF (read-only). |+| `kicad_menu_items` | List a KiCad window's menu bar items (path, id, accelerator), optionally filtered by a substring: what kicad_send_key {menu} and kicad_3d_view can fire. |+| `kicad_open_3d_viewer` | Open the 3D board viewer for a .kicad_pcb. |+| `kicad_open_board` | Open a .kicad_pcb in the PCB editor (pcbnew). |+| `kicad_open_editors` | List currently open KiCad editor windows. |+| `kicad_open_footprint_editor` | Open the Footprint Editor (optionally at a footprint). |+| `kicad_open_schematic` | Open a .kicad_sch in the schematic editor (eeschema). |+| `kicad_open_symbol_editor` | Open the Symbol Editor (optionally at a symbol). |+| `kicad_place_footprint` | Deterministically place a footprint into a board preview. |+| `kicad_plugin_diagnose` | Retired: reports that no plugin is expected and whether a leftover Python plugin payload is still on disk. |+| `kicad_prewarm` | Get this machine ready for a fast first KiCad call: detect KiCad, seed its first-run config, mark its setup wizard done, repair its taskbar shortcuts. Idempotent. |+| `kicad_progress` | What this bridge is doing RIGHT NOW: one progress block per in-flight phase for THIS caller (stepLabel, percent, etaSec, confidence, blockedBy, stepShot), plus the machine's timing history. |+| `kicad_readiness` | Is KiCad installed and usable here; if not, the three alternatives (service-kicad, kicad-cli in your container, another box). |+| `kicad_refresh_board_views` | Refresh the exact PCB editor and its already-open linked 3D viewer without focus or mouse. |+| `kicad_screenshot_all` | Screenshot every open KiCad window. |+| `kicad_send_key` | Send a key to KiCad, or fire a menu item by label; chords and menu labels resolve to menu commands (background) |+| `kicad_set_settings` | Change bridge settings; applies immediately (e.g. overlay badges stamped/cleared on live windows). |+| `kicad_show_2d_board` | Show the 2D board layout in the PCB Editor. |+| `kicad_show_3d_board` | Show the whole board in 3D (the board's 3D viewer). |+| `kicad_show_3d_chip` | Show a single part in 3D (the footprint's 3D view). |+| `kicad_show_footprint` | Show a footprint in the Footprint Editor. |+| `kicad_show_library` | Browse a symbol or footprint library (no part loaded). |+| `kicad_show_project` | Show the whole electronics PROJECT: the KiCad Project Manager (top of the design tree). |+| `kicad_show_schematic` | Show a schematic (2D) in the Schematic Editor. |+| `kicad_show_symbol` | Show a schematic symbol in the Symbol Editor (design/library surface). |+| `kicad_state` | KiCad's full state of the union: every owned window as inline base64 PNG + logical state. |+| `kicad_status` | Bridge health, KiCad version, install location and uptime. |+| `kicad_taskbar_repair` | Fix the white taskbar icon on standalone KiCad editors by writing KiCad's own AUMID onto its Start Menu shortcuts. |+| `kicad_trace_net` | Every pin on one named net (read-only). |+| `kicad_uninstall` | Remove everything this bridge created: plugin payload, Adom symbol/footprint/3D libraries and their lib-table rows, Mesa fallback DLLs, discovery files, demo and staging projects. Never KiCad itself, never user projects. |+| `kicad_upgrade` | Install/upgrade KiCad via the official installer (agent installs for the user). |+| `kicad_verb_times` | Measured per-verb run times ON THIS MACHINE: count, p50, p90, max, last, failures from the append-only run log, sorted by p90 descending. |+| `kicad_window_info` | Report geometry/state for open KiCad windows (auto-expires benign OpenGL dialogs). |++<!-- verbs:end --> ---
bridge.json+194−100@@ -19,130 +19,123 @@ "kicad_" ], "verbs": [- "kicad_open_board",- "kicad_open_schematic",- "kicad_open_symbol_editor",- "kicad_open_footprint_editor",- "kicad_open_3d_viewer",- "kicad_close_symbol_editor",- "kicad_close_footprint_editor",- "kicad_close_3d_viewer",- "kicad_close",- "kicad_window_info",- "kicad_dismiss_dialogs",- "kicad_enable_software_opengl",- "kicad_list_versions",- "kicad_readiness", "kicad_describe",+ "kicad_status",+ "kicad_readiness",+ "kicad_list_versions", "kicad_diagnostics",- "kicad_install_library",- "kicad_uninstall_library",- "kicad_install_library_bundle",- "kicad_install_symbol",- "kicad_install_footprint",- "kicad_install_plugin",- "kicad_place_footprint", "kicad_run_drc", "kicad_run_erc",- "kicad_lint_board",- "kicad_lint_schematic",- "kicad_lint_library",- "kicad_format_upgrade",- "kicad_fix_keyboard",- "kicad_screenshot_all",- "kicad_send_key",- "kicad_click",- "kicad_adom_library_status",- "kicad_status",- "kicad_bridge_status",- "kicad_bridge_call",- "kicad_open_editors", "kicad_export_gerber", "kicad_export_pdf", "kicad_export_svg",+ "kicad_export_pos",+ "kicad_render_board", "kicad_export_step", "kicad_export_bom_csv",- "kicad_check_for_updates",- "kicad_upgrade",- "kicad_demo",- "kicad_launch", "kicad_uninstall",- "kicad_state",- "kicad_show_symbol",- "kicad_show_footprint",- "kicad_show_3d_chip",- "kicad_show_schematic",- "kicad_show_2d_board",- "kicad_show_3d_board",- "kicad_show_library",- "kicad_show_project",- "kicad_routing_state",- "kicad_route_net",- "kicad_remove_route",- "kicad_routing_validate",- "kicad_board_pads",- "kicad_add_track",- "kicad_add_via",- "kicad_route",- "kicad_ipc_api",- "kicad_model_check",- "kicad_silk_text_batch",- "kicad_apply_zone_plan",- "kicad_3d_view",- "kicad_3d_viewer_settings",- "kicad_add_zone",- "kicad_adom_library_heal",- "kicad_analyze_connections",- "kicad_autoroute",- "kicad_errors",- "kicad_export_molecule",- "kicad_export_part", "kicad_extract_netlist",+ "kicad_netlist",+ "kicad_trace_net", "kicad_find_connections",- "kicad_freerouting",+ "kicad_analyze_connections",+ "kicad_lint_board",+ "kicad_lint_schematic",+ "kicad_lint_library",+ "kicad_format_upgrade",+ "kicad_list_symbols",+ "kicad_list_footprints",+ "kicad_adom_library_status", "kicad_get_settings",+ "kicad_set_settings", "kicad_list_design_rules",- "kicad_list_footprints",- "kicad_list_symbols",- "kicad_log_tail",- "kicad_make_part_project",- "kicad_menu_items",- "kicad_move_footprint",- "kicad_pcm_add_repository",- "kicad_pcm_install",+ "kicad_set_design_rules",+ "kicad_3d_viewer_settings",+ "kicad_taskbar_repair",+ "kicad_prewarm",+ "kicad_model_check", "kicad_pcm_list",- "kicad_pcm_remove_repository",+ "kicad_pcm_install", "kicad_pcm_uninstall",- "kicad_placement_state",- "kicad_placement_validate",- "kicad_plugin_diagnose",- "kicad_prewarm",- "kicad_progress",- "kicad_refresh_board_views",- "kicad_remove_zone",- "kicad_rescan_libraries",- "kicad_sch_delete",+ "kicad_pcm_add_repository",+ "kicad_pcm_remove_repository",+ "kicad_check_for_updates",+ "kicad_upgrade",+ "kicad_sch_place_symbol",+ "kicad_sch_wire", "kicad_sch_label", "kicad_sch_move",- "kicad_sch_place_symbol",+ "kicad_sch_delete", "kicad_sch_set_property",- "kicad_sch_wire",- "kicad_select_clear",- "kicad_select_net",- "kicad_set_design_rules",- "kicad_set_settings",+ "kicad_sch_no_connect",+ "kicad_sch_components",+ "kicad_sch_pins",+ "kicad_sch_items",+ "kicad_sch_rotate",+ "kicad_sch_move_region",+ "kicad_sch_annotate",+ "kicad_sch_replace_symbol",+ "kicad_sch_update_from_library",+ "kicad_sch_reset_fields",+ "kicad_sch_delete_wire",+ "kicad_sch_split_wire",+ "kicad_sch_edit_label",+ "kicad_sch_junction",+ "kicad_sch_connect_to_net",+ "kicad_sch_connect_pins",+ "kicad_sch_text",+ "kicad_sch_bus",+ "kicad_sch_bus_entry",+ "kicad_sch_connect_to_bus",+ "kicad_sch_batch",+ "kicad_sch_create",+ "kicad_sch_page",+ "kicad_sch_sheet",+ "kicad_sch_hierarchy",+ "kicad_sch_sheet_pins",+ "kicad_sch_trace_point",+ "kicad_sch_check",+ "kicad_sch_fix_connectivity",+ "kicad_render_schematic",+ "kicad_design_review",+ "kicad_ipc_api",+ "kicad_export_molecule",+ "kicad_errors",+ "kicad_log_tail",+ "kicad_bridge_status",+ "kicad_bridge_call",+ "kicad_install_plugin",+ "kicad_plugin_diagnose",+ "kicad_install_library",+ "kicad_uninstall_library",+ "kicad_install_symbol",+ "kicad_install_footprint",+ "kicad_install_library_bundle",+ "kicad_adom_library_heal",+ "kicad_make_part_project",+ "kicad_export_part", "kicad_silk_fields_batch", "kicad_silk_graphics_batch",- "kicad_taskbar_repair", "kicad_text_bounds",- "kicad_trace_net",- "kicad_verb_times",+ "kicad_silk_text_batch",+ "kicad_routing_state",+ "kicad_route_net",+ "kicad_remove_route",+ "kicad_add_zone",+ "kicad_apply_zone_plan", "kicad_zone_state",- "kicad_export_pos",- "kicad_render_board",+ "kicad_select_net",+ "kicad_select_clear",+ "kicad_remove_zone",+ "kicad_routing_validate",+ "kicad_board_pads",+ "kicad_add_track",+ "kicad_add_via",+ "kicad_route",+ "kicad_autoroute",+ "kicad_freerouting", "kicad_layer_stack", "kicad_pour_state",- "kicad_netlist", "kicad_bodies", "kicad_board_summary", "kicad_list_nets",@@ -151,7 +144,108 @@ "kicad_layers_in_use", "kicad_component_data", "kicad_component_pins",- "kicad_sch_no_connect"+ "kicad_board_outline",+ "kicad_set_layer_count",+ "kicad_delete_graphics",+ "kicad_add_mounting_hole",+ "kicad_import_svg",+ "kicad_refill_zones",+ "kicad_save_board",+ "kicad_add_footprint",+ "kicad_flip_footprint",+ "kicad_delete_footprint",+ "kicad_edit_footprint",+ "kicad_align_footprints",+ "kicad_update_footprints_from_library",+ "kicad_footprint_graphics",+ "kicad_footprint_models",+ "kicad_get_selection",+ "kicad_select_items",+ "kicad_cross_probe",+ "kicad_placement_state",+ "kicad_move_footprint",+ "kicad_placement_validate",+ "kicad_refresh_board_views",+ "kicad_launch",+ "kicad_open_board",+ "kicad_open_schematic",+ "kicad_open_symbol_editor",+ "kicad_open_footprint_editor",+ "kicad_menu_items",+ "kicad_3d_view",+ "kicad_open_3d_viewer",+ "kicad_open_editors",+ "kicad_close",+ "kicad_close_symbol_editor",+ "kicad_close_footprint_editor",+ "kicad_close_3d_viewer",+ "kicad_dismiss_dialogs",+ "kicad_window_info",+ "kicad_state",+ "kicad_screenshot_all",+ "kicad_send_key",+ "kicad_click",+ "kicad_fix_keyboard",+ "kicad_place_footprint",+ "kicad_enable_software_opengl",+ "kicad_rescan_libraries",+ "kicad_show_project",+ "kicad_show_symbol",+ "kicad_show_footprint",+ "kicad_show_3d_chip",+ "kicad_show_schematic",+ "kicad_show_2d_board",+ "kicad_show_3d_board",+ "kicad_show_library",+ "kicad_progress",+ "kicad_verb_times",+ "kicad_export_dxf",+ "kicad_export_gencad",+ "kicad_export_ipc2581",+ "kicad_export_odb",+ "kicad_export_ipc356",+ "kicad_export_fab_package",+ "kicad_validate_for_fab",+ "kicad_estimate_cost",+ "kicad_project_create",+ "kicad_project_info",+ "kicad_project_rename",+ "kicad_project_snapshot",+ "kicad_fp_create",+ "kicad_fp_edit",+ "kicad_fp_info",+ "kicad_sym_create",+ "kicad_sym_delete",+ "kicad_sym_info",+ "kicad_search_library",+ "kicad_netclasses",+ "kicad_board_rules",+ "kicad_custom_rules",+ "kicad_modify_track",+ "kicad_route_diff_pair",+ "kicad_copy_routing",+ "kicad_check_clearance",+ "kicad_score_placement",+ "kicad_place_decoupling",+ "kicad_plan_fanout",+ "kicad_update_pcb_from_schematic",+ "kicad_demo",+ "kicad_net_classes",+ "kicad_repour",+ "kicad_save",+ "kicad_update_pcb",+ "kicad_annotate",+ "kicad_create_symbol",+ "kicad_create_footprint",+ "kicad_read_symbol",+ "kicad_remove_symbol",+ "kicad_sch_sheet_symbol",+ "kicad_selection",+ "kicad_route_pair",+ "kicad_fab_check",+ "kicad_find_symbol",+ "kicad_export_manufacturing",+ "kicad_add_rule" ], "statusVerb": "kicad_status", "timeouts": {
package.json+1@@ -32,6 +32,7 @@ "skills/kicad-place-route-loop/SKILL.md", "skills/kicad-copper-pours/SKILL.md", "skills/kicad-board-model/SKILL.md",+ "skills/kicad-schematic-and-board-edits/SKILL.md", "skills/kicad-tour/SKILL.md", "skills/kicad-tour/tour_runner.py", "skills/kicad-web-control/SKILL.md",
rust/Cargo.lock+3−3@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.61"+version = "1.0.62" dependencies = [ "kicad-core", "kicad-platform",@@ -321,7 +321,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.61"+version = "1.0.62" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.61"+version = "1.0.62" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.61"+version = "1.0.62" edition = "2021" license = "MIT" publish = false
rust/README.md+21−1@@ -32,8 +32,28 @@ curl -X POST http://127.0.0.1:47731/command -d '{"command":"kicad_describe"}' ab spawns it with `spawn.kind: "exe"` and appends `--port`; bind host comes from `ADOM_BIND_HOST`. +## Name verbs from the EDA Bridge Standard++The KiCad, Altium and Fusion bridges share one set of verb names and one board model, the+[EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard), so one AI and the shared checks work on all+three. Before you name a new verb, look its job up in `rust/crates/kicad-bridge/src/standard_names.txt` (the+standard's verbs; `verbs/standard.json` on the standard's page has each one's summary, inputs and reply):++- **The standard has the job**: name the verb after it (`kicad_net_classes`, not `kicad_netclasses`), or keep your+ name and add `(standard name, your verb)` to `catalog::ALIASES`, so both names are served.+- **It covers part of the job**: keep your name, list the verb in `kicad_only.txt`, and record it as partial in the+ standard's `tools/equivalents.json`.+- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`.++`catalog.rs`'s test `every_verb_has_a_standard_decision` fails until every verb is one of the three, and+`bridge_json_declares_exactly_the_catalog` fails until `bridge.json` lists it. Each new verb also needs an entry in+the verb battery (`skills/kicad-bridge-test/run_verb_tests.py`): the battery fails a catalog verb it does not test.+`tools/build_native_release.py --publish` then syncs `bridge.json` and refreshes the standard's KiCad coverage by itself.+ ## Adding a verb +0. Pick its name from the EDA Bridge Standard (above) and record the decision. 1. Add a `Verb` entry to `catalog.rs` (summary, mechanism, risk, timeout, input, example, hint, related, pitfalls). ab's Verbs tab renders it verbatim. 2. Add the match arm in `verbs.rs::dispatch`.-3. Keep the Python contract: same arg names, same `success`/`output`/`data` shape, so the verb runner in `skills/kicad-bridge-test` passes unchanged.+3. Add it to `bridge.json` and give it an entry in the verb battery (`skills/kicad-bridge-test/run_verb_tests.py`).+4. Keep the Python contract: same arg names, same `success`/`output`/`data` shape, so the verb runner in `skills/kicad-bridge-test` passes unchanged.
rust/crates/kicad-bridge/src/catalog.rs+107−3@@ -182,7 +182,36 @@ pub static VERBS: &[Verb] = &[ ]; /// Every verb: the core table plus each ported group's table.-pub fn all() -> Vec<&'static Verb> {+/// The EDA Bridge Standard's names (adom/eda-bridge-standard, verbs/standard.json) for jobs this bridge+/// already does under its own name: (standard name, the verb that does it). Each is a real verb in the+/// catalog and in describe (`aliasOf`), so one AI can call the same name on Altium, KiCad and Fusion and+/// the standard's coverage counts it. Only verbs that do the standard verb's job belong here; a verb that+/// covers part of it is recorded as partial in the standard's tools/equivalents.json instead.+pub static ALIASES: &[(&str, &str)] = &[+ ("kicad_net_classes", "kicad_netclasses"),+ ("kicad_repour", "kicad_refill_zones"),+ ("kicad_save", "kicad_save_board"),+ ("kicad_update_pcb", "kicad_update_pcb_from_schematic"),+ ("kicad_annotate", "kicad_sch_annotate"),+ ("kicad_create_symbol", "kicad_sym_create"),+ ("kicad_create_footprint", "kicad_fp_create"),+ ("kicad_read_symbol", "kicad_sym_info"),+ ("kicad_remove_symbol", "kicad_sym_delete"),+ ("kicad_sch_sheet_symbol", "kicad_sch_sheet"),+ ("kicad_selection", "kicad_get_selection"),+ ("kicad_route_pair", "kicad_route_diff_pair"),+ ("kicad_fab_check", "kicad_validate_for_fab"),+ ("kicad_find_symbol", "kicad_search_library"),+ ("kicad_export_manufacturing", "kicad_export_fab_package"),+ ("kicad_add_rule", "kicad_custom_rules"),+];++/// The verb a standard name stands for, when it is an alias.+pub fn alias_target(name: &str) -> Option<&'static str> {+ ALIASES.iter().find(|(a, _)| *a == name).map(|(_, t)| *t)+}++fn own() -> Vec<&'static Verb> { let mut v: Vec<&'static Verb> = VERBS.iter().collect(); for g in crate::groups::GROUPS { v.extend((g.verbs)().iter());@@ -190,6 +219,37 @@ pub fn all() -> Vec<&'static Verb> { v } +/// The alias entries, built once from their targets (the text is the target's, said to be the+/// standard's name for it).+fn alias_verbs() -> &'static [Verb] {+ static CELL: std::sync::OnceLock<Vec<Verb>> = std::sync::OnceLock::new();+ CELL.get_or_init(|| {+ let own = own();+ ALIASES.iter().filter_map(|(alias, target)| {+ let t = own.iter().find(|v| v.name == *target)?;+ let leak = |s: String| -> &'static str { Box::leak(s.into_boxed_str()) };+ Some(Verb {+ name: alias,+ summary: leak(format!("The EDA Bridge Standard's name for {target}: {}", t.summary)),+ mechanism: t.mechanism,+ risk: t.risk,+ timeout_sec: t.timeout_sec,+ input: t.input,+ example: leak(t.example.replacen(target, alias, 1)),+ hint: leak(format!("Same verb as {target} (same arguments, same reply), under the standard's name so one AI calls the same name on Altium, KiCad and Fusion. {}", t.hint)),+ related: Box::leak(vec![*target].into_boxed_slice()),+ pitfalls: t.pitfalls,+ })+ }).collect()+ })+}++pub fn all() -> Vec<&'static Verb> {+ let mut v = own();+ v.extend(alias_verbs().iter());+ v+}+ pub fn find(name: &str) -> Option<&'static Verb> { all().into_iter().find(|v| v.name == name) }@@ -198,7 +258,7 @@ pub fn describe(version: &str) -> Value { let verbs: Vec<Value> = all() .into_iter() .map(|v| {- json!({+ let mut e = json!({ "name": v.name, "summary": v.summary, "mechanism": v.mechanism.as_str(),@@ -210,7 +270,11 @@ pub fn describe(version: &str) -> Value { "hint": v.hint, "related": v.related, "pitfalls": v.pitfalls,- })+ });+ if let Some(t) = alias_target(v.name) {+ e["aliasOf"] = json!(t);+ }+ e }) .collect(); json!({@@ -226,6 +290,46 @@ pub fn describe(version: &str) -> Value { }) } +#[cfg(test)]+mod standard_tests {+ /// Every verb is a decision against the EDA Bridge Standard: its own standard name, an alias in+ /// ALIASES, or a reviewed KiCad-only job in kicad_only.txt. A new verb that is none of them fails+ /// here, so the name is chosen when the verb is written, not discovered after the release.+ #[test]+ fn every_verb_has_a_standard_decision() {+ let lines = |t: &'static str| -> Vec<&'static str> { t.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')).collect() };+ let standard = lines(include_str!("standard_names.txt"));+ let only = lines(include_str!("kicad_only.txt"));+ let targets: Vec<&str> = super::ALIASES.iter().map(|(_, t)| *t).collect();+ let mut undecided = Vec::new();+ for v in super::own() {+ let bare = v.name.trim_start_matches("kicad_");+ if !standard.contains(&bare) && !targets.contains(&v.name) && !only.contains(&v.name) {+ undecided.push(v.name);+ }+ }+ assert!(undecided.is_empty(), "no standard decision for {undecided:?}: check rust/crates/kicad-bridge/src/standard_names.txt (the EDA Bridge Standard's verbs). If the standard has the job, name the verb after it or add (standard name, verb) to catalog::ALIASES; if it does not, add the verb to kicad_only.txt.");+ let served: Vec<&str> = super::own().iter().map(|v| v.name).collect();+ let stale: Vec<&&str> = only.iter().filter(|n| !served.contains(n)).collect();+ assert!(stale.is_empty(), "kicad_only.txt lists verbs the catalog no longer serves: {stale:?}");+ let mislabeled: Vec<&&str> = only.iter().filter(|n| standard.contains(&n.trim_start_matches("kicad_"))).collect();+ assert!(mislabeled.is_empty(), "kicad_only.txt lists standard names: {mislabeled:?}");+ }+}++#[cfg(test)]+mod alias_tests {+ #[test]+ fn every_alias_names_a_real_verb_and_shadows_none() {+ let own: Vec<&str> = super::own().iter().map(|v| v.name).collect();+ for (alias, target) in super::ALIASES {+ assert!(own.contains(target), "{alias} points at {target}, which the catalog does not serve");+ assert!(!own.contains(alias), "{alias} is already a verb of its own");+ }+ assert_eq!(super::all().len(), own.len() + super::ALIASES.len());+ }+}+ #[cfg(test)] mod manifest_tests { /// bridge.json is what Adom Bridge reads to know this bridge's verbs. It had drifted to 69 of
rust/crates/kicad-bridge/src/kicad_only.txtadded+156@@ -0,0 +1,156 @@+# kicad_* verbs that are neither a standard name nor an alias (catalog::ALIASES): jobs the EDA Bridge Standard+# has no verb for, reviewed 2026-10-27 (1.0.61 baseline). A new verb goes here only after checking+# standard_names.txt: if the standard has the job, name the verb after it or add an alias instead.+# When one of these gains a standard name, move it to ALIASES. Verbs that cover part of a standard job are+# recorded as partial in the standard's tools/equivalents.json.+kicad_3d_view+kicad_3d_viewer_settings+kicad_add_footprint+kicad_add_mounting_hole+kicad_add_track+kicad_add_via+kicad_add_zone+kicad_adom_library_heal+kicad_adom_library_status+kicad_align_footprints+kicad_analyze_connections+kicad_apply_zone_plan+kicad_board_pads+kicad_board_rules+kicad_bodies+kicad_bridge_call+kicad_bridge_status+kicad_check_clearance+kicad_check_for_updates+kicad_click+kicad_close_3d_viewer+kicad_close_footprint_editor+kicad_close_symbol_editor+kicad_copy_routing+kicad_cross_probe+kicad_delete_footprint+kicad_delete_graphics+kicad_design_review+kicad_edit_footprint+kicad_enable_software_opengl+kicad_errors+kicad_estimate_cost+kicad_export_bom_csv+kicad_export_dxf+kicad_export_gencad+kicad_export_ipc2581+kicad_export_ipc356+kicad_export_molecule+kicad_export_odb+kicad_export_part+kicad_export_pos+kicad_export_svg+kicad_extract_netlist+kicad_find_connections+kicad_fix_keyboard+kicad_flip_footprint+kicad_footprint_graphics+kicad_footprint_models+kicad_format_upgrade+kicad_fp_edit+kicad_fp_info+kicad_freerouting+kicad_get_settings+kicad_import_svg+kicad_install_footprint+kicad_install_plugin+kicad_install_symbol+kicad_ipc_api+kicad_lint_board+kicad_lint_library+kicad_lint_schematic+kicad_list_design_rules+kicad_list_footprints+kicad_list_symbols+kicad_list_versions+kicad_log_tail+kicad_make_part_project+kicad_menu_items+kicad_model_check+kicad_modify_track+kicad_move_footprint+kicad_open_3d_viewer+kicad_open_board+kicad_open_editors+kicad_open_footprint_editor+kicad_open_schematic+kicad_open_symbol_editor+kicad_pcm_add_repository+kicad_pcm_install+kicad_pcm_list+kicad_pcm_remove_repository+kicad_pcm_uninstall+kicad_place_decoupling+kicad_place_footprint+kicad_placement_state+kicad_placement_validate+kicad_plan_fanout+kicad_plugin_diagnose+kicad_prewarm+kicad_project_create+kicad_project_info+kicad_project_rename+kicad_project_snapshot+kicad_refresh_board_views+kicad_remove_route+kicad_remove_zone+kicad_render_board+kicad_render_schematic+kicad_rescan_libraries+kicad_route+kicad_routing_validate+kicad_sch_batch+kicad_sch_bus+kicad_sch_bus_entry+kicad_sch_check+kicad_sch_components+kicad_sch_connect_pins+kicad_sch_connect_to_bus+kicad_sch_connect_to_net+kicad_sch_create+kicad_sch_delete+kicad_sch_delete_wire+kicad_sch_edit_label+kicad_sch_fix_connectivity+kicad_sch_hierarchy+kicad_sch_items+kicad_sch_junction+kicad_sch_label+kicad_sch_move+kicad_sch_move_region+kicad_sch_no_connect+kicad_sch_page+kicad_sch_pins+kicad_sch_place_symbol+kicad_sch_replace_symbol+kicad_sch_reset_fields+kicad_sch_rotate+kicad_sch_set_property+kicad_sch_sheet_pins+kicad_sch_split_wire+kicad_sch_text+kicad_sch_trace_point+kicad_sch_update_from_library+kicad_sch_wire+kicad_score_placement+kicad_screenshot_all+kicad_select_clear+kicad_select_items+kicad_send_key+kicad_set_layer_count+kicad_set_settings+kicad_silk_fields_batch+kicad_silk_graphics_batch+kicad_silk_text_batch+kicad_taskbar_repair+kicad_text_bounds+kicad_trace_net+kicad_update_footprints_from_library+kicad_upgrade+kicad_verb_times+kicad_zone_state
rust/crates/kicad-bridge/src/standard_names.txtadded+202@@ -0,0 +1,202 @@+# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference altium 0.2.75). Generated by tools/sync_standard_names.py; do not edit.+a365_configure+a365_files_upload+a365_library_overview+a365_library_schema+a365_logout+a365_oauth_begin+a365_oauth_cancel+a365_oauth_status+a365_query+a365_refresh+a365_status+activate_document+add_body+add_models+add_rule+add_symbol_art+agent_recover+agent_start+agent_status+agent_stop+animate_view+annotate+apply_pours+apply_silkscreen+assembly_check+auto_classes+autoroute+board_features+board_outline+board_report+board_shape+board_summary+bom+build_board+build_footprint+build_schematic+build_symbol_modes+check_dialogs+check_orientation+cleanup_libs+clone_symbol+close+close_document+compile+compile_intlib+component_classes+component_data+component_pins+connect_workspace+copper_balance+copper_layers+copy_footprint+create_footprint+create_symbol+cross_check+current_capacity+demo+describe+diagnostics+dialog_log+diff_pairs+dismiss_dialogs+doc_params+ensure_licensed+eval+export_bom+export_gerber+export_manufacturing+export_pdf+export_step+extension_ping+extract_intlib+fab_check+find_space+find_symbol+footprint_from_land_pattern+footprint_primitives+foreground+height_check+impedance+import_kicad+import_part+install+install_extension+install_library+install_library_bundle+install_receipt+instances+jlcpcb_order+launch+layer_stack+layers+layers_in_use+length_tune+license+license_status+list_commands+list_designators+list_installed_libraries+list_library+list_nets+move_parts+net_classes+net_connections+net_lengths+netlist+normalize_bodies+open_document+open_project+outjob_info+panel+panelize+part_parameters+pcb_report+pin_pad_check+place_on_pcb+place_on_sch+place_vias+placement_check+plate_vias+pour_state+progress+read_board+read_rules+read_schematic+read_sheet_file+read_symbol+readiness+remove_footprint+remove_items+remove_pours+remove_symbol+rename_footprint+rename_symbol+replicate_layout+repour+return_path+review_board+ripup_nets+route_net+route_pair+routing_drc+routing_state+rules_by_kind+run_command+run_drc+run_erc+run_outjob+run_script+save+sch_connect+sch_edit+sch_place_part+sch_sheet_symbol+sch_swap_part+sch_zoom+schematic_data+schematic_review+select_net+selection+selftest_script_error+set_design_rules+set_layer_visibility+set_models+set_outline+set_params+set_track_width+set_visibility+setup_trial+sheet_params+show_2d_board+show_3d_board+show_3d_chip+show_footprint+show_library+show_project+show_schematic+show_symbol+signin+signin_status+silk_designators+silk_state+silkscreen_prep+state+status+swap_footprint+symbol_mode+symbol_prims+teardrops+test_board+thermal_vias+uninstall+uninstall_library+update_pcb+variants+via_fence+window_info+workspace_place+workspace_search+xcopy_footprint+zoom_to
rust/crates/kicad-bridge/src/verbs.rs+4@@ -19,6 +19,10 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal if full == "kicad_export_gerbers" { full = "kicad_export_gerber".into(); // alias kept from the Python bridge }+ // the EDA Bridge Standard's name for a verb this bridge has under its own (catalog::ALIASES)+ if let Some(t) = catalog::alias_target(&full) {+ full = t.to_string();+ } let command = full.as_str(); let Some(verb) = catalog::find(command) else { let names: Vec<&str> = catalog::all().iter().map(|v| v.name).collect();
rust/crates/kicad-bridge/src/verbs_board.rs+7−6@@ -35,7 +35,7 @@ pub static VERBS: &[Verb] = &[ example: "kicad_board_outline {\"filePath\":\"C:/p/b.kicad_pcb\",\"shape\":\"rounded\",\"width\":50,\"height\":30,\"x\":100,\"y\":80,\"radius\":3,\"replace\":true,\"expectedRevision\":\"<rev>\"}", hint: "mm, KiCad's frame (+y down). A board that already has Edge.Cuts items is refused (outline_exists, listing them) unless replace:true (they are deleted in the same Undo step) or append:true (a cutout). The reply's box and outlineAfter are the new extents.", related: &["kicad_board_summary", "kicad_delete_graphics", "kicad_placement_validate"],- pitfalls: &["Moving the outline does not move parts: check kicad_placement_validate afterwards.", "A rounded corner radius cannot exceed half the shorter side.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[3]],+ pitfalls: &["Adding items to a board OPEN in the PCB editor needs KiCad to create them over its API, which KiCad 10.0.x does not do yet: on an open board the verb refuses with live_create_unsupported and changes nothing. Close the board (kicad_save_board, kicad_close) and the verb edits the file instead.", "Moving the outline does not move parts: check kicad_placement_validate afterwards.", "A rounded corner radius cannot exceed half the shorter side.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[3]], }, Verb { name: "kicad_set_layer_count",@@ -75,7 +75,7 @@ pub static VERBS: &[Verb] = &[ example: "kicad_import_svg {\"filePath\":\"C:/p/b.kicad_pcb\",\"svgPath\":\"C:/p/logo.svg\",\"x\":120,\"y\":95,\"width\":10,\"layer\":\"F.SilkS\",\"expectedRevision\":\"<rev>\"}", hint: "Paths (every command), rect, circle, ellipse, polygon and polyline with their group transforms; the root viewBox and width/height set the natural size (px are 1/96 in). Shapes with fill none are skipped; text and images are listed in skipped (convert text to paths first). A shape inside another becomes a hole, joined to its outline by a zero-width cut like KiCad's own fills.", related: &["kicad_silk_text_batch", "kicad_delete_graphics", "kicad_render_board"],- pitfalls: &["Stroke-only artwork imports nothing: give the SVG filled shapes.", "On copper the polygons are graphics (no thermal relief); give net to tie them to a net, and run DRC.", "On a B.* layer the artwork is mirrored so it reads correctly from the back; mirror:false keeps it as drawn.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[3]],+ pitfalls: &["Adding items to a board OPEN in the PCB editor needs KiCad to create them over its API, which KiCad 10.0.x does not do yet: on an open board the verb refuses with live_create_unsupported and changes nothing. Close the board (kicad_save_board, kicad_close) and the verb edits the file instead.", "Adding items to a board OPEN in the PCB editor needs KiCad to create them over its API, which KiCad 10.0.x does not do yet: on an open board the verb refuses with live_create_unsupported and changes nothing. Close the board (kicad_save_board, kicad_close) and the verb edits the file instead.", "Stroke-only artwork imports nothing: give the SVG filled shapes.", "On copper the polygons are graphics (no thermal relief); give net to tie them to a net, and run DRC.", "On a B.* layer the artwork is mirrored so it reads correctly from the back; mirror:false keeps it as drawn.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[3]], }, Verb { name: "kicad_refill_zones",@@ -105,7 +105,7 @@ pub static VERBS: &[Verb] = &[ example: "kicad_add_footprint {\"filePath\":\"C:/p/b.kicad_pcb\",\"libraryId\":\"Capacitor_SMD:C_0402_1005Metric\",\"x\":130,\"y\":90,\"reference\":\"C\",\"value\":\"100n\",\"count\":4,\"pitch\":[1.5,0],\"expectedRevision\":\"<rev>\"}", hint: "libraryId resolves through the project's fp-lib-table, then your global one (KiCad's stock libraries included). reference \"C\" takes the next free numbers; \"C12\" starts there and refuses a taken one (duplicate_reference names the next free). A back-side part is flipped the way KiCad flips (left/right), then turned to rotation. duplicateOf copies the part with fresh UUIDs, without its nets (keepNets:true keeps them) and without its schematic link. The reply lists each added ref with its uuid and pose.", related: &["kicad_move_footprint", "kicad_placement_state", "kicad_add_mounting_hole", "kicad_update_pcb_from_schematic"],- pitfalls: &["A part added on the board is not in the schematic: Update PCB from Schematic would remove it unless the schematic gains the symbol.", "No courtyard check here: run kicad_placement_validate, or place with kicad_move_footprint's checks.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[2]],+ pitfalls: &["Adding items to a board OPEN in the PCB editor needs KiCad to create them over its API, which KiCad 10.0.x does not do yet: on an open board the verb refuses with live_create_unsupported and changes nothing. Close the board (kicad_save_board, kicad_close) and the verb edits the file instead.", "A part added on the board is not in the schematic: Update PCB from Schematic would remove it unless the schematic gains the symbol.", "No courtyard check here: run kicad_placement_validate, or place with kicad_move_footprint's checks.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[2]], }, Verb { name: "kicad_flip_footprint",@@ -155,7 +155,7 @@ pub static VERBS: &[Verb] = &[ example: "kicad_update_footprints_from_library {\"filePath\":\"C:/p/b.kicad_pcb\",\"refs\":[\"U1\",\"J2\"]}", hint: "Plan by default: footprints[] says per ref up_to_date, differs (diff: pads added, removed or changed in shape, size, position, drill or layers; graphics; 3D models) or library_not_found (why). apply:true replaces every differing part as one Undo step live (or one write): pads take their old pad's net, pin function and type by number, the board's own fields and their positions are kept, and DNP, BOM and position-file exclusions stay.", related: &["kicad_install_library", "kicad_list_footprints", "kicad_lint_board"],- pitfalls: &["A pad that is new in the library has no net; padsRemoved pads lose theirs: read the diff before apply.", "Tracks are not moved to follow pads that moved.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[2]],+ pitfalls: &["Adding items to a board OPEN in the PCB editor needs KiCad to create them over its API, which KiCad 10.0.x does not do yet: on an open board the verb refuses with live_create_unsupported and changes nothing. Close the board (kicad_save_board, kicad_close) and the verb edits the file instead.", "A pad that is new in the library has no net; padsRemoved pads lose theirs: read the diff before apply.", "Tracks are not moved to follow pads that moved.", LIVE_PITFALLS[0], LIVE_PITFALLS[1], LIVE_PITFALLS[2]], }, Verb { name: "kicad_footprint_graphics",@@ -828,8 +828,9 @@ mod tests { let rev = stale["currentRevision"].as_str().unwrap().to_string(); let four = dispatch(&mut State::new(), "kicad_set_layer_count", &json!({"filePath": norm(&path), "count": 4, "expectedRevision": rev})).unwrap(); assert_eq!(four["countAfter"], 4, "{four}");- // KiCad's lock file: refused, nothing written- std::fs::write(d.join("~b.kicad_pcb.lck"), "{}").unwrap();+ // KiCad's lock file: refused, nothing written (a lock from another machine is always honoured; one+ // left on this machine with no KiCad running is stale and cleared, see pcb::load)+ std::fs::write(d.join("~b.kicad_pcb.lck"), r#"{"hostname":"ANOTHER-PC","username":"someone"}"#).unwrap(); let before = std::fs::read_to_string(&path).unwrap(); let open = dispatch(&mut State::new(), "kicad_delete_graphics", &json!({"filePath": norm(&path), "source": "file", "layer": "Edge.Cuts"})).unwrap(); assert_eq!(open["errorCode"], "board_open", "{open}");
rust/crates/kicad-core/src/ipc/board_live.rs+75−4@@ -101,15 +101,32 @@ fn commit_built(ctx: &Ctx, args: &Value, s: &Session, text: &str, revision: &str s.client.delete_items(remove.clone())?; } if want > 0 {- let created = s.client.parse_and_create_items_from_string(wrapped.clone())?;- if created.len() != want {- return Err(IpcFailure::from(format!("KiCad created {} of the {want} requested items; rollback requested", created.len())));+ // decoded here, not by the client: KiCad 10.0.4 answers ParseAndCreateItemsFromString with the+ // request status unset (IRS_UNKNOWN) while every item reports ISC_OK, and the client turns that+ // into an error, so a good create was rolled back (1.0.61's kicad_add_footprint, kicad_import_svg)+ let any = s.client.parse_and_create_items_from_string_raw(wrapped.clone())?;+ let (status, items) = decode_create_response(&any.value).map_err(IpcFailure::from)?;+ if status == IRS_UNKNOWN && items.is_empty() {+ // KiCad 10.0.x (checked 10.0.4 and 10.0.7, wrapped board text and bare item text alike) answers this+ // call with an empty reply and creates nothing+ return Err(IpcFailure::from(format!("{LIVE_CREATE_UNSUPPORTED}: KiCad answered ParseAndCreateItemsFromString with an empty reply and created nothing")));+ }+ let all_ok = !items.is_empty() && items.iter().all(|(c, _)| *c == ISC_OK);+ if !(status == IRS_OK || status == IRS_UNKNOWN && all_ok) || items.len() != want || !all_ok {+ let why: Vec<String> = items.iter().filter(|(c, _)| *c != ISC_OK).map(|(c, m)| format!("item status {c}{}", if m.is_empty() { String::new() } else { format!(": {m}") })).collect();+ return Err(IpcFailure::from(format!("KiCad did not create the items (request status {status}, {} of {want} answered{}); rollback requested",+ items.len(), if why.is_empty() { String::new() } else { format!(", {}", why.join("; ")) }))); } } Ok(()) }) .map_err(|e| {- if e.message.contains("AS_UNIMPLEMENTED") || e.message.contains("AS_UNHANDLED") {+ if e.message.contains(LIVE_CREATE_UNSUPPORTED) {+ let mut e = e.with("_hint", json!("KiCad's API does not create board items from text yet (ParseAndCreateItemsFromString answers empty on KiCad 10.0.4 and 10.0.7). Nothing was changed. Save the board in KiCad and close it (kicad_save_board, then kicad_close), then call this verb again: with no editor holding the board, the bridge edits the closed file (a backup is kept) and KiCad shows the change when the board is reopened."))+ .with("kicadVersion", json!(s.version.full_version)).with("mutated", json!(false));+ e.code = LIVE_CREATE_UNSUPPORTED.into();+ e+ } else if e.message.contains("AS_UNIMPLEMENTED") || e.message.contains("AS_UNHANDLED") { e.with("_hint", json!("This KiCad does not serve the IPC call this edit needs (ParseAndCreateItemsFromString). Nothing was changed. Save and close the board, then call again: the bridge edits the closed file instead (a backup is kept).")) } else { e@@ -216,6 +233,43 @@ pub fn decode_flip_response(b: &[u8]) -> Result<(u64, Vec<u64>), String> { Ok((status, codes)) } +/// The error code a live edit answers when KiCad cannot create items over its API.+pub const LIVE_CREATE_UNSUPPORTED: &str = "live_create_unsupported";++/// kiapi ItemRequestStatus / ItemStatusCode values.+const IRS_UNKNOWN: u64 = 0;+const IRS_OK: u64 = 1;+const ISC_OK: u64 = 1;++/// A CreateItemsResponse on the wire: the request status (field 2) and, per item (field 3), its status+/// code and message (field 1: code 1, message 2).+pub fn decode_create_response(b: &[u8]) -> Result<(u64, Vec<(u64, String)>), String> {+ let mut status = 0;+ let mut items = Vec::new();+ for f in parse_fields(b)? {+ match (f.number, f.wire) {+ (2, 0) => status = varint_value(f.data)?,+ (3, 2) => {+ let (mut code, mut msg) = (0, String::new());+ for g in parse_fields(f.data)? {+ if g.number == 1 && g.wire == 2 {+ for h in parse_fields(g.data)? {+ match (h.number, h.wire) {+ (1, 0) => code = varint_value(h.data)?,+ (2, 2) => msg = String::from_utf8_lossy(h.data).to_string(),+ _ => {}+ }+ }+ }+ }+ items.push((code, msg));+ }+ _ => {}+ }+ }+ Ok((status, items))+}+ fn board_document(s: &Session) -> Result<Vec<u8>, KiCadError> { let docs = s.client.get_open_documents(DocumentType::Pcb)?; let wanted = normalize_existing(&s.file_path);@@ -595,6 +649,23 @@ pub fn save_board(ctx: &Ctx, args: &Value) -> RResult<Value> { })) } +#[cfg(test)]+mod create_response_tests {+ #[test]+ fn an_unset_request_status_with_ok_items_decodes_as_such() {+ // field 3 { field 1 { code 1 } field 2 { any } } twice, no field 2: KiCad 10.0.4's reply+ let item = |code: u8| vec![0x1a, 0x06, 0x0a, 0x02, 0x08, code, 0x12, 0x00];+ let mut b = item(1);+ b.extend(item(1));+ let (status, items) = super::decode_create_response(&b).unwrap();+ assert_eq!(status, 0);+ assert_eq!(items, vec![(1, String::new()), (1, String::new())]);+ let refused = [0x10, 0x02, 0x1a, 0x09, 0x0a, 0x07, 0x08, 0x03, 0x12, 0x03, b'b', b'a', b'd'];+ let (status, items) = super::decode_create_response(&refused).unwrap();+ assert_eq!((status, items), (2, vec![(3, "bad".to_string())]));+ }+}+ #[cfg(test)] mod tests { use super::*;
rust/crates/kicad-core/src/pcb.rs+29−2@@ -1288,6 +1288,24 @@ pub fn lock_file(path: &Path) -> Option<PathBuf> { lck.exists().then_some(lck) } +/// A lock file no KiCad holds any more (KiCad killed or crashed) blocks every file edit and makes+/// KiCad itself ask about it on the next open. Remove it only when it is certainly stale: the OS layer+/// lists no KiCad process at all, and the lock was written on this machine (a board on a shared drive+/// may be open elsewhere). Anything uncertain keeps the lock and the board_open refusal.+fn clear_stale_lock(path: &Path) {+ let Some(lck) = lock_file(path) else { return };+ if crate::install_kicad::kicad_running().0 != Some(false) {+ return;+ }+ let here = std::env::var("COMPUTERNAME").or_else(|_| std::env::var("HOSTNAME")).unwrap_or_default();+ let text = std::fs::read_to_string(&lck).unwrap_or_default();+ let host = serde_json::from_str::<Value>(&text).ok().and_then(|v| v["hostname"].as_str().map(str::to_string));+ // a lock that names no machine is KiCad's own on this one; one that names a machine must name this one+ if host.map_or(true, |h| !here.is_empty() && h.eq_ignore_ascii_case(&here)) {+ let _ = std::fs::remove_file(&lck);+ }+}+ /// A loaded board: path, the exact text on disk and the parse of it. #[derive(Debug)] pub struct Loaded {@@ -1307,6 +1325,9 @@ pub fn load(args: &Value, read_only: bool) -> Result<Loaded, Value> { })); }; let path = PathBuf::from(raw);+ if !read_only {+ clear_stale_lock(&path);+ } if !read_only && lock_file(&path).is_some() { return Err(json!({ "success": false, "errorCode": "board_open",@@ -1581,12 +1602,18 @@ pub(crate) mod tests { // A changed file is refused (the text on disk no longer matches `expected`). let err = write_board(&path, &new_text, false, Some(&text)).unwrap_err(); assert_eq!(err["errorCode"], "board_write_refused");- // A lock file marks an open board.- std::fs::write(dir.join("~rp2040-breakout.kicad_pcb.lck"), "open").unwrap();+ // A lock file marks an open board: one written on another machine (a shared drive) is always honoured.+ std::fs::write(dir.join("~rp2040-breakout.kicad_pcb.lck"), r#"{"hostname":"ANOTHER-PC","username":"someone"}"#).unwrap(); assert!(lock_file(&path).is_some()); let err = load(&json!({"filePath": path.to_string_lossy()}), false).unwrap_err(); assert_eq!(err["errorCode"], "board_open"); assert!(load(&json!({"filePath": path.to_string_lossy()}), true).is_ok());+ // One left behind on this machine by a KiCad that is no longer running is stale: cleared, the edit goes ahead.+ if crate::install_kicad::kicad_running().0 == Some(false) {+ std::fs::write(dir.join("~rp2040-breakout.kicad_pcb.lck"), "{}").unwrap();+ assert!(load(&json!({"filePath": path.to_string_lossy()}), false).is_ok());+ assert!(lock_file(&path).is_none());+ } let _ = std::fs::remove_dir_all(&dir); }
skills/kicad-bridge-contributing/SKILL.md+7@@ -41,6 +41,13 @@ Layout you will touch: | `docs/` | Design notes and acceptance checklists (`docs/native-*.md`). | | `RELEASE_NOTES.md`, `rust/Cargo.toml` | Version and notes; the owner bumps these, you do not need to. | +## Verb names come from the EDA Bridge Standard++A new verb takes its name from the [EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard) when the+standard has the job (or adds that name as an alias in `catalog::ALIASES`), else it is listed in+`rust/crates/kicad-bridge/src/kicad_only.txt`; the catalog test `every_verb_has_a_standard_decision` enforces it, and+every verb needs an entry in the verb battery. The details: `rust/README.md`, "Name verbs from the EDA Bridge Standard".+ ## Build and test ```
skills/kicad-bridge-dev/SKILL.md+20@@ -133,8 +133,28 @@ subprocess (like kicad-cli) — NOT Bridge's gated `run_script`. - Recording/demo doctrine agrees: **NEVER foreground a window to fix a blank frame.** Background capture (WGC) + background windows is the standard. +## Name verbs from the EDA Bridge Standard++The KiCad, Altium and Fusion bridges share one set of verb names and one board model, the+[EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard), so one AI and the shared checks work on all+three. Before you name a new verb, look its job up in `rust/crates/kicad-bridge/src/standard_names.txt` (the+standard's verbs; `verbs/standard.json` on the standard's page has each one's summary, inputs and reply):++- **The standard has the job**: name the verb after it (`kicad_net_classes`, not `kicad_netclasses`), or keep your+ name and add `(standard name, your verb)` to `catalog::ALIASES`, so both names are served.+- **It covers part of the job**: keep your name, list the verb in `kicad_only.txt`, and record it as partial in the+ standard's `tools/equivalents.json`.+- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`.++`catalog.rs`'s test `every_verb_has_a_standard_decision` fails until every verb is one of the three, and+`bridge_json_declares_exactly_the_catalog` fails until `bridge.json` lists it. Each new verb also needs an entry in+the verb battery (`skills/kicad-bridge-test/run_verb_tests.py`): the battery fails a catalog verb it does not test.+`tools/build_native_release.py --publish` then syncs `bridge.json` and refreshes the standard's KiCad coverage by itself.+ ## Adding a verb (3 places, keep in sync) +> The native Rust bridge (1.0.x) adds a verb in `rust/crates/kicad-bridge/src` (catalog entry, dispatch arm, `bridge.json`, a battery entry; see `rust/README.md`). The Python steps below are the retired 0.9 server, kept for reference.+ 1. Write `handle_<verb>` in a handler; return a dict with `success` + a rich **`_hint`** (mandatory — the AI reads verb OUTPUT, not this skill). 2. Register in `server.py` `COMMAND_HANDLERS`. 3. Add `kicad_<verb>` to `bridge.json` `verbs[]` AND an entry (summary/hint/related/pitfalls) to `_VERB_CATALOG` in `server.py` (the `kicad_describe` catalog). A test checks these three stay aligned.
skills/kicad-bridge-publish/SKILL.md+10@@ -268,3 +268,13 @@ The account details never go in this repo, a skill, a README, an issue or a repl The public manifest is `adom-bridge-kicad-manifest.json` on the page (bridge.json `updateManifestUrl`). An insiders release never writes or pushes it: only `tools/promote.py` does, with a person's words.++## Every release keeps the EDA Bridge Standard current (from 1.0.62)++Before `--publish`: the verb battery passes on the test box (`run_verb_tests.py`, which fails any catalog verb it does+not test) and `cargo test --workspace` passes (`every_verb_has_a_standard_decision`, the `bridge.json` check).+`build_native_release.py --publish` then syncs `bridge.json`'s verb list to the catalog and refreshes the standard's+KiCad coverage itself: `reference/kicad-<ver>.json` from the release's verbs, `tools/generate.py`, and a push of the+files that moved to adom/eda-bridge-standard. A verb that does a standard job under another name gets an alias in+`catalog::ALIASES` first, or the coverage table does not count it.+
skills/kicad-bridge-test/run_verb_tests.py+297−1@@ -28,6 +28,8 @@ exit 0 only if every non-skipped verb passed AND coverage is complete. """ import argparse import json+import re+import math import os import shlex import subprocess@@ -184,6 +186,85 @@ def matrix(w: dict) -> dict: "layers_in_use": dict(phase=2, args={"filePath": B, "source": "file"}, check="layers_in_use"), "component_data": dict(phase=2, args={"filePath": B, "source": "file", "designator": "U1"}, check="component_data"), "component_pins": dict(phase=2, args={"filePath": B, "source": "file", "designator": "U1"}, check="component_pins"),+ # ---- phase 8: the 1.0.61 verbs (Konnect parity), run in order by k161_phase() on scratch files+ "add_footprint": dict(phase=8, scenario=True),+ "add_mounting_hole": dict(phase=8, scenario=True),+ "align_footprints": dict(phase=8, scenario=True),+ "board_outline": dict(phase=8, scenario=True),+ "board_rules": dict(phase=8, scenario=True),+ "check_clearance": dict(phase=8, scenario=True),+ "copy_routing": dict(phase=8, scenario=True),+ "cross_probe": dict(phase=8, scenario=True),+ "custom_rules": dict(phase=8, scenario=True),+ "delete_footprint": dict(phase=8, scenario=True),+ "delete_graphics": dict(phase=8, scenario=True),+ "design_review": dict(phase=8, scenario=True),+ "edit_footprint": dict(phase=8, scenario=True),+ "estimate_cost": dict(phase=8, scenario=True),+ "export_dxf": dict(phase=8, scenario=True),+ "export_fab_package": dict(phase=8, scenario=True),+ "export_gencad": dict(phase=8, scenario=True),+ "export_ipc2581": dict(phase=8, scenario=True),+ "export_ipc356": dict(phase=8, scenario=True),+ "export_odb": dict(phase=8, scenario=True),+ "flip_footprint": dict(phase=8, scenario=True),+ "footprint_graphics": dict(phase=8, scenario=True),+ "footprint_models": dict(phase=8, scenario=True),+ "fp_create": dict(phase=8, scenario=True),+ "fp_edit": dict(phase=8, scenario=True),+ "fp_info": dict(phase=8, scenario=True),+ "get_selection": dict(phase=8, scenario=True),+ "import_svg": dict(phase=8, scenario=True),+ "modify_track": dict(phase=8, scenario=True),+ "netclasses": dict(phase=8, scenario=True),+ "place_decoupling": dict(phase=8, scenario=True),+ "plan_fanout": dict(phase=8, scenario=True),+ "project_create": dict(phase=8, scenario=True),+ "project_info": dict(phase=8, scenario=True),+ "project_rename": dict(phase=8, scenario=True),+ "project_snapshot": dict(phase=8, scenario=True),+ "refill_zones": dict(phase=8, scenario=True),+ "render_schematic": dict(phase=8, scenario=True),+ "route_diff_pair": dict(phase=8, scenario=True),+ "save_board": dict(phase=8, scenario=True),+ "sch_annotate": dict(phase=8, scenario=True),+ "sch_batch": dict(phase=8, scenario=True),+ "sch_bus": dict(phase=8, scenario=True),+ "sch_bus_entry": dict(phase=8, scenario=True),+ "sch_check": dict(phase=8, scenario=True),+ "sch_components": dict(phase=8, scenario=True),+ "sch_connect_pins": dict(phase=8, scenario=True),+ "sch_connect_to_bus": dict(phase=8, scenario=True),+ "sch_connect_to_net": dict(phase=8, scenario=True),+ "sch_create": dict(phase=8, scenario=True),+ "sch_delete_wire": dict(phase=8, scenario=True),+ "sch_edit_label": dict(phase=8, scenario=True),+ "sch_fix_connectivity": dict(phase=8, scenario=True),+ "sch_hierarchy": dict(phase=8, scenario=True),+ "sch_items": dict(phase=8, scenario=True),+ "sch_junction": dict(phase=8, scenario=True),+ "sch_move_region": dict(phase=8, scenario=True),+ "sch_page": dict(phase=8, scenario=True),+ "sch_pins": dict(phase=8, scenario=True),+ "sch_replace_symbol": dict(phase=8, scenario=True),+ "sch_reset_fields": dict(phase=8, scenario=True),+ "sch_rotate": dict(phase=8, scenario=True),+ "sch_sheet": dict(phase=8, scenario=True),+ "sch_sheet_pins": dict(phase=8, scenario=True),+ "sch_split_wire": dict(phase=8, scenario=True),+ "sch_text": dict(phase=8, scenario=True),+ "sch_trace_point": dict(phase=8, scenario=True),+ "sch_update_from_library": dict(phase=8, scenario=True),+ "score_placement": dict(phase=8, scenario=True),+ "search_library": dict(phase=8, scenario=True),+ "select_items": dict(phase=8, scenario=True),+ "set_layer_count": dict(phase=8, scenario=True),+ "sym_create": dict(phase=8, scenario=True),+ "sym_delete": dict(phase=8, scenario=True),+ "sym_info": dict(phase=8, scenario=True),+ "update_footprints_from_library": dict(phase=8, scenario=True),+ "update_pcb_from_schematic": dict(phase=8, scenario=True),+ "validate_for_fab": dict(phase=8, scenario=True), # ---- phase 2b: native-build verbs (Rust port). All read-only or on the fixture copy. "freerouting": dict(phase=1, args={"action": "status"}, timeout=60, note="status only: install is on demand and asks the user first"),@@ -380,6 +461,11 @@ def main(): live = sorted(v if isinstance(v, str) else v.get("name", "").replace("kicad_", "") for v in (d.get("verbs") or [])) live = [v.replace("kicad_", "") for v in live if v]+ # an EDA Bridge Standard alias runs its target's code: covered when the target is+ aliases = {v["name"].replace("kicad_", ""): v["aliasOf"].replace("kicad_", "") for v in (d.get("verbs") or [])+ if isinstance(v, dict) and v.get("aliasOf")}+ else:+ aliases = {} # discover the Windows user for paths user = "john"@@ -423,7 +509,7 @@ def main(): return 0 # coverage check against the LIVE bridge- missing = [v for v in (live or []) if v not in M]+ missing = [v for v in (live or []) if v not in M and not (v in aliases and aliases[v] in M)] if missing: print(f"COVERAGE GAP: live verbs with no test and no skip reason: {missing}") # keep going — report at the end, fail exit code@@ -532,6 +618,7 @@ def main(): live_src = str(REPO / "demo" / "placement" / "esc-g431-unrouted.kicad_pcb") ab(a.target, "send_files", {"filePaths": [live_src], "dest": base + "/live"}, timeout=200) ab(a.target, "send_files", {"filePaths": [live_src], "dest": base + "/offline"}, timeout=200)+ ab(a.target, "send_files", {"filePaths": [live_src], "dest": base + "/live2"}, timeout=200) # scratch symbol library for install_library (written locally, pushed over) import tempfile as _tf tl = Path(_tf.gettempdir()) / "AdomVerbTest.kicad_sym"@@ -647,6 +734,8 @@ def main(): return if spec.get("live"): return # run by live_phase(), in a fixed order with chained revisions+ if spec.get("scenario"):+ return # run by k161_phase(), each step building on the last if a.skip_gui and spec.get("phase", 9) >= 3: results[verb] = {"result": "SKIP", "reason": "--skip-gui"} return@@ -998,7 +1087,214 @@ def main(): r = ab(a.target, "kicad_pcm_uninstall", {"identifier": ident}, timeout=120) record("pcm_uninstall", r, ok(r)) ++ # ------------------------------------------------------------ phase 8: the 1.0.61 verbs+ def k161_phase():+ wanted = [v for v, sp in M.items() if sp.get("scenario")]+ if only and not (only & set(wanted)):+ return+ if a.skip_gui:+ for v in wanted:+ results[v] = {"result": "SKIP", "reason": "--skip-gui"}+ return+ WHY = "daily verb test: 1.0.61 verbs"+ K = lambda verb, args, t=200: ab(a.target, f"kicad_{verb}", dict(args, reason=WHY), timeout=t)+ def rec(verb, r, good, why=None):+ record(verb, r, bool(good), None if good else (why or f"{r.get('errorCode')}: {str(r.get('error'))[:160]}"))+ refused_cleanly = lambda r: bool(r.get("errorCode")) and r.get("mutated") is not True++ base = w["out"].rsplit("/", 1)[0]+ # names unique per run: a run with --only skips the final cleanup, and these verbs rightly refuse to overwrite+ tag = datetime.now().strftime("%m%d%H%M%S")+ P0, OUT = base + "/k161-" + tag, w["out"]+ pd = P0 + "/kvt"+ pro, sch, pcb = pd + "/kvt.kicad_pro", pd + "/kvt.kicad_sch", pd + "/kvt.kicad_pcb"++ # 8.1 a project and its schematic, built and edited step by step+ print("[8.1] project and schematic")+ r = K("project_create", {"parentDir": P0, "name": "kvt", "layers": 2})+ rec("project_create", r, ok(r))+ r = K("project_info", {"projectPath": pro}); rec("project_info", r, ok(r))+ r = K("sch_page", {"filePath": sch, "paper": "A4", "title": "kvt"}); rec("sch_page", r, ok(r))+ # two resistors: Device:R's pins sit 3.81 mm above and below its anchor+ for ref, x in (("R1", 50.8), ("R2", 76.2)):+ ab(a.target, "kicad_sch_place_symbol", {"filePath": sch, "libId": "Device:R", "reference": ref, "at": [x, 50.8]}, timeout=150)+ r = K("sch_components", {"filePath": sch}); rec("sch_components", r, ok(r) and "R1" in json.dumps(r) and "R2" in json.dumps(r))+ r = K("sch_pins", {"filePath": sch, "references": ["R1", "R2"]}); rec("sch_pins", r, ok(r) and "R2" in json.dumps(r))+ r = K("sch_connect_pins", {"filePath": sch, "from": "R1.2", "to": "R2.2"}); rec("sch_connect_pins", r, ok(r))+ r = K("sch_items", {"filePath": sch, "kinds": ["wires"]}); rec("sch_items", r, ok(r) and "wire" in json.dumps(r).lower())+ r = K("sch_trace_point", {"filePath": sch, "at": [63.5, 54.61]}); rec("sch_trace_point", r, ok(r))+ r = K("sch_split_wire", {"filePath": sch, "at": [63.5, 54.61], "junction": False}); rec("sch_split_wire", r, ok(r))+ r = K("sch_junction", {"filePath": sch, "at": [63.5, 54.61]}); rec("sch_junction", r, ok(r))+ r = K("sch_delete_wire", {"filePath": sch, "at": [57.15, 54.61]}); rec("sch_delete_wire", r, ok(r))+ r = K("sch_connect_to_net", {"filePath": sch, "pin": "R1.1", "net": "VIN"}); rec("sch_connect_to_net", r, ok(r))+ r = K("sch_edit_label", {"filePath": sch, "net": "VIN", "by": [0, -2.54]}); rec("sch_edit_label", r, ok(r))+ r = K("sch_text", {"filePath": sch, "text": "verb test", "at": [30.48, 30.48]}); rec("sch_text", r, ok(r))+ r = K("sch_bus", {"filePath": sch, "points": [[101.6, 38.1], [101.6, 76.2]], "label": "D[0..1]"}); rec("sch_bus", r, ok(r))+ r = K("sch_bus_entry", {"filePath": sch, "at": [99.06, 68.58], "direction": "up-right"}); rec("sch_bus_entry", r, ok(r))+ r = K("sch_connect_to_bus", {"filePath": sch, "pins": ["R2.1"], "prefix": "D", "busLabel": "D[0..1]"}); rec("sch_connect_to_bus", r, ok(r))+ r = K("sch_move_region", {"filePath": sch, "box": [70, 40, 85, 60], "by": [0, 2.54]}); rec("sch_move_region", r, ok(r))+ r = K("sch_rotate", {"filePath": sch, "reference": "R1", "rotation": 90}); rec("sch_rotate", r, ok(r))+ ab(a.target, "kicad_sch_place_symbol", {"filePath": sch, "libId": "Device:R", "reference": "R?", "at": [50.8, 88.9]}, timeout=150)+ r = K("sch_annotate", {"filePath": sch}); rec("sch_annotate", r, ok(r) and "R3" in json.dumps(r))+ r = K("sch_replace_symbol", {"filePath": sch, "reference": "R2", "libId": "Device:R_Small"}); rec("sch_replace_symbol", r, ok(r))+ r = K("sch_reset_fields", {"filePath": sch, "references": ["R2"]}); rec("sch_reset_fields", r, ok(r))+ r = K("sch_update_from_library", {"filePath": sch}); rec("sch_update_from_library", r, ok(r))+ r = K("sch_fix_connectivity", {"filePath": sch, "dryRun": True}); rec("sch_fix_connectivity", r, ok(r) and r.get("mutated") is not True)+ r = K("sch_check", {"filePath": sch}); rec("sch_check", r, ok(r))+ r = K("sch_batch", {"filePath": sch, "ops": [{"op": "text", "text": "batch 1", "at": [30.48, 35.56]}, {"op": "text", "text": "batch 2", "at": [30.48, 40.64]}]})+ rec("sch_batch", r, ok(r))+ r = K("sch_sheet", {"filePath": sch, "action": "add", "name": "Child", "file": "child.kicad_sch", "at": [127, 30.48], "size": [25.4, 20.32], "projectPath": pro})+ rec("sch_sheet", r, ok(r))+ r = K("sch_sheet_pins", {"filePath": sch, "sheet": "Child", "action": "add", "name": "VIN", "shape": "input", "side": "left"})+ rec("sch_sheet_pins", r, ok(r))+ r = K("sch_hierarchy", {"projectPath": pro}); rec("sch_hierarchy", r, ok(r) and "Child" in json.dumps(r))+ r = K("sch_create", {"filePath": pd + "/extra.kicad_sch", "title": "extra"}); rec("sch_create", r, ok(r))+ r = K("render_schematic", {"filePath": sch, "outputDir": OUT + "/render-sch"}, 220); rec("render_schematic", r, ok(r))+ r = K("design_review", {"projectPath": pro}, 220); rec("design_review", r, ok(r))+ r = K("update_pcb_from_schematic", {"filePath": pcb, "schematicPath": sch}, 220)+ rec("update_pcb_from_schematic", r, ok(r) and r.get("mutated") is not True)+ r = K("netclasses", {"projectPath": pro, "set": {"name": "PWR", "trackWidth": 0.5}, "assign": {"netclass": "PWR", "patterns": ["VIN"]}})+ rec("netclasses", r, ok(r))+ al = ab(a.target, "kicad_net_classes", {"projectPath": pro}, timeout=120) # the standard's name for it+ if not (ok(al) and "PWR" in json.dumps(al)):+ record("netclasses", {"netclasses": r, "alias": al}, False, "the alias kicad_net_classes did not read the class back")+ r = K("board_rules", {"projectPath": pro, "rules": {"minClearance": 0.15}}); rec("board_rules", r, ok(r))+ r = K("custom_rules", {"projectPath": pro, "add": {"name": "vt", "condition": "A.NetClass == 'PWR'", "constraints": [{"type": "clearance", "min": 0.3}]}})+ rec("custom_rules", r, ok(r))+ r = K("project_snapshot", {"projectPath": pro, "label": "verbtest"}, 220); rec("project_snapshot", r, ok(r))+ r = K("project_rename", {"projectPath": pro, "newName": "kvt2"}); rec("project_rename", r, ok(r))++ # 8.2 a library+ print("[8.2] symbol and footprint library")+ lib, pretty = f"{OUT}/KVT{tag}.kicad_sym", f"{OUT}/KVT{tag}.pretty"+ r = K("sym_create", {"libraryPath": lib, "name": "KVT1", "pins": [{"number": 1, "name": "A"}, {"number": 2, "name": "B", "side": "right"}]})+ rec("sym_create", r, ok(r))+ r = K("sym_info", {"libraryPath": lib, "name": "KVT1"}); rec("sym_info", r, ok(r) and "KVT1" in json.dumps(r))+ r = K("sym_delete", {"libraryPath": lib, "name": "KVT1"}); rec("sym_delete", r, ok(r))+ r = K("fp_create", {"libraryPath": pretty, "name": "KVT_FP", "pads": [{"number": 1, "size": [1, 1], "at": [-1, 0]}, {"number": 2, "size": [1, 1], "at": [1, 0]}]})+ rec("fp_create", r, ok(r))+ r = K("fp_info", {"libraryPath": pretty, "name": "KVT_FP"}); rec("fp_info", r, ok(r))+ r = K("fp_edit", {"libraryPath": pretty, "name": "KVT_FP", "pads": [{"number": "1", "set": {"size": [1.2, 1]}}]}); rec("fp_edit", r, ok(r))+ r = K("search_library", {"query": "R_0603", "kind": "footprint", "limit": 5}); rec("search_library", r, ok(r))++ # 8.3 the fixture board: fab exports and checks, and a layer count on the offline copy+ print("[8.3] fab exports and checks")+ B = w["board"]+ for verb, extra in (("export_dxf", {"layers": ["Edge.Cuts"]}), ("export_gencad", {}), ("export_ipc2581", {}), ("export_ipc356", {}),+ ("export_odb", {}), ("export_fab_package", {"fab": "jlcpcb"})):+ r = K(verb, dict({"filePath": B, "outputDir": OUT + "/" + verb}, **extra), 300); rec(verb, r, ok(r))+ r = K("validate_for_fab", {"filePath": B, "fab": "jlcpcb"}, 300); rec("validate_for_fab", r, ok(r))+ r = K("estimate_cost", {"filePath": B, "quantities": [5, 50]}); rec("estimate_cost", r, ok(r))+ r = K("set_layer_count", {"filePath": w["offline"], "count": 4}); rec("set_layer_count", r, ok(r))++ # 8.4 the live board (a second scratch copy, never saved over a fixture)+ print("[8.4] live board edits")+ L2 = base + "/live2/esc-g431-unrouted.kicad_pcb"+ ab(a.target, "kicad_close", {"force": True}, timeout=120)+ time.sleep(4)+ o = ab(a.target, "kicad_open_board", {"filePath": L2, "reason": WHY}, timeout=200)+ live_verbs = ["check_clearance", "score_placement", "plan_fanout", "place_decoupling", "cross_probe", "select_items", "get_selection",+ "footprint_graphics", "footprint_models", "add_footprint", "edit_footprint", "flip_footprint", "align_footprints",+ "update_footprints_from_library", "delete_footprint", "add_mounting_hole", "import_svg", "delete_graphics", "board_outline",+ "modify_track", "copy_routing", "route_diff_pair", "refill_zones", "save_board"]+ if not ok(o):+ for v in live_verbs:+ record(v, o, False, "the live board did not open")+ return+ time.sleep(8)+ st = {}+ for _ in range(6):+ st = ab(a.target, "kicad_routing_state", {"filePath": L2}, timeout=150)+ if ok(st) and st.get("revision"):+ break+ time.sleep(5)+ rev = [st.get("revision")]+ def E(verb, args, t=300, chain=True):+ r = K(verb, dict({"filePath": L2, "expectedRevision": rev[0]}, **args), t)+ if chain and r.get("revision"):+ rev[0] = r["revision"]+ return r+ pads = st.get("pads") or []+ xs, ys = [p["x"] for p in pads], [p["y"] for p in pads]+ cx, cy = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2+ r = K("check_clearance", {"filePath": L2, "refs": ["U5"], "mode": "pads"}); rec("check_clearance", r, ok(r))+ r = K("score_placement", {"filePath": L2}); rec("score_placement", r, ok(r))+ r = K("plan_fanout", {"filePath": L2, "ref": "U5"}); rec("plan_fanout", r, ok(r))+ r = K("place_decoupling", {"filePath": L2, "ic": "U5", "dryRun": True}); rec("place_decoupling", r, ok(r) and r.get("mutated") is not True)+ r = K("cross_probe", {"filePath": L2, "reference": "U5"}); rec("cross_probe", r, ok(r) or refused_cleanly(r), "no answer for U5")+ r = K("select_items", {"filePath": L2, "refs": ["U5"]}); rec("select_items", r, ok(r))+ r = K("get_selection", {"filePath": L2}); rec("get_selection", r, ok(r) and "U5" in json.dumps(r))+ ab(a.target, "kicad_select_clear", {"filePath": L2}, timeout=60)+ r = K("footprint_graphics", {"filePath": L2, "ref": "U5"}); rec("footprint_graphics", r, ok(r))+ r = K("footprint_models", {"filePath": L2, "ref": "U5"}); rec("footprint_models", r, ok(r))+ # creating items live needs KiCad's ParseAndCreateItemsFromString, which KiCad 10.0.4 answers empty: one live+ # add must either land or refuse with live_create_unsupported and nothing changed; the edit sequence itself+ # runs on the closed copy below (the file path), which works on every KiCad+ lv = E("add_footprint", {"libraryId": "Capacitor_SMD:C_0402_1005Metric", "x": round(cx, 2), "y": round(cy, 2), "reference": "C", "value": "100n", "validate": False})+ live_create = "landed" if ok(lv) else ("refused" if lv.get("errorCode") == "live_create_unsupported" and lv.get("mutated") is False else "broken")+ r = E("add_mounting_hole", {"x": round(cx, 2), "y": round(cy, 2), "drill": 2.2, "dryRun": True}, chain=False)+ rec("add_mounting_hole", r, ok(r) or refused_cleanly(r), "no answer")+ r = E("board_outline", {"shape": "rectangle", "width": 70, "height": 60, "x": 100, "y": 125, "replace": True, "dryRun": True}, chain=False)+ rec("board_outline", r, ok(r) and r.get("mutated") is not True)+ # copper to work on: the shortest same-side two-pad hop the bridge will route+ remaining = set(st.get("netsRemaining") or [])+ by_net = {}+ for p in pads:+ if p.get("netName") in remaining and p.get("type") == "smd":+ by_net.setdefault(p["netName"], []).append(p)+ cands = sorted(((math.hypot(a1["x"] - b1["x"], a1["y"] - b1["y"]), n, a1, b1) for n, g in by_net.items()+ for i, a1 in enumerate(g) for b1 in g[i + 1:] if a1.get("side") == b1.get("side")), key=lambda c: c[0])+ ids = []+ for _d, net, p1, p2 in cands[:12]:+ rr = E("route_net", {"net": net, "points": [p1["name"], p2["name"]], "layer": p1["side"]})+ if ok(rr):+ ids = rr.get("itemIds") or []+ break+ if ids:+ r = E("modify_track", {"itemIds": ids[:1], "width": 0.3}); rec("modify_track", r, ok(r))+ r = E("copy_routing", {"source": {"minX": cx - 40, "minY": cy - 40, "maxX": cx + 40, "maxY": cy + 40}, "offset": {"dx": 0, "dy": 0.5}, "dryRun": True}, chain=False)+ rec("copy_routing", r, ok(r) or refused_cleanly(r), "no answer")+ else:+ for v in ("modify_track", "copy_routing"):+ results[v] = {"result": "FAIL", "reason": "route_net landed no copper to work on"}+ nets = sorted(by_net) or sorted({p.get("netName") for p in pads if p.get("netName")})+ r = E("route_diff_pair", {"netP": nets[0], "netN": nets[1], "centerline": [[round(cx, 2), round(cy, 2)], [round(cx + 3, 2), round(cy, 2)]], "dryRun": True}, chain=False)+ rec("route_diff_pair", r, ok(r) or refused_cleanly(r), "no answer")+ r = K("refill_zones", {"filePath": L2}, 300); rec("refill_zones", r, ok(r))+ r = K("save_board", {"filePath": L2}); rec("save_board", r, ok(r))+ ab(a.target, "kicad_close", {"force": True}, timeout=120)+ time.sleep(4)++ # 8.5 the same board closed: add a capacitor, edit, flip, align, update and delete it, an SVG and its removal+ print("[8.5] closed-board edits")+ F = lambda verb, args, t=200: K(verb, dict({"filePath": L2}, **args), t)+ before = set(re.findall(r'"(C\d{1,3})"', json.dumps(st)))+ added, r = None, {}+ for fx, fy in ((0.5, 0.5), (0.3, 0.3), (0.7, 0.7), (0.3, 0.7), (0.7, 0.3)):+ x, y = min(xs) + (max(xs) - min(xs)) * fx, min(ys) + (max(ys) - min(ys)) * fy+ r = F("add_footprint", {"libraryId": "Capacitor_SMD:C_0402_1005Metric", "x": round(x, 2), "y": round(y, 2), "reference": "C", "value": "100n"})+ if ok(r):+ found = re.findall(r'"(C\d{1,3})"', json.dumps(r))+ added = next((c for c in found if c not in before), None)+ break+ rec("add_footprint", {"file": r, "live": lv, "liveCreate": live_create}, ok(r) and added and live_create != "broken",+ "the closed-file add failed or named no new part, or the live add neither landed nor refused cleanly")+ if added:+ r = F("edit_footprint", {"ref": added, "value": "1u"}); rec("edit_footprint", r, ok(r))+ r = F("flip_footprint", {"ref": added}); rec("flip_footprint", r, ok(r))+ r = F("align_footprints", {"refs": [added, "C7"], "mode": "top", "dryRun": True, "allowOverlap": True}); rec("align_footprints", r, ok(r) and r.get("mutated") is not True)+ r = F("update_footprints_from_library", {"refs": [added]}); rec("update_footprints_from_library", r, ok(r))+ r = F("delete_footprint", {"refs": [added]}); rec("delete_footprint", r, ok(r))+ else:+ for v in ("edit_footprint", "flip_footprint", "align_footprints", "update_footprints_from_library", "delete_footprint"):+ results[v] = {"result": "FAIL", "reason": "add_footprint placed nothing to work on"}+ svg = "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10'/></svg>"+ r = F("import_svg", {"svg": svg, "x": round(cx, 2), "y": round(cy, 2), "width": 3, "layer": "Dwgs.User"}); rec("import_svg", r, ok(r))+ r = F("delete_graphics", {"layer": "Dwgs.User"}); rec("delete_graphics", r, ok(r))+ live_phase()+ k161_phase() # phase 7: clean up EVERYTHING the suite created on the box (John 2026-08-14) if not only and not a.skip_gui:
skills/kicad-schematic-and-board-edits/SKILL.mdadded+58@@ -0,0 +1,58 @@+---+name: kicad-schematic-and-board-edits+description: >-+ Build and change a KiCad design through the KiCad Bridge without touching the GUI: create a project, draw a schematic (place, rotate, move, wire pin to pin, labels and net stubs, junctions, buses and bus entries, hierarchical sheets and sheet pins, annotate, replace a symbol, reset fields, update from library, batches), read it back (components, pins, items, trace a point, check, design review, PNG render), make symbols and footprints, and edit the board (add, edit, flip, align, delete footprints, mounting holes, outline, layer count, graphics and SVG logos, 3D models, netclasses, board and custom rules, modify or copy tracks, diff pairs, refill, save, update the PCB from the schematic) plus fab outputs (DXF, GenCAD, IPC-2581, IPC-D-356, ODB++, one-zip fab package, fab pre-flight, cost). Trigger words: draw a schematic, wire these pins, add a net label, add a bus, hierarchical sheet, annotate, replace symbol, create a symbol, create a footprint, new kicad project, add a footprint to the board, flip a part, align parts, mounting hole, board outline, import a logo, netclass, design rules, update pcb from schematic, odb++, ipc-2581, fab package, pcb cost estimate, design review.+---++# Schematic and board edits++Every verb here edits files or the live board the way KiCad would, answers what it did, and refuses with a named+`errorCode` and a `_hint` when it cannot. `kicad_describe` has each verb's exact inputs; the README lists them all.++## Schematic (`kicad_sch_*`, edits the .kicad_sch on disk)++- **Build**: `sch_create` (a new sheet, optionally inside a project), `sch_page` (paper, title block),+ `sch_place_symbol`, `sch_connect_pins` (a wire from pin to pin, e.g. `R1.2` to `R2.2`), `sch_connect_to_net`+ (a stub and a label, or a power symbol), `sch_wire`, `sch_label`, `sch_junction`, `sch_text`, `sch_bus`,+ `sch_bus_entry`, `sch_connect_to_bus`, `sch_no_connect`, `sch_sheet` and `sch_sheet_pins` for hierarchy.+- **Change**: `sch_move`, `sch_move_region`, `sch_rotate`, `sch_split_wire`, `sch_delete_wire`, `sch_edit_label`,+ `sch_annotate` (the standard's `annotate`), `sch_replace_symbol`, `sch_reset_fields`, `sch_update_from_library`,+ `sch_fix_connectivity` (a dry run first), `sch_batch` (many of these as one write).+- **Read and check**: `sch_components`, `sch_pins`, `sch_items`, `sch_trace_point`, `sch_hierarchy`, `sch_check`,+ `design_review` (decoupling, I2C pull-ups, reset and enable pins, LED resistors, regulator caps),+ `render_schematic` (PNG, with visual baselines), `kicad_netlist` (`{net: [Ref.Pin]}` and an `expect` check).+- Coordinates are sheet millimetres, y down, on KiCad's 1.27 mm grid; a pin is named `Ref.Number`.+- Every write keeps a `.bak` and reports the ERC change. An open Schematic Editor does not see a file edit until+ it reloads the sheet: close it first, or reload after.++## Board (`kicad_*` on a .kicad_pcb)++- **Parts**: `add_footprint` (from a library, a .kicad_mod or a copy of a placed part), `edit_footprint` (value,+ reference, fields, DNP, attributes), `flip_footprint`, `align_footprints`, `delete_footprint`,+ `update_footprints_from_library`, `footprint_graphics`, `footprint_models`, `place_decoupling`.+- **Board**: `board_outline`, `set_layer_count`, `add_mounting_hole`, `import_svg` (logos), `delete_graphics`,+ `refill_zones` (the standard's `repour`), `save_board` (`save`), `update_pcb_from_schematic` (`update_pcb`).+- **Copper and rules**: `modify_track`, `copy_routing`, `route_diff_pair` (`route_pair`), `plan_fanout`,+ `netclasses` (`net_classes`), `board_rules`, `custom_rules` (`add_rule`), `check_clearance`, `score_placement`.+- **Open or closed**: with the board open in the PCB editor (IPC API on) an edit lands live as one Undo step,+ guarded by `expectedRevision` and checked by KiCad's DRC first (`dryRun` checks without editing). With no editor+ holding the board it edits the file, keeping a backup. A lock file left by a KiCad that is no longer running+ is cleared; a lock from another machine is honoured.+- **Adding items to an open board does not work yet.** KiCad 10.0.x does not create items from text over its+ API (checked on 10.0.4 and 10.0.7), so `add_footprint`, `add_mounting_hole`, `import_svg`, `board_outline`+ and `update_footprints_from_library` refuse an open board with `live_create_unsupported` and change nothing.+ Save and close the board (`kicad_save_board`, `kicad_close`) and call again: the file is edited. Moving,+ editing, routing and deleting work live.++## Libraries and projects++`sym_create`, `sym_info`, `sym_delete`, `fp_create`, `fp_info`, `fp_edit`, `search_library` (every registered+library, PCM ones included), `project_create`, `project_info`, `project_rename`, `project_snapshot`.++## Fab outputs++`export_fab_package` (Gerbers, drill, BOM, CPL, a README, zipped; `fab:"jlcpcb"` for its columns), `export_odb`,+`export_ipc2581`, `export_ipc356`, `export_gencad`, `export_dxf`, `validate_for_fab` (the standard's `fab_check`),+`estimate_cost`.++Every verb here runs in the verb battery on a real Windows box before each release.
tools/build_native_release.py+42−1@@ -118,8 +118,20 @@ def main(): exe = RUST / "target" / TARGET / "release" / "kicad-bridge.exe" desc = describe_via_linux_binary() verbs = [v["name"] for v in desc["verbs"]]+ # the README lists every verb, written from this catalog (tools/readme_verbs.py)+ import tempfile+ dpath = Path(tempfile.mkdtemp(prefix="kicad-describe-")) / "describe.json"+ dpath.write_text(json.dumps(desc))+ sh([sys.executable, str(ROOT / "tools" / "readme_verbs.py"), str(dpath)]) if "kicad_describe" not in verbs or "kicad_uninstall" not in verbs: sys.exit("REFUSING: describe or uninstall missing from the catalog")+ # The repo's bridge.json is what ab and catalog.rs's manifest test read: keep its verb list the catalog's+ # (1.0.61 shipped with 211 verbs while bridge.json still listed 133).+ root_manifest = json.loads((ROOT / "bridge.json").read_text())+ if sorted(root_manifest.get("verbs", [])) != sorted(verbs):+ root_manifest["verbs"] = verbs+ (ROOT / "bridge.json").write_text(json.dumps(root_manifest, indent=2) + "\n")+ print(f"bridge.json: verb list synced to the catalog ({len(verbs)} verbs)") py = json.loads((ROOT / "bridge.json").read_text()) manifest = dict(py) manifest["version"] = ver@@ -198,7 +210,7 @@ def publish_zip(ver, zpath, sha, notes): REF = "adom/kicad-bridge" # 1. The repo must tell the same story as the zip: push every rust source file. src = [str(p.relative_to(ROOT)) for p in sorted((RUST).rglob("*")) if p.is_file() and "target" not in p.parts and "dist" not in p.parts]- src += ["tools/build_native_release.py", "tools/sign_azure.py"]+ src += ["tools/build_native_release.py", "tools/sign_azure.py", "tools/sync_standard_names.py", "tools/readme_verbs.py", "bridge.json", "README.md"] msg = f"{ver}: {notes}" failed = push_chunked(REF, src, msg, cwd=str(ROOT)) if failed:@@ -234,6 +246,8 @@ def publish_zip(ver, zpath, sha, notes): prior = led.read_text(encoding="utf-8") head, _, rest = prior.partition("\n\n## ") led.write_text((head + "\n\n" + entry + ("## " + rest if rest else "")) if rest else (prior + entry), encoding="utf-8")+ # 5b. The EDA Bridge Standard measures this bridge from its verb list: refresh it with every release.+ refresh_standard(ver, zpath) # 6. The page repo keeps the ledger (it was left local before). Never the public manifest here. failed = push_chunked(REF, ["RELEASE_NOTES.md"], f"{ver}: release record", cwd=str(ROOT)) if failed:@@ -242,5 +256,32 @@ def publish_zip(ver, zpath, sha, notes): print(f" Promote with: python3 tools/promote.py {ver} --because \"<your words>\"") print(" Boxes pick it up on the 4 h poll, or now with bridge_install {manifestUrl: the insiders version.json, force: true}") +def refresh_standard(ver, zpath):+ """Write reference/kicad-<ver>.json (this release's verbs, aliases included) into a fresh clone of+ adom/eda-bridge-standard, run its tools/generate.py, and push the files that moved. A failure here is+ reported, never fatal: the release has already shipped."""+ import filecmp, tempfile+ with zipfile.ZipFile(zpath) as z:+ verbs = sorted(set(json.loads(z.read("bridge.json"))["verbs"]))+ base = Path(tempfile.mkdtemp(prefix="eda-std-"))+ fresh, pristine = base / "std", base / "pristine"+ for d in (fresh, pristine):+ r = subprocess.run(["adom-wiki", "repo", "clone", "adom/eda-bridge-standard", "--dir", str(d)], capture_output=True, text=True)+ if r.returncode != 0:+ print(f"WARNING: could not clone adom/eda-bridge-standard; refresh its KiCad coverage by hand ({r.stderr[-200:]})")+ return+ (fresh / "reference" / f"kicad-{ver}.json").write_text(json.dumps({"bridge": "kicad", "version": ver, "verbs": verbs}, indent=1) + "\n")+ g = subprocess.run([sys.executable, "tools/generate.py"], cwd=fresh, capture_output=True, text=True)+ if g.returncode != 0:+ print(f"WARNING: the standard's generate.py failed; nothing pushed there: {g.stderr[-300:]}")+ return+ moved = [f"reference/kicad-{ver}.json"] + [f for f in ("verbs/coverage.json", "verbs/standard.json", "docs/coverage.md", "README.md")+ if not filecmp.cmp(fresh / f, pristine / f, shallow=False)]+ r = subprocess.run(["adom-wiki", "repo", "push", "adom/eda-bridge-standard", "-m", f"KiCad bridge {ver} reference ({g.stdout.strip()})", "--files"] + moved,+ cwd=fresh, capture_output=True, text=True)+ ok = '"ok": true' in r.stdout or "PUSH_UNCHANGED" in r.stdout + r.stderr+ print(f"EDA Bridge Standard: {g.stdout.strip()} -> {'pushed ' + ', '.join(moved) if ok else 'push FAILED: ' + (r.stdout + r.stderr)[-300:]}")++ if __name__ == "__main__": main()
tools/readme_verbs.pyadded+83@@ -0,0 +1,83 @@+#!/usr/bin/env python3+"""Write every verb into README.md between <!-- verbs:start --> and <!-- verbs:end -->, from the catalog.++ python3 tools/readme_verbs.py describe.json # a saved kicad_describe reply+ python3 tools/readme_verbs.py # runs the host build (rust/target/release/kicad-bridge) for it++The README is written for search: every verb is named with what it does, so a person or an AI looking for a job+finds the verb. tools/build_native_release.py runs this on every release.+"""+import json, subprocess, sys, time, urllib.request+from pathlib import Path++ROOT = Path(__file__).resolve().parent.parent+START, END = "<!-- verbs:start (tools/readme_verbs.py writes this) -->", "<!-- verbs:end -->"++GROUPS = [+ ("Board model and reads (EDA Bridge Standard)", lambda n: n in {"routing_state", "layer_stack", "pour_state", "bodies", "netlist", "board_summary", "list_nets", "list_designators", "copper_layers", "layers_in_use", "component_data", "component_pins"}),+ ("Schematic", lambda n: n.startswith("sch_") or n in {"annotate", "update_pcb", "render_schematic", "design_review"}),+ ("Symbols, footprints and libraries", lambda n: n.startswith(("sym_", "fp_", "install_", "uninstall_library", "list_symbols", "list_footprints", "adom_library", "pcm_", "search_library", "rescan")) or n in {"create_symbol", "create_footprint", "read_symbol", "remove_symbol", "find_symbol", "make_part_project", "export_part", "model_check", "lint_library"}),+ ("Placement and board edits", lambda n: n in {"placement_state", "move_footprint", "placement_validate", "add_footprint", "delete_footprint", "flip_footprint", "edit_footprint", "align_footprints", "update_footprints_from_library", "add_mounting_hole", "board_outline", "set_layer_count", "delete_graphics", "footprint_graphics", "footprint_models", "import_svg", "place_decoupling", "score_placement", "check_clearance", "save_board", "save", "silk_text_batch", "silk_graphics_batch", "silk_fields_batch", "text_bounds", "board_pads", "selection", "get_selection", "select_items", "cross_probe", "select_net", "select_clear", "update_pcb_from_schematic"}),+ ("Routing, copper and rules", lambda n: n.startswith(("route", "add_track", "add_via", "remove_route", "autoroute", "freerouting")) or n in {"modify_track", "copy_routing", "plan_fanout", "add_zone", "apply_zone_plan", "zone_state", "remove_zone", "refill_zones", "repour", "netclasses", "net_classes", "board_rules", "custom_rules", "add_rule", "list_design_rules", "set_design_rules", "routing_validate"}),+ ("Checks, exports and manufacturing", lambda n: n.startswith(("run_", "lint_", "export_", "validate", "fab_")) or n in {"estimate_cost", "render_board", "format_upgrade", "errors"}),+ ("Projects", lambda n: n.startswith("project_")),+]++def describe():+ if len(sys.argv) > 1:+ return json.loads(Path(sys.argv[1]).read_text())+ exe = ROOT / "rust/target/release/kicad-bridge"+ p = subprocess.Popen([str(exe), "--port", "47744"], stderr=subprocess.DEVNULL)+ try:+ for _ in range(50):+ try:+ req = urllib.request.Request("http://127.0.0.1:47744/command", data=b'{"command":"kicad_describe"}', headers={"Content-Type": "application/json"})+ with urllib.request.urlopen(req, timeout=2) as r:+ return json.load(r)+ except Exception:+ time.sleep(0.1)+ sys.exit("the host build never answered kicad_describe")+ finally:+ p.kill()++def main():+ d = describe()+ verbs = sorted(d["verbs"], key=lambda v: v["name"])+ groups = {g: [] for g, _ in GROUPS}+ other = []+ for v in verbs:+ n = v["name"].removeprefix("kicad_")+ for g, test in GROUPS:+ if test(n):+ groups[g].append(v)+ break+ else:+ other.append(v)+ out = [START, "", f"Every verb of this build ({len(verbs)}, `kicad_describe` is the live catalog with inputs, hints and pitfalls). "+ "A name marked *standard* is the EDA Bridge Standard's name for the verb after the arrow.", ""]+ for g, vs in list(groups.items()) + [("Windows, editors, install and the bridge", other)]:+ if not vs:+ continue+ out += [f"**{g}** ({len(vs)})", "", "| Verb | What it does |", "|---|---|"]+ for v in vs:+ summ = v["summary"].replace("|", "/").replace("\n", " ")+ if v.get("aliasOf"):+ summ = f"*standard* → `{v['aliasOf']}`"+ out.append(f"| `{v['name']}` | {summ} |")+ out.append("")+ out.append(END)+ readme = (ROOT / "README.md").read_text()+ block = "\n".join(out)+ if START in readme:+ a, b = readme.index(START), readme.index(END) + len(END)+ readme = readme[:a] + block + readme[b:]+ else:+ anchor = "## Verb reference"+ i = readme.index(anchor)+ j = readme.index("\n---", i)+ readme = readme[:i] + "## Verb reference\n\n" + block + "\n" + readme[j:]+ (ROOT / "README.md").write_text(readme)+ print(f"README.md: {len(verbs)} verbs, {len(other)} under windows/editors/install")++if __name__ == "__main__":+ main()
tools/sync_standard_names.pyadded+30@@ -0,0 +1,30 @@+#!/usr/bin/env python3+"""Snapshot the EDA Bridge Standard's verb names into rust/crates/kicad-bridge/src/standard_names.txt.++The catalog test `every_verb_has_a_standard_decision` (catalog.rs) reads it: every kicad_* verb must be a+standard name, an alias in catalog::ALIASES, or listed in kicad_only.txt beside it (a job the standard does+not have, reviewed). Refresh it when the standard changes:++ python3 tools/sync_standard_names.py # clones adom/eda-bridge-standard into a temp dir+ python3 tools/sync_standard_names.py <clone-dir> # or reads an existing clone+"""+import json, os, subprocess, sys, tempfile+from pathlib import Path++ROOT = Path(__file__).resolve().parent.parent+OUT = ROOT / "rust/crates/kicad-bridge/src/standard_names.txt"++def main():+ src = Path(sys.argv[1]) if len(sys.argv) > 1 else None+ if src is None:+ src = Path(tempfile.mkdtemp(prefix="eda-std-")) / "std"+ subprocess.run(["adom-wiki", "repo", "clone", "adom/eda-bridge-standard", "--dir", str(src)], check=True, capture_output=True)+ d = json.loads((src / "verbs/standard.json").read_text())+ names = sorted(v["name"] for v in d["verbs"])+ head = (f"# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference "+ f"{d['reference']['bridge']} {d['reference']['version']}). Generated by tools/sync_standard_names.py; do not edit.\n")+ OUT.write_text(head + "\n".join(names) + "\n")+ print(f"{len(names)} standard names -> {OUT.relative_to(ROOT)}")++if __name__ == "__main__":+ main()