main
Colby Knox 0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom f82b5b5 10h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524
//! Verb dispatch — the FULL surface (110 verbs), at parity with the Python bridge it replaces.
//! Every verb returns the envelope the Python bridge returned, so the CLI and callers see no
//! difference; the engine field says "rust".
use crate::support::{self, s};
use crate::{a365, agent, board, dialogs, library, license, lifecycle, misc, outputs, progress, review, surfaces, taskbar};
use serde_json::{json, Value};
use std::time::Duration;

/// Everything this bridge serves. lint_verbs in scripts/release.py checks bridge.json against
/// THIS list — keep it the single source of truth for the dispatch below.
pub const KNOWN: &[&str] = &[
    // health / lifecycle
    "readiness", "status", "diagnostics", "window_info", "state", "check_dialogs",
    "dismiss_dialogs", "launch", "close", "instances", "dialog_log", "uninstall", "describe", "panel", "list_installed_libraries", "uninstall_library",
    // agent / REPL
    "agent_start", "agent_stop", "agent_status", "agent_recover",
    "eval", "run_command", "run_script", "list_commands", "open_document", "open_project", "close_document",
    "activate_document", "progress",
    // board / schematic reads
    "read_board", "board_summary", "rules_by_kind", "layers_in_use", "read_schematic",
    "cross_check", "review_board", "list_designators", "component_data", "component_pins",
    "list_nets", "net_connections", "layers", "copper_layers", "read_rules",
    "set_layer_visibility", "footprint_primitives", "schematic_data", "layer_stack",
    // the live-board lane AI Flow drives
    "routing_state", "route_net", "move_parts", "place_vias", "remove_items", "ripup_nets", "apply_pours", "pour_state", "remove_pours", "repour", "routing_drc", "build_board", "set_outline", "set_design_rules", "zoom_to", "add_rule", "build_schematic", "export_manufacturing", "select_net", "silk_state", "apply_silkscreen",
    // surfaces
    "show_project", "show_schematic", "show_2d_board", "show_3d_board", "show_library",
    "show_symbol", "show_footprint", "show_3d_chip", "list_library", "demo",
    "animate_view", "foreground",
    // outputs / project
    "compile", "run_erc", "run_drc", "run_outjob", "export_gerber", "export_bom", "export_step", "bom", "sheet_params", "sch_place_part", "sch_connect", "sch_edit", "annotate", "doc_params", "update_pcb", "export_pdf", "sch_zoom", "autoroute",
    "save", "test_board", "board_shape", "pcb_report", "import_kicad",
    "install_extension", "extension_ping",
    // library authoring
    "create_footprint", "create_symbol", "build_footprint", "build_symbol_modes", "add_body",
    "add_models", "normalize_bodies", "plate_vias", "set_params", "set_visibility", "read_symbol",
    "clone_symbol", "rename_symbol", "rename_footprint", "remove_symbol", "remove_footprint",
    "copy_footprint", "xcopy_footprint", "swap_footprint", "symbol_mode", "cleanup_libs",
    "install_library", "install_library_bundle", "install_receipt", "compile_intlib", "extract_intlib",
    "place_on_sch", "place_on_pcb", "import_part", "add_symbol_art", "symbol_prims", "set_models", "selftest_script_error", "install",
    // licence / account
    "license", "license_status", "ensure_licensed", "setup_trial", "signin", "signin_status",
    "connect_workspace",
    // Altium 365
    "a365_configure", "a365_oauth_begin", "a365_oauth_status", "a365_oauth_cancel",
    "a365_refresh", "a365_logout", "a365_status", "a365_query", "a365_files_upload",
    "a365_library_overview", "a365_library_schema",
];

pub fn names() -> Vec<String> {
    KNOWN.iter().map(|v| format!("altium_{v}")).collect()
}

/// One `describe` entry. ab's "Verbs" tab renders these verbatim.
fn verb_doc(name: &str, summary: &str, input: Value, output: Value, timeout: u64, hint: &str, related: &[&str], pitfalls: &[&str], example: Value) -> Value {
    json!({"name": format!("altium_{name}"), "summary": summary, "input": input, "output": output,
           "timeoutSeconds": timeout, "statusVerb": Value::Null, "longRunning": false,
           "hint": hint, "example": example,
           "related": related.iter().map(|r| format!("altium_{r}")).collect::<Vec<_>>(),
           "pitfalls": pitfalls})
}

/// A verb that can outlive ab's budget: the same doc, marked long-running with the verb to poll.
#[allow(clippy::too_many_arguments)]
fn verb_doc_long(name: &str, summary: &str, input: Value, output: Value, timeout: u64, status_verb: &str, hint: &str, related: &[&str], pitfalls: &[&str], example: Value) -> Value {
    let mut d = verb_doc(name, summary, input, output, timeout, hint, related, pitfalls, example);
    d["longRunning"] = json!(true);
    d["statusVerb"] = json!(format!("altium_{status_verb}"));
    d
}

fn describe() -> Value {
    let board = json!({"success": "bool", "data": "object - the agent's payload", "roundtripMs": "int"});
    let shot = json!({"success": "bool", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"});
    // A Vec, not a json! array literal: json! recurses once per element and 120+ entries would
    // sit on the macro recursion limit.
    let verbs: Vec<Value> = vec![
        verb_doc("status", "Is Altium running, licensed, and is the resident agent alive?", json!({}),
            json!({"running": "bool", "licensed": "bool", "licenceState": "string", "agentAlive": "bool"}), 15,
            "Cheapest health call - answers from ab's window list plus the agent heartbeat; never touches the script engine.",
            &["readiness", "agent_status", "instances"], &["'running' can be true while 'licensed' is false - a trial lease can be stranded by a crash."], json!({})),
        verb_doc("instances", "Every running Altium (Designer + Designer Develop can run together).", json!({}),
            json!({"count": "int", "instances": "[{pid, key, edition, licensed, agentAlive, ...}]"}), 15,
            "With several running, other verbs REFUSE without instance:<pid|key|word> - guessing means acting inside another AI thread's Altium.",
            &["status"], &["Develop writes NO licence tail when it HOLDS the seat - blank licenceState there means licensed."], json!({})),
        verb_doc("eval", "Run DelphiScript STATEMENTS inside Altium (Begin ... End;).", json!({"text": "string - statements only, no Var block", "timeout": "int seconds"}),
            json!({"success": "bool", "note": "string"}), 30,
            "Single quotes only; no Var block. Capture output by writing a file, not by returning a value.",
            &["run_command"], &["An undeclared identifier HALTS the engine with a modal that Try/Except cannot catch.", "ShowMessage blocks the loop behind a modal - never use it."], json!({"text": "Begin PCBServer.GetCurrentPCBBoard.ViewManager_FullUpdate; End;"})),
        verb_doc("run_command", "Run an Altium process (Server:Command) through the agent.", json!({"command": "string - e.g. PCB:Zoom", "params": "string or object"}),
            json!({"success": "bool"}), 60, "Params are pipe-joined key=value pairs. altium_list_commands searches the catalog.", &["eval", "list_commands"], &["Some processes open a modal dialog and block the loop until it is answered."], json!({"command": "PCB:Zoom", "params": {"Action": "All"}})),
        verb_doc("list_installed_libraries", "Every library in Altium's Installed Libraries list (read-only).", json!({}),
            json!({"count": "int", "libraries": "[path]"}), 30,
            "Check before and after altium_install_library / altium_uninstall_library without a screenshot.",
            &["uninstall_library", "install_library", "install_library_bundle"], &["Needs the agent (it reads IntegratedLibraryManager inside Altium)."], json!({})),
        verb_doc("uninstall_library", "Remove libraries from Altium's Installed Libraries: the inverse of install_library / install_library_bundle. Library files are never touched.",
            json!({"libraryPath": "exact registered path", "libraryName": "file stem: matches X.SchLib AND X.PcbLib", "dryRun": "bool"}),
            json!({"removed": "[path]", "closedDocuments": "[path]", "remaining": "int", "notInstalled": "bool"}), 60,
            "Any open document for a removed library is closed first. Close Altium with a plain altium_close afterwards: a close that discards changes can drop the unregistration.",
            &["list_installed_libraries", "install_library", "close"], &["Nothing matching answers notInstalled:true (success), so a cleanup can run twice.", "discardChanges:true on altium_close can bring the removed library back on the next launch."],
            json!({"libraryName": "Adom_ToF"})),
        verb_doc("panel", "Activate a docked panel TAB (SCH Library, PCB Library, Components, Projects, Properties) in the BACKGROUND - the 'ganged-up view' needs the right panels in front, and no Altium server process switches a tab.",
            json!({"name": "string - the tab caption exactly as painted, e.g. 'SCH Library'", "ensureId": "string optional - panel ID for Client:CustomizeResources (e.g. TSchLibPanel) when the panel may not exist yet"}),
            json!({"selected": "bool", "lane": "already | ui_select | ui_select_raised", "verified": "bool (false when the tab exposes no selection state)", "elapsedSec": "float"}), 30,
            "One UIA search reads the tab's existence AND selection, then at most one SelectionItemPattern select and one verify, all inside a 24 s budget: every exit is a named answer (panel_not_found, panel_search_timeout, ui_select_gated, panel_not_selected), never ab's generic timeout.",
            &["show_symbol", "show_footprint", "run_command"],
            &["ab gates desktop_ui_select behind the shell approval grant; without it the verb reports ui_select_gated instead of pretending.", "A plain click does not switch panel tabs - that is why this verb exists.", "On Altium Designer Develop the UI Automation search does not finish in time (panel_search_timeout, measured 2026-10-15); opening a library document already brings its library panel to the front.", "lane ui_select_raised means ab fell back to a synthetic click that foregrounded Altium; the dispatch wrapper hands the user's window back."],
            json!({"name": "SCH Library"})),
        verb_doc("read_board", "ONE round trip: components, nets, layers, copper, rules, occupancy, outline.", json!({"sections": "string csv - optional", "limit": "int"}), board.clone(), 120,
            "The primitive every board view derives from; board_summary / rules_by_kind / layers_in_use are shaped from the same read.",
            &["board_summary", "review_board"], &["Needs a .PcbDoc current."], json!({})),
        verb_doc("review_board", "Design-review checks (14) over one board read; schematic checks with includeSchematic.", json!({"board": "path - optional", "sheet": "path - optional", "includeSchematic": "bool", "checks": "[string] - optional filter"}),
            json!({"findings": "[{check, severity, message, ...}]", "counts": "{}", "skipped": "{}", "notImplemented": "{}"}), 120,
            "PARTIAL results by design: skipped says which checks had no data; a clean result means the checks that RAN found nothing.",
            &["read_board", "cross_check"], &["Altium holds ONE current document - the review activates board and sheet in turn."], json!({"includeSchematic": true})),
        verb_doc("import_part", "One call imports a whole part: footprint spec + STEP + symbol modes + models + params + save.", json!({"schLib": "path", "pcbLib": "path", "footprintSpec": "path", "symbolSpec": "path", "step": "path", "params": "{}", "comment": "string", "description": "string"}),
            json!({"symbol": "string", "footprint": "string", "steps": "[] - the add_body step carries replacedBodies, bodiesNow, sameNameFootprints"}), 300,
            "Check the step counts: models:0 fails the call - a symbol with no footprint model places with nothing attached. Importing again REPLACES the symbol and the footprint's 3D body (add_body step: bodiesNow 1); boards that place the part need PCB:UpdateFootprints.",
            &["read_symbol", "show_symbol"], &["add_models APPENDS on every call (not idempotent) - repair is remove_symbol + re-import.", "The add_body step's sameNameFootprints > 1 means the library holds stale footprints of that name: the body went to the FIRST only.", "The first part into a NEW library can silently skip models+params if the bridge restarted mid-flight - verify with read_symbol."], json!({})),
        verb_doc("test_board", "Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the existing board when pcbPath already exists (issue 11).",
            json!({"libraryPath": "path (.PcbLib)", "footprintName": "string | 'A;B;C'", "wMm": "number (new board only)", "hMm": "number (new board only)", "pcbPath": "path (.PcbDoc) - existing = append, new = create+save", "assigns": "des=NET;...", "stubs": "des=x,y;..."}),
            json!({"placed": "string", "count": "int", "pcb": "path", "saved": "bool (checked on disk)", "appended": "bool", "existingParts": "int", "padsOnComp": "int", "bodiesOnComp": "int"}), 240,
            "Three parts on one board: footprintName 'VL53L5CX;VL53L7CX;VL53L8CX' + pcbPath, then altium_show_3d_board {filePath: pcbPath}. Run it again with the same pcbPath to ADD parts: they land right of the current ones, designators continue (U4...).",
            &["place_on_sch", "show_3d_board", "add_body"], &["assigns/stubs apply to the LAST part placed.", "pcb is empty when pcbPath is omitted: the board exists only unsaved as PCB<n>.PcbDoc.", "Appending ignores wMm/hMm and does not grow the outline: parts can land outside it - resize with altium_board_shape if needed.", "Each part's name is its own Comment (moves with the part) and its Pattern is the footprint name.", "errorCode not_saved: the board was built but the file did not land; the folder is created automatically."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX;VL53L7CX;VL53L8CX", "wMm": 39, "hMm": 14, "pcbPath": "C:/proj/tof.PcbDoc"})),
        verb_doc("plate_vias", "Make every drilled pad in a PcbLib a plated MULTI-LAYER pad (all footprints, or footprintName). adom-lbr writes heatsink vias as pads with a hole; Altium loaded them on the Top Layer, so a thermal pad's vias were not through-holes until this ran.",
            json!({"libraryPath": "path (.PcbLib)", "footprintName": "string (optional filter)"}), json!({"holes": "int", "plated": "int", "fixed": "Fp:Pad;..."}), 90,
            "altium_save the PcbLib afterwards; footprint_primitives on a placed part should then report layerId 74 for the vias.", &["normalize_bodies", "footprint_primitives", "save"], &["Read-modify in place: run once per library install."], json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib"})),
        verb_doc("install", "Install Altium Designer on this machine, STAGED, with the rules in the verb (from 0.2.29). stage: preflight | get_installer | launch | wait | license, or omit it and 'auto' picks the stage from the machine's state. The verb looks before it asks, verifies the installer's Authenticode signature (Valid, CN=Altium LLC) before anything runs, refuses to launch into an empty room, toasts the human itself and says truthfully whether the toast displayed, and does the licensing clicks (Refresh / Use License) ITSELF.",
            json!({"stage": "auto (default) | preflight | get_installer | launch | wait | license", "installerPath": "optional path; default = newest AltiumDesignerSetup*.exe in Downloads", "humanIsPresent": "bool, REQUIRED true for stage launch: the UAC prompt auto-denies in ~2 minutes"}),
            json!({"stage": "string", "needsHuman": "[string] - only ever a password, the UAC prompt, or clicks in an elevated window", "toast": "{displayed, _hint}", "licensed": "bool", "_next": "[string]"}), 120,
            "Call it with no args and follow _next. The human's part ENDS at the installer's Finish button; licensing is yours.",
            &["ensure_licensed", "license", "launch", "readiness"],
            &["needsHuman may only name a password/2FA, the UAC prompt, or clicks inside an ELEVATED window. Release, Assign, Use License, Refresh and save prompts are ordinary clicks: do them.",
              "LOOK before you ask: 'Sign Out' on the licence page means the user is ALREADY signed in (the installer's browser sign-in carries over).",
              "'There are no seats available' means another running Altium holds the account's single On-Demand seat: altium_close it there, then stage license again.",
              "Never use the installer's unattended switches: they take the account password on the command line.",
              "A toast only reaches the machine it is shown on. Check toast.displayed and ALSO tell the user in chat."],
            json!({"stage": "launch", "humanIsPresent": true})),
        verb_doc("selftest_script_error", "Regression test for the script-error safety net (from 0.2.23): makes the agent raise a REAL DelphiScript runtime error on purpose and checks that the bridge photographs Altium's Error box, stops the halted script itself and leaves no dialog behind. Flashes one error box for under a second. Run it after any change to agent.rs / dialogs.rs and once on every new Altium build.",
            json!({}), json!({"netWorks": "bool", "outcome": "halted_and_recovered | contained_by_agent | unhandled", "errorShot": "base64 png (halted outcome only)", "autoRecovered": "bool", "dialogsLeft": "int"}), 180,
            "netWorks:true is the pass. The agent is stopped afterwards by design; the next verb restarts it.", &["agent_recover", "dialog_log", "agent_status"],
            &["It really does halt the engine for a moment: do not run it while a user is mid-edit in Altium.",
              "By design this is the SLOWEST verb in the catalog: it halts the engine, waits for the halt to be seen, recovers it and verifies by restarting the agent. Altium Designer Develop parks SILENTLY (no Error box), so the halt is only seen when the wait expires — measured 18 s to over 60 s on the same machine. Its budget is 180 s for that reason; a timeout here means the safety net is broken, not that it was slow."], json!({})),
        verb_doc("symbol_prims", "Read a SchLib component's drawing: primitive counts by kind, body rectangle, polyline bounding box, display mode and the footprint models. Read-only, use it to verify add_symbol_art / set_models.",
            json!({"libraryPath": "path (.SchLib)", "symbolName": "string", "models": "bool, default false (the model walk halts the engine on some builds)"}), json!({"pins": "int", "rectangles": "int", "lines": "int", "polylines": "int", "bodyRectMil": "[l, b, r, t] - the union of the current display mode's rectangles (mode 0, as a sheet draws it)", "polyBboxMil": "[..]", "models": "[\"PCBLIB:NAME\"]"}), 60,
            "polylines should equal what add_symbol_art reported and polyBboxMil should sit inside bodyRectMil.", &["add_symbol_art", "set_models", "read_symbol"], &[], json!({})),
        verb_doc("set_models", "Replace every footprint model on a SchLib component with ONE PCBLIB model (set_params footprint and add_models APPEND, which is how a symbol ends up with two).",
            json!({"libraryPath": "path (.SchLib)", "symbolName": "string", "footprint": "string (PcbLib footprint name)"}), json!({"removed": "int", "footprint": "string"}), 60,
            "altium_save the SchLib afterwards.", &["symbol_prims", "set_params", "add_models"], &["Removes every existing model, including 3D-only ones."], json!({})),
        verb_doc("add_symbol_art", "Draw the chip-outline artwork (the chip-thumbnailer polylines adom-symbol baked into the KiCad symbol) into a SchLib component, fitted to its body rectangle, so the Altium symbol shows the same chip as KiCad and Fusion.",
            json!({"libraryPath": "path (.SchLib)", "symbolName": "string (LibReference)", "kicadSym": "Windows path to the .kicad_sym that carries the baked outline (stage it with send_files)", "clear": "bool, default true: remove the component's existing polylines/lines first", "mode": "lines (default) | polyline"}),
            json!({"polylines": "int", "points": "int", "bodyRectMil": "[x0,y0,x1,y1]"}), 180,
            "Run once per part right after assemble-altium / build_symbol_modes, then altium_save the SchLib and altium_show_symbol to check.",
            &["show_symbol", "symbol_prims", "save"], &["clear:true (default) also removes hand-drawn lines on the symbol; only the pins and the body rectangle survive.", "The KiCad symbol must already carry the outline (adom-symbol create bakes it; --outline-name none for side-etched chips)."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "kicadSym": "C:/lib/VL53L5CX.kicad_sym"})),
        verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sheet sized FROM the measured parts: 500 mil gaps, 600 mil margins, A4 minimum; parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given. When schPath names an EXISTING .SchDoc the symbols are APPENDED to it (right of the current ones, designators continuing; sheet size and title block untouched, issue 11).",
            json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)", "title": "string (Title)", "number": "string (DocumentNumber)", "revision": "string", "drawnBy": "string", "description": "string (what the sheet shows, 2-3 sentences)"}),
            json!({"placed": "string", "count": "int", "designator": "string", "primitives": "int", "sch": "path"}), 90,
            "Three parts on one sheet: symbolName 'VL53L5CX;VL53L7CX;VL53L8CX' + schPath, then altium_show_schematic {filePath: schPath}.",
            &["test_board", "show_schematic", "read_schematic"], &["Every call creates a NEW SchDoc; there is no append-to-sheet yet.", "The SchLib must be the current document (the verb opens it)."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX;VL53L7CX;VL53L8CX", "schPath": "C:/proj/tof.SchDoc"})),
        verb_doc("show_symbol", "Open a SchLib member in the Symbol editor and capture a background proof.", json!({"libraryPath": "path", "symbolName": "string"}), shot.clone(), 60,
            "Never foregrounds: a minimized Altium is restored INTO THE BACKGROUND and the user's window handed straight back.",
            &["show_footprint", "show_3d_chip", "show_library"], &["A capture under 400px wide is refused as proof_unavailable - a title-bar sliver is not evidence."], json!({})),
        verb_doc("animate_view", "Camera/zoom choreography on the current document - native Altium camera commands, animated by Altium itself.",
            json!({"preset": "punch_in|showcase3d|orbit|flip|finale", "steps": "[string | {step, pauseMs}]", "lane": "pcb (default) | sch", "pauseMs": "int default per-step pause"}),
            json!({"stepsRun": "int", "elapsedSec": "float"}), 120,
            "Runs IN-BRIDGE (~40ms per step to the agent) so timing is tight enough to narrate over. Steps: zoom_all/in/out, view_2d/3d, isometric, top/bottom/front/back/left/right, rotate90, ortho, orbit_left/right/up/down, rolls, pans, flip.",
            &["demo", "foreground", "run_command"],
            &["Driving camera steps one-by-one from a container adds ~1.4s of jitter per step - that is what this verb exists to avoid.",
              "sch lane supports zoom steps only; everything else needs a PCB document current."],
            json!({"preset": "showcase3d"})),
        verb_doc("foreground", "Bring Altium in front of the user - the sanctioned exception to the never-foreground rule.",
            json!({"reason": "string >=10 chars, written for the HUMAN (required)"}),
            json!({"windowEtiquette": "foreground"}), 15,
            "For a human who asked to WATCH (a live demo, a walkthrough). Everything else stays background.",
            &["demo", "animate_view"],
            &["Refused without a real reason - same rule as ab's desktop_caption."],
            json!({"reason": "John asked to watch the import live"})),
        verb_doc("demo", "The tour. Default: one background beat per call with a screenshot. live:true = the narrated FOREGROUND show: ab caption + document + in-bridge camera choreography per stop, returning the narration text for YOUR TTS.",
            json!({"stage": "int 0-5", "live": "bool"}),
            json!({"narrate": "string - speak this", "nextNarrate": "string", "nextCall": "{verb, args}", "done": "bool"}), 180,
            "live SYNC MODEL: pre-cache narration with `adom-tts load` (one marker per stop), then per stop fire `adom-tts cue <id> <n> --confirm` in the background and IMMEDIATELY make nextCall - the prefetched cue starts instantly and the ~5-17s of on-screen motion runs under the voice; advance when BOTH finish. If Altium is cold, the first call answers status:launching with a narrate line - speak it, poll again in ~20s.",
            &["animate_view", "foreground", "show_symbol"],
            &["The caller owns the VOICE (adom-tts) - the bridge never synthesizes speech.",
              "After done:true, restore the user's own document (altium_activate_document)."],
            json!({"live": true, "stage": 0})),
        verb_doc("license", "Drive the License Management page (UIA first - no cursor, no foreground).", json!({"action": "show|refresh|use_license|start_trial|sign_out"}), shot.clone(), 60,
            "One On-Demand seat serves EVERY Altium app on this machine. use_license claims a free seat.",
            &["ensure_licensed", "license_status", "close"], &["NEVER kill an Altium holding the seat: the lease strands, the page shows Seats 0/1 while every claim fails. altium_close releases it."], json!({"action": "use_license"})),
        verb_doc("close", "Polite close (releases the licence seat). Frees a script engine parked on a breakpoint first (Altium refuses to close over one). discardChanges:true answers Altium's save prompts with Save None for callers whose modified documents are ALL their own; force needs acceptSeatLoss.", json!({"discardChanges": "bool, default false: press Save None + confirm on 'Confirm Save Locally'", "waitSec": "float, default 20", "force": "bool", "acceptSeatLoss": "bool", "instance": "pid, when several Altiums run"}), json!({"closedInSec": "float", "engineFreed": "object|null", "stuckModalOrphan": "{pid,tid,hwnd} when Altium was wedged in a modal loop and the bridge ended it gracefully", "errorCode": "unsaved_changes | close_blocked_by_dialog | altium_modal_orphan | modal_orphan_survived_quit | close_timeout"}), 120,
            "A save prompt on close NAMES the documents - read it; on a shared machine one may belong to another AI thread. Never defaulted.",
            &["license", "launch"], &["force:true without acceptSeatLoss is refused: a kill strands the On-Demand seat.",
              "errorCode altium_modal_orphan = Altium is stuck inside a modal loop whose dialog is GONE (its main window is disabled, nothing on screen to answer). No menu, keystroke or -R reaches it and it still holds the seat. With discardChanges:true the bridge ends it by posting WM_QUIT - Altium's own shutdown, which RELEASES the seat. Never reach for force/taskkill first."], json!({})),
        verb_doc_long("install_library_bundle", "One-click install of a wiki library bundle (zip of .SchLib/.PcbLib): unpack, register, open the library, prove it with a frame.",
            json!({"zipPath": "string - staged zip", "libraryName": "string", "capture": "bool", "captureMaxWidth": "int", "installId": "string - echoed in the receipt so a client can match it"}),
            json!({"shown": "bool", "symbolCount": "int", "components": "[names]", "shot": "{image, ...}", "thumbnailDataUrl": "string", "phasesMs": "{launch, licence, unpack, register, open, list, panel, capture, agentStop}", "totalMs": "int", "installId": "string", "atMs": "int"}),
            330, "status",
            "Launches a closed Altium first (up to 240s), then registers and opens the library. Past ab's budget the verb keeps running: poll altium_status until lastInstall.installId matches, then altium_install_receipt for the full receipt.",
            &["install_receipt", "status", "install_library"], &["The receipt lives in bridge memory; a bridge restart clears it."],
            json!({"zipPath": "C:\\Users\\me\\adom-wiki-libs\\Lib-altium.zip", "libraryName": "Lib", "capture": true, "installId": "Lib-1757350000000"})),
        verb_doc("install_receipt", "The last bundle install's full receipt (proof frame included), for a client whose install call outlived ab's budget.", json!({}),
            json!({"receipt": "object|null - the install_library_bundle reply"}), 10, "Read after altium_status shows lastInstall with your installId.", &["install_library_bundle", "status"], &[], json!({})),
        verb_doc("describe", "This list - the bridge's verbs and schemas for ab's Verbs tab.", json!({}), json!({"verbs": "[{name, summary, input, output, ...}]"}), 5, "Static; safe to call any time.", &["status"], &[], json!({})),
        // ---- health / lifecycle ----
        verb_doc("readiness", "The one-call gate: is Altium installed, running, licensed, and is the resident agent alive? Answers with Altium closed and never touches the script engine.", json!({}),
            json!({"installed": "bool", "install": "{base_dir, exe, system_dir, major} | null", "running": "bool", "licensed": "bool | null", "licenceState": "string", "adVersion": "string | null", "agentAlive": "bool - the selected instance", "agentResidentAnywhere": "bool - any workspace", "ready": "bool - all four true", "agentLiveWorkspaces": "[{workspace, heartbeatAgeSec}] - only when a loop lives outside the selection"}), 15,
            "Follow _hint: not installed -> altium_install; not running -> altium_launch; licensed:false -> altium_ensure_licensed; agent down -> just call the verb you need (it bootstraps the agent) or altium_agent_start. ready:true -> altium_state.",
            &["status", "launch", "ensure_licensed", "agent_start"],
            &["agentAlive is the SELECTED instance's view. agentResidentAnywhere:true beside agentAlive:false means a loop lives in another workspace: close through altium_close, never the window X."], json!({})),
        verb_doc("diagnostics", "Bridge-side facts for a bug report: environment variables, every Altium install found, Altium's windows, the agent status block and the cache directory.", json!({}),
            json!({"env": "{LOCALAPPDATA, APPDATA, USERPROFILE, ProgramFiles, ADOM_BIND_HOST, ADOM_DIRECT_API_URL}", "installs": "[{base_dir, exe, system_dir, major}] newest major first", "windows": "[{hwnd, title, kind, className, pid, rect}]", "agent": "the altium_agent_status reply", "cacheDir": "path"}), 15,
            "installs:[] means Altium is not installed (altium_install). Otherwise altium_readiness for the verdict and altium_instances when several Altiums run.",
            &["readiness", "agent_status", "instances"], &["Instance-exempt: it runs with no instance selected, so agent.alive reads the legacy workspace; agent.liveWorkspaces is the union that counts."], json!({})),
        verb_doc("window_info", "Altium's top-level windows plus any open dialogs, without screenshots. The cheap way to ask 'is a modal blocking the engine?'.", json!({}),
            json!({"windows": "[{hwnd, title, kind, className, pid, rect}]", "dialogs": "[{hwnd, title, body, buttons, category, safeAction, ownedByMainInstance}]", "hasModalDialogs": "bool - a dialog owned by the main instance"}), 15,
            "hasModalDialogs:true means the agent loop and every -R request are frozen behind it: altium_check_dialogs for the pictures, then altium_dismiss_dialogs {button, titles}. Otherwise the main window's title names the current document.",
            &["check_dialogs", "dismiss_dialogs", "state"], &["Altium PAINTS its dialog text, so body is often empty here; altium_check_dialogs attaches a PNG per dialog."], json!({})),
        verb_doc("state", "The whole picture in one call: licence state, windows, dialogs, a screenshot of every main/dialog window, and the agent's logical state (focused document, focused project, open documents).",
            json!({"screenshots": "bool, default true"}),
            json!({"licence": "{running, licensed, state, adVersion, edition, document, hwnd, pid}", "windows": "[..]", "dialogs": "[..]", "shots": "[{title, hwnd, kind, image: base64 png, width, height}]", "logical": "agent reply; data = {focusedDocument, focusedProject, documents:[{path, kind, project, open}]} | null when no agent is alive", "agentAlive": "bool - union over every workspace", "agentWorkspace": "path | null - where logical came from", "liveWorkspaces": "[{workspace, heartbeatAgeSec}]", "drivingBadge": "{active}"}), 60,
            "Look before acting: logical.data.focusedDocument is what the board/schematic read verbs would read; altium_activate_document {path} changes it. Poll with screenshots:false.",
            &["status", "window_info", "activate_document", "agent_start"],
            &["logical is null when no agent is resident: state never bootstraps one. Call any agent-backed verb or altium_agent_start first.",
              "shots[] are full-size base64 PNGs of every main and dialog window; pass screenshots:false when you only need the data."], json!({"screenshots": false})),
        verb_doc("check_dialogs", "List every Altium dialog currently open, classified, each with a screenshot (the only way to READ one: Altium paints its dialog text).",
            json!({"screenshots": "bool, default true"}),
            json!({"dialogsDetected": "int", "dialogs": "[{hwnd, pid, title, className, body, buttons, category, safeAction, meaning, ownedByMainInstance, rect, shot: base64 png}]"}), 30,
            "READ dialogs[].shot, decide, then altium_dismiss_dialogs {button, titles:[..]}. A script 'Error' box: altium_agent_recover. A save prompt on close: see altium_close discardChanges.",
            &["dismiss_dialogs", "dialog_log", "agent_recover", "window_info"],
            &["category and safeAction are suggestions. A save_prompt is never auto-answered: it names documents that may belong to the user or another AI thread.",
              "Known nuisance boxes are pressed by the standing reaper before you ever see them; altium_dialog_log lists those."], json!({})),
        verb_doc("dismiss_dialogs", "Press one named button (default OK) on the detected Altium dialogs through UIA - no cursor, no focus steal. titles narrows it to dialogs whose title contains one of the strings.",
            json!({"button": "string, default 'OK' - the button's accessible name", "titles": "[string] - optional title substrings; only matching dialogs are pressed"}),
            json!({"detected": "int", "pressed": "[{title, hwnd, button, ok}]"}), 30,
            "ALWAYS pass titles. Then altium_check_dialogs to confirm nothing is left, and repeat the verb that was blocked.",
            &["check_dialogs", "close", "agent_recover"],
            &["Without titles the button is pressed on EVERY detected Altium dialog, save prompts included.",
              "hwnd is NOT read by this verb, although other verbs' hints say {hwnd, button}: target a dialog with titles.",
              "success is true even when nothing was pressed or the click failed: check pressed[].ok."],
            json!({"button": "OK", "titles": ["Error"]})),
        verb_doc("launch", "Start Altium (newest install) and wait for its main window, then hand the user's own window back. Answers alreadyRunning when one is up.",
            json!({"waitSec": "number, default 180", "background": "bool, default TRUE: remember the user's foreground window and restore it once Altium is up. Pass false only when the user asked to SEE Altium come up."}),
            json!({"alreadyRunning": "bool", "startedInSec": "float", "licence": "{running, licensed, state, adVersion, edition, hwnd, pid}", "foregroundReturnedTo": "hwnd|null", "windowEtiquette": "background once the user's window is handed back; foreground only with background:false"}), 240,
            "Cold start is about 60 s. Next: altium_readiness; licensed:false -> altium_ensure_licensed. Most verbs need this first (errorCode altium_not_running).",
            &["readiness", "ensure_licensed", "close", "install"],
            &["Altium foregrounds ITSELF at startup and offers no switch for it, so the window jumps over the user's work for a few seconds even with background:true — the bridge can only hand the focus back afterwards. Call it once; never in a retry loop.",
              "errorCode host_app_not_installed -> altium_install. launch_timeout = no main window after waitSec.",
              "A 'Workspace Unavailable' box at launch (altium_dialog_log) means another running Altium holds the seat: this one is view-only and opens fail with view_only_no_author_seat."],
            json!({})),
        verb_doc("uninstall", "Remove the bridge's own cache directory (agent workspaces, rendered agent scripts, the Altium 365 token file). Altium Designer and the user's designs are never touched.",
            json!({"dryRun": "bool - report only, delete nothing"}),
            json!({"removed": "[path]", "dryRun": "bool", "note": "string"}), 30,
            "This is the manifest's uninstall hook; nothing to call afterwards. To revoke the Altium 365 grant remotely call altium_a365_logout BEFORE this.",
            &["a365_logout", "agent_stop"], &["A resident agent is stopped even with dryRun:true; only the delete is skipped."], json!({"dryRun": true})),
        verb_doc("dialog_log", "What the standing dialog reaper (and the bridge's own recoveries) pressed, newest last: nuisance boxes, script-error boxes, discarded save prompts, silent engine halts cleared.",
            json!({"limit": "int, default 25"}),
            json!({"reaped": "[{title, category, pressed, ok, note, at}]", "running": "bool"}), 10,
            "Read it after a surprise: enginePreviouslyParked on a reply, an agent_script_error, or an open that failed view-only ('Workspace Unavailable' shows here). A dialog that reached the user and is NOT listed is a catalog gap: report it.",
            &["check_dialogs", "status", "agent_recover"], &["An in-memory ring of the last 200 events: a bridge restart clears it.", "Save prompts and unknown dialogs never appear here as pressed: the reaper does not touch them."], json!({"limit": 10})),
        // ---- agent / REPL ----
        verb_doc("agent_start", "Bootstrap the resident DelphiScript agent inside Altium (renders the script, runs it through the -R lane, waits for the heartbeat). Optional: every agent-backed verb does this on demand.",
            json!({"waitSec": "int, default 45 - how long to wait for the first heartbeat"}),
            json!({"startedInSec": "float", "alreadyRunning": "bool", "generation": "string - AdomAgent_<digest>", "install": "object - the rendered script", "strayHelpersKilled": "[pid]"}), 120,
            "After success altium_eval / altium_run_command / altium_state go through the agent in milliseconds. Do NOT use altium_run_script while it is up.",
            &["agent_status", "agent_stop", "agent_recover", "eval"],
            &["The bootstrap ACTIVATES Altium for a moment (it opens the .PrjScr in the script editor); the bridge hands the user's foreground window back.",
              "no_heartbeat = the engine halted behind a script Error modal: altium_check_dialogs, then altium_agent_recover.",
              "The loop exits by itself after 5 minutes idle (residency is the freeze-on-close exposure window); the next verb restarts it."], json!({})),
        verb_doc("agent_stop", "Stop the selected instance's resident agent loop (drops a stop file and waits up to 12 s for the heartbeat to go stale).", json!({}),
            json!({"wasRunning": "bool", "stoppedInSec": "float"}), 20,
            "Needed before altium_run_script (the -R lanes refuse while a loop is resident). altium_close stops every workspace's loop itself. The next agent-backed verb restarts the agent.",
            &["agent_start", "agent_status", "run_script", "close"], &["'the loop did not exit' -> altium_agent_recover.", "With several Altiums running pass instance: to stop the right loop."], json!({})),
        verb_doc("agent_status", "Is the agent loop alive and responsive, in which workspace, and is any loop resident anywhere? Read-only; never starts the agent.", json!({}),
            json!({"alive": "bool - selected instance", "responsive": "bool", "residentAnywhere": "bool", "liveWorkspaces": "[{workspace, heartbeatAgeSec}]", "heartbeat": "{ageSec, tick, ...} | null", "workspace": "path"}), 10,
            "alive with responsive:false = the loop is busy inside a long Altium process: wait, do not start a second agent. Not alive: call the verb you need, it bootstraps.",
            &["agent_start", "agent_recover", "readiness"], &["residentAnywhere:true with alive:false is a freeze-on-close hazard: altium_close tears every loop down before the app exits."], json!({})),
        verb_doc("agent_recover", "Un-halt the script engine: clear the PCB transaction, press OK on open Altium dialogs, send EditScript:Stop, reap the busy notice and delete stale request files.",
            json!({"reason": "string - optional, recorded in steps"}),
            json!({"steps": "[{reason} | {dialog, pressed} | {editScriptStop, exited, sec} | busy-notice reap]"}), 60,
            "Use after agent_timeout / no_heartbeat / instance_busy, or when Altium sits on AdomAgent_*.pas in its debugger. Then call your verb again: it restarts the agent. Never ask the user to click.",
            &["check_dialogs", "agent_start", "dialog_log", "selftest_script_error"],
            &["A modal orphan (main window disabled, no dialog on screen) answers nothing: recover refuses with altium_modal_orphan instead of firing -R hand-offs that all bounce off 'another instance is busy'. Escape it with altium_close {discardChanges:true}.",
              "It presses OK on EVERY Altium dialog it finds, not only the script Error box: altium_check_dialogs first if a save prompt may be up.",
              "An agent_script_error reply was ALREADY recovered by the bridge (autoRecovered): do not recover again, and do not resend the same command - it will halt again."],
            json!({"reason": "agent_timeout on read_board"})),
        verb_doc("run_script", "One shot: run a procedure of YOUR OWN .PrjScr through Altium's -R command line. Only for when the agent is NOT resident.",
            json!({"prjscr": "absolute path to a .PrjScr on this machine (required)", "proc": "string, default 'Main'", "timeout": "seconds, default 60"}),
            json!({"helperSec": "float", "arg": "string - the -R argument sent", "modalsPreempted": "[{title, body}] - bridge-caused boxes cleared in flight"}), 60,
            "While the agent is resident use altium_eval / altium_run_command instead (or altium_agent_stop first). Scripts return nothing: have yours write a file and read that.",
            &["eval", "run_command", "agent_stop", "agent_recover"],
            &["Refused with agent_resident while ANY agent loop is alive: a second script raises 'Another script executing now' on the user's screen.",
              "ONE SHOT: instance_busy means the running instance did not accept the request. Do not retry in a loop; each attempt costs the user a busy notice."],
            json!({"prjscr": "C:/scripts/MyTool.PrjScr", "proc": "Main"})),
        verb_doc("list_commands", "Search Altium's process-launcher catalog (about 1,100 Server:Command entries parsed from the install's System/*.rcs files) by text and/or server.",
            json!({"query": "string - substring over command, captions, descriptions and params", "server": "string - e.g. PCB, Sch, WorkspaceManager", "limit": "int, default 50"}),
            json!({"count": "int", "total": "int - distinct commands", "servers": "{server: count}", "hits": "[{command, server, captions, shortcuts, params, descriptions}]"}), 30,
            "Run a hit with altium_run_command {command, params}; hits[].params shows the Name=Value pairs Altium's own menus pass.",
            &["run_command", "eval"], &["Reads files, not Altium: works with Altium closed but answers host_app_not_installed without an install.", "Being listed does not make a process safe: some open a modal that blocks the agent (PCB:DesignRuleCheck - use altium_run_drc)."],
            json!({"query": "zoom", "server": "PCB"})),
        verb_doc("open_document", "Open a document or project by path through the agent and show it without focusing it. An already-open document is activated, not reloaded.",
            json!({"filePath": "absolute Windows path (alias: path)", "timeout": "seconds; the wait is capped at 45"}),
            json!({"data": "{path, objectKind, kind, shown: bool, lane}", "status": "'in_progress' when Altium is still opening it", "roundtripMs": "int", "windowEtiquette": "background"}), 60,
            "Then the verb for that document: altium_read_board / altium_read_schematic / altium_list_library. status:in_progress -> poll altium_status or altium_window_info until it is current.",
            &["activate_document", "close_document", "state", "show_2d_board"],
            &["Only .PcbDoc .SchDoc .PcbLib .SchLib .OutJob .Pcb3DLib .Prj* .LibPkg .txt .pas .rep are accepted; anything else is refused as unsupported_document (Altium would raise a 'File type not recognised' modal).",
              "success:true with data.shown:false on a non-project means the document did NOT open (this verb does not turn that into an error). In a view-only Altium that is every new document: see view_only_no_author_seat."],
            json!({"filePath": "C:/proj/board.PcbDoc"})),
        verb_doc("open_project", "Open a project (.PrjPcb / .PrjScr / .PrjMbd / .PrjHar / .LibPkg) by path. Same handler as altium_open_document.",
            json!({"filePath": "absolute Windows path (alias: path)", "timeout": "seconds; the wait is capped at 45"}),
            json!({"data": "{path, objectKind: 'Project', shown: false, note}", "roundtripMs": "int", "windowEtiquette": "background"}), 60,
            "Then altium_compile {projectPath} for ERC, or altium_open_document one of its sheets. altium_state lists the project's documents.",
            &["open_document", "compile", "show_project", "state"], &["data.shown:false is NORMAL for a project: projects have no document view."], json!({"filePath": "C:/proj/board.PrjPcb"})),
        verb_doc("close_document", "Close ONE open document by path through its document handle. Never saves.",
            json!({"path": "absolute path of the open document (required)"}),
            json!({"data": "{closed: bool, path} | {closed: false, note: 'not open: <path>'}", "roundtripMs": "int"}), 30,
            "altium_save {filePath} first if anything is worth keeping. After re-running altium_place_on_sch / altium_test_board to the same path, close the stale document before altium_show_*.",
            &["save", "open_document", "state"], &["Unsaved edits are DISCARDED without a prompt.", "success:true with closed:false means the document was not open."], json!({"path": "C:/proj/tof.SchDoc"})),
        verb_doc("activate_document", "Make a document current - the quiet lane (one in-process agent call, no cursor, no OS foreground). Board and schematic read verbs act on the CURRENT document.",
            json!({"path": "full path of the document (aliases: document, name)", "timeout": "seconds; the wait is capped at 45"}),
            json!({"data": "{path, objectKind, kind, shown}", "status": "'in_progress' when still opening", "lane": "agent", "windowEtiquette": "background"}), 60,
            "Activate the .PcbDoc, then altium_read_board; activate the .SchDoc, then altium_read_schematic. After a demo or a show_* call, activate the user's own document again.",
            &["open_document", "state", "read_board", "read_schematic"], &["Needs the FULL path with a supported extension; a bare document name is refused.", "It is the same agent 'open': a path that is not open yet gets opened."], json!({"path": "C:/proj/board.PcbDoc"})),
        verb_doc("progress", "The bridge's timing book: phases in flight right now (percent/ETA blocks ab paints on the taskbar), measured phase history and per-verb run statistics.", json!({}),
            json!({"active": "[progress blocks of phases in flight]", "history": "{phase: {warm|cold: {samples, p50, p90}}}", "verbTimes": "{verb: {count, p50, p90, max, failures}} - p90 descending, top 40"}), 10,
            "Use verbTimes.p90 to choose a timeout for a slow verb on THIS machine, and active[] to see what a long call is doing. Never touches Altium.",
            &["status", "describe"], &[], json!({})),
        // ---- board / schematic reads ----
        verb_doc("board_summary", "One-screen summary of the current board, shaped from one read_board round trip: component counts by side, nets, copper layers actually in use, rule counts.",
            json!({"sections": "string csv - optional", "limit": "int, default 2000", "raw": "bool - return the unshaped agent payload"}),
            json!({"summary": "{board, components, componentsTop, componentsBottom, nets, copperLayersInUse, copperLayersInUseNames, layersWithPrimitives, copperLayersAddressable, rules, rulesEnabled}", "roundtripMs": "int"}), 120,
            "Start here on an unknown board, then altium_review_board for findings or altium_component_data / altium_net_connections for detail.",
            &["read_board", "layers_in_use", "rules_by_kind", "review_board"],
            &["Needs a .PcbDoc current (altium_activate_document).", "copperLayersAddressable is what Altium CAN address (top + 30 mid + 16 planes + bottom), not this board's stack: report copperLayersInUse."], json!({})),
        verb_doc("rules_by_kind", "The current board's ENABLED design rules grouped by rule kind (Clearance, Width, RoutingVias, ...), each group sorted by priority. Shaped from one read_board round trip.",
            json!({"sections": "string csv - optional", "limit": "int, default 2000"}),
            json!({"board": "path", "rulesByKind": "{KindName: [{name, kind, enabled, priority, scope1, scope2, comment}]}", "roundtripMs": "int"}), 120,
            "Use it to answer 'what clearance/width does this board enforce'. altium_read_rules gives the flat list including disabled rules; altium_run_drc checks the board against them.",
            &["read_rules", "run_drc", "board_summary"], &["Needs a .PcbDoc current.", "Disabled rules are dropped; a rule kind the bridge has no name for appears as 'kind <n>'."], json!({})),
        verb_doc("layers_in_use", "Which layers of the current board actually carry primitives: copper layers with copper (densest first) and every layer with anything on it.",
            json!({"sections": "string csv - optional", "limit": "int, default 2000"}),
            json!({"board": "path", "copperLayersInUse": "[{layerId, name, primitives}]", "layersWithPrimitives": "[{layerId, name, primitives}]", "roundtripMs": "int"}), 120,
            "The honest answer to 'how many layers is this board'. altium_set_layer_visibility {layerId} to isolate one, then altium_show_2d_board for a frame.",
            &["board_summary", "layers", "copper_layers", "set_layer_visibility"], &["Needs a .PcbDoc current.", "A floor, not the stack: an empty inner plane carries no primitives and is invisible here."], json!({})),
        verb_doc("read_schematic", "Components on the CURRENT schematic sheet (designator, library reference, comment, position, parameters) in one agent round trip.",
            json!({"sections": "string csv, default 'components'", "params": "bool, default true - include component parameters", "limit": "int, default 500"}),
            json!({"document": "string", "count": "int", "designators": "[string]", "components": "[{designator, libReference, comment, ...}]", "roundtripMs": "int"}), 90,
            "Then altium_cross_check {board, sheet} to compare against the layout, or altium_schematic_data {designator} for one part's pins.",
            &["schematic_data", "cross_check", "show_schematic", "activate_document"],
            &["The CURRENT sheet only: a hierarchical parent holds sheet symbols, not parts. Read each leaf sheet.", "Needs a .SchDoc current; 'no current schematic' -> altium_open_document / altium_show_schematic."], json!({})),
        verb_doc("cross_check", "Compare one schematic sheet's designators with a board's. Altium holds ONE current document, so the verb activates the board, reads it, then activates the sheet and reads that.",
            json!({"board": "path to the .PcbDoc - optional, else the current board", "sheet": "path to the .SchDoc - optional, else the current sheet", "timeout": "seconds per step, default 45"}),
            json!({"sheet": "string", "board": "path", "sameProject": "bool | null - same folder", "matched": "int", "onSheetNotOnBoard": "[designator]", "onBoardNotOnSheet": "[designator]", "schematicComponents": "int", "boardComponents": "int"}), 120,
            "Pass BOTH paths: with neither, one of the two reads has no current document and the call fails at stage board or sheet. Follow differences with altium_component_data / altium_schematic_data.",
            &["read_board", "read_schematic", "review_board"],
            &["The schematic side is ONE sheet: on a hierarchical design run it per leaf sheet before calling anything missing.", "_warning means sheet and board sit in different folders, probably different projects: every difference is then meaningless.", "With paths given it leaves the SHEET current; altium_activate_document the board again before board verbs."],
            json!({"board": "C:/proj/board.PcbDoc", "sheet": "C:/proj/main.SchDoc"})),
        verb_doc("list_designators", "Every component designator on the current board.", json!({}),
            json!({"data": "{count, designators: [string]}", "roundtripMs": "int"}), 90,
            "Then altium_component_data {designator} or altium_component_pins {designator}.",
            &["component_data", "component_pins", "read_board"], &["Needs a .PcbDoc current: 'no current PCB board' carries the _hint to open one."], json!({})),
        verb_doc("component_data", "Placement data for one board component, or for all of them: footprint pattern, comment, layer/side, position, rotation, size, pad count and the nets on its pads.",
            json!({"designator": "string - optional; omit for every component", "limit": "int, default 500"}),
            json!({"data": "{component: {designator, pattern, comment, layerId, bottom, xMil, yMil, rotation, wMil, hMil, pads, nets}} | {count, components: [..]}", "roundtripMs": "int"}), 90,
            "Then altium_component_pins {designator} for pad-level nets, or altium_footprint_primitives {designator} for the geometry.",
            &["list_designators", "component_pins", "footprint_primitives"], &["Needs a .PcbDoc current.", "Units are mils; the designator match ignores case. limit only applies to the all-components form."], json!({"designator": "U1"})),
        verb_doc("component_pins", "Every pad of one board component with its net, position, layer, size and hole.",
            json!({"designator": "string (required), e.g. U1"}),
            json!({"data": "{designator, count, pins: [{name, net, xMil, yMil, layerId, wMil, hMil, holeMil}]}", "roundtripMs": "int"}), 90,
            "Use pins[].net with altium_net_connections {net} to follow a signal across the board.",
            &["component_data", "net_connections", "footprint_primitives"], &["Needs a .PcbDoc current.", "holeMil 0 = an SMD pad."], json!({"designator": "U1"})),
        verb_doc("list_nets", "Every net on the current board with its pad count.", json!({}),
            json!({"data": "{count, nets: [{name, pads}]}", "roundtripMs": "int"}), 90,
            "Then altium_net_connections {net} for the pads of one net.",
            &["net_connections", "read_board"], &["Needs a .PcbDoc current.", "Pad counts are tallied over pads: a net with no pads on it does not appear."], json!({})),
        verb_doc("net_connections", "Every pad on the board with its net and owner (Designator.Pad), plus a bounding span per net. net filters to one net.",
            json!({"net": "string - optional net name", "limit": "int, default 4000 - caps pads"}),
            json!({"data": "{count, pads: [{net, pad: 'U1.3', xMil, yMil}], nets: [{net, minXminYmaxXmaxYpads}]}", "roundtripMs": "int"}), 120,
            "Pass net on anything but a small board. The span string is 'minX,minY,maxX,maxY,pads' in mils: a quick measure of how far a net travels.",
            &["list_nets", "component_pins"], &["Needs a .PcbDoc current.", "limit truncates silently: count is what was returned, not what exists."], json!({"net": "GND"})),
        verb_doc("layers", "The current board's layer set with ids, names, kind and whether each is displayed.", json!({}),
            json!({"data": "{layers: [{name, id, kind, displayed}]}", "roundtripMs": "int"}), 90,
            "ids feed altium_set_layer_visibility {layerId}. For what is actually used, altium_layers_in_use.",
            &["copper_layers", "layers_in_use", "set_layer_visibility"], &["Needs a .PcbDoc current.", "The fixed layer set, not the stackup: thickness and dielectric detail are not read."], json!({})),
        verb_doc("copper_layers", "Every copper layer Altium can address on the current board (top, mid 1-30, planes 1-16, bottom) with names and visibility.", json!({}),
            json!({"data": "{layers: [{name, id, kind, displayed}]}", "roundtripMs": "int"}), 90,
            "Do not count these as the board's layers: altium_layers_in_use says which carry copper.",
            &["layers", "layers_in_use"], &["Needs a .PcbDoc current.", "A capability list (the same on any board), not the stack in use."], json!({})),
        verb_doc("layer_stack", "The CURRENT board's stack-up: copper layers in order with copper thickness and the dielectric under each (height, constant, material), and the total thickness. For impedance, current capacity and fab quotes.",
            json!({}),
            json!({"data": "{copperLayers, totalUm, layers: [{name, copperUm, dielectricUm (the dielectric BELOW this layer), dk1000 (dielectric constant x 1000), material}]}"}), 60,
            "Open the board first (altium_show_2d_board). dk1000 is the dielectric constant times 1000 (4200 = 4.2).",
            &["layers", "copper_layers", "read_rules"], &["Solder mask and overlay layers are not part of this list; the copper and the dielectric under each are."],
            json!({})),
        verb_doc("read_rules", "Every design rule on the current board, flat, disabled ones included: name, kind ordinal, enabled, priority, both scope expressions, comment, and Altium's descriptor with the rule's VALUES (e.g. 'Clearance Constraint (Gap=6mil) (All),(All)').", json!({}),
            json!({"data": "{count, rules: [{name, kind: int, enabled, priority, scope1, scope2, comment, descriptor}]}", "roundtripMs": "int"}), 90,
            "altium_rules_by_kind groups the enabled ones under readable kind names; altium_run_drc checks the board against them.",
            &["rules_by_kind", "run_drc"], &["Needs a .PcbDoc current.", "kind is Altium's TRuleKind ORDINAL; the values are in descriptor, as Altium words them."], json!({})),
        verb_doc("set_layer_visibility", "Show or hide one layer of the current board (by layer id) and refresh the view and the layer tabs.",
            json!({"layerId": "int (required) - an id from altium_layers", "displayed": "bool, default true"}),
            json!({"data": "{layerId, name, displayed}", "roundtripMs": "int"}), 60,
            "Then altium_show_2d_board {filePath} for a frame of the result. Restore what you hid: visibility is view state the user keeps seeing.",
            &["layers", "layers_in_use", "show_2d_board"], &["Needs a .PcbDoc current.", "Only pass ids that altium_layers returned: the id is not range-checked before the layer object is read."], json!({"layerId": 1, "displayed": false})),
        verb_doc("routing_state", "The WHOLE current board as data, in mm: tracks, arcs, vias, pads (with owner and net), components (ref, x, y, rotation, side), polygon pours, the outline, the unrouted (ratsnest) count, and a revision hash.",
            json!({"expectPath": "string (optional) - refuse with wrong_board unless this .PcbDoc is the current board"}),
            json!({"revision": "string - pass as expectedRevision to the edit verbs", "boardId": "string (the .PcbDoc path)", "unconnected": "int", "counts": "{tracks, arcs, vias, pads, components, polygons, nets}", "tracks": "[{start{x,y}, end{x,y}, width, layer, net}]", "vias": "[{position{x,y}, size, drill, layers[2], net}]", "pads": "[{ref, name, x, y, sizeX, sizeY, rotation, layer, drill, net}]", "components": "[{ref, x, y, rotation, side, pattern}]", "polygons": "[{net, layer, points[[x,y]]}]", "outline": "[[x,y]]"}), 180,
            "Layers use KiCad names (F.Cu, B.Cu, In<n>.Cu; *.Cu = a multi-layer pad) with Altium's layerId alongside. Coordinates are Altium's own frame (y up), in mm. This is the read AI Flow's Altium lane calls before and after every edit.",
            &["read_board", "net_connections", "footprint_primitives"], &["Needs a .PcbDoc current (altium_show_2d_board {filePath}).", "Footprint-internal tracks and arcs are left out; only free copper is listed.", "unconnected counts Altium's ratsnest lines: re-read after an edit for a fresh count."], json!({"expectPath": "C:/designs/buck/buck.PcbDoc"})),
        verb_doc("route_net", "Route one net as tracks, segment by segment; a waypoint on another layer drops a via there. One undo step, read back.",
            json!({"net": "string (required)", "width": "mm (default 0.25)", "layer": "F.Cu|B.Cu|In<n>.Cu or an Altium layer id (default F.Cu)", "viaSize": "mm (default 0.6)", "viaDrill": "mm (default 0.3)", "paths": "[[waypoint,...]] - a waypoint is [x,y], {x,y,layer} or {pad:\"U1.3\"}", "expectedRevision": "string (optional) - the revision you last read; refused with stale_board if the board changed", "boardId": "string (optional) - refused with board_mismatch unless this is the current board", "expectPath": "string (optional)"}),
            json!({"revision": "string - the board after the edit", "applied": "int", "failed": "int", "results": "[ok|err:...] per segment/via", "unconnected": "int", "counts": "{tracks, vias, ...}", "state": "the full altium_routing_state after"}), 300,
            "Coordinates are mm in Altium's frame, the same as altium_routing_state. There is no clearance check here yet: run your own DRC (AI Flow's gate) or altium_run_drc after.",
            &["routing_state", "place_vias", "remove_items", "ripup_nets"], &["Needs the board current.", "A net name must already exist on the board (nets come from the schematic / pads)."], json!({"net": "VOUT", "width": 0.5, "paths": [[{"pad": "L1.2"}, [120.0, 90.0], {"pad": "C6.1"}]]})),
        verb_doc("move_parts", "Move and rotate placed components; each part is read back to 0.001 mm and the exact rotation. One undo step.",
            json!({"parts": "[{ref, x, y, rotation}] - mm and degrees, Altium's frame", "expectedRevision": "string (optional) - the revision you last read; refused with stale_board if the board changed", "boardId": "string (optional) - refused with board_mismatch unless this is the current board", "expectPath": "string (optional)"}),
            json!({"revision": "string", "results": "[ok|err:...]", "mismatches": "[{ref, asked, got}] when a part did not land where asked (errorCode readback_mismatch)"}), 300,
            "Rotation is absolute, not a delta.", &["routing_state", "route_net"], &["Moving to the other side of the board is not supported yet."], json!({"parts": [{"ref": "U1", "x": 112.0, "y": 92.0, "rotation": 90}]})),
        verb_doc("place_vias", "Place vias (through, top to bottom). One undo step, read back.",
            json!({"vias": "[{net, x, y, size, drill}] - mm", "expectedRevision": "string (optional) - the revision you last read; refused with stale_board if the board changed", "boardId": "string (optional) - refused with board_mismatch unless this is the current board", "expectPath": "string (optional)"}),
            json!({"revision": "string", "results": "[ok|err:...]", "counts": "{vias, ...}"}), 300, "Through vias, top to bottom, on the named net. Heatsink vias under a pad: place them here, then pour or route to them.", &["route_net", "routing_state"], &[], json!({"vias": [{"net": "GND", "x": 110.0, "y": 90.0, "size": 0.6, "drill": 0.3}]})),
        verb_doc("remove_items", "Delete specific free tracks and vias, given exactly as altium_routing_state returned them. One undo step.",
            json!({"tracks": "[{start{x,y}, end{x,y}, layer}]", "vias": "[{position{x,y}}] or [{x,y}]", "expectedRevision": "string (optional) - the revision you last read; refused with stale_board if the board changed", "boardId": "string (optional) - refused with board_mismatch unless this is the current board", "expectPath": "string (optional)"}),
            json!({"revision": "string", "results": "[ok|err:not found]"}), 300, "Undo a bad segment or via without touching the rest of the net; altium_ripup_nets clears a whole net.", &["ripup_nets", "routing_state"], &["Matching is exact to the internal unit: pass the coordinates you read, not rounded ones."], json!({"vias": [{"x": 110.0, "y": 90.0}]})),
        verb_doc("ripup_nets", "Delete every free track, arc and via on each named net. One undo step.",
            json!({"nets": "[string]", "expectedRevision": "string (optional) - the revision you last read; refused with stale_board if the board changed", "boardId": "string (optional) - refused with board_mismatch unless this is the current board", "expectPath": "string (optional)"}),
            json!({"revision": "string", "results": "[ok:<deleted count>]"}), 300, "Pours are not touched.", &["remove_items", "route_net"], &[], json!({"nets": ["SW", "VOUT"]})),
        verb_doc("apply_pours", "Add solid polygon pours from outlines and pour them, each with its own connect style (solid joints or thermal spokes), plus pour keepouts (polygon cutouts); answers each pour's real poured copper area. One undo step.",
            json!({"pours": "[{net, layer (F.Cu|B.Cu|In<n>.Cu), points:[[x,y]...] mm, order?: pour order (1 = poured first), name?: polygon name, connect?: direct|relief|none, reliefWidth?: mm spoke width, reliefGap?: mm air gap (not settable by script on AD 26: Altium keeps 10 mil and notes[] says so), reliefEntries?: 2|4, removeDead?: bool (default true), removeSmallIslands?: bool (default false: connected copper of any size stays, as in KiCad; true = Altium's own 2500 sq mil island filter), minNeck?: mm - copper narrower than this is removed (KiCad's zone min thickness), solidPads?: [[x1,y1,x2,y2]] mm - pads completely inside each rect join THIS pour solid whatever its connect (KiCad's small solid joint zone over a pad; Altium pours nothing for a same-net polygon inside another)}]",
                   "cutouts": "(optional) [{layer, points:[[x,y]...] mm, name?}] - areas every pour on that layer stays out of (KiCad's pour keepout); landed before the pours",
                   "expectedRevision": "string (optional) - refused with stale_board if the board changed since you read it"}),
            json!({"results": "[ok:<polygon name>|err:...]", "zones": "[{net, layer, outline, filledMm2, islands, filled}]", "filledAreaByLayerMm2": "{F.Cu: mm2, ...}", "unfilled": "[zone] that poured nothing", "revision": "string"}), 600,
            "connect writes a PolygonConnect rule 'AdomPC <name>' scoped IsNamedPolygon('<name>') that outranks the board's own, so a power patch can join its pads solid while the ground pour keeps spokes. Without connect the board's Polygon Connect Style applies (Altium's default is 4 relief spokes of 10 mil, a current neck). Clearance comes from the board's Clearance rule. Dead copper is removed unless removeDead is false.",
            &["pour_state", "repour", "remove_pours", "routing_state", "set_design_rules"], &["Needs the board current.", "The outline need not repeat its first point.", "A name already on the board gets a ' 2' suffix: read the real name from results."],
            json!({"pours": [{"net": "GND", "layer": "B.Cu", "points": [[0, 0], [28, 0], [28, 20], [0, 20]], "connect": "relief", "reliefWidth": 0.35, "reliefGap": 0.25}, {"net": "SW", "layer": "F.Cu", "points": [[10, 5], [16, 5], [16, 9], [10, 9]], "connect": "direct", "order": 1}], "cutouts": [{"layer": "F.Cu", "points": [[20, 2], [22, 2], [22, 4], [20, 4]], "name": "Kelvin tap"}]})),
        verb_doc("pour_state", "Every polygon pour with the copper it actually poured: filled area per pour and per layer, and the pours that poured nothing.",
            json!({}), json!({"zones": "[{net, name, layer, pourIndex, outline, filledMm2, islands, filled}]", "filledAreaByLayerMm2": "{layer: mm2}", "unfilled": "[zone]"}), 180,
            "filledMm2 is regions minus holes, computed from Altium's own poured geometry.", &["apply_pours", "repour"], &["Needs the board current.", "A shelved or unpoured polygon reads filledMm2 0."], json!({})),
        verb_doc("remove_pours", "Delete polygon pours by net (and layer), or all of them, and the pour keepouts (cutouts) apply_pours made. Tracks and vias are untouched.",
            json!({"pours": "(optional) \"all\" or [{net, layer?} | {name} - one pour by the name apply_pours answered]", "cutouts": "(optional) \"all\" or [{layer}] - only the cutouts apply_pours landed (named Adom...)", "expectedRevision": "string (optional) - refused with stale_board if the board changed since you read it"}), json!({"removed": "int", "cutoutsRemoved": "int", "rulesRemoved": "int - AdomPC connect rules whose pour is gone", "revision": "string"}), 300,
            "Use before re-pouring a changed plan; the connect rules of removed pours go with them. altium_ripup_nets clears copper.", &["apply_pours", "ripup_nets"], &[], json!({"pours": [{"net": "GND"}]})),
        verb_doc("repour", "Repour every polygon on the board (Altium's Repour All) and answer the copper after. Runs outside the script engine so scoped rules apply.",
            json!({"inScript": "bool (default false) - repour from inside the agent instead (scoped rules then match nothing)"}), json!({"zones": "[...]", "filledAreaByLayerMm2": "{...}", "unfilled": "[...]", "run": "{helperExited, seconds, agentStart}"}), 600,
            "Run after moving parts or copper that pours flow around. Altium evaluates rule queries through its scripting system, so a repour from inside the resident agent ignores every scoped rule (a pour's own connect style, net-class clearances); this verb stops the agent, repours, and starts it again (a few seconds). apply_pours does the same on its own when it wrote connect rules.",
            &["pour_state", "apply_pours"], &["Needs the board current.", "The agent restarts: a verb sent meanwhile waits for it."], json!({})),
        verb_doc("routing_drc", "Design-rule violations on the current board WITH locations, plus the unrouted (ratsnest) count, as one clean/not-clean answer.",
            json!({"full": "bool (default false) - run Altium's batch DRC on this board first"}),
            json!({"clean": "bool - no violations and nothing unrouted", "errors": "int", "unconnected": "int", "violations": "[{rule, description, at{x,y} mm}]"}), 300,
            "Without full, this reads online DRC's live violations (fast). full:true runs the batch check through Altium's DRC dialog first.",
            &["routing_state", "run_drc"], &["Needs the board current.", "violations[] is capped at 500; errors is the real count."], json!({"full": true})),
        verb_doc("build_board", "Build a NEW board from a parts list and a netlist: outline (rounded corners allowed), every footprint from one PcbLib placed by its own origin, pad nets assigned, saved; then read back pin by pin.",
            json!({"pcbPath": "string (required) - the .PcbDoc to write", "libraryPath": "string (required) - the .PcbLib holding every footprint", "outline": "{rect:[x0,y0,w,h], cornerRadius?} or {points:[[x,y]...]} (mm)", "parts": "[{ref, footprint, x, y, rotation, comment?}] (mm, degrees; x,y is the footprint's own origin)", "nets": "{net: [\"R1.1\", \"U1.3\", ...]}"}),
            json!({"placed": "int", "padsWired": "int", "missingParts": "[ref]", "wrongPins": "[{pin, want, got}]", "revision": "string", "unconnected": "int - the ratsnest to route", "counts": "{...}"}), 900,
            "The board AI Flow's Altium project builds from its netlist. Footprint pad names must match the netlist's pin numbers (R1.1 = pad 1 of R1). Coordinates are Altium's frame (y up).",
            &["routing_state", "set_outline", "install_library"], &["The library must open in this Altium; a footprint missing from it refuses the whole build (footprint_not_found).", "A new board each call: pcbPath is overwritten."], json!({"pcbPath": "C:/designs/buck/buck.PcbDoc", "libraryPath": "C:/designs/buck/Buck.PcbLib", "outline": {"rect": [0, 0, 28, 20], "cornerRadius": 1}, "parts": [{"ref": "U1", "footprint": "SOT-23-6", "x": 12, "y": 10, "rotation": 0}], "nets": {"GND": ["U1.1"]}})),
        verb_doc("set_outline", "Reshape the current board's outline (a rectangle with rounded corners, or a polygon). One undo step.",
            json!({"outline": "{rect:[x0,y0,w,h], cornerRadius?} or {points:[[x,y]...]} (mm)", "expectedRevision": "string (optional) - refused with stale_board if the board changed"}),
            json!({"vertices": "int", "revision": "string", "outlineEdges": "[{kind: line|arc, ...}]"}), 120,
            "Rounding corners: pass the same rectangle with cornerRadius (1 mm is a common default).", &["build_board", "routing_state", "repour"], &["Needs the board current."], json!({"outline": {"rect": [0, 0, 28, 20], "cornerRadius": 1}})),
        verb_doc("set_design_rules", "Write a fab's design rules into the current board: clearance, track width, routing via style, hole size, annular ring, hole-to-hole and board-edge clearance (mm).",
            json!({"clearance": "mm", "trackWidth": "{min, preferred?, max?} mm", "via": "{diameter:{min, preferred?, max?}, drill:{min, preferred?, max?}} mm", "hole": "{min, max?} mm", "annularRing": "mm", "holeToHole": "mm", "edgeClearance": "mm - copper to the board edge (the rule it creates is scoped OnCopper; silkscreen keeps its own rules)", "polygonConnect": "the board's catch-all polygon connect style: direct|relief|none, or {style, reliefWidth: mm, reliefEntries: 2|4} (per-pour rules from apply_pours keep theirs)"}),
            json!({"landed": "[{kind, rulesWritten}]", "failed": "[{kind, error}]", "rules": "the board's rules after"}), 300,
            "AI Flow's `rules use` calls this with the fab pack's numbers (adom/pcb-design-rules). Each kind is written separately, so one this Altium build lacks fails alone.",
            &["read_rules", "rules_by_kind", "build_board"], &["Needs the board current.", "Every existing rule of a kind gets the values; rule scopes are kept, except an Adom edge rule an earlier bridge scoped to All, which becomes OnCopper (#26)."], json!({"clearance": 0.15, "trackWidth": {"min": 0.15}, "via": {"diameter": {"min": 0.45}, "drill": {"min": 0.2}}, "hole": {"min": 0.2}, "edgeClearance": 0.3})),
        verb_doc("add_rule", "Add one SCOPED design rule (a net's clearance or width, a net class's, any Altium query) that set_design_rules' board-wide values cannot express.",
            json!({"kind": "clearance|width", "name": "string - the rule is named 'Adom <name>'; the same name replaces it", "scope1": "Altium query, e.g. InNet('SW') or InNetClass('Power')", "scope2": "query (default All)", "gap": "mm (clearance)", "min": "mm (width)", "preferred": "mm (width, optional)", "max": "mm (width, optional)", "comment": "optional", "remove": "bool - delete the Adom rule of this kind and name instead"}),
            json!({"name": "string", "priority": "int (1 = first of its kind)"}), 60,
            "AI Flow's net clearances (a switch node kept farther from everything) land here. A scope with a typo matches nothing and DRC then passes silently: check it in the rules editor (Test Queries).",
            &["set_design_rules", "read_rules", "routing_drc"], &["Needs the board current.", "One rule per call."], json!({"kind": "clearance", "name": "SW node", "scope1": "InNet('SW')", "scope2": "All", "gap": 0.3})),
        verb_doc("build_schematic", "Draw a design's schematic from its netlist with the project library's symbols, inside a new project (.PrjPcb with the board and libraries), compile it, and check its netlist pin by pin.",
            json!({"schPath": "string - the .SchDoc to write", "libraryPath": "string - the project .SchLib", "parts": "[{ref, symbol: LibReference in the SchLib, block?: column name (input, core, output...), comment?: value, footprint?: its footprint in pcbLibPath (the part's PCB model link)}]", "nets": "{net: [\"U1.3\", ...]} - pins of parts not on the sheet are ignored", "projectPath": "(optional) .PrjPcb, default beside the sheet", "pcbPath": "(optional) the board, listed in the project", "pcbLibPath": "(optional)", "title": "optional", "number": "optional", "revision": "optional", "drawnBy": "optional"}),
            json!({"sch": "path", "project": "path", "placed": "int", "wires": "int", "labels": "int", "pinsWithoutNet": "ref.pin;...", "mismatches": "[{net, missing, extra}]", "compileErrors": "[..]"}), 900,
            "One column per block, each part with a wire stub and a net label on every connected pin; designator above, value below; title block filled. The compiled netlist is the proof: success means it matches the given nets.",
            &["schematic_data", "compile", "build_board"], &["The SchLib must hold every symbol (altium_import_part).", "Parts are placed unrotated."],
            json!({"schPath": "C:\\work\\Buck.SchDoc", "libraryPath": "C:\\work\\library\\Buck.SchLib", "parts": [{"ref": "U1", "symbol": "TPS54202DDCR", "block": "core", "comment": "TPS54202"}], "nets": {"VIN": ["U1.3"]}})),
        verb_doc("export_manufacturing", "A project's manufacturing set in one call: Gerbers, NC drill, pick and place (CSV, mm) and the BOM, from an OutJob the bridge writes beside the project (composed from this Altium's own OutJob templates, every output on one medium that opens nothing).",
            json!({"projectPath": "the .PrjPcb", "pcbPath": "its .PcbDoc", "bomColumns": "(optional) [column or parameter name] - default Comment, Description, Designator, Footprint, Quantity, MPN, Manufacturer, Mouser/DigiKey/LCSC Part #", "clean": "bool (default true) - empty the output folder first", "timeout": "seconds, default 300"}),
            json!({"outjob": "path", "outputDir": "path", "files": "[relative paths]", "dialogs": "[..]", "elapsedSec": "float"}), 600,
            "Pull files[] with pull_file and check them against the board (layers, drill hits, every placed part in the pick and place and the BOM) before handing them over. AI Flow's `altium export` does both.",
            &["run_outjob", "build_schematic", "routing_state"], &["The board must be a document of the project.", "The BOM is the compiled project's (schematic parts); board-only pads are not in it."],
            json!({"projectPath": "C:\\work\\Buck.PrjPcb", "pcbPath": "C:\\work\\Buck.PcbDoc"})),
        verb_doc("select_net", "Select one net as a whole on the current board (tracks, arcs, vias, pads, pours) and nothing else, optionally framing it; no net clears the selection.",
            json!({"net": "string (optional) - empty or absent clears", "zoom": "bool - frame the net (altium_zoom_to)", "margin": "mm, default 1"}),
            json!({"net": "string", "selected": "int", "byKind": "{tracks, arcs, vias, pads, pours, other}", "zoom": "[l, b, r, t] mm when zoom"}), 120,
            "AI Flow's net walkthrough lights each loaded net in turn. Altium draws a selection in its highlight colour.",
            &["zoom_to", "routing_state", "list_nets"], &["Needs the board current.", "Pour regions are counted once, as their polygon."], json!({"net": "SW", "zoom": true})),
        verb_doc("silk_state", "The silkscreen as it stands: every part's body box, its designator and comment (text, location, size, rotation, shown, Altium's measured box), and the free overlay texts and lines on both faces.",
            json!({}), json!({"components": "[{ref, pattern, x, y, rotation, side, body:[l,b,r,t], name:{text,x,y,size,rotation,shown,box}, comment:{...}}]", "texts": "[{side, text, x, y, size, rotation, mirror, box, raw}]", "lines": "[{side, x1, y1, x2, y2, width, raw}]"}), 180,
            "The input to a silkscreen plan (AI Flow's `altium silkscreen`), and its check after landing: compare each box with the pads and vias from routing_state.",
            &["apply_silkscreen", "routing_state"], &["Needs the board current.", "raw is the exact handle apply_silkscreen's remove takes."], json!({})),
        verb_doc("apply_silkscreen", "Land a silkscreen plan in one undo step: designators and comments moved, sized and turned (or hidden), free overlay texts and lines added (bottom texts mirrored), earlier ones removed; answers silk_state after.",
            json!({"names": "[{ref, x, y, size, rotation?, show?}] - the designator's own location, mm", "comments": "[same] - the value", "texts": "[{text, x, y, size, rotation?, side?: top|bottom}]", "lines": "[{x1, y1, x2, y2, width?: mm (0.15), side?}]", "remove": "{texts:[raw], lines:[raw]} - silk_state raw handles of earlier free items"}),
            json!({"applied": "int", "failed": "int", "results": "[ok|err:...]", "after": "silk_state"}), 600,
            "A text's location is its lower left before rotation (for 90 degrees: the box's lower right). Stroke width is a seventh of the height.",
            &["silk_state", "routing_drc"], &["Needs the board current.", "Removal matches the raw handle exactly: pass what silk_state answered."], json!({"names": [{"ref": "U1", "x": 12.0, "y": 14.2, "size": 0.8}], "lines": [{"x1": 1, "y1": 1, "x2": 5, "y2": 1}]})),
        verb_doc("zoom_to", "Frame the PCB view on a rectangle, a part (or several) or a whole net, with a margin.",
            json!({"rect": "(optional) [x1, y1, x2, y2] mm", "ref": "(optional) a designator, e.g. U1", "refs": "(optional) [designator]", "net": "(optional) a net: its pads, tracks and vias", "margin": "mm (default 1)"}),
            json!({"rect": "[left, bottom, right, top] mm framed"}), 60,
            "For a close look at a joint or a camera shot of one part or net; altium_run_command PCB:Zoom Action=All frames the whole board.",
            &["routing_state", "run_command", "animate_view"], &["Needs the board current.", "Coordinates are Altium's (y up), as routing_state reports them."], json!({"net": "SW", "margin": 0.5})),
        verb_doc("footprint_primitives", "Geometry dump of one PLACED component: its tracks, arcs, pads and texts in mils with layer ids; other primitive kinds are counted.",
            json!({"designator": "string (required) - a component on the current board, e.g. U1"}),
            json!({"data": "{designator, pattern, count, otherPrimitives, primitives: [{kind: track|arc|pad|text, layerId, ...}]}", "roundtripMs": "int"}), 120,
            "Use it to verify an imported footprint on a altium_test_board: pad layerId 74 = multi-layer (plated through), which is what altium_plate_vias produces.",
            &["component_pins", "plate_vias", "test_board", "pcb_report"], &["Needs a .PcbDoc current: it reads a placed component, not a PcbLib member (altium_pcb_report reads libraries).", "3D bodies, fills and regions only show up in otherPrimitives."], json!({"designator": "U1"})),
        verb_doc("schematic_data", "Components on a schematic sheet (filePath, else the CURRENT sheet) with their pins (number, name, electrical type), position and orientation. designator filters to one part.",
            json!({"filePath": "path to the .SchDoc - optional, made current first (alias sheet)", "designator": "string - optional filter", "limit": "int, default 500", "pins": "bool, default true", "params": "bool, default false - include each component's parameters", "paramNamesOnly": "bool, default false - parameter names without values (a library with blob parameters reads fast)"}),
            json!({"data": "{document, count, components: [{designator, partId, libReference, comment, description (empty for a workspace-managed part), kind (ComponentKind: 0 Standard, 1 Mechanical, 2 Graphical, 3 Net Tie BOM, 4 Net Tie, 5 Standard no BOM, 6 Jumper), itemGuid, vaultGuid, sourceLibrary, xMil, yMil, orientation, pins: [{number, name, electrical (0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power), xMil, yMil, orientation, lengthMil}], params: []}]}", "roundtripMs": "int"}), 120,
            "Pin-level view of a sheet. params[] is empty unless you pass params:true; altium_read_schematic is the fuller parameter view.",
            &["read_schematic", "cross_check", "show_schematic"], &["One sheet per call. Without filePath the current document must be a .SchDoc: a schematic library is refused (no_current_schematic), not read as an empty sheet.", "electrical is Altium's pin-type ordinal, not a name."], json!({"designator": "U1"})),
        // ---- surfaces ----
        verb_doc("show_project", "Open a project (.PrjPcb) through the agent and return a proof frame of Altium's main window. With no filePath it opens Altium's bundled example project (Bluetooth Sentinel).",
            json!({"filePath": "path to a .PrjPcb - default: the bundled Bluetooth Sentinel example", "timeout": "seconds, default 60"}),
            json!({"surface": "project", "path": "string", "steps": "[{open, result, ms}]", "title": "string", "document": "string", "dialogsDetected": "int", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "elapsedSec": "float", "windowEtiquette": "background"}), 120,
            "A project has no document view of its own: follow with altium_show_schematic / altium_show_2d_board {filePath} for something to look at, or altium_compile {projectPath}.",
            &["show_schematic", "show_2d_board", "compile", "open_project"],
            &["ALWAYS pass filePath for the user's work: the default opens Altium's example project in their session.",
              "Measured live 2026-09-20: opening documents RAISES Altium over the user's work even though the reply says background. One call per proof; never loop show_* verbs.",
              "success:false with dialogs[] means a box came up during the open: read it before anything else."],
            json!({"filePath": "C:/proj/board.PrjPcb"})),
        verb_doc("show_schematic", "Open a .SchDoc, zoom to fit, and return a proof frame. With no filePath it opens the bundled Bluetooth Sentinel top sheet.",
            json!({"filePath": "path to a .SchDoc - default: the bundled example sheet", "timeout": "seconds, default 60"}),
            json!({"surface": "schematic", "path": "string", "steps": "[{open | process, ...}]", "title": "string", "document": "string", "dialogsDetected": "int", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "elapsedSec": "float", "windowEtiquette": "background"}), 120,
            "LOOK at shot.image before claiming anything. Then altium_read_schematic for the data, or altium_animate_view {lane:'sch'} for zoom moves.",
            &["read_schematic", "place_on_sch", "animate_view", "show_2d_board"],
            &["ALWAYS pass filePath: the default opens Altium's example design in the user's session.",
              "Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it.",
              "After re-generating a sheet at the same path, altium_close_document the stale one first or the frame shows the OLD document."],
            json!({"filePath": "C:/proj/tof.SchDoc"})),
        verb_doc("show_2d_board", "Open a .PcbDoc, switch to 2D, zoom to fit, and return a proof frame. With no filePath it opens the bundled Bluetooth Sentinel board.",
            json!({"filePath": "path to a .PcbDoc - default: the bundled example board", "timeout": "seconds, default 120"}),
            json!({"surface": "2d_board", "path": "string", "steps": "[{open | process, ...}]", "title": "string", "document": "string", "dialogsDetected": "int", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "elapsedSec": "float", "windowEtiquette": "background"}), 120,
            "It also makes the board the CURRENT document, so altium_read_board / altium_board_summary work right after. altium_show_3d_board for the 3D frame.",
            &["show_3d_board", "read_board", "set_layer_visibility", "animate_view"],
            &["ALWAYS pass filePath: the default opens Altium's example board in the user's session.",
              "Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it."],
            json!({"filePath": "C:/proj/tof.PcbDoc"})),
        verb_doc("show_3d_board", "Open a .PcbDoc, switch to 3D, zoom to fit, and return a proof frame. With no filePath it opens the bundled Bluetooth Sentinel board.",
            json!({"filePath": "path to a .PcbDoc - default: the bundled example board", "timeout": "seconds, default 120"}),
            json!({"surface": "3d_board", "path": "string", "steps": "[{open | process, ...}]", "title": "string", "document": "string", "dialogsDetected": "int", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "elapsedSec": "float", "windowEtiquette": "background"}), 120,
            "The proof for altium_test_board {pcbPath}: check every part shows a body. Missing body = the footprint has none (altium_add_body). altium_animate_view {preset:'showcase3d'} for motion.",
            &["show_2d_board", "test_board", "add_body", "animate_view"],
            &["ALWAYS pass filePath: the default opens Altium's example board in the user's session.",
              "Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it.",
              "It leaves the board in 3D mode for the user; altium_show_2d_board switches back."],
            json!({"filePath": "C:/proj/tof.PcbDoc"})),
        verb_doc("show_library", "Open a .SchLib or .PcbLib (kind taken from the extension), select its first component, return a proof frame and the component names.",
            json!({"libraryPath": "path to a .SchLib/.PcbLib (alias: filePath) - default: the bundled SpiritLevel SchLib", "timeout": "seconds, default 60"}),
            json!({"surface": "library", "path": "string", "selected": "string", "components": "[string] up to 200", "componentCount": "int", "steps": "[..]", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 120,
            "Then altium_show_symbol / altium_show_footprint / altium_show_3d_chip {libraryPath, name} for one member. altium_list_library gives the names without a frame.",
            &["list_library", "show_symbol", "show_footprint", "show_3d_chip"],
            &["ALWAYS pass libraryPath: the default opens Altium's example library in the user's session.",
              "Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it.",
              "errorCode proof_unavailable = opened and selected correctly, only the capture failed (window minimized)."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib"})),
        verb_doc("show_footprint", "Open a PcbLib member in the footprint editor in 2D, zoom to fit, and capture a proof frame.",
            json!({"libraryPath": "path to a .PcbLib (alias: filePath) - default: the bundled SpiritLevel PcbLib", "footprintName": "string (alias: name) - default: the first footprint of YOUR library, SO-G3 in the demo library", "timeout": "seconds, default 60"}),
            json!({"surface": "footprint", "path": "string", "selected": "string", "steps": "[..]", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "available": "[string] - when the name was not found", "windowEtiquette": "background"}), 120,
            "Check pads, silkscreen and courtyard in shot.image; altium_pcb_report {path, name} gives the numbers. altium_show_3d_chip for the body.",
            &["show_3d_chip", "pcb_report", "build_footprint", "list_library"],
            &["Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it.",
              "A wrong footprintName fails with available[] (up to 50 names): pick from it.",
              "An empty-looking canvas usually means a 3D body sits 50000 mil from the pads: altium_normalize_bodies."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX"})),
        verb_doc("show_3d_chip", "Open a PcbLib member in the footprint editor in 3D, zoom to fit, and capture a proof frame of the part's 3D body.",
            json!({"libraryPath": "path to a .PcbLib (alias: filePath) - default: the bundled SpiritLevel PcbLib", "footprintName": "string (alias: name) - default: the first footprint of YOUR library, SO-G3 in the demo library", "timeout": "seconds, default 60"}),
            json!({"surface": "3d_chip", "path": "string", "selected": "string", "steps": "[..]", "dialogs": "[..]", "shot": "{image: base64 png, width, height}", "available": "[string] - when the name was not found", "windowEtiquette": "background"}), 120,
            "The proof after altium_add_body / altium_import_part {step}. Pads with no body in the frame: altium_pcb_report to see whether a body exists, altium_normalize_bodies if it sits off the pads.",
            &["show_footprint", "add_body", "normalize_bodies", "pcb_report"],
            &["Measured live 2026-09-20: it RAISES Altium over the user's work. One call per proof; never loop it.",
              "It leaves the library editor in 3D mode; altium_show_footprint switches back to 2D."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX"})),
        verb_doc("list_library", "Names of every component in a .SchLib or every footprint in a .PcbLib (kind taken from the extension). Opens the library; no screenshot.",
            json!({"libraryPath": "path to a .SchLib/.PcbLib (alias: filePath) - default: the bundled SpiritLevel SchLib", "limit": "int, default 500"}),
            json!({"data": "{kind: sch|pcb, count, names: [string]}", "path": "string", "kind": "sch | pcb", "roundtripMs": "int"}), 60,
            "Use the exact names with altium_show_symbol / altium_read_symbol / altium_show_footprint / altium_pcb_report.",
            &["show_library", "read_symbol", "pcb_report"], &["It makes the library the current document.", "count is the true total; names[] stops at limit."], json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib"})),
        verb_doc("save", "Save the focused document, or open filePath first and save that (WorkspaceManager:SaveObject on the focused document).",
            json!({"filePath": "path - optional; opened and made current before the save"}),
            json!({"data": "{process, params}", "roundtripMs": "int"}), 60,
            "Pass filePath every time: every library authoring verb leaves the library UNSAVED. Then altium_check_dialogs - a 'Save As' box means the document never had a path.",
            &["close_document", "check_dialogs", "close"], &["Without filePath it saves whatever has focus, which may be the user's document rather than yours.", "success only means the save process was dispatched; a Save As dialog blocks the engine until answered."],
            json!({"filePath": "C:/lib/Adom_ToF.SchLib"})),
        verb_doc("board_shape", "Redefine the CURRENT board's outline as a wMm x hMm rectangle at (x0Mm, y0Mm) by rewriting the board outline's segments.",
            json!({"pcbPath": "path to a .PcbDoc - optional; opened first", "x0Mm": "number, default 0", "y0Mm": "number, default 0", "wMm": "number, default 15", "hMm": "number, default 15", "mechOutline": "bool - also draw the rectangle as 0.1 mm tracks on Mechanical 1 so the fab Gerber (.GM1) has an outline", "timeout": "seconds, default 120"}),
            json!({"data": "{outlineMil: [left, bottom, right, top] read from the outline vertices, outlineSource, mechOutlineTracks}", "roundtripMs": "int"}), 120,
            "Then altium_save {filePath: pcbPath} and altium_show_2d_board {filePath} to see it. altium_pcb_report {want:'board'} reads outlineMil back.",
            &["test_board", "pcb_report", "show_2d_board", "save"],
            &["errorCode no_board_outline: the current document is a PcbLib (its internal board has no outline) - pass pcbPath.", "bad_size = wMm/hMm not greater than 0; bad_number = a value was not numeric.", "It REPLACES the outline; the previous shape is not kept.", "The board shape alone is NOT plotted into any Gerber: pass mechOutline:true (once, at the final size: each call adds a new set of tracks)."],
            json!({"pcbPath": "C:/proj/tof.PcbDoc", "wMm": 39, "hMm": 14})),
        verb_doc("pcb_report", "Numbers instead of pixels: for a PcbLib, every footprint's pads, pad bounding box, primitive census and 3D bodies; for a board, outline, components with nets and bodies, census and tracks.",
            json!({"path": "path to a .PcbLib or .PcbDoc - optional; opened first", "name": "string - footprint name filter (library form)", "want": "'board' - report the focused BOARD even though a PcbLib is also answering", "timeout": "seconds, default 60"}),
            json!({"data": "{kind: library, components: [{name, pads, padsBboxMil, census, bodies}], bodyCols} | {kind: board, outlineMil, components: [{.., pads, padsBboxMil, nets, bodies}], census, tracks, bodyCols}", "roundtripMs": "int"}), 60,
            "bodies[] rows follow bodyCols (layerId,wMil,hMil,leftMil,bottomMil,standoffMil,overallMil,hasModel). A body whose leftMil is ~50000 away from padsBboxMil needs altium_normalize_bodies.",
            &["footprint_primitives", "normalize_bodies", "add_body", "show_3d_chip"],
            &["Altium's GetCurrentPCBLibrary answers even when a BOARD is focused: pass want:'board' for a .PcbDoc or you get the library report.", "A footprint with bodies:[] places with no 3D model."],
            json!({"path": "C:/lib/Adom_ToF.PcbLib", "name": "VL53L5CX"})),
        // ---- outputs / project ----
        verb_doc("compile", "Compile a project and return Altium's Messages panel (ERC + connectivity) with counts by class. With no path it compiles Altium's bundled example project.",
            json!({"projectPath": "path to a .PrjPcb (alias: filePath) - default: the bundled Bluetooth Sentinel example", "objectKind": "string, default 'Project'", "limit": "int, default 200 messages", "timeout": "seconds"}),
            json!({"project": "path", "messageCount": "int", "byClass": "{class: n}", "messages": "[{class, source, document, text}]", "elapsedSec": "float", "windowEtiquette": "background"}), 180,
            "Zero messages = clean. Fix what messages[] names, recompile, then altium_run_drc for the board side.",
            &["run_erc", "run_drc", "open_project", "cross_check"],
            &["ALWAYS pass projectPath: the default compiles Altium's example project in the user's session.", "messages[] is the WHOLE Messages panel after the compile, capped at limit; messageCount is the true total."],
            json!({"projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("run_erc", "Electrical rules check. Same handler as altium_compile: in Altium, compiling the project IS the ERC.",
            json!({"projectPath": "path to a .PrjPcb (alias: filePath) - default: the bundled Bluetooth Sentinel example", "objectKind": "string, default 'Project'", "limit": "int, default 200 messages", "timeout": "seconds"}),
            json!({"project": "path", "messageCount": "int", "byClass": "{class: n}", "messages": "[{class, source, document, text}]", "elapsedSec": "float", "windowEtiquette": "background"}), 180,
            "byClass gives the error/warning split; read messages[].text and .document for each. Then altium_run_drc for the layout.",
            &["compile", "run_drc", "review_board"], &["ALWAYS pass projectPath: the default compiles Altium's example project.", "Needs the PROJECT path, not a sheet: a free .SchDoc has no project to compile."],
            json!({"projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("run_drc", "Run the design rule check on a board and read the violation markers back. PCB:DesignRuleCheck opens a MODAL; the bridge clicks 'Run Design Rule Check' through UIA in the background, waits for the agent, re-activates the board and reads the violations.",
            json!({"filePath": "path to a .PcbDoc - default: the bundled Bluetooth Sentinel board", "limit": "int, default 200 violations", "timeout": "seconds to wait for the DRC, default 180"}),
            json!({"board": "path", "violationCount": "int", "violations": "[{rule, description}]", "steps": "[..]", "dialogs": "[..]", "elapsedSec": "float", "windowEtiquette": "background"}), 300,
            "violationCount is the true total; violations[] stops at limit. Altium also opens its HTML DRC report as a new document: altium_state shows it, altium_close_document {path} removes it.",
            &["read_rules", "rules_by_kind", "review_board", "compile"],
            &["ALWAYS pass filePath: the default runs DRC on Altium's example board.", "drc_dialog_not_found = the DRC dialog never opened; check that a PCB is focused (altium_window_info).", "Never run PCB:DesignRuleCheck through altium_run_command: the modal freezes the agent with nobody to click it."],
            json!({"filePath": "C:/proj/board.PcbDoc"})),
        verb_doc("run_outjob", "Run one output medium of an .OutJob (gerbers, drill, BOM, PDFs...) and list what it wrote under 'Project Outputs for <project>'.",
            json!({"outjobPath": "path to an .OutJob (alias: filePath) - default: the bundled example Fabrication.OutJob", "medium": "string, default 'Folder Structure' - the OutputMedium name inside the OutJob", "projectPath": "path to the .PrjPcb the OutJob belongs to; opened first", "objectKind": "string, default 'OutputBatch'", "timeout": "seconds, default 300"}),
            json!({"outjob": "path", "medium": "string", "agent": "{outjob, medium, objectKind}", "outputDir": "path | null", "files": "[relative paths] up to 60", "dialogs": "[..]", "elapsedSec": "float", "windowEtiquette": "background"}), 600,
            "files[] is the proof. Pull what you need with ab's pull_file. OutJobs OPEN what they generate (CAMtastic, PDFs): altium_state afterwards and altium_close_document the viewers.",
            &["export_gerber", "export_bom", "export_step", "check_dialogs"],
            &["Pass projectPath with your own OutJob: outputDir/files are looked up beside the PROJECT, and without it beside the OutJob.", "medium must be the name of an output medium inside the OutJob. The agent cannot tell whether it ran: files[] is the only proof.", "A dialog in dialogs[] means a medium needed a decision (printer, PDF path): read dialogs[].shot."],
            json!({"outjobPath": "C:/proj/Fabrication.OutJob", "projectPath": "C:/proj/board.PrjPcb", "medium": "Folder Structure"})),
        verb_doc("export_gerber", "Gerber/drill export: altium_run_outjob with the fabrication OutJob and the 'Folder Structure' medium as defaults.",
            json!({"outjobPath": "path to an .OutJob - default: the bundled example Fabrication.OutJob", "medium": "string, default 'Folder Structure'", "projectPath": "path to the .PrjPcb", "timeout": "seconds, default 300"}),
            json!({"surface": "gerber", "outjob": "path", "outputDir": "path | null", "files": "[relative paths]", "dialogs": "[..]", "elapsedSec": "float"}), 300,
            "Check files[] for the gerber and NC drill files, then pull them with ab's pull_file. Close the CAMtastic documents the OutJob opened (altium_state, altium_close_document).",
            &["run_outjob", "export_bom", "export_step", "run_drc"],
            &["There is NO board-path argument: without outjobPath it exports Altium's EXAMPLE design. The user's project needs its own OutJob.", "OutJobs open what they generate: expect CAM documents left open in the user's session."],
            json!({"outjobPath": "C:/proj/Fabrication.OutJob", "projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("bom", "The project's bill of materials read straight from Altium: every schematic sheet the .PrjPcb lists (placed parts and their parameters) merged with the board (footprint, position, side), a variant applied, grouped one line per manufacturer part number, checked for what an order needs, and written as bom.csv, bom-jlcpcb.csv + cpl-jlcpcb.csv, bom-mouser.csv and bom-digikey.csv (+ bom.json).",
            json!({"projectPath": "path to the .PrjPcb (required)", "pcbPath": "path - optional, default the project's board", "variant": "string - optional, a project variant's description (Not Fitted parts become DNP, alternates are listed)", "outputDir": "path - default <project folder>\\BOM", "boards": "int, default 1 - multiplies the quantities in the Mouser/DigiKey files", "write": "bool, default true", "timeout": "seconds per sheet read, default 120"}),
            json!({"lines": "[{quantity, designators, value, comment, description, footprint, package, mpn, manufacturer, mouser, digikey, lcsc, libReference}]", "checks": "{missingMpn, missingManufacturer, missingPartNumbers{mouser, digikey, lcsc}, inconsistent[{field, designators, values}], dnp, excludedKinds, sheetNotOnBoard, boardNotOnSheet}", "variants": "[string]", "alternates": "[{designator, alternatePart}]", "files": "[path]", "fittedParts": "int"}), 600,
            "Parameter names are matched across libraries (MPN = Manufacturer Part Number = Mfr Part #; Mouser Part # / Digi-Key Part Number; LCSC Part # / JLCPCB Part #). Missing numbers: look them up (adom-parts-search search <MPN>) and write them back with altium_sheet_params, then run this again.",
            &["sheet_params", "export_manufacturing", "read_schematic"],
            &["Graphical, Net Tie (no BOM) and Standard (no BOM) components are left out (checks.excludedKinds).", "DNP: a variant's Not Fitted parts, or a DNP / Do Not Populate / Fitted=false parameter.", "Variants are read from the .PrjPcb text; alternate parts are listed, not substituted.", "CPL rotations are Altium's: JLCPCB's preview may need per-footprint corrections.", "A multi-channel (repeated) sheet is read once: its copies' designators land in checks.boardNotOnSheet.", "Only the parameters a BOM needs are read (some libraries carry huge blob parameters); a workspace-managed part's description comes from a Description parameter, never its vault."],
            json!({"projectPath": "C:/proj/board.PrjPcb", "boards": 10})),
        verb_doc("sch_place_part", "Place a component from a .SchLib onto an EXISTING sheet at a position, with its library parameters (MPN, Manufacturer, distributor numbers) and its footprint link.",
            json!({"sheet": "path to the .SchDoc (required)", "library": "path to the .SchLib (required)", "symbol": "the component's name in that library (required)", "designator": "string, default the library's (e.g. R?)", "x": "mil, default 1000 (snapped to 100)", "y": "mil, default 1000", "rotation": "0|90|180|270", "comment": "string, default the library's", "partId": "int, default 1 (a multi-part component's part)", "footprint": "a PcbLib footprint name to link instead of the library's own", "pcbLib": "path of the .PcbLib the footprint lives in (optional)", "save": "bool, default true"}),
            json!({"designator": "string", "libref": "string", "pins": "int", "footprints": "[string]", "atMil": "[x, y]", "saved": "bool"}), 120,
            "Then wire it: altium_sch_connect {sheet, connections:[{designator, pin, net}]}; number a '?' designator with altium_annotate; push it to the board with altium_update_pcb.",
            &["sch_connect", "annotate", "update_pcb", "list_library"],
            &["Places part 1 of a multi-part component unless partId says otherwise.", "Sheet coordinates are mils from the sheet's lower-left corner; altium_schematic_data shows where the other parts are."],
            json!({"sheet": "C:/proj/Main.SchDoc", "library": "C:/proj/Lib.SchLib", "symbol": "TPS54202DDCR", "designator": "U2", "x": 3000, "y": 4000})),
        verb_doc("sch_connect", "Connect pins of placed parts: a wire stub from each pin's outer end plus a net label (kind label), a power port (kind power: GND-like nets get a ground symbol) or the stub alone (kind wire). One undo step.",
            json!({"sheet": "path to the .SchDoc (required)", "connections": "[{designator, pin (number or name), net, kind: label|power|wire (default label), stubMil (default 200)}] (required)", "save": "bool, default true"}),
            json!({"connected": "int", "results": "[{designator, pin, net, kind, result, endMil}]", "failed": "[..]", "saved": "bool"}), 90,
            "Net labels with the same text connect across the project; power ports connect by net name everywhere. Check the result with altium_compile (ERC) or altium_schematic_data.",
            &["sch_place_part", "compile", "schematic_data"],
            &["A pin that already has a wire gets a second stub; read the sheet first when unsure.", "A part rotated after connecting leaves its stubs behind."],
            json!({"sheet": "C:/proj/Main.SchDoc", "connections": [{"designator": "U2", "pin": "1", "net": "GND", "kind": "power"}, {"designator": "U2", "pin": "VIN", "net": "VIN"}]})),
        verb_doc("sch_edit", "Change placed parts on a sheet: move (to x,y mil), moveBy (dx,dy), rotate (degrees), rename (designator), re-comment (comment), delete. One undo step.",
            json!({"sheet": "path to the .SchDoc (required)", "edits": "[{designator, action: move|moveBy|rotate|designator|comment|delete, x,y | dx,dy | degrees | to | text}] (required)", "save": "bool, default true"}),
            json!({"applied": "int", "results": "[{designator, action, result}]", "failed": "[..]", "saved": "bool"}), 90,
            "Wires do not follow a moved part; reconnect with altium_sch_connect. Renames and deletions reach the board with altium_update_pcb.",
            &["sch_connect", "update_pcb", "sheet_params"],
            &["Every part of a multi-part component with that designator is changed together."],
            json!({"sheet": "C:/proj/Main.SchDoc", "edits": [{"designator": "R5", "action": "comment", "text": "10k"}, {"designator": "C9", "action": "delete"}]})),
        verb_doc("sch_zoom", "Find parts on a sheet: select them (everything else deselected), frame the view on them and return a proof frame. The schematic half of cross-probing (the board half: altium_zoom_to).",
            json!({"sheet": "path to the .SchDoc (required)", "designators": "[string] or 'R1,U2' (required)", "shot": "bool, default true"}),
            json!({"parts": "[{designator, xMil, yMil}]", "notOnSheet": "[string]", "shot": "frame of Altium's main window"}), 60,
            "To find which sheet holds a part, altium_bom (or altium_schematic_data per sheet) lists every designator with its sheet.",
            &["zoom_to", "schematic_data", "sch_edit"], &[],
            json!({"sheet": "C:/proj/Main.SchDoc", "designators": ["U1", "L1"]})),
        verb_doc("autoroute", "Altium's own autorouter (Situs) on the current board: picks a strategy, keeps existing routing locked by default, routes, and answers the unconnected count and DRC errors before and after. Not saved unless save:true.",
            json!({"filePath": "path to the .PcbDoc - optional, else the current board", "strategy": "Default 2 Layer Board | Default Multi Layer Board | Default 2 Layer With Edge Connectors | General Orthogonal | Via Miser | Cleanup (default: the dialog's current)", "lockPreroutes": "bool, default true - existing tracks are kept", "ripupViolations": "bool, default false", "save": "bool, default false", "timeout": "seconds, default 600"}),
            json!({"before": "{unconnected, errors}", "after": "{unconnected, errors}", "steps": "[..]", "saved": "bool"}), 900,
            "Look at the result before keeping it (altium_show_2d_board) and check it with altium_routing_drc. Altium's rules drive Situs: set them first (altium_set_design_rules, altium_add_rule).",
            &["route_net", "routing_drc", "set_design_rules", "save"],
            &["Parts outside the board outline get routed to where they sit: place them first (altium_move_parts).", "With lockPreroutes:false Situs rips up and reroutes existing tracks."],
            json!({"filePath": "C:/proj/board.PcbDoc", "strategy": "Default 2 Layer Board"})),
        verb_doc("annotate", "Number every '?' designator (R?, C?, U?) across the project's sheets: the lowest free number per prefix, top-left first on each sheet. dryRun shows the plan.",
            json!({"projectPath": "path to the .PrjPcb (required)", "dryRun": "bool, default false", "save": "bool, default true"}),
            json!({"renamed": "[{sheet, from, to, result}]", "planned": "[..] (dryRun)", "skippedMultiPart": "[{sheet, designator, libReference, partId}]"}), 300,
            "Then altium_update_pcb carries the designators to the board.",
            &["sch_place_part", "update_pcb", "sch_edit"],
            &["Multi-part components with '?' are left alone (their parts share one designator); use Altium's Tools > Annotation for those."],
            json!({"projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("doc_params", "Read or set a sheet's document parameters: the title block (Title, DocumentNumber, Revision, DrawnBy, Author, CompanyName, ...).",
            json!({"sheet": "path to the .SchDoc (required)", "set": "{name: value} (optional; missing parameters are added)", "save": "bool, default true"}),
            json!({"params": "[{name, value}]", "set": "int", "saved": "bool"}), 60,
            "The title block shows them through =Title, =DocumentNumber, =Revision ... special strings.",
            &["sch_edit", "export_pdf"], &[],
            json!({"sheet": "C:/proj/Main.SchDoc", "set": {"Title": "12 V to 5 V buck", "Revision": "B"}})),
        verb_doc("update_pcb", "Design > Update PCB Document from the schematic (the engineering change order): parts added, removed or renamed, nets and footprints updated on the board. Previews by default; execute:true runs it (Execute Changes, Close), refusing to delete board parts unless allowRemovals:true.",
            json!({"projectPath": "path to the .PrjPcb (required)", "execute": "bool, default false - false only previews", "allowRemovals": "bool, default false - let the change order delete board parts that are on no sheet", "timeout": "seconds, default 300"}),
            json!({"executed": "bool", "preview": "{addParts, removeParts, sheetParts, boardParts}", "changeOrder": "[string] - the dialog's first page (groups carry counts)", "afterExecute": "[string]", "steps": "[..]", "dialogs": "[..] - anything Altium asked", "board": "path"}), 600,
            "Preview first (the default): removeParts are board parts on no sheet (test pads, fiducials, mounting holes) that the change order would DELETE; it refuses them unless allowRemovals:true. New parts land beside the board outline: place them (altium_move_parts) and check with altium_routing_drc.",
            &["annotate", "sch_place_part", "move_parts", "routing_drc"],
            &["Save the sheets first (the sch_* verbs save by default).", "The project must contain a .PcbDoc."],
            json!({"projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("export_pdf", "Every sheet of a project as one PDF (and the board's composite print with board:true), from an OutJob in a throwaway project; the user's project is not touched.",
            json!({"projectPath": "path to the .PrjPcb (required)", "output": "path of the .pdf, default <project>.pdf beside it", "board": "bool, default false", "timeout": "seconds, default 300"}),
            json!({"output": "path", "outputBytes": "int", "sheets": "int"}), 600,
            "Pull the file with ab's pull_file.",
            &["run_outjob", "export_manufacturing"], &[],
            json!({"projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("sheet_params", "Write parameters onto PLACED parts on the schematic sheets (the BOM write-back): MPN, Manufacturer, Mouser/DigiKey/LCSC numbers, anything. A missing parameter is added hidden; one undo step per sheet; each touched sheet is saved.",
            json!({"designators": "[string] or 'R1,R2' (required; every part of a multi-part component)", "params": "{name: value} or 'Key=Value;Key=Value' (required)", "sheet": "path to a .SchDoc - or", "projectPath": "path to the .PrjPcb: each designator is found on whichever sheet holds it", "save": "bool, default true", "timeout": "seconds, default 90"}),
            json!({"updated": "[designator]", "notFound": "[designator]", "sheets": "[{sheet, designators, paramsSet}]"}), 300,
            "The board picks the new parameters up with Design > Update PCB Document; altium_bom reads the sheets, so it shows them at once.",
            &["bom", "read_schematic", "set_params"],
            &["Sheet parameters, not the library: altium_set_params changes a library part."],
            json!({"projectPath": "C:/proj/board.PrjPcb", "designators": ["R1", "R2"], "params": {"MPN": "RC0402FR-0710KL", "Manufacturer": "YAGEO", "Mouser Part #": "603-RC0402FR-0710KL"}})),
        verb_doc("export_bom", "Bill of materials export: altium_run_outjob with the assembly OutJob and the 'Folder Structure' medium as defaults.",
            json!({"outjobPath": "path to an .OutJob - default: the bundled example Assembly.OutJob", "medium": "string, default 'Folder Structure'", "projectPath": "path to the .PrjPcb", "timeout": "seconds, default 300"}),
            json!({"surface": "bom", "outjob": "path", "outputDir": "path | null", "files": "[relative paths]", "dialogs": "[..]", "elapsedSec": "float"}), 300,
            "files[] names the BOM the OutJob wrote; pull it with ab's pull_file. For part data without an OutJob, altium_read_schematic returns parameters per component.",
            &["run_outjob", "export_gerber", "read_schematic"],
            &["Without outjobPath it exports the BOM of Altium's EXAMPLE design.", "The default OutJob is the example's Assembly.OutJob, which runs every output in that medium, not only a BOM."],
            json!({"outjobPath": "C:/proj/Assembly.OutJob", "projectPath": "C:/proj/board.PrjPcb"})),
        verb_doc("export_step", "Headless STEP export of a board. Default lane 'outjob': Altium's own ExportSTEP output run from a throwaway project in %TEMP% that points at the board - no dialog, no click (measured on AD 26.9.1). lane:'extension' is the older AdomBridge .NET extension path.",
            json!({"filePath": "path to a .PcbDoc (saved state is exported) - default: the bundled example board", "output": "path - default: <board>.step", "holes": "bool, default true (mechanical, electrical and free pad holes)", "suffixType": "none | board | custom | id, default none - Altium always names parts by DESIGNATOR; this only picks the suffix", "suffix": "string, required with suffixType custom", "folded": "bool, default false", "singlePart": "bool, default false (one merged part)", "copperOption": "int, Altium ExportCopperOption, default 0 (no copper)", "modelsOption": "int, Altium ExportModelsOption, default 2 (template default)", "componentsOption": "int, Altium ExportComponentOptions, default 0 (all)", "lane": "outjob | extension, default outjob", "nameOccurrences": "bool, default true - name each component occurrence after its designator, as KiCad's STEP does (step2glb --molecule finds MP1-MP4 by it)", "timeout": "seconds, default 300"}),
            json!({"lane": "string", "output": "path", "outputExists": "bool", "outputBytes": "int", "products": "[string] - STEP PRODUCT names (how parts are named)", "occurrences": "{named: int} - component occurrences named after their designator", "elapsedSec": "float"}), 360,
            "outputExists and products[] are the proof; then pull the file with ab's pull_file. Save the board first: the export reads the file on disk.",
            &["run_outjob", "save", "show_3d_board"],
            &["Part names are designators (J1, U3...), never the Comment or footprint: Altium has no option for that (suffixType 0-3 measured).", "Altium leaves every assembly occurrence unnamed; the bridge names each board component's occurrence after its designator (0.2.54), so tools that find parts by occurrence name (step2glb --molecule's MP1-MP4 anchoring) work as on a KiCad STEP.", "The throwaway project and OutJob are closed afterwards and the temp folder removed; the user's own project is never opened or changed.", "pcb_not_found / step_not_written / bad_args are named; dialogs from the OutJob come back under outjob.dialogs."],
            json!({"filePath": "C:/proj/board.PcbDoc", "output": "C:/proj/board.step"})),
        verb_doc("import_kicad", "Import a KiCad file with Altium's own KiCad importer, driven through the AdomBridge .NET extension.",
            json!({"filePath": "path to a .kicad_sym / .kicad_sch / .kicad_pcb (required)", "mode": "string, default 'Import'", "timeout": "seconds, default 300"}),
            json!({"result": "string - the extension's result file", "elapsedSec": "float"}), 300,
            "Output lands in 'Imported <stem>' beside the source. Then link footprints with altium_set_params {footprint}. For Adom parts prefer altium_import_part, which needs no extension.",
            &["extension_ping", "install_extension", "import_part", "set_params"],
            &["Footprints are NOT imported by Altium's KiCad importer.", "Same extension caveat as altium_export_step: on AD 26.9.1 it answered extension_timeout."],
            json!({"filePath": "C:/lib/VL53L5CX.kicad_sym"})),
        verb_doc("install_extension", "Install, update or remove the AdomBridge .NET extension (the PowerShell installer shipped beside the bridge). Altium must be CLOSED.",
            json!({"remove": "bool, default false - uninstall instead"}),
            json!({"removed": "bool", "output": "string - installer stdout tail", "stderr": "string"}), 200,
            "Sequence: altium_close, altium_install_extension, altium_launch, altium_extension_ping. Only altium_export_step and altium_import_kicad need it.",
            &["extension_ping", "export_step", "import_kicad", "close"],
            &["Refused with altium_running while any Altium is up (it holds AdomBridge.dll open); remove:true is allowed while running.", "A failed extension load can stop Altium starting: put extension/REMOVE-AdomBridge.cmd on the Desktop first.", "It was unregistered on AD 26.9.1 after coinciding with a 'Could not start Vault Explorer' warning: install only when you need those two verbs."],
            json!({})),
        verb_doc("extension_ping", "Does the AdomBridge .NET extension answer? Sends AdomBridge:Ping through the -R lane and waits up to 30 s for its result file.", json!({}),
            json!({"result": "string", "installedOnDisk": "bool", "elapsedSec": "float"}), 60,
            "success -> altium_export_step / altium_import_kicad will work. installedOnDisk:true with extension_timeout = installed but not loading on this Altium build.",
            &["install_extension", "export_step", "import_kicad"], &["It spawns an X2 -R helper against the running instance: do not loop it."], json!({})),
        // ---- library authoring ----
        verb_doc("create_footprint", "Create one footprint from a pad list in a PcbLib (the library is created and saved if the path does not exist) and return a proof frame.",
            json!({"name": "string (required)", "libraryPath": "path to a .PcbLib (required)", "description": "string", "pads": "'des,x_mil,y_mil,w_mil,h_mil,rect|round,hole_mil;...' - hole 0 = SMD", "timeout": "seconds, default 90"}),
            json!({"data": "{footprint, library, libraryState: opened|created, pads: int}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 90,
            "The footprint is registered but the library is NOT saved: altium_save {filePath: libraryPath}. Then altium_add_body for 3D and altium_show_footprint to check. For a real part use altium_build_footprint (spec with silkscreen and courtyard).",
            &["build_footprint", "add_body", "save", "show_footprint"],
            &["errorCode bad_number = a pad field that must be a number was not; nothing is created.", "Units are MILS here; altium_build_footprint specs are millimetres.", "A fresh library carries the stray PCBComponent_1: altium_cleanup_libs."],
            json!({"name": "R0603", "libraryPath": "C:/lib/Test.PcbLib", "pads": "1,-30,0,35,37,rect,0;2,30,0,35,37,rect,0"})),
        verb_doc("create_symbol", "Create one rectangular symbol from a pin list in a SchLib (the library is created and saved if the path does not exist) and return a proof frame.",
            json!({"name": "string (required)", "libraryPath": "path to a .SchLib (required)", "description": "string", "designator": "string, default 'U'", "bodyWidthMil": "int, default 800", "bodyHeightMil": "int, default 600", "pins": "'des,name,left|right|top|bottom,offset_mil;...'", "timeout": "seconds, default 90"}),
            json!({"data": "{symbol, library, libraryState: opened|created, pins: int}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 90,
            "Library is UNSAVED: altium_set_params {footprint, params}, then altium_save {filePath: libraryPath}. For multi-mode symbols use altium_build_symbol_modes.",
            &["build_symbol_modes", "set_params", "read_symbol", "save"],
            &["A fresh library carries the stray Component_1: altium_cleanup_libs.", "Pin offsets are mils along the chosen side; check shot.image for overlapping pins."],
            json!({"name": "CH340G", "libraryPath": "C:/lib/Test.SchLib", "pins": "1,GND,left,100;2,TXD,left,200;16,VCC,right,100"})),
        verb_doc("build_footprint", "Build a complete footprint (pads, silkscreen, courtyard, arcs) in a PcbLib from a kicad2altium spec file on this machine, and return a proof frame.",
            json!({"libraryPath": "path to a .PcbLib (required; created if missing)", "spec": "Windows path to the spec (tools/kicad2altium.py output; mm, Y up) (required)", "timeout": "seconds, default 240"}),
            json!({"data": "{footprint, description, library, libraryState, pads, throughHolePads, drawingPrimitives}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 240,
            "Stage the spec with ab's send_files first. Then altium_add_body {step}, altium_plate_vias for thermal vias, altium_save. altium_import_part does all of it in one call.",
            &["import_part", "add_body", "plate_vias", "show_footprint"],
            &["The footprint name comes from the spec's META line, not from an argument.", "Pads with a repeated designator are legal and all map to that pin (the heatsink-via convention).", "Library is left UNSAVED."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "spec": "C:/lib/specs/VL53L5CX.fp.txt"})),
        verb_doc("build_symbol_modes", "Build ONE SchLib component with N display modes (Normal + Alternate 1..N-1) from a symbol_modes spec file on this machine, and return a proof frame.",
            json!({"libraryPath": "path to a .SchLib (required; created if missing)", "spec": "Windows path to the spec (tools/symbol_modes.py output; mils, Y up) (required)", "timeout": "seconds, default 240"}),
            json!({"data": "{libref, library, libraryState, modes, pins, lines, displayModeCount}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 240,
            "Then altium_add_symbol_art for the chip outline, altium_add_models / altium_set_models for the footprint, altium_save. altium_symbol_mode {mode} frames each alternate.",
            &["import_part", "add_symbol_art", "symbol_mode", "add_models"],
            &["libref comes from the spec's META line.", "Compare data.displayModeCount with data.modes before trusting the alternates.", "Library is left UNSAVED."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "spec": "C:/lib/specs/VL53L5CX.sym.txt"})),
        verb_doc("add_body", "Put a 3D body on a footprint of a PcbLib: a STEP model, or an extruded box when no step is given. It REPLACES the footprint's existing 3D bodies (a re-import never stacks a second model) unless keepExisting:true. The bridge then re-aligns bodies onto the pads (normalize_bodies) and returns a frame.",
            json!({"libraryPath": "path to a .PcbLib (required)", "footprintName": "string - required when the library holds more than one footprint", "step": "Windows path to a .step - optional", "bwMil": "int, default 100 - box width", "bhMil": "int, default 100 - box depth", "hMil": "int, default 40 - box height", "soMil": "int, default 0 - standoff", "keepExisting": "bool, default false - ADD next to the existing bodies instead of replacing them (a deliberate second body)", "timeout": "seconds, default 90"}),
            json!({"data": "{footprint, body, replacedBodies, keptExisting, bodiesBefore, sameNameFootprints, bodiesNow, bodiesOnCanvas}", "replacedBodies": "int - old bodies removed before the add", "normalized": "string - the shifts applied", "shot": "{image: base64 png, width, height}", "_hint": "string - what was replaced, and a WARNING when bodies are still stacked"}), 90,
            "altium_save {filePath: libraryPath}, then altium_show_3d_chip to look at it; boards that already place the part need PCB:UpdateFootprints. Never engrave the MPN into the STEP.",
            &["normalize_bodies", "show_3d_chip", "pcb_report", "import_part", "footprint_primitives"],
            &["It REPLACES by default (from 0.2.52): data.replacedBodies says how many old bodies went and data.bodiesNow should read 1. keepExisting:true is the only way to stack a second body.", "A bad step path ('could not load model') removes nothing: the old body stays.", "data.sameNameFootprints > 1 = several footprints share the name and only the FIRST got the body: altium_remove_footprint the stale copies.", "Without footprintName it acts on the only footprint in the library (name_required when there are several).", "'could not load model' = the STEP path is not on THIS machine: stage it with send_files."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX", "step": "C:/lib/VL53L5CX.step"})),
        verb_doc("add_models", "Attach one or more PCBLIB footprint models, parameters, comment and description to a SchLib component (Altium's several-footprints-per-symbol mechanism).",
            json!({"libraryPath": "path to a .SchLib (required)", "symbolName": "string (required)", "models": "[{name, pcblib, description}]", "params": "{key: value}", "comment": "string", "description": "string", "timeout": "seconds, default 120"}),
            json!({"data": "{libref, models: int, params: int}", "shot": "{image: base64 png, width, height}"}), 120,
            "Check data.models is what you sent, then altium_save. To REPLACE the footprint use altium_set_models.",
            &["set_models", "set_params", "symbol_prims", "remove_symbol"],
            &["NOT idempotent: every call APPENDS another implementation. Repair = altium_set_models, or altium_remove_symbol + re-import.", "';' and '|' inside a description are rewritten (they are the spec separators).", "altium_read_symbol does not report models; use altium_symbol_prims {models:true}."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "models": [{"name": "VL53L5CX", "pcblib": "C:/lib/Adom_ToF.PcbLib", "description": "LGA-16 6.4x3.0"}], "params": {"Manufacturer Part Number": "VL53L5CXV0GC/1"}})),
        verb_doc("normalize_bodies", "Move every 3D body of a PcbLib's footprints (or one footprint) onto its pad cluster. A body added through the canvas lands 50000 mil from the pads in the stored frame, which makes Zoom All frame 50 inches of nothing.",
            json!({"libraryPath": "path to a .PcbLib - optional; opened first", "footprintName": "string - optional filter", "timeout": "seconds, default 120"}),
            json!({"data": "{moved: int, shifts: string}", "roundtripMs": "int"}), 120,
            "altium_add_body already runs it. Use it on libraries built elsewhere when altium_show_footprint shows an empty canvas; then altium_save.",
            &["add_body", "pcb_report", "show_3d_chip", "save"], &["Without libraryPath it acts on whatever PcbLib is current ('no PCB library is current' otherwise).", "It centres bodies on the pad-cluster centre: a deliberately offset body is moved too."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib"})),
        verb_doc("set_params", "Set parameters on a SchLib component, optionally its comment, and optionally link a footprint by name.",
            json!({"libraryPath": "path to a .SchLib (required)", "symbolName": "string - omit for the FIRST component", "params": "{key: value} or 'Key=Value;Key=Value'", "footprint": "string - PcbLib footprint name to link", "comment": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{component, paramsSet: int, footprint}", "roundtripMs": "int"}), 90,
            "Unsaved: altium_save {filePath: libraryPath}. 'LCSC Part #' and 'Manufacturer Part Number' are the JLC/BOM keys. altium_read_symbol shows the result.",
            &["read_symbol", "set_visibility", "set_models", "save"],
            &["footprint APPENDS an implementation on every call; use altium_set_models to end up with exactly one.", "Parameters are created HIDDEN (visible ones pile up over the symbol): altium_set_visibility to show one.", "Without symbolName it edits the first component in the library."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "params": {"LCSC Part #": "C2888092", "Manufacturer": "ST"}, "comment": "VL53L5CX"})),
        verb_doc("set_visibility", "Show or hide one named parameter on a SchLib component.",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "symbolName": "string", "param": "string - the parameter name", "visible": "bool, default true", "timeout": "seconds, default 90"}),
            json!({"data": "{param, visible: '1'|'0'}", "roundtripMs": "int"}), 90,
            "altium_save afterwards; altium_show_symbol to see where the text landed.",
            &["set_params", "read_symbol", "show_symbol"], &["'parameter not found' = the name must match a parameter altium_read_symbol lists.", "Visible parameters stack over the symbol body: show few."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "param": "Manufacturer Part Number", "visible": true})),
        verb_doc("read_symbol", "Read a SchLib component back: description, display-mode count, the pins of mode 0 (designator, name, electrical type), every parameter with its hidden flag, and its model links (footprints, 3D, sim).",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "symbolName": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{libref, description, displayModeCount, pinsMode0: int, pins: [{des, name, electrical: 0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power}], params: [{name, value, hidden}], models: [{type, name, isCurrent}]}", "roundtripMs": "int"}), 90,
            "The verification step after altium_import_part / altium_set_params: models[] shows which footprint the symbol is bound to (type PCBLIB, isCurrent true for the one a placed part uses). For the drawing use altium_symbol_prims.",
            &["symbol_prims", "set_params", "list_library", "show_symbol"], &["The SCH Library panel's Model pane can lag behind altium_show_symbol; models[] here is the symbol's own record.", "Only mode 0 pins are listed."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX"})),
        verb_doc("clone_symbol", "Copy a symbol inside the current SchLib under a new name, all display modes included.",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "from": "string - existing LibReference", "to": "string - new LibReference", "timeout": "seconds, default 90"}),
            json!({"data": "{from, to, primitives: int, displayModeCount}", "roundtripMs": "int"}), 90,
            "Then altium_set_params on the clone (its own MPN and comment) and altium_save.",
            &["rename_symbol", "remove_symbol", "set_params", "save"], &["'target already exists' - pick another name or altium_remove_symbol first.", "Same library only; there is no cross-library symbol copy."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "from": "VL53L5CX", "to": "VL53L7CX"})),
        verb_doc("rename_symbol", "Rename a component (LibReference) in the current SchLib.",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "from": "string", "to": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{renamed: string}", "roundtripMs": "int"}), 90,
            "altium_save, then altium_list_library to confirm.",
            &["clone_symbol", "rename_footprint", "list_library", "save"], &["'to' is NOT checked: an empty or already-used name is applied as given. altium_list_library first."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "from": "Component_1", "to": "VL53L5CX"})),
        verb_doc("rename_footprint", "Rename a footprint in the current PcbLib.",
            json!({"libraryPath": "path to a .PcbLib - optional; opened first", "from": "string", "to": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{renamed: string}", "roundtripMs": "int"}), 90,
            "altium_save, then re-link symbols that referenced the old name: altium_set_models {footprint: <new name>}.",
            &["copy_footprint", "rename_symbol", "set_models", "save"], &["'to' is NOT checked: an empty or already-used name is applied as given.", "Symbols link footprints BY NAME: a rename silently breaks their models."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "from": "PCBComponent_1", "to": "VL53L5CX"})),
        verb_doc("remove_symbol", "Delete one component from the current SchLib.",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "symbolName": "string", "timeout": "seconds, default 60"}),
            json!({"data": "{removed: string}", "roundtripMs": "int"}), 60,
            "The repair step for a symbol with duplicate models: remove, altium_import_part again, altium_save.",
            &["import_part", "cleanup_libs", "list_library", "save"], &["No undo through the bridge; the library is unsaved until altium_save, so altium_close_document without saving is the way back."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX"})),
        verb_doc("remove_footprint", "Delete one footprint from the current PcbLib.",
            json!({"libraryPath": "path to a .PcbLib - optional; opened first", "footprintName": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{removed: string}", "roundtripMs": "int"}), 90,
            "altium_save afterwards. Symbols that linked it keep a dangling model: altium_set_models.",
            &["remove_symbol", "cleanup_libs", "list_library", "save"], &["No undo through the bridge; closing the library without saving is the way back."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX"})),
        verb_doc("copy_footprint", "Duplicate a footprint inside the current PcbLib under a new name (every primitive replicated).",
            json!({"libraryPath": "path to a .PcbLib - optional; opened first", "from": "string", "to": "string", "timeout": "seconds, default 90"}),
            json!({"data": "{from, to, primitives: int}", "roundtripMs": "int"}), 90,
            "For a footprint from ANOTHER library use altium_xcopy_footprint. Then altium_save.",
            &["xcopy_footprint", "rename_footprint", "pcb_report"], &["'target already exists' - choose another name or altium_remove_footprint first."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "from": "VL53L5CX", "to": "VL53L5CX_HANDSOLDER"})),
        verb_doc("xcopy_footprint", "Copy a footprint ACROSS libraries: from fromLib into toLib (opened and made current), optionally under a new name.",
            json!({"fromLib": "path to the source .PcbLib", "from": "string - footprint name in the source", "toLib": "path to the destination .PcbLib - optional; else the current PcbLib", "to": "string - default: same as from", "timeout": "seconds, default 120"}),
            json!({"data": "{to, primitives: int}", "roundtripMs": "int"}), 120,
            "altium_pcb_report {path: toLib, name} to confirm pads and bodies arrived, then altium_save {filePath: toLib}.",
            &["copy_footprint", "pcb_report", "save"], &["'open the destination PcbLib first' = pass toLib.", "'target already exists' in the destination: pass a different to."],
            json!({"fromLib": "C:/lib/Vendor.PcbLib", "from": "LGA-16", "toLib": "C:/lib/Adom_ToF.PcbLib", "to": "VL53L5CX"})),
        verb_doc("swap_footprint", "Replace the FIRST component on the current board with a footprint from a PcbLib at the same position, re-assigning nets by pad designator. Tracks are untouched, so the wiring survives.",
            json!({"pcbPath": "path to the .PcbDoc - optional; opened first", "libraryPath": "path to the .PcbLib holding the new footprint", "footprintName": "string", "assigns": "'padDes=NET;...'", "timeout": "seconds, default 240"}),
            json!({"data": "{placed, padsOnComp, bodiesOnComp}", "windowEtiquette": "background"}), 240,
            "Built for single-part test boards (altium_test_board): iterate on a footprint and re-prove with altium_show_3d_board. altium_save the board afterwards.",
            &["test_board", "show_3d_board", "footprint_primitives"], &["There is NO designator argument: on a multi-part board it replaces whichever component the iterator returns first.", "Nets come ONLY from assigns (padDes=NET, all pads sharing a designator get the net): pads you do not list end up with no net."],
            json!({"pcbPath": "C:/proj/tof.PcbDoc", "libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX", "assigns": "1=GND;2=VDD"})),
        verb_doc("symbol_mode", "Show one display mode (0 = Normal, 1.. = alternates) of the CURRENT SchLib component in the editor and capture a frame. Drives the editor's own mode toggle, because setting DisplayMode alone does not repaint.",
            json!({"libraryPath": "path to a .SchLib - optional; opened first", "mode": "int, default 0", "timeout": "seconds, default 60"}),
            json!({"data": "{mode, displayModeCount, libref}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 60,
            "Select the component first with altium_show_symbol {libraryPath, symbolName}, then one call per mode up to displayModeCount-1.",
            &["show_symbol", "build_symbol_modes", "read_symbol"], &["There is no symbolName argument: it acts on the component current in the editor.", "Leave it on mode 0 when done: the editor keeps showing the last mode to the user."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "mode": 1})),
        verb_doc("cleanup_libs", "Remove the factory-default stray every fresh library carries: Component_1 from the current SchLib and an empty (no pads) PCBComponent_1 from the current PcbLib.",
            json!({"libraryPath": "path to a .SchLib/.PcbLib - optional; opened first", "timeout": "seconds, default 90"}),
            json!({"data": "{removed: int}", "roundtripMs": "int"}), 90,
            "Run once per new library after the first import, then altium_save and altium_list_library.",
            &["list_library", "remove_symbol", "remove_footprint", "save"], &["The SchLib side removes ANY component named Component_1 without checking that it is empty.", "removed:0 is normal on a library that was already clean."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib"})),
        verb_doc("install_library", "Register a library file in Altium's Installed Libraries (IntegratedLibraryManager.InstallLibrary) so its parts appear in the Components panel.",
            json!({"libraryPath": "path to a .IntLib/.SchLib/.PcbLib/.DbLib (required)", "timeout": "seconds, default 90"}),
            json!({"data": "{installed: path}", "roundtripMs": "int"}), 150,
            "Takes effect live, no restart. For a wiki bundle zip use altium_install_library_bundle (unpack + register + proof).",
            &["install_library_bundle", "list_library", "show_library"], &["'library not found' = the path must exist on THIS machine.", "Registers the file where it is: moving or deleting it later leaves a dead entry."],
            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib"})),
        verb_doc("compile_intlib", "Compile a .LibPkg into an integrated library (.IntLib).",
            json!({"libpkgPath": "path to a .LibPkg (required)", "output": "destination directory", "timeout": "seconds, default 300"}),
            json!({"data": "{libpkg, outputDir}", "roundtripMs": "int"}), 300,
            "Then altium_install_library {libraryPath: <the .IntLib>}. Check the file exists: the reply does not verify it was written.",
            &["extract_intlib", "install_library"], &["The LibPkg is opened as a project in the user's workspace and stays open.", "success only means the compile was dispatched; verify the .IntLib on disk."],
            json!({"libpkgPath": "C:/lib/Adom_ToF.LibPkg", "output": "C:/lib/out"})),
        verb_doc("extract_intlib", "Extract the editable .SchLib/.PcbLib sources from a compiled .IntLib (Altium writes them next to the IntLib and opens them).",
            json!({"intlibPath": "path to an .IntLib (required)", "timeout": "seconds, default 300"}),
            json!({"data": "{intlib, sources: [path]}", "roundtripMs": "int"}), 300,
            "Then altium_list_library / altium_read_symbol on the extracted sources.",
            &["compile_intlib", "list_library", "install_library"], &["sources[] lists EVERY .SchLib/.PcbLib in any open project, not only the ones this call extracted."],
            json!({"intlibPath": "C:/lib/Vendor.IntLib"})),
        verb_doc("place_on_pcb", "Place ONE footprint from a PcbLib on a new board and return a proof frame. The single-part predecessor of altium_test_board.",
            json!({"libraryPath": "path to a .PcbLib (required)", "footprintName": "string", "pcbPath": "path to save the new .PcbDoc - optional", "timeout": "seconds, default 90"}),
            json!({"data": "{pcb, placed, libFile, libBodies, primitives, padsOnComp, bodiesOnComp}", "shot": "{image: base64 png, width, height}", "windowEtiquette": "background"}), 90,
            "Prefer altium_test_board (outline, several parts, nets). bodiesOnComp:0 with libBodies>0 means the body did not copy: report it.",
            &["test_board", "place_on_sch", "show_3d_board"], &["Every call creates a NEW PcbDoc; without pcbPath it exists only unsaved.", "'footprint not found in PcbLib' - names from altium_list_library."],
            json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX", "pcbPath": "C:/proj/one.PcbDoc"})),
        // ---- licence / account ----
        verb_doc("license_status", "Plain-language answer to 'what Altium licence am I on?': the state from the window title plus a capture of the License Management page.", json!({}),
            json!({"running": "bool", "licensed": "bool | null", "licenceState": "string", "seatStuck": "'possible' | null", "summary": "string", "shot": "{image: base64 png, ...}"}), 60,
            "Read shot.image for the product row (Trial/Full, days left, seat n/n): that lives ONLY on the page. Not licensed -> altium_ensure_licensed.",
            &["ensure_licensed", "license", "instances"],
            &["It brings up the License Management page inside Altium (when the agent is resident), replacing the view the user had.", "One On-Demand seat serves every Altium on the account: 'no seats available' usually means ANOTHER running Altium holds it - altium_close that one."], json!({})),
        verb_doc("ensure_licensed", "One call to get a usable Altium: launch if needed, then claim a seat (Use License via UIA). Optional escalations: sign out to force a seat release, or start the free-trial flow.",
            json!({"launchWaitSec": "int, default 240", "allowSignInReset": "bool - if no seat is free, Sign Out (the human must sign back in)", "allowTrial": "bool - if no seat is free, open the trial flow in the browser"}),
            json!({"alreadyLicensed": "bool", "licenceState": "string", "steps": "[..]", "summary": "string", "needsHuman": "string - only for browser sign-in / trial activation", "seatStuck": "bool", "shot": "{..}"}), 300,
            "success -> drive. errorCode no_seat_available: find the Altium holding the seat (altium_instances here, or another machine) and altium_close it, then call again.",
            &["license", "license_status", "instances", "close"],
            &["One On-Demand seat serves EVERY Altium on the account. A KILLED Altium strands the seat (page shows Seats 0/1 while every claim fails): altium_close releases it, waiting does not.",
              "allowSignInReset:true signs the user OUT and ends with needsHuman (browser sign-in). Use it only when nothing else holds the seat.",
              "It may launch Altium, which raises Altium over the user's work once."],
            json!({})),
        verb_doc("setup_trial", "Onboarding state machine: from nothing to a working trial. Each call reports the stage it found (install | launch | signin | done) and the ONE next action; human steps are named, not faked.", json!({}),
            json!({"stage": "install | launch | signin | done", "summary": "string", "nextAction": "string", "needsHuman": "string - when a person must act", "licenceState": "string"}), 300,
            "Follow nextAction and call again until stage is done. Not installed -> altium_install (the staged install verb; its licence stage does the clicks itself).",
            &["install", "ensure_licensed", "signin_status", "launch"],
            &["Signed in but unlicensed, it starts the free-trial flow in the BROWSER and answers trial_needs_browser: a person names the workspace, accepts terms and clicks Activate.", "stage launch answers success:true without launching anything: call altium_launch yourself."], json!({})),
        verb_doc("signin", "Report-only alias of altium_signin_status: it does NOT sign anyone in. Signing in to Altium is a browser flow only the human can complete.", json!({}),
            json!({"running": "bool", "licensed": "bool | null", "licenceState": "string", "workspaceConnected": "bool", "workspaceMenu": "string", "currentUserProcess": "object - only when the agent is resident"}), 300,
            "To see WHO is signed in: altium_license {action:'show'} and read the page ('Sign Out' visible = already signed in). Not connected to a workspace -> altium_connect_workspace.",
            &["signin_status", "license", "connect_workspace", "install"], &["Despite the name there are no credential arguments and nothing is typed or clicked.", "Never ask the user for their Altium password: the browser flow is theirs."], json!({})),
        verb_doc("signin_status", "Account and Altium 365 workspace connection as Altium's own UI reports it: licence state from the title, and whether the workspace menu reads 'Not Connected'.", json!({}),
            json!({"running": "bool", "licensed": "bool | null", "licenceState": "string", "workspaceConnected": "bool", "workspaceMenu": "string", "workspace": "null - the name is not read", "currentUserProcess": "object - only when the agent is resident"}), 60,
            "workspaceConnected:false -> altium_connect_workspace opens the picker. The signed-in account name is on the License Management page: altium_license {action:'show'}.",
            &["connect_workspace", "license", "license_status"], &["workspaceConnected is inferred from the ABSENCE of a 'Not Connected' menu item: a UIA miss reads as connected."], json!({})),
        verb_doc("connect_workspace", "Open Altium's workspace picker (the 'Not Connected' menu) and return a screenshot with a coordMap. The list is a custom control with no UIA children, so the caller clicks the row.", json!({}),
            json!({"picker": "{hwnd, title, className: TWorkspacesForm, rect}", "shot": "ab desktop_screenshot_window reply: image + coordMap/shotId", "windowEtiquette": "background"}), 60,
            "Click the wanted row with ab's desktop_click {space:'image', shotId, x, y} straight away, then altium_signin_status to confirm.",
            &["signin_status", "a365_status", "license"], &["picker_not_open usually means Altium is ALREADY connected (the menu shows the workspace name).", "The picker is a popup: it closes on focus loss, so click before doing anything else.", "A workspace with no free Author seat connects view-only: new documents then fail with view_only_no_author_seat."], json!({})),
        // ---- Altium 365 (cloud lane: works with Altium closed) ----
        verb_doc("a365_configure", "Store the Altium 365 workspace domain and, optionally, a Bearer token created by hand in the workspace web UI (Admin - Developer). Local-only; tokens never appear in replies.",
            json!({"workspace": "string - e.g. acme.365.altium.com (required unless already stored)", "token": "string - optional developer token"}),
            json!({"workspace": "string", "hasToken": "bool"}), 15,
            "Then altium_a365_status to prove the token is live. Preferred over a manual token: altium_a365_oauth_begin {clientId, workspace}.",
            &["a365_status", "a365_oauth_begin", "a365_query"], &["The token is written to a365.json in the bridge cache: altium_uninstall deletes it, altium_a365_logout removes it.", "Never echo a token back to the user or into a log."],
            json!({"workspace": "acme.365.altium.com"})),
        verb_doc("a365_oauth_begin", "Start the Altium 365 OAuth (PKCE + ActionWait) sign-in: the bridge starts listening FIRST, then returns the authorize URL for the user's browser. Works with Altium closed.",
            json!({"clientId": "string - an Altium-issued public OAuth client id (or the stored one)", "workspace": "string - e.g. acme.365.altium.com (or the stored one)", "selectWorkspace": "strict (default) | optional | none", "timeout": "int seconds, default 300 - how long to wait for the consent"}),
            json!({"status": "waiting", "workspace": "string", "authorizeUrl": "string"}), 30,
            "Open authorizeUrl in the user's EXISTING browser only after this returns, then poll altium_a365_oauth_status until status is authorized.",
            &["a365_oauth_status", "a365_oauth_cancel", "a365_status"],
            &["a365_client_id_required: OAuth clients come from Altium's Developer Center enrollment. Never reuse a first-party client id.", "a365_oauth_in_progress: one flow at a time - altium_a365_oauth_cancel the old one first.", "The consent page is the human's step; tokens never appear in any reply."],
            json!({"clientId": "<your Altium-issued client id>", "workspace": "acme.365.altium.com"})),
        verb_doc("a365_oauth_status", "State of the OAuth flow started by altium_a365_oauth_begin: idle | waiting | authorized | cancelled | error.", json!({}),
            json!({"status": "idle | waiting | authorized | cancelled | error", "workspace": "string", "claims": "object - unverified token claim summary (authorized)", "errorCode": "string (error)", "error": "string (error)"}), 10,
            "Poll every few seconds while waiting. authorized -> altium_a365_library_overview proves the workspace-scoped token. error -> read errorCode, then altium_a365_oauth_begin again.",
            &["a365_oauth_begin", "a365_oauth_cancel", "a365_library_overview"], &["success is false only for status error; waiting and idle are success:true.", "State lives in bridge memory: a bridge restart mid-flow reads idle."], json!({})),
        verb_doc("a365_oauth_cancel", "Cancel a waiting OAuth flow.", json!({}),
            json!({"status": "cancelling | <current status>", "alreadyStopped": "bool"}), 10,
            "Poll altium_a365_oauth_status until it reads cancelled, then altium_a365_oauth_begin again if wanted.",
            &["a365_oauth_begin", "a365_oauth_status"], &["The worker notices the cancel between long-polls (up to about 75 s)."], json!({})),
        verb_doc("a365_refresh", "Exchange the stored refresh token for a new access token (and store a rotated refresh token if one comes back).", json!({}),
            json!({"workspace": "string", "claims": "object - unverified token claim summary"}), 60,
            "Call when altium_a365_query answers a365_graphql_http 401 or altium_a365_status shows tokenLive:false, then retry.",
            &["a365_status", "a365_query", "a365_oauth_begin"], &["a365_refresh_not_configured = no OAuth grant stored (a manual developer token cannot be refreshed).", "a365_refresh_failed usually means the grant was revoked: altium_a365_oauth_begin again."], json!({})),
        verb_doc("a365_logout", "Revoke the refresh grant at Altium and remove every cached OAuth token. Workspace and client id stay configured.", json!({}),
            json!({"revoked": "bool - a remote revocation happened", "workspace": "string"}), 60,
            "altium_a365_status afterwards should read hasToken:false. Sign in again with altium_a365_oauth_begin.",
            &["a365_status", "a365_oauth_begin", "uninstall"], &["If the remote revocation fails (a365_revoke_failed / a365_network_error) the LOCAL tokens are kept on purpose: retry.", "This is the Altium 365 API grant only; it does not sign Altium Designer out or release a licence seat."], json!({})),
        verb_doc("a365_status", "Is the Altium 365 lane configured, is there a token, and is it LIVE (probed with a one-row GraphQL query)?", json!({}),
            json!({"configured": "bool", "workspace": "string | null", "hasToken": "bool", "hasRefreshToken": "bool", "clientConfigured": "bool", "endpoint": "string - the GraphQL URL", "tokenLive": "bool", "probe": "object - probe data or error"}), 60,
            "hasToken:false -> altium_a365_oauth_begin (or altium_a365_configure {token}). tokenLive:false with hasRefreshToken -> altium_a365_refresh. Live -> altium_a365_library_overview / altium_a365_query.",
            &["a365_oauth_begin", "a365_refresh", "a365_query", "a365_configure"], &["Independent of Altium Designer: this lane works while X2.exe is closed and says nothing about the desktop licence seat."], json!({})),
        verb_doc("a365_query", "Run one GraphQL query or mutation against the workspace's GraphQL gateway (https://<workspace>/svc/napi/gateway/graphql/) with the stored token.",
            json!({"query": "string - GraphQL (required)", "variables": "object", "timeout": "int seconds, default 60"}),
            json!({"data": "object | null", "errors": "[..] | null"}), 60,
            "Discover fields first with altium_a365_library_schema (or https://<workspace>/api/voyager). a365_graphql_http / a365_network_error -> altium_a365_status, then altium_a365_refresh.",
            &["a365_library_schema", "a365_library_overview", "a365_refresh", "a365_files_upload"],
            &["success is false whenever the reply carries errors[], even with partial data.", "Mutations change the user's shared workspace for everyone: know the rollback/delete mutation before running one.", "a365_not_configured = no workspace or token stored."],
            json!({"query": "{ desProjects(first: 10) { nodes { name } } }"})),
        verb_doc("a365_files_upload", "Upload one local file to the workspace's Files Service and get a temporary opaque file id to pass to a library mutation.",
            json!({"filePath": "path on THIS machine (required)", "maxBytes": "int, default 104857600 (100 MB)", "timeout": "int seconds, default 120"}),
            json!({"fileId": "string - opaque", "expiresAfterHoursInactive": "24"}), 120,
            "Pass fileId straight into the matching mutation via altium_a365_query {query, variables}. Do not parse or persist it.",
            &["a365_query", "a365_library_schema"], &["a365_file_not_found: the path must be on the bridge's machine - stage it with send_files.", "The id expires after 24 h of inactivity."],
            json!({"filePath": "C:/lib/Adom_ToF.SchLib"})),
        verb_doc("a365_library_overview", "One fixed read-only query: the workspace library's name, description, update time, component ids/names and footprint/symbol totals.", json!({}),
            json!({"data": "{desLibrary: {name, description, updatedAt, components: {nodes: [{id, name}]}, footprints: {totalCount}, symbols: {totalCount}}}", "errors": "[..] | null"}), 60,
            "The proof that a fresh OAuth token is workspace-scoped. Then altium_a365_library_schema before attempting any mutation.",
            &["a365_status", "a365_library_schema", "a365_query"], &["Same error codes as altium_a365_query (a365_not_configured, a365_graphql_http, a365_network_error)."], json!({})),
        verb_doc("a365_library_schema", "Read-only schema map: the GraphQL queries and mutations whose names mention lib, component, symbol, footprint, part or datasheet, with mutation argument types.",
            json!({"timeout": "int seconds, default 60"}),
            json!({"queries": "[{name, description}]", "mutations": "[{name, description, args: [{name, type}]}]"}), 60,
            "Pick the mutation, understand its input types AND its rollback/delete counterpart, upload inputs with altium_a365_files_upload, then altium_a365_query.",
            &["a365_query", "a365_files_upload", "a365_library_overview"], &["A name filter over introspection: a relevant field named differently is not listed - the full schema is at https://<workspace>/api/voyager."], json!({})),
    ];
    json!({"success": true, "bridge": "altium", "engine": "rust",
        "facts": ["one script at a time; a modal freezes everything",
                  "never foreground Altium; proofs are captured in the background",
                  "one On-Demand seat serves every Altium app on the machine; killing Altium strands it",
                  "instance:<pid|key|word> targets one of several running Altiums",
                  "agent-backed verbs bootstrap the resident agent on demand (~8 s) and answer {success, data:{ok,...}, roundtripMs}; the loop exits after 5 min idle",
                  "errorCode agent_busy names the command the agent is still running (busyWith, busyForSec): wait and retry",
                  "errorCode agent_script_error = a DelphiScript runtime error the bridge ALREADY cleared (read errorShot); do not resend the same command",
                  "errorCode bad_number = a caller value that must be numeric was not",
                  "errorCode view_only_no_author_seat = another running Altium holds the account's single seat; close it cleanly, then close + launch this one",
                  "errorCode blocked_by_dialog = a modal on the main window froze scripting: read dialogs[].shot, altium_dismiss_dialogs, retry"],
        "verbs": verbs,
        "allVerbs": names()})
}

fn need_licensed() -> Option<Value> {
    support::need_licensed()
}

fn board_read(cmd: &str, params: Vec<(&str, String)>, timeout: u64) -> Value {
    if let Some(g) = need_licensed() { return g; }
    let mut r = agent::call(cmd, &params, None, Duration::from_secs(timeout));
    let err = r.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str()).unwrap_or("");
    if err.contains("no current PCB board") {
        r["_hint"] = json!("Open a .PcbDoc first (altium_open_document / altium_show_2d_board), then retry.");
    } else if err.contains("no current schematic") {
        r["_hint"] = json!("Open a .SchDoc first (altium_open_document / altium_show_schematic), then retry.");
    }
    r
}

/// The dispatch chokepoint — instance selection, taskbar badge/progress and the run log wrap
/// EVERY verb here (one wrapper, not per-verb calls someone forgets), exactly like the Python
/// bridge's `dispatch_command`.
pub fn handle(command: &str, args: &Value, forwarded: &[(String, String)]) -> Value {
    let cmd = command.strip_prefix("altium_").unwrap_or(command);
    // Verbs that describe the machine rather than drive ONE Altium are exempt from instance
    // resolution — `instances` itself must work when several run, which is exactly when
    // resolution refuses.
    let instance_exempt = matches!(cmd, "instances" | "describe" | "diagnostics" | "progress" | "state" | "uninstall" | "readiness"
        | "a365_configure" | "a365_oauth_begin" | "a365_oauth_status" | "a365_oauth_cancel"
        | "a365_refresh" | "a365_logout" | "a365_status" | "a365_query" | "a365_files_upload"
        | "a365_library_overview" | "a365_library_schema");
    if instance_exempt {
        crate::instance::select(None);
    } else {
        match crate::instance::resolve(args.get("instance")) {
            Ok(inst) => crate::instance::select(Some(inst)),
            Err(refusal) => {
                let code = refusal.get("errorCode").and_then(|c| c.as_str()).unwrap_or("");
                // not-running stays a per-verb concern (many verbs answer usefully without
                // Altium); AMBIGUITY must stop here, or one thread's verb lands in another
                // thread's Altium.
                if matches!(code, "ambiguous_instance" | "instance_not_found") {
                    return refusal;
                }
                crate::instance::select(None);
            }
        }
    }
    let driving = !matches!(cmd, "readiness" | "status" | "describe" | "diagnostics" | "window_info"
        | "check_dialogs" | "progress" | "agent_status" | "list_commands" | "state" | "instances" | "dialog_log")
        && !cmd.starts_with("a365_");
    // The pacing phase: a live demo STAGE is a ~12s beat, not the 210s full background tour —
    // pacing it by the "demo" seed left the taskbar bar crawling 0→7% per stop, which John read
    // as no bar at all (2026-09-02).
    let phase = if cmd == "demo" && args.get("live").and_then(|b| b.as_bool()) == Some(true) {
        "demo_live_stage"
    } else {
        cmd
    };
    let mut bar_started = false;
    if driving {
        taskbar::set_driving_badge();
        taskbar::note_driving(); // feeds the 5-min idle clock that auto-clears the overlay
        let (est, conf) = progress::estimate(phase, "warm");
        if est >= 3.0 {
            taskbar::start_activity_progress(cmd, Some(est), &conf);
            bar_started = true;
        }
    }
    // Foreground etiquette on EVERY driving verb (#12; John, twice in two days: "why do you
    // always have to force altium to the foreground ... it really disrupts my ability to type").
    // The capture verbs already restore into the background, but the document steps
    // (open/close_document, place_on_sch, install_library, agent start/stop, run_command Zoom)
    // raise Altium through Client.ShowDocument / script execution. Same cure as launch (0.2.37):
    // remember the window the user is in; if Altium's process holds the foreground afterwards
    // when it did not before, hand the user's window straight back and say so in the reply.
    // Exempt: verbs whose whole point is the screen (foreground, launch, demo, install) and
    // close (the windows are going away).
    let etiquette = driving
        && !matches!(cmd, "foreground" | "launch" | "demo" | "close" | "install" | "uninstall");
    let user_fg = if etiquette { crate::lifecycle::foreground_info() } else { None };
    let t0 = std::time::Instant::now();
    let mut out = dispatch(command, args, forwarded);
    let ok = out.get("success").and_then(|b| b.as_bool()).unwrap_or(true);
    progress::log_run(cmd, t0.elapsed().as_secs_f64(), ok);
    if bar_started {
        taskbar::end_activity_progress(cmd, !ok);
        if ok {
            // feed the SAME key the estimate reads, so bars converge on this machine's truth
            // (measured > typical > unknown — the ladder only climbs if we record here)
            progress::record(phase, t0.elapsed().as_secs_f64(), "warm");
        }
    }
    if let Some((fg0, pid0)) = user_fg {
        let x2: Vec<u32> = crate::win32::pids_of("X2.EXE").unwrap_or_default();
        let was_altium = x2.iter().any(|p| u64::from(*p) == pid0);
        if !was_altium {
            // the user was NOT watching Altium; if Altium's process now holds the foreground,
            // that is a steal — reclaim. A user who deliberately switched to some OTHER window
            // mid-verb is left where they went (fg changed but not to Altium = no action).
            if let Some((fg1, pid1)) = crate::lifecycle::foreground_info() {
                if fg1 != fg0 && x2.iter().any(|p| u64::from(*p) == pid1) {
                    crate::lifecycle::restore_foreground_pub(fg0);
                    if let Some(o) = out.as_object_mut() {
                        o.insert("foregroundReturnedTo".into(), json!(fg0));
                        o.insert("windowEtiquette".into(), json!("background"));
                    }
                }
            }
        }
        if let Some(o) = out.as_object_mut() {
            o.entry("windowEtiquette").or_insert(json!("background"));
        }
    }
    out
}

pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> Value {
    let cmd = command.strip_prefix("altium_").unwrap_or(command);
    let timeout = args.get("timeout").and_then(|t| t.as_u64()).unwrap_or(60);
    match cmd {
        // ---- health / lifecycle ----
        "readiness" => misc::readiness(args),
        "status" => misc::status(args),
        "diagnostics" => misc::diagnostics(args),
        "window_info" => surfaces::window_info(args),
        "state" => misc::state(args),
        "instances" => misc::instances_verb(args),
        "dialog_log" => misc::dialog_log(args),
        "panel" => misc::panel(args),
        "list_installed_libraries" => library::list_installed_libraries(args),
        "uninstall_library" => library::uninstall_library(args),
        "launch" => misc::launch(args),
        "close" => misc::close(args),
        "uninstall" => misc::uninstall_verb(args),
        "describe" => describe(),
        "check_dialogs" => {
            let mut d = dialogs::scan();
            if args.get("screenshots").and_then(|b| b.as_bool()).unwrap_or(true) {
                crate::shot::attach_dialog_shots(&mut d);
            }
            json!({"success": true, "dialogsDetected": d.len(), "dialogs": d,
                   "_hint": if d.is_empty() { "No Altium dialogs open." } else {
                       "ANALYZE before acting: category/safeAction are suggestions; a save prompt is never auto-answered." }})
        }
        "dismiss_dialogs" => {
            let button = s(args, "button").unwrap_or_else(|| "OK".into());
            // {hwnd, button} is what every hint in this bridge tells callers to pass; it was IGNORED
            // and the button was pressed on EVERY dialog, save prompts included (found 2026-09-21).
            if let Some(hw) = args.get("hwnd").and_then(|h| h.as_i64()) {
                let r = dialogs::press(hw, &button);
                let ok = r.get("success").and_then(|b| b.as_bool()).unwrap_or(false);
                return json!({"success": ok, "pressed": [{"hwnd": hw, "button": button, "ok": ok}],
                    "errorCode": if ok { Value::Null } else { json!("button_not_pressed") },
                    "_hint": if ok { "Pressed on that one dialog only. altium_check_dialogs to confirm it is gone." } else { "UIA could not press that button on that window: read dialogs[].buttons from altium_check_dialogs for the exact label." }});
            }
            let titles: Option<Vec<String>> = args.get("titles").and_then(|a| a.as_array())
                .map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect());
            dialogs::dismiss(&button, titles.as_deref())
        }
        // ---- agent / REPL ----
        "agent_start" => {
            if let Some(g) = need_licensed() { return g; }
            let t0 = support::now();
            let r = lifecycle::start(Duration::from_secs(args.get("waitSec").and_then(|v| v.as_u64()).unwrap_or(45)));
            progress::record("agent_start", support::now() - t0, "warm");
            let mut r = r;
            if r.get("success").and_then(|b| b.as_bool()) == Some(true) {
                r["_hint"] = json!("Agent resident. altium_eval / altium_run_command / altium_state now go through it (~ms). Do NOT use altium_run_script while it is up.");
            }
            r
        }
        "agent_stop" => lifecycle::stop(),
        "agent_status" => misc::agent_status(args),
        "agent_recover" => {
            agent::call_no_autostart("tx_clear", &[], Duration::from_secs(30));
            let why = s(args, "reason").unwrap_or_else(|| "requested".into());
            lifecycle::recover(&why)
        }
        "eval" => {
            if let Some(g) = need_licensed() { return g; }
            let text = match s(args, "text").or_else(|| s(args, "code")) {
                Some(t) => t,
                None => return json!({"success": false, "error": "text is required (DelphiScript STATEMENTS: Begin ... End;)"}),
            };
            if !agent::is_alive() {
                // not resident → the -R RunScriptText lane is available and simplest
                let mut r = lifecycle::run_script_text_lane(&text, Duration::from_secs(timeout.max(30)));
                r["lane"] = json!("cli_run_script_text");
                if r.get("_hint").is_none() {
                    r["_hint"] = json!("Ran via X2 -RScriptingSystem:RunScriptText (statements only). Errors surface as an 'Error'/'X2' modal — altium_check_dialogs.");
                }
                return r;
            }
            let mut r = agent::call("eval", &[], Some(&text), Duration::from_secs(timeout.max(30)));
            r["lane"] = json!("agent");
            r
        }
        "run_command" => {
            if let Some(g) = need_licensed() { return g; }
            let c = s(args, "command").or_else(|| s(args, "process")).unwrap_or_default();
            if !c.contains(':') {
                return json!({"success": false, "error": "command must be Server:Command, e.g. PCB:Zoom",
                              "_hint": "altium_list_commands {query:'zoom'}"});
            }
            let params = match args.get("params") {
                Some(Value::Object(m)) => m.iter().map(|(k, v)| format!("{k}={}", v.as_str().map(|x| x.to_string()).unwrap_or(v.to_string()))).collect::<Vec<_>>().join("|"),
                Some(Value::String(p)) => p.clone(),
                _ => String::new(),
            };
            agent::call("run_process", &[("process", c), ("params", params)], None, Duration::from_secs(timeout))
        }
        "run_script" => outputs::run_script(args),
        "list_commands" => misc::list_commands(args),
        "open_document" | "open_project" => surfaces::open_document(args),
        "close_document" => {
            // through the agent's document-handle close: the menu processes are broken for this
            // on ADDevelop (RunProcess Client:CloseDocument no-ops; CloseAllOpenDocuments
            // unregisters documents while their views stay open - measured 2026-09-02, #7)
            if let Some(g) = need_licensed() { return g; }
            match s(args, "path") {
                None => json!({"success": false, "error": "path is required (absolute path of the open document to close; never saves - altium_save first if anything is worth keeping)"}),
                Some(p) => agent::call("close_document", &[("path", p)], None, Duration::from_secs(30)),
            }
        }
        "activate_document" => surfaces::activate_document(args),
        "progress" => misc::progress_verb(args),
        // ---- board-read primitive: ONE agent round trip, views shaped here ----
        "read_board" | "board_summary" | "rules_by_kind" | "layers_in_use" => {
            if let Some(g) = need_licensed() { return g; }
            let sections = s(args, "sections").unwrap_or_else(|| "components,nets,layers,copper,rules,occupancy,outline".into());
            let r = agent::call("read_board", &[("sections", sections), ("limit", s(args, "limit").unwrap_or("2000".into()))],
                                None, Duration::from_secs(timeout.max(120)));
            let data = r.get("data").cloned().unwrap_or(Value::Null);
            if data.get("ok").and_then(|b| b.as_bool()) != Some(true) {
                return r;
            }
            if args.get("raw").and_then(|b| b.as_bool()) == Some(true) {
                // the unshaped agent payload — this is what gets recorded as a test fixture
                return json!({"success": true, "raw": data, "roundtripMs": r.get("roundtripMs")});
            }
            let d = board::BoardData::new(data);
            // NB: match `cmd` (prefix-stripped), not `command`.
            match cmd {
                "board_summary" => json!({"success": true, "summary": d.summary(), "roundtripMs": r.get("roundtripMs"),
                                          "_hint": "One agent round trip; every view below is derived in the bridge."}),
                "rules_by_kind" => json!({"success": true, "board": d.board_path(), "rulesByKind": d.rules_by_kind(),
                                          "roundtripMs": r.get("roundtripMs")}),
                "layers_in_use" => json!({"success": true, "board": d.board_path(),
                                          "copperLayersInUse": d.copper_layers_in_use(),
                                          "layersWithPrimitives": d.layers_with_primitives(),
                                          "roundtripMs": r.get("roundtripMs"),
                                          "_hint": "copperLayersInUse = COPPER layers carrying copper, densest first — a floor, not the stack (an empty inner plane is invisible). layersWithPrimitives additionally counts silkscreen and mechanical."}),
                _ => json!({"success": true, "board": d.board_path(), "summary": d.summary(),
                            "designators": d.designators(), "nets": d.nets(), "layers": d.layers(),
                            "copperLayers": d.copper_layers(), "rules": d.rules(),
                            "roundtripMs": r.get("roundtripMs"),
                            "_hint": "The raw read behind every introspection view — record it as a test fixture with `raw:true`."})
            }
        }
        "read_schematic" => {
            if let Some(g) = need_licensed() { return g; }
            let params = if args.get("params").and_then(|b| b.as_bool()) == Some(false) { "0" } else { "1" };
            let r = agent::call("read_schematic", &[("sections", s(args, "sections").unwrap_or("components".into())),
                                                    ("params", params.into()),
                                                    ("limit", s(args, "limit").unwrap_or("500".into()))],
                                None, Duration::from_secs(timeout.max(90)));
            let data = r.get("data").cloned().unwrap_or(Value::Null);
            if data.get("ok").and_then(|b| b.as_bool()) != Some(true) { return r; }
            let sch = board::SchematicData::new(data);
            json!({"success": true, "document": sch.document(), "count": sch.components().len(),
                   "designators": sch.designators(), "components": sch.components(),
                   "roundtripMs": r.get("roundtripMs"),
                   "_hint": "The CURRENT sheet only — a hierarchical parent holds sheet symbols, not parts."})
        }
        "cross_check" => cross_check(args),
        "review_board" => review::review_board(args),
        // ---- direct board reads ----
        "list_designators" => board_read("list_designators", vec![], 90),
        "component_data" => board_read("component_data", vec![("designator", s(args, "designator").unwrap_or_default()), ("limit", s(args, "limit").unwrap_or("500".into()))], 90),
        "component_pins" => match s(args, "designator") {
            None => json!({"success": false, "error": "designator is required (e.g. U1)"}),
            Some(d) => board_read("component_pins", vec![("designator", d)], 90),
        },
        "list_nets" => board_read("list_nets", vec![], 90),
        "net_connections" => board_read("net_connections", vec![("net", s(args, "net").unwrap_or_default()), ("limit", s(args, "limit").unwrap_or("4000".into()))], 120),
        "layers" => board_read("layers", vec![], 90),
        "copper_layers" => board_read("copper_layers", vec![], 90),
        "read_rules" => board_read("read_rules", vec![], 90),
        "set_layer_visibility" => match s(args, "layerId") {
            None => json!({"success": false, "error": "layerId is required (from altium_layers)"}),
            Some(l) => {
                let shown = args.get("displayed").and_then(|b| b.as_bool()).unwrap_or(true);
                board_read("set_layer_visibility", vec![("layerId", l), ("displayed", if shown { "1".into() } else { "0".into() })], 60)
            }
        },
        "layer_stack" => board_read("layer_stack", vec![], 60),
        "footprint_primitives" => match s(args, "designator") {
            None => json!({"success": false, "error": "designator is required (a placed component, e.g. U1)"}),
            Some(d) => board_read("footprint_primitives", vec![("designator", d)], 120),
        },
        "schematic_data" => {
            // a named sheet becomes the current document first; without one the verb reads the current sheet
            // and refuses a library (#21)
            if let Some(e) = surfaces::stray_path(args, &["filePath", "sheet"]) { return e; }
            if let Some(p) = s(args, "filePath").or_else(|| s(args, "sheet")) {
                if !p.to_lowercase().ends_with(".schdoc") {
                    return json!({"success": false, "errorCode": "not_a_sheet", "error": format!("{p} is not a .SchDoc"), "_hint": "schematic_data reads a schematic sheet; a .SchLib is read with altium_read_symbol / altium_list_library."});
                }
                if let Some(e) = support::must_open(&p, 60) { return e; }
            }
            let pins = if args.get("pins").and_then(|b| b.as_bool()) == Some(false) { "0" } else { "1" };
            // The agent has always honoured params=1; the dispatch never sent it, so every reply
            // carried an empty params[] and the doc had to apologise for it. Off by default:
            // a sheet's parameters are bulky and most callers want pins.
            let params = if args.get("params").and_then(|b| b.as_bool()) == Some(true) { "1" } else { "0" };
            let names_only = if args.get("paramNamesOnly").and_then(|b| b.as_bool()) == Some(true) { "1" } else { "0" };
            board_read("schematic_data", vec![("designator", s(args, "designator").unwrap_or_default()), ("limit", s(args, "limit").unwrap_or("500".into())), ("pins", pins.into()), ("params", params.into()), ("paramNamesOnly", names_only.into())], 120)
        }
        // ---- surfaces ----
        "show_project" => surfaces::show_project(args),
        "show_schematic" => surfaces::show_schematic(args),
        "show_2d_board" => surfaces::show_2d_board(args),
        "show_3d_board" => surfaces::show_3d_board(args),
        "show_library" => surfaces::show_library(args),
        "show_symbol" => surfaces::show_symbol(args),
        "show_footprint" => surfaces::show_footprint(args),
        "show_3d_chip" => surfaces::show_3d_chip(args),
        "list_library" => surfaces::list_library(args),
        "demo" => {
            if args.get("live").and_then(|b| b.as_bool()) == Some(true) {
                surfaces::demo_live(args)
            } else {
                surfaces::demo(args)
            }
        }
        "animate_view" => surfaces::animate_view(args),
        "foreground" => surfaces::foreground(args),
        "save" => surfaces::save(args),
        "board_shape" => surfaces::board_shape(args),
        "pcb_report" => surfaces::pcb_report(args),
        "routing_state" => { if let Some(g) = need_licensed() { return g; } crate::routing::routing_state(args) }
        "route_net" => { if let Some(g) = need_licensed() { return g; } crate::routing::route_net(args) }
        "move_parts" => { if let Some(g) = need_licensed() { return g; } crate::routing::move_parts(args) }
        "place_vias" => { if let Some(g) = need_licensed() { return g; } crate::routing::place_vias(args) }
        "remove_items" => { if let Some(g) = need_licensed() { return g; } crate::routing::remove_items(args) }
        "ripup_nets" => { if let Some(g) = need_licensed() { return g; } crate::routing::ripup_nets(args) }
        "apply_pours" => { if let Some(g) = need_licensed() { return g; } crate::routing::apply_pours(args) }
        "pour_state" => { if let Some(g) = need_licensed() { return g; } crate::routing::pour_state(args) }
        "remove_pours" => { if let Some(g) = need_licensed() { return g; } crate::routing::remove_pours(args) }
        "repour" => { if let Some(g) = need_licensed() { return g; } crate::routing::repour(args) }
        "routing_drc" => { if let Some(g) = need_licensed() { return g; } crate::routing::routing_drc(args) }
        "build_board" => { if let Some(g) = need_licensed() { return g; } crate::routing::build_board(args) }
        "set_outline" => { if let Some(g) = need_licensed() { return g; } crate::routing::set_outline(args) }
        "set_design_rules" => { if let Some(g) = need_licensed() { return g; } crate::routing::set_design_rules(args) }
        "zoom_to" => crate::routing::zoom_to(args),
        "select_net" => crate::routing::select_net(args),
        "silk_state" => crate::routing::silk_state(args),
        "apply_silkscreen" => { if let Some(g) = need_licensed() { return g; } crate::routing::apply_silkscreen(args) }
        "export_manufacturing" => { if let Some(g) = need_licensed() { return g; } crate::manufacturing::export_manufacturing(args) }
        "build_schematic" => { if let Some(g) = need_licensed() { return g; } crate::routing::build_schematic(args) }
        "add_rule" => { if let Some(g) = need_licensed() { return g; } crate::routing::add_rule(args) }
        // ---- outputs / project ----
        "compile" | "run_erc" => outputs::compile(args),
        "run_drc" => outputs::run_drc(args),
        "run_outjob" => outputs::run_outjob(args),
        "export_gerber" => outputs::export_gerber(args),
        "export_bom" => outputs::export_bom(args),
        "bom" => crate::bom::bom(args),
        "sheet_params" => crate::bom::sheet_params(args),
        "sch_place_part" => crate::sch::place_part(args),
        "sch_connect" => crate::sch::connect(args),
        "sch_edit" => crate::sch::edit(args),
        "annotate" => crate::sch::annotate(args),
        "doc_params" => crate::sch::doc_params(args),
        "update_pcb" => crate::sch::update_pcb(args),
        "export_pdf" => crate::sch::export_pdf(args),
        "sch_zoom" => crate::sch::zoom(args),
        "autoroute" => crate::route::autoroute(args),
        "export_step" => outputs::export_step(args),
        "test_board" => library::test_board(args),
        "import_kicad" => outputs::import_kicad(args),
        "install_extension" => outputs::install_extension(args),
        "extension_ping" => outputs::extension_ping(args),
        // ---- library authoring ----
        "create_footprint" => library::create_footprint(args),
        "create_symbol" => library::create_symbol(args),
        "build_footprint" => library::build_footprint(args),
        "build_symbol_modes" => library::build_symbol_modes(args),
        "add_body" => library::add_body(args),
        "add_models" => library::add_models(args),
        "normalize_bodies" => library::normalize_bodies(args),
        "plate_vias" => library::plate_vias(args),
        "set_params" => library::set_params(args),
        "set_visibility" => library::set_visibility(args),
        "read_symbol" => library::read_symbol(args),
        "clone_symbol" => library::clone_symbol(args),
        "rename_symbol" => library::rename_symbol(args),
        "rename_footprint" => library::rename_footprint(args),
        "remove_symbol" => library::remove_symbol(args),
        "remove_footprint" => library::remove_footprint(args),
        "copy_footprint" => library::copy_footprint(args),
        "xcopy_footprint" => library::xcopy_footprint(args),
        "swap_footprint" => library::swap_footprint(args),
        "symbol_mode" => library::symbol_mode(args),
        "cleanup_libs" => library::cleanup_libs(args),
        "install_library" => library::install_library(args),
        "install_library_bundle" => library::install_library_bundle(args),
        "install_receipt" => library::install_receipt(args),
        "compile_intlib" => library::compile_intlib(args),
        "extract_intlib" => library::extract_intlib(args),
        "place_on_sch" => library::place_on_sch(args),
        "add_symbol_art" => library::add_symbol_art(args),
        "selftest_script_error" => {
            // Two SAFE outcomes exist, and which one you get depends on the Altium build:
            //  halted   - AD 26.9.1: an Error box, the script parks on a breakpoint; the bridge must
            //             photograph it and send EditScript:Stop (agent_script_error + autoRecovered).
            //  contained - Altium Develop: the engine raises a catchable exception, the agent's own
            //             Try/Except answers ok:false in ~2 s and the loop never stops.
            // Anything else (a timeout, a dialog left on screen) is the failure this test exists for.
            let mut r = agent::call("selftest_halt", &[], None, Duration::from_secs(45));
            let halted_ok = r.get("errorCode").and_then(|c| c.as_str()) == Some("agent_script_error")
                && r.get("autoRecovered").and_then(|b| b.as_bool()).unwrap_or(false);
            let contained_ok = r.get("data").and_then(|d| d.get("error")).and_then(|e| e.as_str())
                .map(|e| e.starts_with("exception in agent")).unwrap_or(false) && agent::is_alive();
            let dialogs_left = crate::dialogs::scan().len();
            let pass = (halted_ok || contained_ok) && dialogs_left == 0;
            r["netWorks"] = json!(pass);
            r["outcome"] = json!(if halted_ok { "halted_and_recovered" } else if contained_ok { "contained_by_agent" } else { "unhandled" });
            r["dialogsLeft"] = json!(dialogs_left);
            r["success"] = json!(pass);
            r["_hint"] = json!(if pass && halted_ok {
                "PASS (halted_and_recovered): this Altium build halts on a script error; the bridge photographed the Error box (errorShot), sent EditScript:Stop and left nothing on screen. The next verb restarts the agent."
            } else if pass {
                "PASS (contained_by_agent): this Altium build raises a catchable exception, so the agent's own handler answered and the loop never stopped. No box, no halt."
            } else {
                "FAIL: a deliberate script error was neither contained by the agent nor recovered by the bridge, or it left a dialog up. altium_agent_recover, then fix agent.rs/dialogs.rs before shipping."
            });
            r
        }
        "install" => crate::install::install(args),
        "symbol_prims" => library::symbol_prims(args),
        "set_models" => library::set_models(args),
        "place_on_pcb" => library::place_on_pcb(args),
        "import_part" => library::import_part(args),
        // ---- licence / account ----
        "license" => license::license(args),
        "license_status" => license::license_status(args),
        "ensure_licensed" => license::ensure_licensed(args),
        "setup_trial" => license::setup_trial(args),
        "signin" | "signin_status" => license::signin_status(args),
        "connect_workspace" => license::connect_workspace(args),
        // ---- Altium 365 ----
        "a365_configure" => a365::configure(args),
        "a365_oauth_begin" => a365::oauth_begin(args),
        "a365_oauth_status" => a365::oauth_status(args),
        "a365_oauth_cancel" => a365::oauth_cancel(args),
        "a365_refresh" => a365::refresh(args),
        "a365_logout" => a365::logout(args),
        "a365_status" => a365::status(args),
        "a365_query" => a365::query(args),
        "a365_files_upload" => a365::files_upload(args),
        "a365_library_overview" => a365::library_overview(args),
        "a365_library_schema" => a365::library_schema(args),
        _ => json!({"success": false, "error": format!("Unknown command: {command}"),
                    "_hint": format!("Valid: {}", names().join(", "))}),
    }
}

/// Compare a schematic sheet against a board. Altium CANNOT hold both current at once —
/// `GetCurrentPCBBoard` follows the focused document — so this reads them in TURN: activate the
/// board, read it, activate the sheet, read it.
fn cross_check(args: &Value) -> Value {
    if let Some(g) = need_licensed() { return g; }
    let timeout = Duration::from_secs_f64(support::f_or(args, "timeout", 45.0));
    let read = |kind: &str, path: Option<String>| -> Result<Value, Value> {
        if let Some(p) = &path {
            if let Some(bad) = support::validate_openable(p) {
                return Err(bad);
            }
            let o = agent::call("open", &[("path", p.clone())], None, timeout);
            let d = o.get("data").cloned().unwrap_or(json!({}));
            if d.get("ok").and_then(|b| b.as_bool()) == Some(false) {
                return Err(json!({"success": false, "stage": kind, "error": d.get("error"),
                                  "_hint": format!("could not open {p}")}));
            }
        }
        let cmd = if kind == "board" { "read_board" } else { "read_schematic" };
        let r = agent::call(cmd, &[("sections", "components".to_string())], None, timeout);
        let d = r.get("data").cloned().unwrap_or(json!({}));
        if d.get("ok").and_then(|b| b.as_bool()) != Some(true) {
            return Err(json!({"success": false, "stage": kind, "error": d.get("error"),
                "_hint": format!("No current {}. Pass {kind}:<path>, or open one first — Altium can only hold ONE document current, so cross_check must switch between them.",
                    if kind == "board" { "PCB board" } else { "schematic sheet" })}));
        }
        Ok(d)
    };
    let bd = match read("board", s(args, "board")) { Ok(d) => d, Err(e) => return e };
    let sd = match read("sheet", s(args, "sheet")) { Ok(d) => d, Err(e) => return e };
    let mut out = board::cross_check(&board::SchematicData::new(sd), &board::BoardData::new(bd));
    out["success"] = json!(true);
    out
}

#[cfg(test)]
mod describe_tests {
    use super::*;

    /// The SDK rule: EVERY verb declares a hint, related verbs, and an example. A verb added to
    /// KNOWN without a verb_doc entry, or a `related` name that is not a real verb, fails here.
    #[test]
    fn every_known_verb_is_documented_once_with_real_related_verbs() {
        let d = describe();
        let docs = d["verbs"].as_array().expect("verbs[]");
        let all = names();
        let mut seen = std::collections::BTreeSet::new();
        for v in docs {
            let name = v["name"].as_str().unwrap_or("").to_string();
            assert!(all.contains(&name), "{name} is documented but not in KNOWN");
            assert!(seen.insert(name.clone()), "{name} is documented twice");
            assert!(!v["summary"].as_str().unwrap_or("").is_empty(), "{name}: empty summary");
            assert!(!v["hint"].as_str().unwrap_or("").is_empty(), "{name}: empty hint");
            assert!(v["timeoutSeconds"].as_u64().unwrap_or(0) > 0, "{name}: no timeout");
            for r in v["related"].as_array().expect("related[]") {
                let r = r.as_str().unwrap_or("").to_string();
                assert!(all.contains(&r), "{name}: related {r} is not a verb");
                assert!(r != name, "{name}: related to itself");
            }
        }
        let missing: Vec<&String> = all.iter().filter(|n| !seen.contains(*n)).collect();
        assert!(missing.is_empty(), "verbs with no verb_doc entry: {missing:?}");
    }
}