Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
f82b5b5
8h ago
Changelog
0.2.58 (2026-10-21)
altium_autoroute: Altium's Situs autorouter on the current board.- It picks a strategy and keeps existing routing locked by default; Situs ripped it up with the dialog's default.
- It answers the unconnected count and DRC errors before and after, and doesn't save unless asked.
- Measured: one ripped-up net went from 4 unconnected to 1 in 14 s, with the existing copper untouched.
altium_layer_stack: copper layers in order, with copper thickness and the dielectric under each (height, Dk, material) and the total thickness.- It uses
Board.LayerStack_V7; on AD 26 the classicLayerStack.FirstLayerhalts the script. - Measured: the buck reads 2 layers, 36 um copper over 320 um FR-4; the Mini PC example reads 16 layers of TU-883, 2.0 mm.
- It uses
altium_read_rulescarries each rule'sdescriptor, Altium's wording with the values (e.g.Clearance Constraint (Gap=6mil)), so a DRC result can be explained.altium_sch_zoom: selects parts on a sheet, frames them, and returns a frame. It is the schematic half of cross-probing.
0.2.57 (2026-10-21)
Schematic editing on an existing design, and the change order to the board.
altium_sch_place_part: places a .SchLib part on an existing sheet at a position, with its library parameters and footprint links (or a named footprint).altium_sch_connect: wire stubs from pins' outer ends, with a net label (kind:"label"), a power port (kind:"power"; GND-like nets get a ground symbol) or nothing. Each connection reports ok or why not.altium_sch_edit: move, moveBy, rotate, rename, re-comment or delete placed parts, in one undo step.altium_annotate: numbers everyR?,C?,U?with the lowest free number per prefix across the project. Each part is named by its position, so severalR?on one sheet are told apart.dryRunshows the plan.altium_doc_params: reads or sets a sheet's title-block parameters.altium_update_pcb: Design > Update PCB Document.- It previews by default: parts to add, and board-only parts the change order would delete.
execute:trueruns it, and refuses deletions unlessallowRemovals:true.- It answers the one-time "link parts by designator" prompt on a board built by script (a real click: that dialog's buttons ignore Invoke) and the Information box after it.
- It presses Execute Changes, waits until every row is done (the agent is parked while the modal is up, so it cannot be the signal), closes, and saves the board. The change order edits it in memory only; a first test lost every change by closing it.
- Verified live on AD 26.11.1: R9 and R10 placed, annotated and connected on a sheet reached the board with their nets (R9: VOUT/GND, R10: VIN/GND) and survived a close and reopen. A comment-only change on an existing part opens no change order on that build; documented.
altium_export_pdf: every sheet as one PDF, plus the board's composite print withboard:true. It runs through an OutJob in a throwaway project, so the user's project is untouched. Verified: a 2-page PDF in 14 s.altium_run_outjob(and every verb built on it: manufacturing, STEP, PDF) answers Altium's "PCB Health Check Issues Detected" modal, which otherwise stopped the job indefinitely. It presses Generate Outputs (healthCheck:"stop"cancels) and returns the dialog's text inhealthCheck.schematic_dataalso reports each part'suniqueId.- New skill altium-schematic-edit. The main skill opens with an "I want to..." map of the whole bridge, and notes that
altium_evaldoes nothing while the agent is resident.
0.2.56 (2026-10-21)
altium_bom: the project's BOM, read straight from Altium.- Reads every schematic sheet the
.PrjPcblists, managed device sheets included, with each part's parameters. - Merges the board's placement and an optional variant (Not Fitted parts become DNP; alternates are listed).
- Groups one line per manufacturer part number. It matches parameter names across libraries:
MPN=Manufacturer Part Number=PartNumber, plus the Mouser, DigiKey and LCSC spellings. - Checks what an order needs: missing MPNs and distributor numbers, one part number on two footprints, board-only and sheet-only parts, DNP, and parts that stay out of a BOM.
- Writes
bom.csv,bom-jlcpcb.csv+cpl-jlcpcb.csv,bom-mouser.csv,bom-digikey.csvandbom.json. - Verified on AD 26.11.1: the TPS54202 buck (17 lines in 0.5 s), Altium's Mini PC example (46 sheets, 1046 parts, 132 lines in 19 s) and Bluetooth Sentinel (15 sheets including device sheets).
- Reads every schematic sheet the
altium_sheet_paramswrites parameters onto placed parts, finding each designator on whichever sheet holds it, and saves the sheets. It is the BOM write-back.altium-bom(container CLI, installed with the package):readrunsaltium_bomand saves the result.pricemakes oneadom-parts-searchcall per line and reports stock against the boards needed, price, lead time, lifecycle and JLCPCB-only parts, plus a cheapest in-stock total.fillwrites the missing distributor numbers back onto the sheets.- New skill: altium-bom.
schematic_data:- Also reports each part's
kind(ComponentKind),itemGuid,vaultGuidandsourceLibrary, and itsdescription. - A workspace-managed part's description is not read: on Altium's own Content Vault parts that call never returned.
- New
paramFilterandparamNamesOnlyoptions read only the values a caller needs; one example IC's three preview-image parameters took minutes to read.
- Also reports each part's
- The agent's JSON escaping is linear. It appended one character at a time, which is quadratic in DelphiScript; every verb that returns long text gets faster.
- The
altium-wiki-importskill no longer says the Cloud Library cannot export Altium: it can, without distributor numbers, and the skill says how to add them. uninstall.shalso removes thealtium-*container commands.
0.2.55 (2026-10-20)
- The edge rule no longer flags silkscreen (#26).
set_design_rulestakesedgeClearancefrom the fab pack, where it means copper to the board edge (KiCad'sedge_clearance). When the board had no outline-clearance rule, it created "Adom edge" scoped to everything, so Altium's DRC flagged silkscreen near the edge too, such as AI Flow's contact frames 0.12 mm in. The new rule is scopedOnCopper, and an existing "Adom edge" still scopedAllis re-scoped the next time the rules are set. Silkscreen keeps Altium's own silk rules. - Verified live on Altium Designer 26.11.1, on two copies of the same saved board with the gap widened to 0.6 mm so silkscreen near the edge is in reach: scope
Allgave 28 edge violations,OnCoppergave 5 (the copper). Every other rule's count was identical (137 silk-to-mask, 48 silk-to-silk, 8 mask sliver).
0.2.54 (2026-10-20)
- An Altium board's STEP anchors as a molecule. Altium's STEP export names each component's PRODUCT after its designator but leaves every assembly occurrence unnamed, where KiCad names the occurrence (
MP1).step2glb convert --moleculefinds the four machine pins by occurrence name, so an Altium molecule board convertedmolecule_anchored: false(0 MP markers) and its GLB was unanchored.altium_export_stepnow names each board component's occurrence after its designator (both lanes;occurrences.namedin the reply,nameOccurrences: falseto keep Altium's file as written). Nested model occurrences and occurrences Altium already named are left alone. altium_routing_stateanswerserrorCode: "no_current_board"and the agent's message when another document (a schematic) is in front, instead of an empty error.- Verified: the TPS54202 molecule board's Altium STEP (34 components) converts
molecule_anchored: trueafter the pass; a unit test covers the root/nested/escaped-quote cases.
0.2.53 (2026-10-20)
symbol_primsanswers the drawn mode's body.bodyRectMilwas the first rectangle found, which could belong to any display mode: the TPS54202's 10-mode symbol answered a wider mode's 1200 mil body for its 700 mil drawn body, and a capacitor answered one plate. It is now the union of the current display mode's rectangles (mode 0, the one a sheet draws).- Library pins keep their electrical type.
altium-symbol-modescarries each KiCad pin type into the spec (a 10th field), andbuild_symbol_modessets Altium's pin type from it: input, output, bidirectional (IO), tri-state (HiZ), open collector, open emitter, power in/out (Power), else Passive. Every pin used to be Passive, so Altium's ERC and AI Flow's pin-type check could not tell a power or output pin from a resistor's.read_symbolreturns each pin'selectrical(0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power). Live on AD 26.11.1: the TPS54202's GND, SW and VIN read 7, FB and EN 0, BOOT 4.
0.2.52 (2026-10-20)
- Importing a part again replaces its 3D body instead of stacking a second one.
add_body(and thestepofimport_part, which calls it) added a new body every call and never removed the old one, andnormalize_bodiesthen centred both on the pads: the AMPLH5030S inductor reached a board (2026-10-16) with the old model and the fixed one inside each other, repaired only byremove_footprint, a re-import andPCB:UpdateFootprints. The agent now deletes the footprint's existing 3D bodies before it adds the new one, the way the editor's Edit > Delete does (it selects exactly those bodies and runsPCB:Clear;RemovePCBObjectleft them in the stored footprint after save), howbuild_symbol_modesreplaces a symbol of the same LibReference. It removes nothing until the new STEP has loaded, so a bad path keeps the old body. - The
add_bodyreply saysreplacedBodies(also at the top level),keptExisting,bodiesBefore,sameNameFootprintsandbodiesNow, and its_hintwarns when bodies are still stacked or when several footprints share the name (only the first gets the body).import_part's add_body step carriesreplacedBodies,bodiesNowandsameNameFootprints. - New
add_bodyargumentkeepExisting(default false): add next to the existing bodies, for a deliberate second body. - Boards that already place the part keep the old body until
PCB:UpdateFootprints. - Verified live on Altium Designer 26.11.1: three repeated adds keep 1 body,
keepExistingmakes 2 and the next plain add makes 1 again, a bad STEP path keeps the old body, and the saved and reopened footprint has 1 body and its 2 pads.
0.2.51 (2026-10-19)
- IC symbols attach to their box again.
altium-symbol-modes(tools/symbol_modes.py, the spec behindbuild_symbol_modes/import_part) wrote each pin's outer end where Altium'sPin.Locationwants the end on the body, so every pin of a 2-column or 4-side symbol floated one pin length (300 mil) off the box with its name in the gap. Pins now start on the body edge in all ten display modes. Small parts drawn from their KiCad symbol (resistors, capacitors, inductors, LEDs) were never affected. dev-skills/altium-bridge-test/test_symbol_modes.py: every pin of every mode must start on its body edge (fails on 0.2.50, passes on 0.2.51).- Symbols already built with 0.2.50 or earlier keep the gap until rebuilt: run
altium-symbol-modesagain andaltium_build_symbol_modes(it replaces the symbol), thenaltium_add_modelsto put the footprint link and parameters back.
0.2.50 (2026-10-16)
- Templates are found on every Altium build.
build_schematicandexport_manufacturinglook for Altium's sheet and OutJob templates under the running install's own Public Documents folder (ADDevelop, AD26, ...) instead of AD26 only, so they work on Altium Designer Develop. symbol_modeno longer halts the script on Altium Designer Develop: it referenced an undeclared identifier (NOld).build_symbol_modesreportsreplaced.- Full verb battery green on BOTH builds: Altium Designer Develop (Colby-ROG2) and Altium Designer 26.11.1 (DESKTOP-BAR6S0F), each 145 pass, 0 fail, 1 skip (
a365_logout), 0 uncovered. 0.2.49 failed on Develop, which this release fixes; 0.2.50 is the build promoted to public.
0.2.49 (2026-10-16)
- Altium Designer 26.x with no licence tail reads as licensed (#19): the absent-tail rule covered only the Develop edition's window, so Altium Designer running on a Develop licence read
Unknown. The Use License pixel fallback now works through the bridge (it takes its own screenshot instead of needing a shotId). - No
X2 -Rhelper is spawned while a non-script dialog blocks Altium, and recover stops and names the dialog: one save prompt had turned into 15 minutes of "another instance is busy" notices. build_boardandbuild_schematicnever close a project over documents they did not make: the project reopen and rewrite read the project's members from Altium itself, save them before closing, and never close documents the project file does not list.- The verb battery covers all 146 verbs (phase 3.5 runs the AI Flow board lane) and refuses to start over a running Altium. Run twice on Altium Designer 26.11.1: 145 pass, 0 fail, 1 skip (
a365_logout),selftest_script_errornetWorks true.
0.2.48 (2026-10-16)
list_libraryand theshow_*surfaces refuse a document named under an argument they do not read, instead of opening the bundled demo library; every reply saysdemoFallback(#20).schematic_datarefuses a schematic library as the current sheet and takesfilePath(#21).build_boardinto a project that already lists the board reopens the project, so the board has one entry and one view (#23).read_symbolreturns the symbol's model links (#24).release.pychecks and pushes page.json and bridge-rs/src/main.rs with the other version stamps (#22).
0.2.47 (2026-10-16)
- The AI Flow Altium lane's verbs. Live board edits:
routing_state,route_net,move_parts,place_vias,remove_items,ripup_nets. Pours with a connect style per pour, solid joints, cutouts and island/neck options, repoured outside the script engine (scoped rules apply only there). set_design_rules,build_board, andbuild_schematicon Altium's own sheet template inside its project, compiled and checked pin for pin.project_nets,export_manufacturing(the project's own OutJob),silk_stateandapply_silkscreen,select_net,zoom_to,add_rule.- Fixes: passive symbols keep their standard drawing;
route_netplaces no via inside a same-net through-hole pad;run_drcanswers the repour prompt; agent start closes old script projects; verb timeouts match the docs (release.py now refuses a bridge.json timeout shorter than a verb's documented one).
0.2.46 (2026-10-15)
- Headless STEP export works (adom/altium-bridge PR #3, Aditya Angajala).
altium_export_stepno longer needs Barrett's .NET extension, which never loads into a running AD 26. The default laneoutjobwrites an ExportSTEP OutJob and a throwaway project pointing at the board into %TEMP%, runs it through the existing OutJob path, copies the .step tooutput, then closes both documents and removes the folder, so the user's own project is never opened or changed. Measured on AD 26.9.1: a 30 x 30 mm board in 3.5 s with no dialog. - New arguments:
holes(default true),suffixTypenone/board/custom/id plussuffix,folded,singlePart, and the raw Altium optionscopperOption,modelsOption,componentsOption. The reply listsproducts[], the STEP part names: Altium names parts by DESIGNATOR only. - Named failures:
pcb_not_found,bad_args,step_not_written(with the OutJob reply and its dialogs),copy_failed; the failure replies nametempDir, which is kept for inspection.lane:"extension"keeps the old path.
0.2.45 (2026-10-15)
First live run since 0.2.39 on a licensed Altium (Altium Designer Develop, Colby-ROG2). It verified the held 0.2.40-0.2.44 features and fixed what the run and two new issues found.
- A save into a missing folder no longer fails silently.
altium_test_board {pcbPath}andaltium_place_on_sch {schPath}answered with the path while Altium's save wrote nothing (the folder did not exist), so the next "append" found no board and started a new one. The bridge now creates the folder first and checks the file on disk:saved:true, orerrorCode: not_saved. A document that is open but not yet saved now counts as existing for append. - Appending to a schematic grows the sheet (
sheetGrown), so the added parts stay inside the drawing frame instead of landing in the gray past its right edge. altium_board_shapeproves its own result (adom/altium-bridge#16). It,pcb_report {want:'board'}andread_board's outline now read the outline's vertices:BoardOutline.BoundingRectangleis cached and kept the 6000 x 4000 mil default after a reshape, on AD 26.9.1 and Altium Develop alike. NewmechOutline:truedraws the rectangle on Mechanical 1 so the fab Gerber has an outline (the board shape alone is plotted into none).altium_test_boardparts carry their own name and footprint (adom/altium-bridge#17). The part name is the component's Comment, so it moves with the part (loose overlay text stayed behind and piled up in the silkscreen Gerber), andPatternis the footprint name (it was empty, so BOM, STEP naming and footprint comparison saw nothing).- New
altium_uninstall_libraryandaltium_list_installed_libraries(adom/altium-bridge#14). Uninstall matcheslibraryName(file stem: the SchLib and the PcbLib together) or an exactlibraryPath, closes any open document for the library first, never touches the files, and answersnotInstalled:truewhen nothing matches. Verified across a clean restart. It also cleared two dead "UI" entries left behind when 0.2.9 flattened the bundle folder. - The "X2 / Continue execution?" script box is classified (
script_runtime_error, safe action Cancel) instead of scanning as unknown: its text is painted, so it is only in the photo. altium_panel's timeout now says why: on Altium Develop a UI Automation search of the main window takes over 20 s, and no Altium process switches a docked tab; opening a library already brings its panel to the front.- Verified live, no code change: append on board and sheet (#11), focus kept on the user's window through document opens and agent restarts (#12),
altium_panelanswers inside its budget and the next verb is not delayed (#13).
0.2.44 (2026-09-29)
- The Altium extension DLL is signed too.
extension/AdomBridge/AdomBridge.dllnow carries the same Adom Industries, Inc. signature asaltium-bridge.exe: Windows Smart App Control checks the DLLs a program loads, not only the exe it starts, so an unsigned plugin could be refused on a PC where it is on. The release script signs and verifies every exe and DLL it ships and refuses to package any that is unsigned. Code otherwise identical to 0.2.43.
0.2.43 (2026-09-29)
- The exe is signed.
altium-bridge.exenow carries an Authenticode signature from Adom Industries, Inc., with a timestamp, so Windows 11 Smart App Control no longer blocks it ("Part of this app has been blocked"). The release script signs every exe it ships as the last step before packaging, verifies the signature on Windows and again after the file comes back, and refuses to package an unsigned exe. - Same code as 0.2.42 otherwise (bounded
altium_panel,altium_demoerror reporting), which is still waiting for its licensed test run.
0.2.42 (2026-09-23), STAGED for the licensed battery, not on any tier
Fixes the two defects John's licensed live test of the held 0.2.41 found (adom/altium-bridge#13, arav-rog, AD 26.10.1).
altium_panelanswers inside 24 s with a named result, every time. It was not a loop that never ended: it made up to five sequential UIA searches (read, select, read, click, read) with 10+15+10+15+10 s timeouts, exactly ab's 60 s budget, and each search of Altium's large window tree ran into its timeout, so the verb ended in ab's generictimed_outwith the abandoned searches still running (the likely cause of the collateralrun_drctimeout that followed it in the battery). Now one search reads the tab's existence and selection together, then at most one select and one verify, inside a 24 s budget; the answers arealready,ui_select,panel_not_found,panel_search_timeout(with the stage),ui_select_gated,panel_not_selected. The click fallback is gone (a click does not switch panel tabs), and when ab's select falls back to its raising SendInput click the reply sayslane: ui_select_raisedinstead of hiding a foreground steal.altium_democarries the failing beat's own error. A failed beat came back as a baresuccess:false(John saw it on Altium Designer Develop). The reason was real and discarded: the tour opens Altium's bundled AD26 example projects, which a Develop install does not have (file not found). The reply now carries the beat'serrorCode/error/_hint, names this casedemo_examples_missing, and falls back todemo_beat_failed.
Unreleased (docs, 2026-09-21)
0.2.39 was promoted to the PUBLIC tier (John Lauer's call and his words; public had been on 0.2.9, so 0.2.13-0.2.39 all landed at once), then re-verified by installing from the public manifest as a stranger would and running the full battery against that artifact: 121 pass, 0 fail, 1 safety skip.
- Documented in the user skills what 0.2.35-0.2.38 actually changed: the modal orphan and the never-half-answer-a-dialog rule (altium-instances, altium-licensing, README),
cleanup_libskeeping a non-empty stray,set_params/add_bodyrefusing an ambiguous empty name,extract_intlib'ssearchedUnder(altium-libraries),schematic_data {params:true}(altium-boards), andaltium_launchhanding the user's window back (README). - The battery's skip list is down from nine verbs to one. Eight were skipped for reasons that did not survive a dedicated test box; each now runs and proves its named refusal. Only
a365_logoutstays out, because it really does sign the user out. The test skill says what each one does and what to check before adding a tenth skip. - The dev, test and publishing docs now say that this bridge has more than one publisher: clone the page before editing, push only what you changed, and read a tier before assuming an older version is a bug rather than a deliberate hold.
0.2.41 (2026-09-21)
From the expired-licence incident on the 0.2.40 verification run (Colby's Develop entitlement lapsed mid-test):
- The expired-licence box is catalogued ("has expired or been deactivated", category
licence_expired): it scanned asunknownwhile it blocked every verb and wedged the close into a modal orphan that survived even WM_QUIT. The entry records the detection gap it exposed: Develop shows NO title licence tail in this state either, so the absent-tail "Licensed" inference reads wrong while this box is up. - Lone-OK boxes whose OK refuses UIA are dismissed natively. This box's OK exposed no InvokePattern (
button_not_pressedin 19 ms); its titlebar X performs the same acknowledgement, sopressnow falls back to a POSTED WM_CLOSE on the dialog - only ever for an OK, never for a button that answers a question.
0.2.40 (2026-09-21)
The two asks still open from John's ToF session (adom/altium-bridge#11 and #12), on top of what 0.2.13-0.2.39 already covered.
- Append to an EXISTING board or sheet (#11).
altium_test_board {pcbPath}andaltium_place_on_sch {schPath}now OPEN the document when the path already exists and ADD the parts: placed to the right of the current ones (board: right of the rightmost anchor, advancing by 1.5x each footprint's measured pad extent; sheet: 4000 mil right of the rightmost symbol, in line with the existing row), designators continuing from the existing count (U4, U5, ...). The board keeps its outline and layers (wMm/hMm ignored; resize with altium_board_shape); the sheet keeps its size and title block. Replies carryappendedandexistingParts. A new path still creates, exactly as before. - Foreground etiquette on EVERY driving verb (#12). The dispatch chokepoint now remembers the window the user is in before each driving verb and, when Altium's process holds the foreground afterwards where it did not before, hands the user's window straight back - the open/close_document, place_on_sch, install_library, agent start/stop and run_command steals John measured are all covered by the one wrapper. Replies say so:
windowEtiquette: background, plusforegroundReturnedTowhen a steal was reclaimed. A user deliberately on some OTHER window mid-verb is left where they went; verbs whose point is the screen (foreground, launch, demo, install, close) are exempt. - New verb
altium_panel {name, ensureId?}(#12's ganged-up view). Activates a docked panel TAB (SCH Library, PCB Library, Components, ...) in the background: UIA SelectionItemPattern on the dock tab, InvokePattern fallback, verified by reading the tab's own selection state - no server process can do this (Client:CustomizeResources only ensures the panel exists; a plain click does not switch tabs, measured 2026-09-02). Honest failure modes:ui_select_gatedwhen ab's shell grant is off (adom/adom-bridge#123),panel_not_selectedwith the attempted lanes otherwise.
0.2.39 (2026-09-21)
selftest_script_errorgets an honest timeout budget (60 s → 180 s). It is the slowest verb in the catalog by design — it halts the engine, waits for the halt to be noticed, recovers it and verifies by restarting the agent — and Altium Designer Develop parks SILENTLY, so the halt is only seen when the wait expires: measured 18 s on one run and over 60 s on the next, on the same machine. The bridge was cutting its own safety-net test off at 60 s and reporting a timeout, which reads as "the net is broken" when it is not. A timeout here now means something real.
0.2.38 (2026-09-21)
Correctness pass over the findings from the code review, each one verified in the source before it was touched.
cleanup_libsno longer deletes a symbol someone drew. It removed a SchLib component namedComponent_1unconditionally, pins and all; the PcbLib side had always checked the footprint was empty first. Now both sides only remove the empty factory stray, and the reply reportskeptNotEmpty.set_paramsandadd_bodyrefuse an ambiguous empty name (errorCode: name_required, with the count). With no name they took whichever component the iterator yielded first, so against a multi-symbol library they silently wrote a footprint, a comment or a 3D body onto an arbitrary part. An omitted name now only means "the one component in here".extract_intlibreports the libraries it actually extracted.sources[]listed every.SchLib/.PcbLibopen anywhere in the workspace; it is now scoped to the IntLib's own folder, and says so insearchedUnder.schematic_data {params:true}now reaches the agent. The agent has always honoured it, but the dispatch never sent the flag, soparams[]came back empty and the docs had to apologise for it.symbol_primsandimport_kicadgate on the licence like every other authoring verb, instead of failing later and less clearly.setup_trialno longer says "Launching it next" at a stage where it launches nothing — it names the next action, which is its whole contract.- Reviewed and found correct, no change:
run_outjobopens the .OutJob withObjectKind=Document(the object kind for the RUN step is separate, and is passed), andrun_drcpromises nothing it does not return.
0.2.37 (2026-09-21)
altium_launchhands the user's window back. Altium foregrounds itself at startup and offers no switch for it, so the bridge now remembers the window the user was working in and restores it once Altium has settled (foregroundReturnedTo,windowEtiquette: background).background:falseopts out when the user asked to watch Altium come up. John, twice in two days: "dude u keep forcing altium in front of me while i'm typing".- Full battery GREEN ON BOTH BUILDS: 122 verbs, 113 pass, 0 fail, 9 skipped for safety, on Altium Designer 26.10.1 (arav-rog) and Altium Designer Develop (ConfRoomROG) —
closeincluded, for the first time. Both machines were left with no Altium running and no dialog on screen.
0.2.36 (2026-09-21)
Fixes the last failing verb in the battery, close, and with it an overnight hostage situation: an Altium on the test box held the single licence seat for eight hours because nothing could reach it.
- The close-time save prompt is answered in full. "Save None" does not dismiss Altium's "Confirm Save Locally for (N) Modified Documents": it unticks the documents and RENAMES the same window to "Confirm Not Saving (N) Modified Documents", which OK then commits.
close {discardChanges:true}pressed only the first half and went back to polling, leaving Altium sitting modal. - A modal Altium disables its own main window — that is how a half-answered prompt strands it. When the dialog is then gone, the app is inside a modal loop with nothing on screen to answer: no menu, no Alt+F4, no WM_CLOSE, and every
-Rhand-off bounces off "There is another instance ... but it is busy".closeandagent_recovernow DETECT this (errorCode: altium_modal_orphan) instead of timing out or firing helper processes at it. - A graceful escape that keeps the seat.
close {discardChanges:true}ends a stuck instance with a postedWM_QUIT— Altium's ownApplication.Terminate, which runs its shutdown and RELEASES the On-Demand seat (measured: exit in under 8 s, the next launch licensed). Ataskkillstrands the seat; this does not. closeno longer runs the breakpoint recovery on every call. It asked "is a loop alive?" AFTER tearing the loop down, so the answer was always no: every close paid ~10 s of-Rhand-offs, and each one that failed spawned a helper X2. It now asks before the teardown.- A swallowed bootstrap is re-sent, not reported as a failure. Altium can accept the
RunScripthand-off and then drop it while it is busy with its own startup or unloading the script it was just told to stop; the agent never heartbeats and a healthy engine looked broken (no_heartbeaton the first start after a launch, and on the verifying start insiderecover).agent_startnow sends it once more before answering, and says so withresentBootstrap. A dialog on screen or a modal orphan is still reported immediately — those are real answers, never retried over. agent_recover's success hint no longer claims "a fresh agent loop is running" when it was asked not to start one.
0.2.34 (2026-09-21)
Third full battery on Altium Designer 26.10.1: 122 verbs, 112 pass, 1 fail (close), 9 skipped for safety.
- The reaper photographs every box before pressing it, not only ones titled "Error". An "Information" box explaining why Altium refused to quit was pressed away five times in a row, unread.
altium_dialog_log {shots:true}returns the last 8 pictures. - Every failed reply carries
_reportIssues: where and how to report a bridge bug, and what evidence to include (Bridge SDK). - A document that did not open is a failure on
altium_open_documentand everyshow_*verb too (0.2.25 covered only the library/board verbs). altium_uninstall {dryRun:true}no longer stops the resident agent.altium_licenserefuses an unknownactionby name instead of answering success.- Agent:
rename_symbol/rename_footprintrefuse an empty or already-taken name;set_layer_visibilityrefuses a layer id that is not in the board instead of dereferencing a nil layer object. closecatalog entry documentsdiscardChanges,waitSecand its error codes.
0.2.33 (2026-09-21)
- Every verb is documented for the AI: 122 of 122 have a catalog entry with summary, input, output, hint (the next call), related verbs, pitfalls and an example (98 had none). Each was written from its handler and its agent command. A unit test now fails the build if a verb is undocumented, documented twice, or names a related verb that does not exist.
describe.factslists the error codes added this week. - Two dialog verbs could answer questions nobody asked them.
altium_dismiss_dialogs {hwnd, button}IGNOREDhwndand pressed the button on every dialog, save prompts included, although every hint in the bridge tells callers to pass{hwnd, button}; it now presses that one dialog. A sweep (nohwnd) skips save prompts and any dialog with more than one button.recoverpressed OK on every dialog it found, andagent::callruns it automatically; it now touches only the lone-OK script error box.
0.2.32 (2026-09-21)
altium_agent_recoveractually recovers, and says the truth. It sentEditScript:Stopand answered success without checking. That process only acts when a SCRIPT document is the active document: with a board in front it was a silent no-op, the script stayed on its breakpoint,altium_selftest_script_errorreportedhalted_and_recoveredfor an engine that was never freed, and Altium then refused to close (AD 26.10.1; it had only ever worked where the script happened to be in front). Recover now brings each agent script in the workspace to the front (WorkspaceManager:OpenObject) and stops it there, warns if a hand-off started a second Altium, and VERIFIES by starting a fresh agent loop:engineFree:true, orerrorCode: engine_still_parkedwith what to do.altium_closefrees a parked script engine before and during the close (Altium refuses to close "when a script is stopped on a breakpoint"), without starting a new agent loop.altium_run_outjob/export_bom/export_gerberrefuse a missing OutJob by name (outjob_not_found); the agent answeredok:falsewith no text.- Docs:
PUBLISHING.md; every sub-skill starts with itsParent skill:line; README declares thealtiumkeyword, describes the native exe (it still said "Python server") and points installs ataltium_install; four blunt verbatim quotes in this changelog reworded.
0.2.31 (2026-09-21)
- FIX
altium_board_shape, which halted Altium's script engine on two builds (AD 26.10.1, Altium Develop). It drew four temporary tracks, ran the editor processPCB:PlaceBoardOutline, then readBoard.BoardOutline.BoundingRectanglewith no nil guard; on those builds the process left no outline, the read raised on a nil object and the script parked on a breakpoint with no error box. It now sets the outline the wayaltium_test_boardalways has (PointCount + Segments[] + Rebuild, which renders correctly on every build), with every access nil-guarded, and refusesbad_size/no_board_outlineby name.
0.2.30 (2026-09-21)
From the first full battery on a third Altium build (Altium Designer 26.10.1, fresh install): 121 verbs, 106 pass, 6 fail.
- The dialog scan saw none of the boxes that mattered.
Confirm Save Locally(Save All | Save None | OK | Cancel),Confirm Not SavingandZoom Value(OK | Cancel) matched no class allowlist, had more than one button and an uncatalogued title, soaltium_check_dialogsanswered "none" while they blockedaltium_closefor minutes. A titled window carrying two or more real dialog buttons is now a dialog whatever its class; the save prompts are catalogued assave_promptandZoom Valueasinput_prompt(safe action Cancel). The reaper still never presses a save prompt. altium_close {discardChanges:true}now WORKS (the arg was accepted and ignored): it presses Save None and confirms, for callers whose modified documents are all their own. Without it a save prompt returnsunsaved_changeswith a screenshot naming the documents, instead ofclose_timeout.- Battery: never runs a demo OutJob (they open what they generate on the user's screen);
show_projectwith no path is the bundled demo by design;read_schematicruns with its sheet shown.
0.2.29 (2026-09-20)
- New verb
altium_install: the install lane with its rules IN the verb. A from-scratch install on a fresh machine went badly because the AI driving it kept forgetting the rules in a 650-line skill: it asked the human to sign in on a page that was already signed in (it never looked), left Release / Assign / Use License / Refresh to the human although they are ordinary clicks, did not toast when it did need him, and launched the installer into an empty room so the UAC prompt timed out. Stagespreflight,get_installer,launch,wait,license(or no stage:autoreads the machine). It verifies the installer's Authenticode signature (Valid, CN=Altium LLC) before anything runs, refuseslaunchwithouthumanIsPresent:true, toasts the human itself and reportstoast.displayedtruthfully, and does the licensing clicks itself. Every reply carries_hint,_next,pitfallsandneedsHuman, andneedsHumanmay only ever name a password/2FA, the UAC prompt, or clicks inside an elevated window. detect.installVerb=altium_installanddetect.hostAppOptionalVerbsdeclared, so ab forwards the install/readiness verbs when Altium is absent andbridge_readinessrecommends this installer (two SDK audit misses).
0.2.28 (2026-09-20)
- One thread per request. The bridge handled HTTP requests one at a time, inline in its accept loop, so ONE slow verb blocked every verb behind it,
/healthincluded:altium_signin's ~25 s UIA search, or an agent call waiting on a parked engine. ab dropped its connection at its own budget (an opaque "HTTP error"), the bridge kept working the abandoned request, and the queue only grew: the 30-verb cascades the verb battery produced, twice, from two different first causes. Each request now runs on its own thread. Agent-bound work still serialises at the agent's one-file inbox, which answers a NAMEDagent_busywithbusyWithandbusyForSecinstead of a dropped connection.
0.2.27 (2026-09-20)
- The silent-halt fix in 0.2.26 modelled the wrong moment and did not fire. What really happens (heartbeat:
requests: 1, then nothing): a command raises an exception the agent's own Try/Except handles, the command ANSWERS normally, and THEN Altium's debugger parks the script on a breakpoint, on Altium Develop with no error box. The caller of that command sees nothing; the NEXT verb finds a dead agent, starts a second script into a parked engine and blocks until ab's HTTP timeout, and so does every verb after it.agent::callnow checks before bootstrapping: agent dead + Altium running + the agent script in front = parked, so it sendsEditScript:Stopfirst, restarts the agent, runs the verb, and marks the replyenginePreviouslyParked:true(also logged inaltium_dialog_logasengine_parked_cleared). The in-flight title trigger from 0.2.26 is removed: the title alone cannot tell a halt from a long command.
0.2.26 (2026-09-20)
Found by the new verb battery (dev-skills/altium-bridge-test), first full pass on Altium Designer Develop:
- Silent script halts are caught. Altium Develop can stop the agent in its script debugger WITHOUT raising an error box (it did on
board_shape): the box watcher from 0.2.21 saw nothing, the agent stayed dead, and ~30 verbs in a row queued behind it and timed out. Altium does give one tell: it pulls the agent's script document to the front.agent::callnow treats "the main window flipped toAdomAgent_*while my command is in flight" as a halt after a 1.5 s grace for the box watcher: it photographs the editor, sendsEditScript:Stopand returnsagent_script_errorwithsilentHalt:true. - Malformed numbers can no longer raise inside Altium. The agent called raw
StrToFloat/StrToInton caller values in 47 places; anything malformed (create_footprintwith pads in the wrong shape) raisedEConvertError, i.e. an Error box on the user's screen. All of them now go throughAdomFloat/AdomInt, which never raise, and the dispatcher answerserrorCode: bad_numbernaming the offending value.scripts/release.pyrefuses a build with a raw conversion outside those two helpers. altium_import_partwith nothing to import answeredsuccess:true; it now refuses withnothing_to_import.- New dev skill + runner:
dev-skills/altium-bridge-test/run_verb_tests.py, every verb in the live catalog, coverage-enforced, a dialog check after every verb, scratch documents only, closes Altium when done (Author seats), and REFUSES to start unless the target machine has been idle (it raises Altium, and it did so over the user's typing on its first run).
0.2.25 (2026-09-20)
- A document that did not open is now a failure, with the reason.
altium_open_document(and the open every library/board verb does first) answeredok:truewithshown:falseburied indatawhen Altium silently refused the file; the verb then failed with a misleading "open the SchLib first" / "current document is not a schematic library". Now:errorCode: file_not_found,view_only_no_author_seatordocument_not_opened, and 14 verbs that need the document stop there instead of running against the wrong one (run_drc's refocus andimport_part's opens stay best-effort). Projects are exempt (they have no document view). view_only_no_author_seat: measured on Altium Designer Develop. A second running Altium (another machine) held the workspace's only Author seat; Develop raised "Workspace Unavailable" at launch (acknowledged by the reaper), showed "There are no Author seats available ... view-only mode" behind the warning triangle, kept already-open documents working and refused every new one. The hint says so and gives the fix: close the other Altium cleanly, relaunch this one. No sign-in is involved.- The SOIC-8 / QFN-24 (9 heatsink vias) / BGA-36 suite now passes on BOTH Altium Designer 26.9.1 and Altium Designer Develop.
0.2.24 (2026-09-20)
altium_selftest_script_errorknows both safe outcomes and reports which one it saw inoutcome. Running 0.2.23 on a second Altium (Altium Designer Develop) showed the same deliberate error does NOT halt there: the engine raises a catchable exception, the agent's own handler answersok:falsein about 2 s and the loop never stops (contained_by_agent). AD 26.9.1 halts on a breakpoint and needs the bridge's recovery (halted_and_recovered). 0.2.23 called the first a failure. A timeout or a dialog left on screen is still the only FAIL. This is also why the 0.2.20 wedge never showed on the machine it was developed on.
0.2.23 (2026-09-20)
- New verb
altium_selftest_script_error: makes the agent raise a real DelphiScript runtime error on purpose and checks the safety net end to end (Error box photographed, halted script stopped withEditScript:Stop, no dialog left,netWorks: true). It is the regression test for the failure 0.2.21 fixed; run it after any change toagent.rs/dialogs.rsand once on each new Altium build.
0.2.22 (2026-09-20)
- Script-error safety net, second variant. 0.2.21 caught the lone-OK
Errorbox (an error in the agent's own code). Proving it with a deliberate badaltium_evalon AD 26.9.1 showed the OTHER shape: a script run from inside the agent raises anX2box, "Undeclared identifier: ... at Line:N. Continue execution?", with OK + Cancel. The reaper rightly never touches a two-button box, so the agent sat blocked and the caller saw a timeout.agent::callnow recognises that box while its own command is in flight, photographs it, answers OK (continue, so the agent loop survives) and returnserrorCode: agent_script_errorwitherrorShot,answered,agentAlive. It is logged inaltium_dialog_logasscript_error_continue. - The changelog line about removed internal files no longer names them.
0.2.21 (2026-09-20)
- A script runtime error can no longer wedge Altium silently. Found by running the 0.2.20 suite on a second machine (AD 26.9.1 trial; SOIC-8, QFN-24 with 9 heatsink vias, BGA-36): a DelphiScript runtime error raises a lone-OK
Errorbox, the standing reaper pressed OK within a second, and the script was left STOPPED ON A BREAKPOINT in Altium's script debugger. The agent loop was dead, every later verb timed out, Altium refused to close ("cannot close the current document when a script is stopped on a breakpoint"), and the error text was gone. Now: the reaper photographs anErrorbox before pressing it;agent::callsees it within 400 ms, sendsEditScript:Stopitself (thealtium_agent_recoversteps) and returnserrorCode: agent_script_errorwitherrorShot(the PNG of Altium's message),autoRecoveredandrecoverSteps. The next verb restarts the agent. - FIX the error that exposed it:
altium_symbol_prims {models:true}andaltium_set_modelsreadComp.ImplementationCount/Comp.Implementation[i], which AD 26.9.1 does not expose to DelphiScript. Both now walk a component's models with theeImplementationchild iterator. scripts/release.pyrefuses to build an agent template that uses an identifier measured to halt the engine (ENGINE_HALTING: the list from the dev skill, as code).- Docs: every declared verb is in the README and a skill (21 were in neither);
altium_plate_vias,altium_add_symbol_art,altium_symbol_prims,altium_set_modelsdocumented; newaltium-a365skill;install.shinstalls all user skills (instances, licensing, uia, wiki-import, a365 were packaged but never copied). - Internal material removed from the public repo: machine names, an account display name and a private workspace hostname in
PLAN.md/ the plan skill / code comments and error strings; one internal memo deleted. place_on_schdescribe text matches the agent (sheet sized from the measured parts, not "A4 or B"). Repo-rootbridge.jsonsynced to the shippedsrc/bridge.json(it was a stale 0.1.41 leftover).
0.2.20 (2026-09-20)
- New verb
altium_plate_vias {libraryPath, footprintName?}: every drilled pad in a PcbLib (all footprints, or the one named) that sits on a layer other than Multi-Layer becomes a plated Multi-Layer pad (Pad.Layer := eMultiLayer,Pad.Plated := True). adom-lbr writes heatsink vias as pads with a hole and Altium loaded them on the Top Layer, so a thermal pad's vias arrived with their holes but were not through-holes (John, 2026-09-20: "did Altium support bringing in those heatsink vias?"). The reply carriesholes(drilled pads seen),plated(pads changed) andfixed(Footprint:Pad;...). Modifies the open library in place:altium_savethe PcbLib afterwards;altium_footprint_primitiveson a placed part then reports layerId 74 for the vias. Pads already on Multi-Layer are left alone, so a second run changes nothing. - Version number: 0.2.19 was consumed by an interrupted publish, so this shipped as 0.2.20.
0.2.19 (2026-09-20)
altium_test_boardsilkscreen part name: its top now sits 2.5 mm below the part centre, clear of a 3 mm part's courtyard (0.2.18 overlapped it).
0.2.18 (2026-09-20)
altium_test_boardwrites each part's name on the Top Overlay under its footprint (35 mil stroke text, centred, its top 2.5 mm below the part centre so it clears the courtyard) so a human reading the board knows which chip is which (John, 2026-09-20: "place onto the silkscreen the name of the chip at that spot on the board"). Same rule as the KiCad and Fusion boards the ToF demo makes. Agent template only.
0.2.17 (2026-09-19)
altium_place_on_schsizes the sheet FROM the parts: pass 1 measures every symbol's bounding box (pins included, +250 mil a side for pin numbers), the custom sheet is the row plus 500 mil between parts and 600 mil margins (never smaller than A4 landscape, 11500 x 7600), any slack is spread evenly, designators sit above and part numbers below each body. TheSheetSizeX/Yread-back is gone: on ADDevelop it reports the 9500 x 7500 default after a custom size is set, which is what squeezed three parts on top of each other in 0.2.16. John's two reviews in one afternoon: "no real EE would smoosh symbols together, they'd space them out", then "a bit closer together so we get a better screenshot" - the numbers above are the ones he accepted.
0.2.16 (2026-09-19)
altium_place_on_sch: the designator sits centred ABOVE the body and the Comment (part number) centred BELOW it, whatever the library part did with them (+200 mil had put U1 on top of the chip outline inside a 3700 mil body); the description text frame sits INSIDE the drawing frame (700 mil below the top edge, 700 mil short of the right edge) instead of above the sheet; the sheet size is read back from the document (SheetSizeX/Y,UseCustomSheet := FalsebeforeSheetStyle) so parts and text land inside whatever Altium actually gave us. Agent template only.- Doctrine, measured with John typing in Hydrogen on the same box: the capture verbs restore Altium INTO THE BACKGROUND, but the document steps (
open_document,close_document,place_on_sch,install_library,agent_stop/agent_start) still raise the Altium window throughClient.ShowDocument/ script execution and steal his keyboard. Filed as an issue; until it is fixed, batch the document work and stop touching Altium the moment the proofs are in. - The "ganged-up view" (symbol + 3D chip in the Editor panel, footprint preview in the SCH Library panel): there is no server process that activates a docked panel TAB (
Client:CustomizeResources Action=Show|ObjectKind=Panel|ID=TSchLibPanelonly ensures the panel exists); apanelverb is on the wish list.
0.2.15 (2026-09-19)
altium_add_symbol_artreplaces instead of stacking (clear, default true) and draws the outline as eLine segments (mode: lines, default), the primitive that BuildSymbolModes has always rendered with; ISch_Polyline objects rendered on VL53L5CX but stayed invisible on VL53L8CX with identical geometry inside the body (measured withaltium_symbol_prims).symbol_primsgates its model walk behindmodels:true(the ImplementationCount / PartCount walk halted the engine on ADDevelop).
0.2.14 (2026-09-19)
- A sheet a human would sign (John, reviewing the demo capture: symbols on top of the title block and outside the frame, empty title block, '*' comments).
altium_place_on_schnow sizes the sheet for the part count (A4 up to two parts, B from three), places every part INSIDE the drawing frame centred on its body rectangle, sets each Comment to the part number, fills the title block fromtitle/number/revision/drawnBy(document parameters behind the standard title block's special strings) and dropsdescriptionas a text frame across the top. The reply carriessheetMil,titleBlock{}and_hintsthat name whatever the AI left empty and say how to write it. - The chip outline reaches Altium. New
altium_add_symbol_art {libraryPath, symbolName, kicadSym}reads the polylines adom-symbol baked into the KiCad symbol (chip-thumbnailer's outline of the side-etched model), maps the KiCad body rectangle onto the SchLib component's rectangle and draws them as polylines, so the Altium symbol carries the same chip as the KiCad and Fusion ones. Appends; run once per part. altium_test_boardreplies with_hints(list form, pcbPath, outline sizing, body check).
0.2.13 (2026-09-19)
- Several parts on ONE document (John's ToF demo: three VL53Lx sensors on one schematic and one board).
altium_test_boardandaltium_place_on_schnow accept a;list infootprintName/symbolName: every part lands on the same new board (spread evenly along X inside the wMm x hMm outline) or the same new sheet (U1..Un, 5000 mil apart), and both save topcbPath/schPathwhen given. Single-part calls are unchanged;assigns/stubsapply to the last part placed. Both verbs finally havedescribeentries. Agent template only: deploy the tmpl beside the exe andaltium_agent_stop+_start; the 0.2.9 exe keeps working with it. - Measured on a second test machine (ADDevelop):
altium_evalscripts that callPCBServer.PreProcess/PostProcessor evenTStringList.SaveToFilehalt before their first statement completes and leave an X2 error modal whose text is not readable (dialogs[].bodyempty); clear withaltium_dismiss_dialogs {Cancel}+altium_agent_recover, twice. adom/altium-bridge#11 asks for readable eval errors and for append-to-existing placement.
0.2.12 (2026-09-08)
- The one-click install no longer dies at Adom Bridge's 60 s budget (Colby, live on 2026-09-08: a 7-part Cloud Library export landed in Altium, SCH Library panel and all, while the page said "Could not install: 'altium_install_library_bundle' did not respond within ab's 60s budget"). ab enforces a per-verb budget it reads from
bridge.json, andinstall_library_bundlewas never declared there after 0.2.8 gave it a cold launch and 0.2.9 the open, panel-select and capture finish. Declared now: 330 s for the bundle install (the launch alone is 240), 150 s forinstall_library, and the verb'sdescribeentry is marked long-running withaltium_statusas its status verb. The reply carriesphasesMs(launch, licence, unpack, register, open, list, panel, capture, agentStop) andtotalMs, so the next slow run names its step instead of leaving a guess. - The receipt survives a timeout. The bridge serves one verb at a time and finishes an install ab has already given up on; the receipt, proof frame included, is now kept in memory.
altium_statuscarries a lightlastInstall(with theinstallIdthe client sent), and the newaltium_install_receiptreturns the full reply. The wiki page waits through this path (git-wiki e1c66a1), so a timed-out install still ends in the success line and the proof shot.
0.2.11 — 2026-09-07
- Fresh-workspace install fixed (adom/altium-bridge#10, found by the Hydrogen pre-production test: the dock's Altium Dashboard tile failed on every virgin box since 0.2.10). The 0.2.10 lint fix moved
altium-bridge-devout of the shipping tarball butinstall.shstill copied it, socpfailed and the whole install script was skipped, takingaltium-dashboardwith it.install.shnow installs only user skills;altium-bridge-publishmoved topublish-skills/in the same pass (publish skills are source-only, same doctrine);package.jsonfiles[] andpage.jsonscrubbed of both stale paths.uninstall.shstill removes previously-installed copies. Runtime unchanged.
0.2.10 — 2026-09-02
- Two-tier (insiders) publish adopted (adom/altium-bridge#9, John's ask; pattern from adom/pup-bridge which did it first).
src/bridge.jsondeclaresinsidersManifestUrl; a desktop on ab 2.0.99+ whose login can read the org's early-tier page follows the insiders manifest, everyone else follows the public one and never learns the field means anything.scripts/release.py --publishnow ships to INSIDERS by default (zip on this page, manifest + release-log entry on the early-tier page, source stamps, skills pkg in lockstep per adom/wiki#170); the public manifest moves ONLY through--promote <ver> --attest "<the human's exact words>" --by "<name>", which refuses placeholders and records the words in the manifest.--rollbackre-points the public tier at an archived build, also attested. No runtime changes beyond the manifest field.
0.2.9 — 2026-09-02
The dedicated freeze-fix release (adom/altium-bridge#6, #7, #8). Every mechanism here traces to the evidence gathered on a colleague's machine on 2026-09-02: three hard desktop freezes, all enabled by a resident agent loop the per-instance views could not see.
- The workspace split-brain is closed. The agent REFUSES to bootstrap without a resolved instance (with a 12s resolve retry for just-launched windows) instead of silently landing in the legacy root where status and teardown were blind to it.
altium_closenow stops EVERY workspace's live loop (legacy included) before its WM_CLOSE; readiness/agent_status reportagentResidentAnywhere+liveWorkspaces;altium_state(#8) answers from the union too, naming the workspace itslogicalread came from.install_library_bundlere-resolves the instance after its own auto-launch. - Close watchdog v2 detects natively and can end a wedged shutdown. 0.2.8's watchdog missed both freezes it watched because its detection ran through ab, whose enumeration hangs or returns empty exactly mid-freeze. v2 asks Win32 directly (non-messaging calls only; "cannot answer" is distinguished from "none"), drops stop files into every live workspace the instant the main window vanishes, and after 45s of "window gone, X2 alive, our loop was involved" it warns by toast and terminates the wedged process, recording the seat consequence. A ten-second blip instead of a bricked desktop.
- The one-click install lands the user on their library (#7). After registering, the bundle opens the installed SchLib, selects the SCH Library panel (UIA SelectionItemPattern - a plain click does not switch panel tabs), captures the proof frame, and reports an honest
shown: true|falseplus real component counts (symbolCount; the oldsymbols/footprintsfile counts remain one release). The agent is STOPPED when the install lane finishes, so the user's later X-press finds an idle engine. The wrapper folder inside export zips is flattened (no moreUI\UI\). altium_close_document {path}(new verb): closes one open document through its document handle, never saving. The menu processes are broken for this on ADDevelop: RunProcessClient:CloseDocumentno-ops andClient:CloseAllOpenDocumentsunregisters documents while their views stay open (both measured).- Agent template hardening:
Application.Terminatedjoins the loop's exit checks (Client.IsQuittingdemonstrably never fired in time), idle auto-stop cut from 30 to 5 minutes (residency IS the freeze window), and theclose_documentcommand. Per the probe campaign on #6: no interception design is possible in this engine (timers die at script return, the VCL context is a sandbox, runtime event assignment is illegal in every spelling, TApplicationEvents is unknown to the dfm loader), andClient.StopServerrejects both plausible signatures, so server hygiene is deliberately NOT attempted. - Dialog catalog: "Error Reading Form" (dfm class-not-found; Cancel is the clean dismissal) and a body-text fallback that reads Text-role control names, so the script debugger's "Continue execution?" prompt (title "X2", previously empty body) classifies instead of evading the reaper.
0.2.8 — 2026-09-02
- User-initiated-close watchdog, ported from Colby Knox's PR 74 (adom/altium-bridge#4) to the Rust runtime. A standing service polls every 2 s; for any per-instance workspace with a live heartbeat whose main window has vanished while an X2 process still lives, it drops the agent's stop file that instant, so the loop exits during shutdown instead of wedging under the engine teardown (the freeze that hard-locked a desktop). Cold starts never trigger it (a workspace must have been SEEN with a window). Each firing is logged in
altium_dialog_log. Honest limit, as in the PR: a 2 s race, not a guarantee — the complete fix is the agent noticing shutdown itself. altium_install_library_bundleauto-launches a closed Altium (Colby, #5): one-click parity with the KiCad/Fusion lanes; cold start runs with the measured taskbar bar, the agent bootstraps on the first call, and the reply carrieslaunchedwith the cold-start time.
0.2.7 — 2026-09-02
- The reaper pre-empts Altium's licence dialogs too (John, with two of them on his screen: "you should be handling this automatically for the user" / "you should pre-empt all of these dialog boxes btw"). Three gaps closed:
- the scan no longer gates lone-OK dialogs on a window-class allowlist — any titled, visible, non-main window on an Altium pid whose only UIA button is OK is an acknowledgement, whatever its class;
- the licence chooser (
TThemedConfirmDialog, "You are not currently using an Altium Designer license", painted text) is recognised by its BUTTON SET (Choose a license | Continue): the reaper presses Choose a license and then Use License on that instance's License Management page via UIA — Continue would leave Altium in Viewer mode, which is never right for a bridge user; - License Renewal Failed (
TFailedToRenewWarningForm) exposes NO buttons to UIA enumeration yet its OK clicks by name, so it is admitted by class and cleared by a TITLE rule; a title that matches the catalog now qualifies a window on its own.
- Honest limit, stated in the reaper log's
followup: a claim against a STRANDED seat (leased to a killed Altium) still fails with "maximum usage reached" — the bridge keeps that dialog off the screen, but only a sign-out/sign-in or the lease expiring frees the seat.
0.2.6 — 2026-09-02
- The standing reaper was blind since 0.2.0 — fixed. The VaultExplorer startup warning was still reaching the user's screen. Root cause: ab's
desktop_find_controlsreturns buttons undermatches; the Rust port readcontrols, so every dialog scanned asbuttons: []and the lone-OK acknowledgement rule could never fire. Parser fixed with a regression test built from the exact reply ab gave for that warning; title-bar controls are dropped so a lone OK still reads as lone. Verified minutes later: two startup warnings reaped before anyone saw them.
0.2.5 — 2026-09-02
altium_foregroundverifies instead of assuming: after the raise it reads the z-order and reportsvisiblyOnTop— a raise can "succeed" while another fullscreen window still covers Altium (John watched an entire show that never became visible because his fullscreen Claude window sat on top, and his own typing kept it there — which the foreground-once rule correctly refuses to fight).demo {live}surfaces the verdict asforegroundNoteso the narrating AI can TELL the user the show is running behind their current window rather than silently losing to it.- The taskbar progress bar is visible during live-demo stages: pacing used the 210 s full-tour "demo" seed, so a ~12 s stage filled 0→7% — invisible. Live stages now pace against their own
demo_live_stagephase (12 s seed), and every driving verb with a bar records its actual duration into the SAME key its estimate reads, so bars converge on this machine's measured truth instead of staying seeded forever.
0.2.4 — 2026-09-02
- The live demo foregrounds Altium ONCE, at stop one — never again mid-show (John: "just foreground altium once. then don't try to foreground again for the rest of the demo just to make sure you strike a balance for the user"). The opening raise is an invitation; re-raising every stop would drag back a user who deliberately glanced at something else. Documents keep opening in Altium either way, so a watching user misses nothing.
0.2.3 — 2026-09-02
- The punch_in preset stops flying the view into blank canvas. Altium zooms TOWARD THE CURSOR, so the 4-step zoom dive compounded drift toward wherever the user's mouse sat and scrolled the content off screen (John, watching take three: "you are like scrolling the content off the screen ... the screen going blank sometimes"). Per his fix: one gentle zoom-in/zoom-out breath, reframed with zoom_all — single-step drift is invisible.
- The demo/live hints now teach the adom-tts cue lane (1.9.47's
load/cue --confirm/cue-status, built from this thread's spec on adom/adom-tts#4): pre-cache the narration as markers, cue each stop in the background as the stage call starts, advance when both finish. Prefetched cues start instantly, which closes the last sync gap the say --play lane had.
0.2.2 — 2026-09-02
- The live show is code now, not a hand-rolled AI script (review feedback: everything the demo needs should be a verb, and the sync jitter came from driving the show step by step from the AI instead of from code). Driven from a container, every camera step paid the full CLI→ab→bridge→agent round trip (~1.4 s, jittery); in-bridge it is one agent file-drop (~40 ms), so choreography holds its timing.
altium_animate_view— camera/zoom choreography on the current document from NATIVE Altium camera commands (SetCamera/ResetCamera/FlipBoard/Zoom; Altium animates the transitions itself — no synthetic mouse). Steps (orbit_left/right/up/down, isometric, flip, zoom_in/out, pans, rolls…) or presets (punch_in, showcase3d, orbit, flip, finale), each with a pauseMs.altium_foreground— the sanctioned exception to the never-foreground rule, for a human who asked to WATCH. Refuses without a human-readablereason(>=10 chars), mirroring ab's caption rule.altium_demo {live:true}— the narrated, animated, foreground tour: per stop it paints the ab caption, opens the document, holds Altium in front, runs the choreography in-bridge, and returnsnarrate+nextNarrateso the caller's TTS speaks over exactly the motion described (fire nextNarrate detached, then make nextCall). When Altium is cold it narrates the launch too: caption + a spoken line + a background launch,status: "launching"until up.
- Fixed the zoom that never happened: schematic zoom-in is
Sch:Zoom Action=ZoomIn—Action=Inis silently IGNORED by Altium (John watched a whole stop where "pushing in" never moved). The rcs catalog carried the truth all along; the step map now encodes it.
0.2.1 — 2026-09-02
- A finished verb can no longer leave a stale partial progress bar on Altium's taskbar icon. John spotted it minutes after the 0.2.0 swap. Root cause: every progress paint is an ab round trip, and the clear was painted from the dispatch thread — so a tick still in flight from the ticker thread could land AFTER the clear and park a "47%" frame on the icon forever (a race the Python bridge carried latently; the Rust timing exposed it). The terminal frames (error flash, clear, flash:stop) are now painted by the ticker thread itself, where paints are sequential, so the clear is guaranteed to be last; the ticker also sleeps in 100 ms slices so the clear lands promptly.
0.2.0 — 2026-09-02
- The bridge is Rust, end to end. John: "let's finish getting everything into the rust code. i don't like that we're half rust half python now." All 112 verbs now dispatch from
bridge-rs(altium-bridge.exe,spawn.kind: "exe"per ab's SDK); the Python runtime no longer ships. What crossed over, at behavioural parity with 0.1.41:- the instance layer (
instance:<pid|key|word>,ambiguous_instancerefusal, per-instance agent workspacesagent/<key>, instance-aware licence gate incl. the Develop absent-tail rule); - background proof capture (restore-minimized-into-background → capture → hand the foreground back; sub-400px slivers refused as
proof_unavailable); - the standing dialog reaper (catalog safe-actions only, lone-OK acknowledgement rule, never a save prompt,
dialog_logring) and the in-flight-Rmodal watcher; - taskbar signals (driving badge, measured-estimate progress bar, 5-minute idle auto-clear) and the measured-progress store — same on-disk paths/JSON as the Python bridge, so each machine's history survives the engine swap;
- the seat rules (force-close refuses without
acceptSeatLoss, seat hints on every licence-shaped failure, UIA-first licence page driving viaUseLicenseButton); - all surfaces, library authoring,
import_part(zero-model attach still fails loudly),install_library_bundle(hardened unzip in Rust), outputs (compile/ERC, OutJobs, STEP via the extension, the DRC modal dance), review_board (all 14 checks as pure functions, now fixture-tested), cross_check, the rcs command catalog, and the Altium 365 OAuth/GraphQL lane (PKCE per RFC 7636, verified against its test vector).
- the instance layer (
- The verb lint now reads the RUST dispatch (
verbs.rsKNOWN) againstbridge.json, and the release also refuses on version drift acrossbridge.json/BRIDGE_VERSION/package.json/main.rs.cargo test(38 tests, incl. the agent-generation digest interop test that keeps Rust and the DelphiScript template in lockstep) runs before every build. - Two Rust-side views that existed but were never declared —
altium_board_summary,altium_rules_by_kind— are now inbridge.json(declared-vs-dispatchable is exactly what the lint exists for). - Still intentionally not Rust: the DelphiScript agent template (it runs INSIDE Altium; that lane is Pascal by nature) and the container-side converters (
tools/kicad2altium.py,tools/symbol_modes.py), which never ship in the bridge zip.
0.1.41 — 2026-09-01
- The tab favicon actually reaches the tab now. 0.1.40 shipped the correct line-mark
favicon.svg, but the dashboard's icon route aliased EVERY icon path —/favicon.svgincluded — todock-altium.svg, which is a 60 KB SVG wrapping an embedded PNG raster. Hydrogen's webview reads the page's favicon after load (hydrogen-tab-icons) and renders nothing for that blob, which is precisely the icon-less tab John reported twice. The favicon route now servesui/favicon.svg(the monochrome #e6edf3 line mark, same file as the header icon);/dock-altium.svgkeeps serving the dock card art. install.shnow installsfavicon.svg. Its asset list was explicit, so 0.1.40's new file never landed in~/.local/share/adom/altium-bridge/— the pkg carried it and the installer dropped it.- Verified end to end this time: the live server returns the 340-byte mark for
/favicon.svg, and a Hydrogen workspace capture shows the restored header (mark, subtitle, pill chip with LED, ghost buttons) live in the dock tab.
0.1.40 — 2026-09-01
- The dashboard header is whole again, and the dock tab has a favicon. John, from one glance at the dock: "why don't i see a favicon ... your header looks wonky ... almost like you're missing a css." Both right.
index.htmllinkedfavicon.svgbut the file did not exist (the route 404s honestly since the 2026-08-25 route-table sweep, so the tab showed the generic mark), and an earlier CSS deletion had taken the header's visual rules —.iconbtn,.vsep, the target-chip pill, the brand font — while the markup and a.iconbtn:focus-visiblerule survived, so the buttons rendered as UA-default white boxes on the dark bar. - Restored per dash-header's numbers: 28x28 ghost buttons with 18px glyphs (muted at rest, teal on hover), mono pill target chip with the LED inside, 1px vsep, name in Familjen Grotesk 600 15px
var(--text)— NOT the accent, per the 2026-08-24 ruling, deliberately unlike fusion's current teal name. favicon.svgis the SAME line-drawing mark as the header SVG, as the icons skill requires — one mark, tab and header.
0.1.39 — 2026-09-01
- 0.1.38's idle-watcher never actually started. The boot-site call used
taskbarunimported (dispatch imports it lazily inside the handler), so boot loggedname 'taskbar' is not defined— and logged it as a DIALOG REAPER failure, because the watcher call sat inside the reaper's try-block. Fixed the import, and each boot service now has its own try so a failure names the thing that failed. Verified by the boot log this time, not by assumption.
0.1.38 — 2026-09-01
- The taskbar overlay badge auto-clears after 5 minutes without a driving verb. John, seeing the Adom mark still parked on Altium's taskbar icon: "do you auto-remove that overlay after 5 minutes of verb inactivity?" The answer was no — the badge was set on the first driving verb and never cleared until the bridge died, while this module's own header PROMISED "cleared when the bridge goes idle". A permanent overlay stops meaning "an AI is driving right now" and starts meaning nothing. An idle watcher (started at boot beside the dialog reaper) now clears it at 5 idle minutes; the next driving verb re-badges instantly.
- For the record, what already worked: every driving verb with a measured estimate ≥3 s runs a REAL taskbar progress bar on Altium's button (percent paced by history, marquee when unmeasured, red flash + clear on failure) and stops it when the verb returns — progress rides the taskbar icon, never toasts, per the house rule.
0.1.37 — 2026-09-01
- Altium's dialog text is unreadable — so the reaper stops relying on it. Measured: the VaultExplorer warning's entire UIA subtree is
Window "Warning" -> panes -> Button "OK". The message is PAINTED pixels, absent fromWM_GETTEXTand from the accessibility tree, so no amount of text classification will ever see it. - The rule that does work: a dialog whose ONLY button is OK carries no decision. Dismissing it can acknowledge, never choose. Everything that asks something of the user has two or more buttons — a save prompt (Save / Don't Save / Cancel), a licence chooser (Choose a license / Continue), a confirm (Yes / No). The standing reaper now clears lone-OK dialogs even when their text is unreadable, logging them as
single_ok_acknowledgement. - Multi-button dialogs are still never improvised on: if the catalog's safe action is not literally one of the dialog's buttons, the reaper leaves it alone.
- This is what finally makes
Could not start Vault Explorer— and any future painted, single-OK Altium warning — invisible to the user.
0.1.36 — 2026-09-01
- The bridge can finally READ Altium's dialog messages. Altium PAINTS its dialog text instead of using Static controls, so
WM_GETTEXTreturned nothing but the BUTTON labels —Could not start Vault Explorer…came back as a body of just"OK". Every such dialog therefore classified asunknown, which is why the standing reaper (correctly, per its own rules) refused to touch the exact warning it was built for. _body_of()now strips the button labels and, when what is left is too short to be a message, asks UIA (desktop_ui_text) for the painted text. Classification, the reaper,check_dialogsand every_hintthat quotes a dialog all get the real message.- This was the missing half of 0.1.35: the reaper was correct, its input was blind.
0.1.35 — 2026-09-01
- A standing dialog reaper, running from bridge start. John: "and always pre-empt these from ever showing up." Clearing a dialog after a verb notices it is not pre-emption — the user has already seen it. The reaper polls every 1.5 s and presses the catalog's SAFE action the instant a KNOWN nuisance appears, whether or not any verb is in flight. That is the only thing that can catch Altium's STARTUP warnings (
Could not start Vault Explorer,File type not recognised), which appear with nothing running. - Hard rules, so it can never answer for the user: only dialogs whose catalog entry declares a safe action; never
unknown; never a save prompt (those name documents that may belong to another AI thread and are surfaced for a human decision). Every press is recorded and readable via the newaltium_dialog_log. - Catalog additions with safe actions:
Could not start Vault Explorer(Altium's own extension failing to load — NOT the bridge: the extension is correctly registered, its payload is complete at 115 files including VaultExplorer.dll, and Altium's private .NET 8.0.15 runtime loads fine),File type not recognised(at startup it is Altium restoring a document it can no longer open). - 110 verbs.
0.1.34 — 2026-09-01
- The force-close guard now fires on INTENT, not on a blocked dialog. 0.1.33 put it inside the blocked-by-dialog branch, so
altium_close {force:true}sailed straight through whenever nothing happened to block — exactly the path that strands the seat. It is now checked at the top of the verb:force:trueon a licensed instance is refused withwould_strand_licence_seatunlessacceptSeatLoss:true. altium_instancesapplies the Develop licence rule too. 0.1.33 fixed onlylicence_state(), so the instance list still reported Altium Designer Develop as unlicensed while its agent was running and importing.
0.1.33 — 2026-09-01
- Three new skills, because AI reads verb hints and skills, not tribal memory (John: "ai hints are way more valuable to help the ai know how to navigate the stuff you didn't know how to"):
- altium-licensing — one On-Demand seat serves EVERY Altium app (Designer and Designer Develop share it); killing an instance strands the seat and the licence page then reports it FREE while refusing every claim; the only recovery is a polite
altium_closethenuse_license. Also: Develop writes NO licence tail in its title when it HOLDS the seat. - altium-instances — addressing one Altium of several, why
ambiguous_instanceis a feature, the per-instance agent workspace, and the three places single-instance assumptions hid. - altium-wiki-import — wiki part page or Cloud Library → Altium, the Cloud Library's Altium gap (phase 3) and the pipeline that closes it, plus the background proof captures.
- altium-licensing — one On-Demand seat serves EVERY Altium app (Designer and Designer Develop share it); killing an instance strands the seat and the licence page then reports it FREE while refusing every claim; the only recovery is a polite
- AI hints where the knowledge bites, not only in the skills:
altium_instancesand everynot_licensedrefusal now carry_licenceHint/_seatHintexplaining the one-seat rule, the stranded-seat signature (Seats 0/1 while claims fail) and the exact fix. altium_close {force:true}refuses unlessacceptSeatLoss: true— a forced kill strands the seat, and this used to be one keystroke away. The blocked-by-dialog reply now also warns that an Unsaved Changes prompt NAMES documents that may belong to another AI thread.- Dialog catalog gained
License maximum usage reached,License Renewal Failed(re-fires on a timer; clearing it is not the fix),Cannot focus a disabled or invisible window(Altium's crash reporter — its only button SENDS data) and a sharperUnsaved Changesentry. - Title parser: an absent licence tail on Altium Designer Develop now means LICENSED, not unknown.
altium_instancesreported ADD unlicensed for an entire test while its agent imported five parts.
0.1.32 — 2026-09-01
- Instance-awareness now reaches the licence gate.
main_window()returned "the first Altium main window" regardless of the selected instance, andlicence_state()→_need_licensed()are built on it — so with two Altiums running, every verb targeting one instance was judged against the OTHER instance's licence. Measured live:altium_license_status {instance:"develop"}correctly reportedTrial, whilealtium_agent_start {instance:"develop"}was refusednot_licensedbecause Designer held no seat. - Verified with a real second instance (Altium Designer Develop): the layer distinguishes
AD26/DesignerfromADDevelop/Develop, gives each its own agent workspace, and REFUSES a verb withambiguous_instancewhen no selector is given — rather than acting inside the other AI thread's Altium.
0.1.31 — 2026-09-01
- Systemic fix:
ad_callbacknow unwraps ab's reply envelope. ab's direct API answers{success, status, error, output}with the verb's REAL result JSON-encoded as a STRING inoutput. The Python bridge never unwrapped it, so EVERY field read back from an ab callback —hwnd,windows,processes,controls— was silentlyNone. Calls that only ACTED (click, kill) worked, which is why this hid for so long; anything that tried to READ a result quietly saw nothing. - Found because 0.1.30's background-capture helper decided a minimized window was not minimized: it was reading
rectout of an envelope it could not see into. The Rust bridge has always unwrapped (ab.rs::unwrap_output) — this brings Python in line.
0.1.30 — 2026-09-01
- Proof captures work with Altium minimized, without ever moving the window. Windows does not render a minimized window, so a capture of one returned a ~237x39 title-bar sliver — which the bridge handed back as "proof" of a successful import, and which the wiki front-end would have displayed as evidence. Now every capture first makes the window CAPTURABLE: restore it if minimized, push it to the BOTTOM of the z-order, and hand the user's foreground straight back. Measured end to end at ~0.6 s; the capture then reads the occluded window at full size.
- The rejected alternative, recorded so nobody rebuilds it: parking the window off-screen. Rejected in review on 2026-09-01: a user who brings Altium forward by hand would not find it. Never move a window the user might reach for.
- A capture narrower than 400 px is refused, not returned.
_shot_of_mainreportsproofUnavailablewith the reason, and the surface verbs (show_symbol/show_footprint/show_3d_chip/ …) returnsuccess: false, errorCode: "proof_unavailable"rather than a confident nothing. The frame also carriesprepared— what the bridge had to do to the window — so a caller can say honestly whether the user's screen was touched.
0.1.29 — 2026-09-01
import_partno longer reports a silent half-import. Theadd_modelsstep recorded a bare boolean, so attaching ZERO footprint models looked identical to attaching one — and a symbol with no model places on a schematic with no footprint. It now surfaces the agent's own counts and FAILS withfailedAt: "add_models"when the count is zero.- It also sanitises the model spec the way
add_modelsalways did: the agent splits models on;and fields on|, so an unescaped separator inside a DESCRIPTION silently truncated the spec.import_partpassed the description raw. - Known defect, recorded not fixed:
add_modelsis not idempotent — each call appends another implementation, so calling it twice leaves a symbol with duplicate footprint models. Removing a model needs an implementation-read API this agent has not proven yet, so the safe repair today isremove_symbol+ re-import. - Verified end to end against a real wiki component (
adom/stm32f303cbt6, published by ray): kicad_sym → 48-pin symbol with 10 display modes, kicad_mod → LQFP-48 land pattern with courtyard/fab/pin-1 marker, STEP → 3D body, all three vendor part numbers attached, zero dialogs, 7.75 s, and the user's foreground window never changed.
0.1.28 — 2026-09-01
- Four design-rule hygiene checks, all from the rules the board read already carries:
disabled_design_rules— a rule that exists but is switched OFF. Worse than a missing rule: the board LOOKS ruled, DRC passes, and the constraint was never applied. Raised to error for Clearance / Width / HoleSize / ShortCircuit.missing_critical_rules(error) — no ENABLED rule of a kind a fabricator relies on.duplicate_rule_priority(info) — two enabled rules of one kind sharing a priority. Altium resolves the tie internally, so which one wins is not readable from the board; usually a copy-paste never re-prioritised.components_without_footprint(warning) — a placed part with an empty footprint pattern name, so it cannot be matched back to a library or checked for updates.
- Rule kinds are named from the ordinal table read off a live default rule set; an unknown ordinal renders as
kind <n>rather than being invented. - 14 checks.
0.1.27 — 2026-09-01
review_boardswitches documents and returns PARTIAL results. Altium holds one current document, so a board read and a sheet read cannot both be "now" — with a schematic focused, the previous version failed the whole review becauseread_boardhad nothing current. It now takes optionalboardandsheetpaths and activates each in turn (likecross_check), and when one document cannot be read it still runs the checks that CAN run.skippednames every check that did not run and why — "no board read", "no schematic read — pass includeSchematic:true". A review that silently drops half its checks reads exactly like a clean board, which is the failure mode worth engineering against.
0.1.26 — 2026-09-01
- The Adom house rule is now a check.
vendor_part_numbers(warning) flags any purchased component missing Mouser / DigiKey / LCSC part numbers — Mouser named first because it is the preferred vendor, so if only one gets filled in, that is the one. Test points, fiducials and mounting holes are exempt: they are not purchased parts, and flagging them would train people to ignore the check. missing_datasheets(info) andschematic_placeholder_values(error) — the latter catches default Comments at the SOURCE, where the BOM is actually generated from, rather than only on placed parts.- The schematic read now carries component parameters (
params: "1"), using the sameMkSet(eParameter)iterator the parameter WRITER has always used — so three checks arrived with no new Altium API surface. Capped at 40 parameters per component. - The three schematic checks return nothing unless
includeSchematic: true, since without a sheet read they have nothing to judge. - 10 checks.
0.1.25 — 2026-09-01
checks:[...]now filters on the ID you actually see. The selector was derived from the Python function name (_chk_components_outside_board) while findings reportcomponents_outside_board_outline, so filtering by the visible name silently ran ZERO checks and returned a clean review. Each check now declares its own ID, the reply listsavailableChecks, and an unrecognised name comes back asunknownChecksinstead of being ignored.- New verb
altium_read_board— the raw read (components, nets, layers, copper, rules, occupancy, outline) in one round trip. It existed only in the Rust bridge, which meant nobody could see what a check saw; a review's silence had to be taken on trust. Now it can be inspected directly.
0.1.24 — 2026-09-01
read_boardgains anoutlinesection — the board extent, read through the SAME guarded pattern the library reader already uses, because a library's internal board has a nilBoardOutlineand touchingBoundingRectangleon it halts the engine (measured 2026-08-30). It reports which source it used:boardOutline(the real outline's bounding box) orboardBounds(everything on the board, including strays outside it).- Two review checks unlocked by it, both named for what they actually measure:
components_outside_board_outline(error) — a part outside the board's bounding box.components_near_board_edge(warning) — a component ORIGIN within a margin of the edge (default 20 mil). Not calledpads_near_board_edge: per-pad geometry is not in the board read, and the catalog's name would claim a precision this does not have.
- Both go silent when the extent came from
boardBounds. That fallback includes stray primitives outside the real outline, so using it to declare a part "off-board" would produce confident false errors. A weaker source yields no finding rather than a wrong one. notImplementednow names the bounding-box limitation explicitly: on an L-shaped or slotted board, a part inside the box but outside the real outline is NOT caught.
0.1.23 — 2026-09-01
altium_review_board: design review over ONE board read. Phase 2 of the plan, from the lint catalog. Five checks, all pure functions over data the bridge already reads — so adding them cost no new Altium API surface, which is the whole point: a wrong identifier in the agent halts the script engine behind a modal on the user's screen.placeholder_values(error) — a component still carrying Altium's default Comment. It reaches the BOM as "Comment".duplicate_designators(error) — the same designator twice on a sheet or board.single_pin_nets(warning) — a net with one pad connects to nothing; usually a typo'd label.unused_mechanical_layers(info) — an enabled mechanical layer carrying nothing. It travels into fab outputs and is invisible in the editor precisely because it is empty.off_grid_components(info) — a placed part off the grid (configurable; a metric board legitimately fails a 5 mil test).
- A clean result is not claimed to be a complete one. The reply carries
notImplementednaming the checks this review cannot see yet and why — board outline, ratsnest, silkscreen geometry, via topology, decoupling. Silence is not a pass. - Findings name the exact designator, net or layer and say what is wrong; never "verify X". Sorted error → warning → info, with counts.
0.1.22 — 2026-09-01
altium_cross_checkreads the two documents in TURN, because Altium cannot hold both current.PCBServer.GetCurrentPCBBoardfollows the FOCUSED document, so focusing the schematic clears the current board — the first cut of this verb read the sheet, then failed on the board every time. It now activates the board, reads it, activates the sheet, reads it, and reports the document the user had in front of them. Takesboardandsheetpaths, or uses what is already open.- The two caveats still travel with the answer: one sheet only (a hierarchical parent holds sheet symbols, not parts), and a folder mismatch between sheet and board is reported as
sameProject: falsewith a warning, because comparing two unrelated documents makes every difference meaningless.
0.1.21 — 2026-09-01
- "File type not recognised." pre-empted at the cause. The agent opened every non-project file with
ObjectKind='Document', which Altium rejects with a modal on the user's screen even for a valid .PcbDoc. It now maps the extension to the kind Altium actually uses (PCB, SCH, PCBLIB, SCHLIB, OUTPUTJOB, PCB3DLIB, TEXT, Project) and opens throughClient.OpenDocument— the sibling ofShowDocument, which this unit already relied on — falling back to the process lane only if that raises. - The bridge refuses unsupported types before Altium can complain. An extension outside the openable set returns
unsupported_documentwith the supported list, instead of handing Altium something it will answer with a dialog. - A slow open is no longer reported as a failure. ab gives a verb 60 s; a cold document open can exceed it.
open_documentandactivate_documentnow wait just under the budget and returnstatus: "in_progress"with a poll hint, rather than timing out while Altium is still working.
0.1.20 — 2026-09-01
altium_read_schematicandaltium_cross_checkin the shipped runtime (they existed only in the Rust bridge).cross_checkcompares the current sheet against the current board — what is drawn but not placed, and placed but not drawn — which neither read answers alone. Two caveats travel WITH the answer because both make it wrong to act on: the schematic side is ONE sheet (a hierarchical parent holds sheet symbols, not parts), and Altium's SchServer and PCBServer each hold their own "current document", so the two can belong to different projects — detected by comparing folders and reported assameProject: falsewith a loud warning.- Manifest drift fixed, in both directions.
bridge.jsondeclared three verbs with no handler (close_document,export_pdf,screenshot_all) — ab routed them and the bridge answered "Unknown command" — and omitted three that work (ensure_licensed,license_status,setup_trial), leaving them unreachable through ab. The phantoms are gone (they never worked; implementing them is separate work) and the three real ones are now routable. - The release refuses to build on drift.
scripts/release.pylintsbridge.jsonagainst whatserver.pycan actually dispatch and fails with both lists rather than shipping a manifest that lies. 106 verbs declared and dispatchable.
0.1.19 — 2026-09-01
- Pre-emption, properly. Two changes so a script modal cannot reach the user:
- A script lane now refuses when ANY resident loop is alive, in ANY workspace — not just the current instance's. That gap is exactly how "Another script executing now" got on screen after per-instance workspaces landed: the pre-upgrade loop was still resident under the legacy path,
is_alive()looked only atagent/<key>, saw nothing, and fired a bootstrap into an engine that was already running a script. The refusal names the workspace and heartbeat age so the fix is obvious. - A watcher runs FOR THE DURATION of every
-Rlane and presses OK the instant one of our own modals appears, so it exists for a few hundred milliseconds instead of sitting on screen until someone goes looking. Cleared modals come back asmodalsPreemptedrather than being hidden. The watcher only ever touches modals this bridge CAUSES (script-busy, another-instance, scripting-system); a save prompt or a licence question is the user's and is never pressed.
- A script lane now refuses when ANY resident loop is alive, in ANY workspace — not just the current instance's. That gap is exactly how "Another script executing now" got on screen after per-instance workspaces landed: the pre-upgrade loop was still resident under the legacy path,
- Detection was always the weaker half: the engine is single-threaded, so the modal it raises is also what freezes it — which is precisely why watching during the operation works.
0.1.18 — 2026-09-01
- Instances. More than one Altium can run at once — John runs Altium Designer Develop beside Altium Designer so two AI threads can drive both — and the bridge assumed one in every place it mattered. New verb
altium_instanceslists each running Altium with the pid/key to address it; every other verb takes an optionalinstance(a pid, a key likeADDevelop, or a word from the edition/title). One Altium running: omit it, nothing changes. Several: verbs REFUSE withambiguous_instancerather than guess, because guessing means acting inside the other thread's Altium. - Each instance gets its own agent workspace (
bridges-cache/altium/agent/<key>), keyed by install folder so it survives a restart. Two resident loops sharing onerequest.txtis the failure that does not announce itself: whichever loop notices first eats the other's request and answers it from the wrong board. - The -R lane spawns the selected instance's own exe, not a hardcoded AD26 path — Designer and Designer Develop live in different install folders.
- Dialog scanning is scoped to the selected instance, so one thread can no longer see, or
dismiss_dialogsclear, the other thread's modal. - The stray-helper reaper can no longer kill a real Altium. It now requires
-R…ScriptingSystemin the process's own command line (ab reports it). The old rule — "any X2 that is not the main window's process" — killed John's Develop instance on 2026-09-01 while it was hosting another thread's agent. - 97 verbs.
0.1.17 — 2026-09-01
- New verb
altium_layers_in_use: how many layers the board ACTUALLY has. Until now the only layer answer was "48", which is every copper layer Altium CAN address, not a property of the board. Altium's in-stack flag has no identifier proven on AD26 and a wrong guess halts the engine behind a modal, so the agent's newlayer_occupancycounts primitives by layer in ONE pass (using only identifiers already shipped in this unit) and the bridge filters that tally to the copper layers. Verified live: LAN7800_test reports 1 copper layer in use (Top Layer), 7 layers carrying primitives, against 48 addressable. - Read the result as a FLOOR: an inner plane carrying no primitives is invisible to a primitive count. The reply says so, and
copperLayersAddressableis labelled as a capability rather than a board property. read_boardgains theoccupancysection (default on).- The first cut of this view forgot to filter to copper and cheerfully reported a MECHANICAL layer (296 primitives) as the board's busiest layer. Now covered by a unit test against that exact shape.
- 96 verbs.
Unreleased — bridge-rs only (no change to the shipped Python runtime)
check_dialogsno longer sees other applications' windows. The first cut matched window TITLES containing "Error"/"Warning" and picked up four "RemoteApp security warning" windows on John's desktop that had nothing to do with Altium — whichdismiss_dialogswould then have clicked OK on. Pressing buttons in someone else's application is exactly what this bridge must never do. Now scoped by PID:process_listfor Altium's pids,desktop_enum_windows {pids}for only those windows, then class + "not the main window". Also 300× faster (one enumeration instead of six title searches): 1 ms.- Note for the Python bridge: it was never affected —
handlers/dialogs.pyhas always attributed dialogs by pid.
0.1.16 — 2026-09-01
- Renamed
dump_board→read_board(agent command and Rust verb), before anything depended on it. John, on the 0.1.15 naming: "that's not a great verb name ... we should probably pick a better verb thats more like a read or write*"*. He is right — "dump" names the implementation rather than the caller's intent, and it has no counterpart, so it breaks the symmetry the moment writes arrive. The convention is now recorded in the dev skill and the Rust README:read_*to get data out,write_*/apply_*to put it back. Rust moduledump.rs→board.rs,BoardDump→BoardData, fixtureread_board.json. - For the record, since it came up: the name was mine, not inherited — no verb names were copied from any third-party Altium MCP server.
0.1.15 — 2026-09-01
- Agent gains
dump_board: components + nets + layers + copper + rules in ONE round trip, composed from the existing readers so it adds no new Altium API surface (and therefore cannot introduce an engine-halting identifier). This is the primitive the Rust bridge shapes its views from; the Python verbs are unchanged. - Rust bridge (
bridge-rs/): cold bootstrap through the-Rlane proven live — 1.59 s, zero dialogs, which was the last unverified path. Dump-derived views land with it (board_summary232 ms,rules_by_kind152 ms), plusfixtures/dump_board.json— a recorded live dump that letscargo testexercise all the shaping logic with no Altium, no Windows and no agent. 8 tests; they already caught a dispatch bug and one wrong assumption about the Fanout rule group. rule_kind_name(): Altium emitsTRuleKindas a bare ordinal; 31 ordinals are now mapped to names read off a live default rule set. Unknown ordinals render askind <n>rather than being invented, and a test fails loudly if a board carries one we do not know.
0.1.14 — 2026-09-01
altium_activate_documentre-implemented over the AGENT instead of UIA. 0.1.13 shipped it as a UIA document-bar click; measured on a live box that call takes >100 s (ab's budget is 60 s) because Altium's main-window UIA tree is huge. The agent runs IN-PROCESS: ~4 s, no cursor, no OS foreground steal, and Altium activates an already-open document rather than reloading it. Verified: title switched to the target .PcbDoc and board verbs answered in 0.2 s.- altium-uia skill corrected accordingly — the rule has two halves: UIA for dialogs and WebView pages (small trees, fast), the resident agent for anything inside the application.
desktop_clickonly when neither can reach it.
0.1.13 — 2026-09-01
- UIA first, cursor last. John, watching the bridge drive his box: "you keep turning on the cursor, but you could maybe use UIA to do those clicks". He was right —
altium_licensehad been clicking hardcoded pixel coordinates since 0.1.x. Altium DOES expose a UIA tree, including the WebView2 License Management page (automationId: UseLicenseButton, invokable). The licence lane now invokes programmatically (no foreground, no cursor movement) and falls back to the image-space click only on a genuine UIA miss; the reply says which lane ran (lane,windowEtiquette). - New verb
altium_activate_document {name}— every open document is an invokable button in Altium's document bar (GUIBinderView.TGUIDocBarButton, accessible name = the tab caption). Making a .PcbDoc current no longer needsopen_document(which raises Altium's window) or a pixel click on the tab. 95 verbs. - New DEVELOPER skill altium-uia: the measured catalog of what AD26.9.1 exposes to UI Automation (dialogs, main menus, title bar, document bar, licence page) plus what it does NOT (the canvas — that is the agent's scripting API), the two costs (UIA searches over Altium's tree are slow; nothing reaches an app while a higher-integrity window such as a screensaver owns the foreground), and the honest list of what still legitimately foregrounds.
- Diagnosed the "Could not start Vault Explorer" warning users see on every launch: the extension is correctly registered (
ExtensionsRegistry.xml) with a complete payload, and Altium's private .NET 8.0.15 runtime loads fine, so it is neither a broken registry nor a runtime mismatch. Investigation continues.
0.1.12 — 2026-08-31
- 14× faster agent round trip: 450–600 ms → 31–47 ms. Root cause:
Breakinside aTry..Exceptin this script engine is implemented as an exception, so theExceptarm swallowed it — every file write ran all 20 retry iterations (Sleep(10)each, ~220–310 ms) even though the FIRST write had already succeeded. Both write primitives now use aDoneflag with aWhileguard instead. Verified across the whole template: no otherBreak-inside-Trysite exists. hb.txtkeeps tmp+rename (readers hold it open, and a direct rewrite raced them intoEFCreateError);response_<id>.jsonwrites direct — one file op instead of save+delete+rename.- Responses carry
agentPhases—{readMs, dispatchMs, detectTick, sleepMs, hbSaveMs, hbSaveTries, hbSaveExcept, directMs, directTries, directExcept}.*Tries: 1, *Except: 0is the healthy shape; anything higher means the box is contending for the file and the retry loop is earning its keep. - New agent-side
io_benchcommand (not a bridge verb): times SaveToFile new/overwrite/delete against the workspace, %TEMP% and Documents, plus rename/Sleep(1)/loop cost. This is what localized the stall — keep it for the next latency regression. - Measured after the fix (LAN7800_test.PcbDoc, live): list_designators 44 ms, list_nets 57 ms, read_rules 68 ms (35 rules), layers 47 ms (28), copper_layers 67 ms (48), component_data 87 ms — agent round trip, dialogs 0.
0.1.11 — 2026-08-31
- Phase 1 of the parity plan: 11 read-only board/schematic introspection verbs — list_designators, component_data, component_pins, list_nets, net_connections, layers, copper_layers, schematic_data (pins + part IDs), read_rules, set_layer_visibility, footprint_primitives. All verified live on Bluetooth Sentinel (16–350 ms), zero dialogs.
- Busy-notice reaper rewritten to go through ab's desktop verbs (find_window → find_control → ui_click) with a 4 s watch, after the X2-pid scan missed the notice twice on a live box.
- Parked: create_net_class (IPCB_ObjectClass.SuperClass/.Name are undeclared in AD26 scripting) — function kept, not dispatched.
- Last Python release of the bridge process: the next major moves the bridge to a Rust exe that calls ab's verbs and shrinks the DelphiScript agent to generic dump/apply primitives (ab#987 requests the binary runtime kind).
0.1.10 — 2026-08-31
- #3 (Colby) root-cause FIX: the agent registers the PCB/SCH scripting APIs at startup (
ServersUp:Client.StartServer('PCB')/('SCH')before the firstPCBServerreference) — on a fresh session that never opened a PCB document the API unit is unregistered and the old template halted with an "Undeclared identifier: PostProcess" modal on the user's screen. - #3 follow-up:
altium_agent_recovernow closes the scratchAdomAgent_<digest>.PrjScrout of the Projects panel after freeing the engine (only there — after a successful start the resident loop runs from it).
0.1.9 — 2026-08-31
- #2/PR #64 (Colby): detect the ADDevelop build — _TITLE_RE now accepts an optional edition word (Develop/Viewer/…) and optional version parens, with a guard so a bare mid-string "Altium Designer" in a dialog cannot pose as the main window. Proven against six title shapes.
- #3 (Colby, partial): the -R helpers now reap Altium's single-instance "another instance is busy" notice in the background (both RunScript/RunScriptText lanes and recover), so a stray helper never leaves that modal on the user's screen.
- Repo now carries the FULL source tree (handlers/, agent/, extension/, tools/) — previously only src/server.py was pushed, forcing PRs against the release zip.
0.1.8 — 2026-08-31
- FIX: bridge.json verbs[] must be plain strings — 0.1.5–0.1.7 shipped four entries as objects, which ab's bridge_registry could not parse (the bridge vanished from the registry after its next reap while bridge_install still reported ok). Release assets are now uploaded explicitly (release create --binary-path does not attach).
0.1.7 — 2026-08-31
- altium_install_library_bundle: the no-AI web-control lane (kicad-parity envelope, zip-slip hardened, restart contract, proof frame). Acceptance test: docs/webcontrol-altium-test.html.
0.1.6 — 2026-08-30
- Full skill slate (10): added altium-install (the install recipe, finally packed), altium-libraries, altium-boards, altium-outputs, altium-bridge-dev, altium-bridge-publish — matching the kicad-bridge fleet depth.
0.1.5 — 2026-08-30
- Barrett consolidation complete: cleanup_libs, set_visibility, xcopy_footprint (cross-library), and import_part (his fat verb on the resident lane) — full verb parity with barrett-land/adom-desktop-altium-bridge.
- Skillpack: the pkg now ships altium-part-import (his 3-path catalog-part workflow, rewritten on these verbs, with the tri-vendor rule: Mouser preferred + DigiKey + LCSC part numbers on every component) and altium-kicad-import (his measured KiCad-10 findings + the vendored v10→v9 downgrader + the native no-importer lane).
- Tools: kicad_v10_to_v9.py vendored from barrett-land/kicad10-to-altium.
0.1.4 — 2026-08-30
- Barrett-parity library manipulation: clone_symbol (all display modes, 555 primitives verified), rename_symbol, read_symbol (pins/params/modes read-back), copy_footprint (pads+bodies+drawing), rename_footprint, remove_footprint. Smoke-tested on the LAN7800 libraries with zero dialogs.
0.1.3 — 2026-08-30
- Symbol quality pass (John caught colliding 4-side names): tools/symbol_modes.py sizes bodies from measured pin-name lengths, std = CCW datasheet wrap (distinct from 2-column), die outline is a centred square skipped when it cannot fit; all ten modes frame-checked.
- set_params/add_models parameters are hidden metadata (visible ones piled over the symbol).
- New verb: altium_remove_symbol.
0.1.2 — 2026-08-30
- LAN7800 creative-import lane: build_footprint (spec from tools/kicad2altium.py: via pads by duplicate designator, roundrect %, named mech layers incl. Adom.SolderBalls), build_symbol_modes (10 display modes on one component) + symbol_mode (editor-driven), add_models (N footprint models + params), normalize_bodies, test_board, board_shape (PCB:PlaceBoardOutline), swap_footprint (nets kept by designator), pcb_report with pads/bodies/nets/tracks.
- Dialog pre-emption: ReadAll probes with FileOpen (the debugger breaks on any exception), TxClear on agent start / before save / after recover (dangling PreProcess = "a command is currently active").
- Library canvas offset understood: bodies added through the canvas realign onto the pads automatically.
0.1.0 — 2026-08-30
- Schematic placement fixed: place_on_sch copies each lib primitive onto a fresh sheet component (Replicate alone drops pins/body); proven on the CH340G (17 primitives render).
- 3D pipeline fixed end-to-end: bodies persist (added via the PcbLib editor CANVAS with the footprint current), STEP + extruded both render on a real board; place_on_pcb builds a fresh eComponentObject, copies children BEFORE the board add, normalizes the pad cluster and each body to the target separately. Ray's CH340G STEP renders in 3D on a board.
- New diagnostics: altium_pcb_report (per-component pads/bodies with layer+bbox, board census by ObjectId), place/agent stage breadcrumbs.
- Agent lifecycle: agent_start now detects a STALE resident generation (gen stamp in the heartbeat) and auto-restarts it — redeployed agent code can no longer silently not load.
0.1.0 — 2026-08-29
- Barrett merge: set_params (LCSC/MPN + footprint link), add_body (STEP/extruded), install_library, compile_intlib, extract_intlib; vendored AdomBridge extension (export_step/import_kicad — not loading on AD 26.9.1 yet).
- Taskbar: Adom overlay badge while driving, measured progress bar on slow verbs, no AUMID stamping.
- Phase 5: signin_status, license (WebView2 page driver), connect_workspace, a365_configure/status/query.
- First cut (phases 0-4): readiness/status/diagnostics, resident DelphiScript agent (eval, run_command, state, open), dialogs by PID with screenshots, agent_recover, the eight show_* surfaces + demo, compile/ERC with Messages panel, DRC with violations, OutJob runner (Gerber/ODB++/NC drill/BOM proven on the Bluetooth Sentinel example), create_footprint / create_symbol / save.
# Changelog
## 0.2.58 (2026-10-21)
- `altium_autoroute`: Altium's Situs autorouter on the current board.
- It picks a strategy and keeps existing routing locked by default; Situs ripped it up with the dialog's default.
- It answers the unconnected count and DRC errors before and after, and doesn't save unless asked.
- Measured: one ripped-up net went from 4 unconnected to 1 in 14 s, with the existing copper untouched.
- `altium_layer_stack`: copper layers in order, with copper thickness and the dielectric under each (height, Dk, material) and the total thickness.
- It uses `Board.LayerStack_V7`; on AD 26 the classic `LayerStack.FirstLayer` halts the script.
- Measured: the buck reads 2 layers, 36 um copper over 320 um FR-4; the Mini PC example reads 16 layers of TU-883, 2.0 mm.
- `altium_read_rules` carries each rule's `descriptor`, Altium's wording with the values (e.g. `Clearance Constraint (Gap=6mil)`), so a DRC result can be explained.
- `altium_sch_zoom`: selects parts on a sheet, frames them, and returns a frame. It is the schematic half of cross-probing.
## 0.2.57 (2026-10-21)
**Schematic editing on an existing design, and the change order to the board.**
- `altium_sch_place_part`: places a .SchLib part on an existing sheet at a position, with its library parameters and footprint links (or a named footprint).
- `altium_sch_connect`: wire stubs from pins' outer ends, with a net label (`kind:"label"`), a power port (`kind:"power"`; GND-like nets get a ground symbol) or nothing. Each connection reports ok or why not.
- `altium_sch_edit`: move, moveBy, rotate, rename, re-comment or delete placed parts, in one undo step.
- `altium_annotate`: numbers every `R?`, `C?`, `U?` with the lowest free number per prefix across the project. Each part is named by its position, so several `R?` on one sheet are told apart. `dryRun` shows the plan.
- `altium_doc_params`: reads or sets a sheet's title-block parameters.
- `altium_update_pcb`: Design > Update PCB Document.
- It previews by default: parts to add, and board-only parts the change order would delete.
- `execute:true` runs it, and refuses deletions unless `allowRemovals:true`.
- It answers the one-time "link parts by designator" prompt on a board built by script (a real click: that dialog's buttons ignore Invoke) and the Information box after it.
- It presses Execute Changes, waits until every row is done (the agent is parked while the modal is up, so it cannot be the signal), closes, and **saves the board**. The change order edits it in memory only; a first test lost every change by closing it.
- Verified live on AD 26.11.1: R9 and R10 placed, annotated and connected on a sheet reached the board with their nets (R9: VOUT/GND, R10: VIN/GND) and survived a close and reopen. A comment-only change on an existing part opens no change order on that build; documented.
- `altium_export_pdf`: every sheet as one PDF, plus the board's composite print with `board:true`. It runs through an OutJob in a throwaway project, so the user's project is untouched. Verified: a 2-page PDF in 14 s.
- `altium_run_outjob` (and every verb built on it: manufacturing, STEP, PDF) answers Altium's "PCB Health Check Issues Detected" modal, which otherwise stopped the job indefinitely. It presses Generate Outputs (`healthCheck:"stop"` cancels) and returns the dialog's text in `healthCheck`.
- `schematic_data` also reports each part's `uniqueId`.
- New skill **altium-schematic-edit**. The main skill opens with an "I want to..." map of the whole bridge, and notes that `altium_eval` does nothing while the agent is resident.
## 0.2.56 (2026-10-21)
- **`altium_bom`: the project's BOM, read straight from Altium.**
- Reads every schematic sheet the `.PrjPcb` lists, managed device sheets included, with each part's parameters.
- Merges the board's placement and an optional variant (Not Fitted parts become DNP; alternates are listed).
- Groups one line per manufacturer part number. It matches parameter names across libraries: `MPN` = `Manufacturer Part Number` = `PartNumber`, plus the Mouser, DigiKey and LCSC spellings.
- Checks what an order needs: missing MPNs and distributor numbers, one part number on two footprints, board-only and sheet-only parts, DNP, and parts that stay out of a BOM.
- Writes `bom.csv`, `bom-jlcpcb.csv` + `cpl-jlcpcb.csv`, `bom-mouser.csv`, `bom-digikey.csv` and `bom.json`.
- Verified on AD 26.11.1: the TPS54202 buck (17 lines in 0.5 s), Altium's Mini PC example (46 sheets, 1046 parts, 132 lines in 19 s) and Bluetooth Sentinel (15 sheets including device sheets).
- **`altium_sheet_params`** writes parameters onto placed parts, finding each designator on whichever sheet holds it, and saves the sheets. It is the BOM write-back.
- **`altium-bom`** (container CLI, installed with the package):
- `read` runs `altium_bom` and saves the result.
- `price` makes one `adom-parts-search` call per line and reports stock against the boards needed, price, lead time, lifecycle and JLCPCB-only parts, plus a cheapest in-stock total.
- `fill` writes the missing distributor numbers back onto the sheets.
- New skill: **altium-bom**.
- `schematic_data`:
- Also reports each part's `kind` (ComponentKind), `itemGuid`, `vaultGuid` and `sourceLibrary`, and its `description`.
- A workspace-managed part's description is not read: on Altium's own Content Vault parts that call never returned.
- New `paramFilter` and `paramNamesOnly` options read only the values a caller needs; one example IC's three preview-image parameters took minutes to read.
- **The agent's JSON escaping is linear.** It appended one character at a time, which is quadratic in DelphiScript; every verb that returns long text gets faster.
- The `altium-wiki-import` skill no longer says the Cloud Library cannot export Altium: it can, without distributor numbers, and the skill says how to add them.
- `uninstall.sh` also removes the `altium-*` container commands.
## 0.2.55 (2026-10-20)
- **The edge rule no longer flags silkscreen** (#26). `set_design_rules` takes `edgeClearance` from the fab pack, where it means copper to the board edge (KiCad's `edge_clearance`). When the board had no outline-clearance rule, it created "Adom edge" scoped to everything, so Altium's DRC flagged silkscreen near the edge too, such as AI Flow's contact frames 0.12 mm in. The new rule is scoped `OnCopper`, and an existing "Adom edge" still scoped `All` is re-scoped the next time the rules are set. Silkscreen keeps Altium's own silk rules.
- Verified live on Altium Designer 26.11.1, on two copies of the same saved board with the gap widened to 0.6 mm so silkscreen near the edge is in reach: scope `All` gave 28 edge violations, `OnCopper` gave 5 (the copper). Every other rule's count was identical (137 silk-to-mask, 48 silk-to-silk, 8 mask sliver).
## 0.2.54 (2026-10-20)
- **An Altium board's STEP anchors as a molecule.** Altium's STEP export names each component's PRODUCT after its designator but leaves every assembly occurrence unnamed, where KiCad names the occurrence (`MP1`). `step2glb convert --molecule` finds the four machine pins by occurrence name, so an Altium molecule board converted `molecule_anchored: false` (0 MP markers) and its GLB was unanchored. `altium_export_step` now names each board component's occurrence after its designator (both lanes; `occurrences.named` in the reply, `nameOccurrences: false` to keep Altium's file as written). Nested model occurrences and occurrences Altium already named are left alone.
- `altium_routing_state` answers `errorCode: "no_current_board"` and the agent's message when another document (a schematic) is in front, instead of an empty error.
- Verified: the TPS54202 molecule board's Altium STEP (34 components) converts `molecule_anchored: true` after the pass; a unit test covers the root/nested/escaped-quote cases.
## 0.2.53 (2026-10-20)
- **`symbol_prims` answers the drawn mode's body.** `bodyRectMil` was the first rectangle found, which could belong to any display mode: the TPS54202's 10-mode symbol answered a wider mode's 1200 mil body for its 700 mil drawn body, and a capacitor answered one plate. It is now the union of the current display mode's rectangles (mode 0, the one a sheet draws).
- **Library pins keep their electrical type.** `altium-symbol-modes` carries each KiCad pin type into the spec (a 10th field), and `build_symbol_modes` sets Altium's pin type from it: input, output, bidirectional (IO), tri-state (HiZ), open collector, open emitter, power in/out (Power), else Passive. Every pin used to be Passive, so Altium's ERC and AI Flow's pin-type check could not tell a power or output pin from a resistor's. `read_symbol` returns each pin's `electrical` (0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power). Live on AD 26.11.1: the TPS54202's GND, SW and VIN read 7, FB and EN 0, BOOT 4.
## 0.2.52 (2026-10-20)
- **Importing a part again replaces its 3D body instead of stacking a second one.** `add_body` (and the `step` of `import_part`, which calls it) added a new body every call and never removed the old one, and `normalize_bodies` then centred both on the pads: the AMPLH5030S inductor reached a board (2026-10-16) with the old model and the fixed one inside each other, repaired only by `remove_footprint`, a re-import and `PCB:UpdateFootprints`. The agent now deletes the footprint's existing 3D bodies before it adds the new one, the way the editor's Edit > Delete does (it selects exactly those bodies and runs `PCB:Clear`; `RemovePCBObject` left them in the stored footprint after save), how `build_symbol_modes` replaces a symbol of the same LibReference. It removes nothing until the new STEP has loaded, so a bad path keeps the old body.
- The `add_body` reply says `replacedBodies` (also at the top level), `keptExisting`, `bodiesBefore`, `sameNameFootprints` and `bodiesNow`, and its `_hint` warns when bodies are still stacked or when several footprints share the name (only the first gets the body). `import_part`'s add_body step carries `replacedBodies`, `bodiesNow` and `sameNameFootprints`.
- New `add_body` argument `keepExisting` (default false): add next to the existing bodies, for a deliberate second body.
- Boards that already place the part keep the old body until `PCB:UpdateFootprints`.
- Verified live on Altium Designer 26.11.1: three repeated adds keep 1 body, `keepExisting` makes 2 and the next plain add makes 1 again, a bad STEP path keeps the old body, and the saved and reopened footprint has 1 body and its 2 pads.
## 0.2.51 (2026-10-19)
- **IC symbols attach to their box again.** `altium-symbol-modes` (`tools/symbol_modes.py`, the spec behind `build_symbol_modes` / `import_part`) wrote each pin's outer end where Altium's `Pin.Location` wants the end on the body, so every pin of a 2-column or 4-side symbol floated one pin length (300 mil) off the box with its name in the gap. Pins now start on the body edge in all ten display modes. Small parts drawn from their KiCad symbol (resistors, capacitors, inductors, LEDs) were never affected.
- `dev-skills/altium-bridge-test/test_symbol_modes.py`: every pin of every mode must start on its body edge (fails on 0.2.50, passes on 0.2.51).
- Symbols already built with 0.2.50 or earlier keep the gap until rebuilt: run `altium-symbol-modes` again and `altium_build_symbol_modes` (it replaces the symbol), then `altium_add_models` to put the footprint link and parameters back.
## 0.2.50 (2026-10-16)
- **Templates are found on every Altium build.** `build_schematic` and `export_manufacturing` look for Altium's sheet and OutJob templates under the running install's own Public Documents folder (ADDevelop, AD26, ...) instead of AD26 only, so they work on Altium Designer Develop.
- **`symbol_mode` no longer halts the script on Altium Designer Develop**: it referenced an undeclared identifier (`NOld`). `build_symbol_modes` reports `replaced`.
- Full verb battery green on BOTH builds: Altium Designer Develop (Colby-ROG2) and Altium Designer 26.11.1 (DESKTOP-BAR6S0F), each 145 pass, 0 fail, 1 skip (`a365_logout`), 0 uncovered. 0.2.49 failed on Develop, which this release fixes; 0.2.50 is the build promoted to public.
## 0.2.49 (2026-10-16)
- **Altium Designer 26.x with no licence tail reads as licensed** (#19): the absent-tail rule covered only the Develop edition's window, so Altium Designer running on a Develop licence read `Unknown`. The Use License pixel fallback now works through the bridge (it takes its own screenshot instead of needing a shotId).
- **No `X2 -R` helper is spawned while a non-script dialog blocks Altium**, and recover stops and names the dialog: one save prompt had turned into 15 minutes of "another instance is busy" notices.
- **`build_board` and `build_schematic` never close a project over documents they did not make**: the project reopen and rewrite read the project's members from Altium itself, save them before closing, and never close documents the project file does not list.
- The verb battery covers all 146 verbs (phase 3.5 runs the AI Flow board lane) and refuses to start over a running Altium. Run twice on Altium Designer 26.11.1: 145 pass, 0 fail, 1 skip (`a365_logout`), `selftest_script_error` netWorks true.
## 0.2.48 (2026-10-16)
- `list_library` and the `show_*` surfaces refuse a document named under an argument they do not read, instead of opening the bundled demo library; every reply says `demoFallback` (#20).
- `schematic_data` refuses a schematic library as the current sheet and takes `filePath` (#21).
- `build_board` into a project that already lists the board reopens the project, so the board has one entry and one view (#23).
- `read_symbol` returns the symbol's model links (#24).
- `release.py` checks and pushes page.json and bridge-rs/src/main.rs with the other version stamps (#22).
## 0.2.47 (2026-10-16)
- **The AI Flow Altium lane's verbs.** Live board edits: `routing_state`, `route_net`, `move_parts`, `place_vias`, `remove_items`, `ripup_nets`. Pours with a connect style per pour, solid joints, cutouts and island/neck options, repoured outside the script engine (scoped rules apply only there).
- `set_design_rules`, `build_board`, and `build_schematic` on Altium's own sheet template inside its project, compiled and checked pin for pin.
- `project_nets`, `export_manufacturing` (the project's own OutJob), `silk_state` and `apply_silkscreen`, `select_net`, `zoom_to`, `add_rule`.
- Fixes: passive symbols keep their standard drawing; `route_net` places no via inside a same-net through-hole pad; `run_drc` answers the repour prompt; agent start closes old script projects; verb timeouts match the docs (release.py now refuses a bridge.json timeout shorter than a verb's documented one).
## 0.2.46 (2026-10-15)
- **Headless STEP export works** (adom/altium-bridge PR #3, Aditya Angajala). `altium_export_step` no longer needs Barrett's .NET extension, which never loads into a running AD 26. The default lane `outjob` writes an ExportSTEP OutJob and a throwaway project pointing at the board into %TEMP%, runs it through the existing OutJob path, copies the .step to `output`, then closes both documents and removes the folder, so the user's own project is never opened or changed. Measured on AD 26.9.1: a 30 x 30 mm board in 3.5 s with no dialog.
- New arguments: `holes` (default true), `suffixType` none/board/custom/id plus `suffix`, `folded`, `singlePart`, and the raw Altium options `copperOption`, `modelsOption`, `componentsOption`. The reply lists `products[]`, the STEP part names: Altium names parts by DESIGNATOR only.
- Named failures: `pcb_not_found`, `bad_args`, `step_not_written` (with the OutJob reply and its dialogs), `copy_failed`; the failure replies name `tempDir`, which is kept for inspection. `lane:"extension"` keeps the old path.
## 0.2.45 (2026-10-15)
First live run since 0.2.39 on a licensed Altium (Altium Designer Develop, Colby-ROG2). It verified the held 0.2.40-0.2.44 features and fixed what the run and two new issues found.
- **A save into a missing folder no longer fails silently.** `altium_test_board {pcbPath}` and `altium_place_on_sch {schPath}` answered with the path while Altium's save wrote nothing (the folder did not exist), so the next "append" found no board and started a new one. The bridge now creates the folder first and checks the file on disk: `saved:true`, or `errorCode: not_saved`. A document that is open but not yet saved now counts as existing for append.
- **Appending to a schematic grows the sheet** (`sheetGrown`), so the added parts stay inside the drawing frame instead of landing in the gray past its right edge.
- **`altium_board_shape` proves its own result** (adom/altium-bridge#16). It, `pcb_report {want:'board'}` and `read_board`'s outline now read the outline's vertices: `BoardOutline.BoundingRectangle` is cached and kept the 6000 x 4000 mil default after a reshape, on AD 26.9.1 and Altium Develop alike. New `mechOutline:true` draws the rectangle on Mechanical 1 so the fab Gerber has an outline (the board shape alone is plotted into none).
- **`altium_test_board` parts carry their own name and footprint** (adom/altium-bridge#17). The part name is the component's Comment, so it moves with the part (loose overlay text stayed behind and piled up in the silkscreen Gerber), and `Pattern` is the footprint name (it was empty, so BOM, STEP naming and footprint comparison saw nothing).
- **New `altium_uninstall_library` and `altium_list_installed_libraries`** (adom/altium-bridge#14). Uninstall matches `libraryName` (file stem: the SchLib and the PcbLib together) or an exact `libraryPath`, closes any open document for the library first, never touches the files, and answers `notInstalled:true` when nothing matches. Verified across a clean restart. It also cleared two dead "UI" entries left behind when 0.2.9 flattened the bundle folder.
- **The "X2 / Continue execution?" script box is classified** (`script_runtime_error`, safe action Cancel) instead of scanning as unknown: its text is painted, so it is only in the photo.
- `altium_panel`'s timeout now says why: on Altium Develop a UI Automation search of the main window takes over 20 s, and no Altium process switches a docked tab; opening a library already brings its panel to the front.
- Verified live, no code change: append on board and sheet (#11), focus kept on the user's window through document opens and agent restarts (#12), `altium_panel` answers inside its budget and the next verb is not delayed (#13).
## 0.2.44 (2026-09-29)
- **The Altium extension DLL is signed too.** `extension/AdomBridge/AdomBridge.dll` now carries the same Adom Industries, Inc. signature as `altium-bridge.exe`: Windows Smart App Control checks the DLLs a program loads, not only the exe it starts, so an unsigned plugin could be refused on a PC where it is on. The release script signs and verifies every exe and DLL it ships and refuses to package any that is unsigned. Code otherwise identical to 0.2.43.
## 0.2.43 (2026-09-29)
- **The exe is signed.** `altium-bridge.exe` now carries an Authenticode signature from Adom Industries, Inc., with a timestamp, so Windows 11 Smart App Control no longer blocks it ("Part of this app has been blocked"). The release script signs every exe it ships as the last step before packaging, verifies the signature on Windows and again after the file comes back, and refuses to package an unsigned exe.
- Same code as 0.2.42 otherwise (bounded `altium_panel`, `altium_demo` error reporting), which is still waiting for its licensed test run.
## 0.2.42 (2026-09-23), STAGED for the licensed battery, not on any tier
Fixes the two defects John's licensed live test of the held 0.2.41 found (adom/altium-bridge#13, arav-rog, AD 26.10.1).
- **`altium_panel` answers inside 24 s with a named result, every time.** It was not a loop that never ended: it made up to five sequential UIA searches (read, select, read, click, read) with 10+15+10+15+10 s timeouts, exactly ab's 60 s budget, and each search of Altium's large window tree ran into its timeout, so the verb ended in ab's generic `timed_out` with the abandoned searches still running (the likely cause of the collateral `run_drc` timeout that followed it in the battery). Now one search reads the tab's existence and selection together, then at most one select and one verify, inside a 24 s budget; the answers are `already`, `ui_select`, `panel_not_found`, `panel_search_timeout` (with the stage), `ui_select_gated`, `panel_not_selected`. The click fallback is gone (a click does not switch panel tabs), and when ab's select falls back to its raising SendInput click the reply says `lane: ui_select_raised` instead of hiding a foreground steal.
- **`altium_demo` carries the failing beat's own error.** A failed beat came back as a bare `success:false` (John saw it on Altium Designer Develop). The reason was real and discarded: the tour opens Altium's bundled AD26 example projects, which a Develop install does not have (`file not found`). The reply now carries the beat's `errorCode`/`error`/`_hint`, names this case `demo_examples_missing`, and falls back to `demo_beat_failed`.
## Unreleased (docs, 2026-09-21)
0.2.39 was promoted to the PUBLIC tier (John Lauer's call and his words; public had been on 0.2.9, so 0.2.13-0.2.39 all landed at once), then re-verified by installing from the public manifest as a stranger would and running the full battery against that artifact: 121 pass, 0 fail, 1 safety skip.
- Documented in the user skills what 0.2.35-0.2.38 actually changed: the modal orphan and the never-half-answer-a-dialog rule (altium-instances, altium-licensing, README), `cleanup_libs` keeping a non-empty stray, `set_params`/`add_body` refusing an ambiguous empty name, `extract_intlib`'s `searchedUnder` (altium-libraries), `schematic_data {params:true}` (altium-boards), and `altium_launch` handing the user's window back (README).
- The battery's skip list is down from nine verbs to one. Eight were skipped for reasons that did not survive a dedicated test box; each now runs and proves its named refusal. Only `a365_logout` stays out, because it really does sign the user out. The test skill says what each one does and what to check before adding a tenth skip.
- The dev, test and publishing docs now say that this bridge has more than one publisher: clone the page before editing, push only what you changed, and read a tier before assuming an older version is a bug rather than a deliberate hold.
## 0.2.41 (2026-09-21)
From the expired-licence incident on the 0.2.40 verification run (Colby's Develop entitlement lapsed mid-test):
- **The expired-licence box is catalogued** ("has expired or been deactivated", category `licence_expired`): it scanned as `unknown` while it blocked every verb and wedged the close into a modal orphan that survived even WM_QUIT. The entry records the detection gap it exposed: Develop shows NO title licence tail in this state either, so the absent-tail "Licensed" inference reads wrong while this box is up.
- **Lone-OK boxes whose OK refuses UIA are dismissed natively.** This box's OK exposed no InvokePattern (`button_not_pressed` in 19 ms); its titlebar X performs the same acknowledgement, so `press` now falls back to a POSTED WM_CLOSE on the dialog - only ever for an OK, never for a button that answers a question.
## 0.2.40 (2026-09-21)
The two asks still open from John's ToF session (adom/altium-bridge#11 and #12), on top of what 0.2.13-0.2.39 already covered.
- **Append to an EXISTING board or sheet (#11).** `altium_test_board {pcbPath}` and `altium_place_on_sch {schPath}` now OPEN the document when the path already exists and ADD the parts: placed to the right of the current ones (board: right of the rightmost anchor, advancing by 1.5x each footprint's measured pad extent; sheet: 4000 mil right of the rightmost symbol, in line with the existing row), designators continuing from the existing count (U4, U5, ...). The board keeps its outline and layers (wMm/hMm ignored; resize with altium_board_shape); the sheet keeps its size and title block. Replies carry `appended` and `existingParts`. A new path still creates, exactly as before.
- **Foreground etiquette on EVERY driving verb (#12).** The dispatch chokepoint now remembers the window the user is in before each driving verb and, when Altium's process holds the foreground afterwards where it did not before, hands the user's window straight back - the open/close_document, place_on_sch, install_library, agent start/stop and run_command steals John measured are all covered by the one wrapper. Replies say so: `windowEtiquette: background`, plus `foregroundReturnedTo` when a steal was reclaimed. A user deliberately on some OTHER window mid-verb is left where they went; verbs whose point is the screen (foreground, launch, demo, install, close) are exempt.
- **New verb `altium_panel {name, ensureId?}` (#12's ganged-up view).** Activates a docked panel TAB (SCH Library, PCB Library, Components, ...) in the background: UIA SelectionItemPattern on the dock tab, InvokePattern fallback, verified by reading the tab's own selection state - no server process can do this (Client:CustomizeResources only ensures the panel exists; a plain click does not switch tabs, measured 2026-09-02). Honest failure modes: `ui_select_gated` when ab's shell grant is off (adom/adom-bridge#123), `panel_not_selected` with the attempted lanes otherwise.
## 0.2.39 (2026-09-21)
- **`selftest_script_error` gets an honest timeout budget (60 s → 180 s).** It is the slowest verb in the catalog by design — it halts the engine, waits for the halt to be noticed, recovers it and verifies by restarting the agent — and Altium Designer Develop parks SILENTLY, so the halt is only seen when the wait expires: measured 18 s on one run and over 60 s on the next, on the same machine. The bridge was cutting its own safety-net test off at 60 s and reporting a timeout, which reads as "the net is broken" when it is not. A timeout here now means something real.
## 0.2.38 (2026-09-21)
Correctness pass over the findings from the code review, each one verified in the source before it was touched.
- **`cleanup_libs` no longer deletes a symbol someone drew.** It removed a SchLib component named `Component_1` unconditionally, pins and all; the PcbLib side had always checked the footprint was empty first. Now both sides only remove the empty factory stray, and the reply reports `keptNotEmpty`.
- **`set_params` and `add_body` refuse an ambiguous empty name** (`errorCode: name_required`, with the count). With no name they took whichever component the iterator yielded first, so against a multi-symbol library they silently wrote a footprint, a comment or a 3D body onto an arbitrary part. An omitted name now only means "the one component in here".
- **`extract_intlib` reports the libraries it actually extracted.** `sources[]` listed every `.SchLib`/`.PcbLib` open anywhere in the workspace; it is now scoped to the IntLib's own folder, and says so in `searchedUnder`.
- **`schematic_data {params:true}`** now reaches the agent. The agent has always honoured it, but the dispatch never sent the flag, so `params[]` came back empty and the docs had to apologise for it.
- `symbol_prims` and `import_kicad` gate on the licence like every other authoring verb, instead of failing later and less clearly.
- `setup_trial` no longer says "Launching it next" at a stage where it launches nothing — it names the next action, which is its whole contract.
- Reviewed and found correct, no change: `run_outjob` opens the .OutJob with `ObjectKind=Document` (the object kind for the RUN step is separate, and is passed), and `run_drc` promises nothing it does not return.
## 0.2.37 (2026-09-21)
- **`altium_launch` hands the user's window back.** Altium foregrounds itself at startup and offers no switch for it, so the bridge now remembers the window the user was working in and restores it once Altium has settled (`foregroundReturnedTo`, `windowEtiquette: background`). `background:false` opts out when the user asked to watch Altium come up. John, twice in two days: "dude u keep forcing altium in front of me while i'm typing".
- Full battery GREEN ON BOTH BUILDS: 122 verbs, **113 pass, 0 fail**, 9 skipped for safety, on Altium Designer 26.10.1 (arav-rog) and Altium Designer Develop (ConfRoomROG) — `close` included, for the first time. Both machines were left with no Altium running and no dialog on screen.
## 0.2.36 (2026-09-21)
Fixes the last failing verb in the battery, `close`, and with it an overnight hostage situation: an Altium on the test box held the single licence seat for eight hours because nothing could reach it.
- **The close-time save prompt is answered in full.** "Save None" does not dismiss Altium's "Confirm Save Locally for (N) Modified Documents": it unticks the documents and RENAMES the same window to "Confirm Not Saving (N) Modified Documents", which OK then commits. `close {discardChanges:true}` pressed only the first half and went back to polling, leaving Altium sitting modal.
- **A modal Altium disables its own main window** — that is how a half-answered prompt strands it. When the dialog is then gone, the app is inside a modal loop with nothing on screen to answer: no menu, no Alt+F4, no WM_CLOSE, and every `-R` hand-off bounces off "There is another instance ... but it is busy". `close` and `agent_recover` now DETECT this (`errorCode: altium_modal_orphan`) instead of timing out or firing helper processes at it.
- **A graceful escape that keeps the seat.** `close {discardChanges:true}` ends a stuck instance with a posted `WM_QUIT` — Altium's own `Application.Terminate`, which runs its shutdown and RELEASES the On-Demand seat (measured: exit in under 8 s, the next launch licensed). A `taskkill` strands the seat; this does not.
- **`close` no longer runs the breakpoint recovery on every call.** It asked "is a loop alive?" AFTER tearing the loop down, so the answer was always no: every close paid ~10 s of `-R` hand-offs, and each one that failed spawned a helper X2. It now asks before the teardown.
- **A swallowed bootstrap is re-sent, not reported as a failure.** Altium can accept the `RunScript` hand-off and then drop it while it is busy with its own startup or unloading the script it was just told to stop; the agent never heartbeats and a healthy engine looked broken (`no_heartbeat` on the first start after a launch, and on the verifying start inside `recover`). `agent_start` now sends it once more before answering, and says so with `resentBootstrap`. A dialog on screen or a modal orphan is still reported immediately — those are real answers, never retried over.
- `agent_recover`'s success hint no longer claims "a fresh agent loop is running" when it was asked not to start one.
## 0.2.34 (2026-09-21)
Third full battery on Altium Designer 26.10.1: 122 verbs, **112 pass, 1 fail** (`close`), 9 skipped for safety.
- **The reaper photographs every box before pressing it**, not only ones titled "Error". An "Information" box explaining why Altium refused to quit was pressed away five times in a row, unread. `altium_dialog_log {shots:true}` returns the last 8 pictures.
- Every failed reply carries `_reportIssues`: where and how to report a bridge bug, and what evidence to include (Bridge SDK).
- A document that did not open is a failure on `altium_open_document` and every `show_*` verb too (0.2.25 covered only the library/board verbs).
- `altium_uninstall {dryRun:true}` no longer stops the resident agent. `altium_license` refuses an unknown `action` by name instead of answering success.
- Agent: `rename_symbol` / `rename_footprint` refuse an empty or already-taken name; `set_layer_visibility` refuses a layer id that is not in the board instead of dereferencing a nil layer object.
- `close` catalog entry documents `discardChanges`, `waitSec` and its error codes.
## 0.2.33 (2026-09-21)
- **Every verb is documented for the AI: 122 of 122** have a catalog entry with summary, input, output, hint (the next call), related verbs, pitfalls and an example (98 had none). Each was written from its handler and its agent command. A unit test now fails the build if a verb is undocumented, documented twice, or names a related verb that does not exist. `describe.facts` lists the error codes added this week.
- **Two dialog verbs could answer questions nobody asked them.** `altium_dismiss_dialogs {hwnd, button}` IGNORED `hwnd` and pressed the button on every dialog, save prompts included, although every hint in the bridge tells callers to pass `{hwnd, button}`; it now presses that one dialog. A sweep (no `hwnd`) skips save prompts and any dialog with more than one button. `recover` pressed OK on every dialog it found, and `agent::call` runs it automatically; it now touches only the lone-OK script error box.
## 0.2.32 (2026-09-21)
- **`altium_agent_recover` actually recovers, and says the truth.** It sent `EditScript:Stop` and answered success without checking. That process only acts when a SCRIPT document is the active document: with a board in front it was a silent no-op, the script stayed on its breakpoint, `altium_selftest_script_error` reported `halted_and_recovered` for an engine that was never freed, and Altium then refused to close (AD 26.10.1; it had only ever worked where the script happened to be in front). Recover now brings each agent script in the workspace to the front (`WorkspaceManager:OpenObject`) and stops it there, warns if a hand-off started a second Altium, and VERIFIES by starting a fresh agent loop: `engineFree:true`, or `errorCode: engine_still_parked` with what to do.
- `altium_close` frees a parked script engine before and during the close (Altium refuses to close "when a script is stopped on a breakpoint"), without starting a new agent loop.
- `altium_run_outjob` / `export_bom` / `export_gerber` refuse a missing OutJob by name (`outjob_not_found`); the agent answered `ok:false` with no text.
- Docs: `PUBLISHING.md`; every sub-skill starts with its `Parent skill:` line; README declares the `altium` keyword, describes the native exe (it still said "Python server") and points installs at `altium_install`; four blunt verbatim quotes in this changelog reworded.
## 0.2.31 (2026-09-21)
- FIX `altium_board_shape`, which halted Altium's script engine on two builds (AD 26.10.1, Altium Develop). It drew four temporary tracks, ran the editor process `PCB:PlaceBoardOutline`, then read `Board.BoardOutline.BoundingRectangle` with no nil guard; on those builds the process left no outline, the read raised on a nil object and the script parked on a breakpoint with no error box. It now sets the outline the way `altium_test_board` always has (PointCount + Segments[] + Rebuild, which renders correctly on every build), with every access nil-guarded, and refuses `bad_size` / `no_board_outline` by name.
## 0.2.30 (2026-09-21)
From the first full battery on a third Altium build (Altium Designer 26.10.1, fresh install): 121 verbs, 106 pass, 6 fail.
- **The dialog scan saw none of the boxes that mattered.** `Confirm Save Locally` (Save All | Save None | OK | Cancel), `Confirm Not Saving` and `Zoom Value` (OK | Cancel) matched no class allowlist, had more than one button and an uncatalogued title, so `altium_check_dialogs` answered "none" while they blocked `altium_close` for minutes. A titled window carrying two or more real dialog buttons is now a dialog whatever its class; the save prompts are catalogued as `save_prompt` and `Zoom Value` as `input_prompt` (safe action Cancel). The reaper still never presses a save prompt.
- `altium_close {discardChanges:true}` now WORKS (the arg was accepted and ignored): it presses Save None and confirms, for callers whose modified documents are all their own. Without it a save prompt returns `unsaved_changes` with a screenshot naming the documents, instead of `close_timeout`.
- Battery: never runs a demo OutJob (they open what they generate on the user's screen); `show_project` with no path is the bundled demo by design; `read_schematic` runs with its sheet shown.
## 0.2.29 (2026-09-20)
- **New verb `altium_install`: the install lane with its rules IN the verb.** A from-scratch install on a fresh machine went badly because the AI driving it kept forgetting the rules in a 650-line skill: it asked the human to sign in on a page that was already signed in (it never looked), left Release / Assign / Use License / Refresh to the human although they are ordinary clicks, did not toast when it did need him, and launched the installer into an empty room so the UAC prompt timed out. Stages `preflight`, `get_installer`, `launch`, `wait`, `license` (or no stage: `auto` reads the machine). It verifies the installer's Authenticode signature (Valid, CN=Altium LLC) before anything runs, refuses `launch` without `humanIsPresent:true`, toasts the human itself and reports `toast.displayed` truthfully, and does the licensing clicks itself. Every reply carries `_hint`, `_next`, `pitfalls` and `needsHuman`, and `needsHuman` may only ever name a password/2FA, the UAC prompt, or clicks inside an elevated window.
- `detect.installVerb` = `altium_install` and `detect.hostAppOptionalVerbs` declared, so ab forwards the install/readiness verbs when Altium is absent and `bridge_readiness` recommends this installer (two SDK audit misses).
## 0.2.28 (2026-09-20)
- **One thread per request.** The bridge handled HTTP requests one at a time, inline in its accept loop, so ONE slow verb blocked every verb behind it, `/health` included: `altium_signin`'s ~25 s UIA search, or an agent call waiting on a parked engine. ab dropped its connection at its own budget (an opaque "HTTP error"), the bridge kept working the abandoned request, and the queue only grew: the 30-verb cascades the verb battery produced, twice, from two different first causes. Each request now runs on its own thread. Agent-bound work still serialises at the agent's one-file inbox, which answers a NAMED `agent_busy` with `busyWith` and `busyForSec` instead of a dropped connection.
## 0.2.27 (2026-09-20)
- The silent-halt fix in 0.2.26 modelled the wrong moment and did not fire. What really happens (heartbeat: `requests: 1`, then nothing): a command raises an exception the agent's own Try/Except handles, the command ANSWERS normally, and THEN Altium's debugger parks the script on a breakpoint, on Altium Develop with no error box. The caller of that command sees nothing; the NEXT verb finds a dead agent, starts a second script into a parked engine and blocks until ab's HTTP timeout, and so does every verb after it. `agent::call` now checks before bootstrapping: agent dead + Altium running + the agent script in front = parked, so it sends `EditScript:Stop` first, restarts the agent, runs the verb, and marks the reply `enginePreviouslyParked:true` (also logged in `altium_dialog_log` as `engine_parked_cleared`). The in-flight title trigger from 0.2.26 is removed: the title alone cannot tell a halt from a long command.
## 0.2.26 (2026-09-20)
Found by the new verb battery (`dev-skills/altium-bridge-test`), first full pass on Altium Designer Develop:
- **Silent script halts are caught.** Altium Develop can stop the agent in its script debugger WITHOUT raising an error box (it did on `board_shape`): the box watcher from 0.2.21 saw nothing, the agent stayed dead, and ~30 verbs in a row queued behind it and timed out. Altium does give one tell: it pulls the agent's script document to the front. `agent::call` now treats "the main window flipped to `AdomAgent_*` while my command is in flight" as a halt after a 1.5 s grace for the box watcher: it photographs the editor, sends `EditScript:Stop` and returns `agent_script_error` with `silentHalt:true`.
- **Malformed numbers can no longer raise inside Altium.** The agent called raw `StrToFloat` / `StrToInt` on caller values in 47 places; anything malformed (`create_footprint` with pads in the wrong shape) raised `EConvertError`, i.e. an Error box on the user's screen. All of them now go through `AdomFloat` / `AdomInt`, which never raise, and the dispatcher answers `errorCode: bad_number` naming the offending value. `scripts/release.py` refuses a build with a raw conversion outside those two helpers.
- `altium_import_part` with nothing to import answered `success:true`; it now refuses with `nothing_to_import`.
- New dev skill + runner: `dev-skills/altium-bridge-test/run_verb_tests.py`, every verb in the live catalog, coverage-enforced, a dialog check after every verb, scratch documents only, closes Altium when done (Author seats), and REFUSES to start unless the target machine has been idle (it raises Altium, and it did so over the user's typing on its first run).
## 0.2.25 (2026-09-20)
- **A document that did not open is now a failure, with the reason.** `altium_open_document` (and the open every library/board verb does first) answered `ok:true` with `shown:false` buried in `data` when Altium silently refused the file; the verb then failed with a misleading "open the SchLib first" / "current document is not a schematic library". Now: `errorCode: file_not_found`, `view_only_no_author_seat` or `document_not_opened`, and 14 verbs that need the document stop there instead of running against the wrong one (`run_drc`'s refocus and `import_part`'s opens stay best-effort). Projects are exempt (they have no document view).
- `view_only_no_author_seat`: measured on Altium Designer Develop. A second running Altium (another machine) held the workspace's only Author seat; Develop raised "Workspace Unavailable" at launch (acknowledged by the reaper), showed "There are no Author seats available ... view-only mode" behind the warning triangle, kept already-open documents working and refused every new one. The hint says so and gives the fix: close the other Altium cleanly, relaunch this one. No sign-in is involved.
- The SOIC-8 / QFN-24 (9 heatsink vias) / BGA-36 suite now passes on BOTH Altium Designer 26.9.1 and Altium Designer Develop.
## 0.2.24 (2026-09-20)
- `altium_selftest_script_error` knows both safe outcomes and reports which one it saw in `outcome`. Running 0.2.23 on a second Altium (Altium Designer Develop) showed the same deliberate error does NOT halt there: the engine raises a catchable exception, the agent's own handler answers `ok:false` in about 2 s and the loop never stops (`contained_by_agent`). AD 26.9.1 halts on a breakpoint and needs the bridge's recovery (`halted_and_recovered`). 0.2.23 called the first a failure. A timeout or a dialog left on screen is still the only FAIL. This is also why the 0.2.20 wedge never showed on the machine it was developed on.
## 0.2.23 (2026-09-20)
- New verb `altium_selftest_script_error`: makes the agent raise a real DelphiScript runtime error on purpose and checks the safety net end to end (Error box photographed, halted script stopped with `EditScript:Stop`, no dialog left, `netWorks: true`). It is the regression test for the failure 0.2.21 fixed; run it after any change to `agent.rs` / `dialogs.rs` and once on each new Altium build.
## 0.2.22 (2026-09-20)
- Script-error safety net, second variant. 0.2.21 caught the lone-OK `Error` box (an error in the agent's own code). Proving it with a deliberate bad `altium_eval` on AD 26.9.1 showed the OTHER shape: a script run from inside the agent raises an `X2` box, "Undeclared identifier: ... at Line:N. Continue execution?", with OK + Cancel. The reaper rightly never touches a two-button box, so the agent sat blocked and the caller saw a timeout. `agent::call` now recognises that box while its own command is in flight, photographs it, answers OK (continue, so the agent loop survives) and returns `errorCode: agent_script_error` with `errorShot`, `answered`, `agentAlive`. It is logged in `altium_dialog_log` as `script_error_continue`.
- The changelog line about removed internal files no longer names them.
## 0.2.21 (2026-09-20)
- **A script runtime error can no longer wedge Altium silently.** Found by running the 0.2.20 suite on a second machine (AD 26.9.1 trial; SOIC-8, QFN-24 with 9 heatsink vias, BGA-36): a DelphiScript runtime error raises a lone-OK `Error` box, the standing reaper pressed OK within a second, and the script was left STOPPED ON A BREAKPOINT in Altium's script debugger. The agent loop was dead, every later verb timed out, Altium refused to close ("cannot close the current document when a script is stopped on a breakpoint"), and the error text was gone. Now: the reaper photographs an `Error` box before pressing it; `agent::call` sees it within 400 ms, sends `EditScript:Stop` itself (the `altium_agent_recover` steps) and returns `errorCode: agent_script_error` with `errorShot` (the PNG of Altium's message), `autoRecovered` and `recoverSteps`. The next verb restarts the agent.
- FIX the error that exposed it: `altium_symbol_prims {models:true}` and `altium_set_models` read `Comp.ImplementationCount` / `Comp.Implementation[i]`, which AD 26.9.1 does not expose to DelphiScript. Both now walk a component's models with the `eImplementation` child iterator.
- `scripts/release.py` refuses to build an agent template that uses an identifier measured to halt the engine (`ENGINE_HALTING`: the list from the dev skill, as code).
- Docs: every declared verb is in the README and a skill (21 were in neither); `altium_plate_vias`, `altium_add_symbol_art`, `altium_symbol_prims`, `altium_set_models` documented; new `altium-a365` skill; `install.sh` installs all user skills (instances, licensing, uia, wiki-import, a365 were packaged but never copied).
- Internal material removed from the public repo: machine names, an account display name and a private workspace hostname in `PLAN.md` / the plan skill / code comments and error strings; one internal memo deleted.
- `place_on_sch` describe text matches the agent (sheet sized from the measured parts, not "A4 or B"). Repo-root `bridge.json` synced to the shipped `src/bridge.json` (it was a stale 0.1.41 leftover).
## 0.2.20 (2026-09-20)
- New verb `altium_plate_vias {libraryPath, footprintName?}`: every drilled pad in a PcbLib (all footprints, or the one named) that sits on a layer other than Multi-Layer becomes a plated Multi-Layer pad (`Pad.Layer := eMultiLayer`, `Pad.Plated := True`). adom-lbr writes heatsink vias as pads with a hole and Altium loaded them on the Top Layer, so a thermal pad's vias arrived with their holes but were not through-holes (John, 2026-09-20: "did Altium support bringing in those heatsink vias?"). The reply carries `holes` (drilled pads seen), `plated` (pads changed) and `fixed` (`Footprint:Pad;...`). Modifies the open library in place: `altium_save` the PcbLib afterwards; `altium_footprint_primitives` on a placed part then reports layerId 74 for the vias. Pads already on Multi-Layer are left alone, so a second run changes nothing.
- Version number: 0.2.19 was consumed by an interrupted publish, so this shipped as 0.2.20.
## 0.2.19 (2026-09-20)
- `altium_test_board` silkscreen part name: its top now sits 2.5 mm below the part centre, clear of a 3 mm part's courtyard (0.2.18 overlapped it).
## 0.2.18 (2026-09-20)
- `altium_test_board` writes each part's name on the Top Overlay under its footprint (35 mil stroke text, centred, its top 2.5 mm below the part centre so it clears the courtyard) so a human reading the board knows which chip is which (John, 2026-09-20: "place onto the silkscreen the name of the chip at that spot on the board"). Same rule as the KiCad and Fusion boards the ToF demo makes. Agent template only.
## 0.2.17 (2026-09-19)
- `altium_place_on_sch` sizes the sheet FROM the parts: pass 1 measures every symbol's bounding box (pins included, +250 mil a side for pin numbers), the custom sheet is the row plus 500 mil between parts and 600 mil margins (never smaller than A4 landscape, 11500 x 7600), any slack is spread evenly, designators sit above and part numbers below each body. The `SheetSizeX/Y` read-back is gone: on ADDevelop it reports the 9500 x 7500 default after a custom size is set, which is what squeezed three parts on top of each other in 0.2.16. John's two reviews in one afternoon: "no real EE would smoosh symbols together, they'd space them out", then "a bit closer together so we get a better screenshot" - the numbers above are the ones he accepted.
## 0.2.16 (2026-09-19)
- `altium_place_on_sch`: the designator sits centred ABOVE the body and the Comment (part number) centred BELOW it, whatever the library part did with them (+200 mil had put U1 on top of the chip outline inside a 3700 mil body); the description text frame sits INSIDE the drawing frame (700 mil below the top edge, 700 mil short of the right edge) instead of above the sheet; the sheet size is read back from the document (`SheetSizeX/Y`, `UseCustomSheet := False` before `SheetStyle`) so parts and text land inside whatever Altium actually gave us. Agent template only.
- Doctrine, measured with John typing in Hydrogen on the same box: the capture verbs restore Altium INTO THE BACKGROUND, but the document steps (`open_document`, `close_document`, `place_on_sch`, `install_library`, `agent_stop`/`agent_start`) still raise the Altium window through `Client.ShowDocument` / script execution and steal his keyboard. Filed as an issue; until it is fixed, batch the document work and stop touching Altium the moment the proofs are in.
- The "ganged-up view" (symbol + 3D chip in the Editor panel, footprint preview in the SCH Library panel): there is no server process that activates a docked panel TAB (`Client:CustomizeResources Action=Show|ObjectKind=Panel|ID=TSchLibPanel` only ensures the panel exists); a `panel` verb is on the wish list.
## 0.2.15 (2026-09-19)
- `altium_add_symbol_art` replaces instead of stacking (`clear`, default true) and draws the outline as eLine segments (`mode: lines`, default), the primitive that BuildSymbolModes has always rendered with; ISch_Polyline objects rendered on VL53L5CX but stayed invisible on VL53L8CX with identical geometry inside the body (measured with `altium_symbol_prims`). `symbol_prims` gates its model walk behind `models:true` (the ImplementationCount / PartCount walk halted the engine on ADDevelop).
## 0.2.14 (2026-09-19)
- **A sheet a human would sign** (John, reviewing the demo capture: symbols on top of the title block and outside the frame, empty title block, '*' comments). `altium_place_on_sch` now sizes the sheet for the part count (A4 up to two parts, B from three), places every part INSIDE the drawing frame centred on its body rectangle, sets each Comment to the part number, fills the title block from `title` / `number` / `revision` / `drawnBy` (document parameters behind the standard title block's special strings) and drops `description` as a text frame across the top. The reply carries `sheetMil`, `titleBlock{}` and `_hints` that name whatever the AI left empty and say how to write it.
- **The chip outline reaches Altium.** New `altium_add_symbol_art {libraryPath, symbolName, kicadSym}` reads the polylines adom-symbol baked into the KiCad symbol (chip-thumbnailer's outline of the side-etched model), maps the KiCad body rectangle onto the SchLib component's rectangle and draws them as polylines, so the Altium symbol carries the same chip as the KiCad and Fusion ones. Appends; run once per part.
- `altium_test_board` replies with `_hints` (list form, pcbPath, outline sizing, body check).
## 0.2.13 (2026-09-19)
- **Several parts on ONE document** (John's ToF demo: three VL53Lx sensors on one schematic and one board). `altium_test_board` and `altium_place_on_sch` now accept a `;` list in `footprintName` / `symbolName`: every part lands on the same new board (spread evenly along X inside the wMm x hMm outline) or the same new sheet (U1..Un, 5000 mil apart), and both save to `pcbPath` / `schPath` when given. Single-part calls are unchanged; `assigns`/`stubs` apply to the last part placed. Both verbs finally have `describe` entries. Agent template only: deploy the tmpl beside the exe and `altium_agent_stop` + `_start`; the 0.2.9 exe keeps working with it.
- Measured on a second test machine (ADDevelop): `altium_eval` scripts that call `PCBServer.PreProcess` / `PostProcess` or even `TStringList.SaveToFile` halt before their first statement completes and leave an X2 error modal whose text is not readable (`dialogs[].body` empty); clear with `altium_dismiss_dialogs {Cancel}` + `altium_agent_recover`, twice. adom/altium-bridge#11 asks for readable eval errors and for append-to-existing placement.
## 0.2.12 (2026-09-08)
- **The one-click install no longer dies at Adom Bridge's 60 s budget** (Colby, live on 2026-09-08: a 7-part Cloud Library export landed in Altium, SCH Library panel and all, while the page said "Could not install: 'altium_install_library_bundle' did not respond within ab's 60s budget"). ab enforces a per-verb budget it reads from `bridge.json`, and `install_library_bundle` was never declared there after 0.2.8 gave it a cold launch and 0.2.9 the open, panel-select and capture finish. Declared now: 330 s for the bundle install (the launch alone is 240), 150 s for `install_library`, and the verb's `describe` entry is marked long-running with `altium_status` as its status verb. The reply carries `phasesMs` (launch, licence, unpack, register, open, list, panel, capture, agentStop) and `totalMs`, so the next slow run names its step instead of leaving a guess.
- **The receipt survives a timeout.** The bridge serves one verb at a time and finishes an install ab has already given up on; the receipt, proof frame included, is now kept in memory. `altium_status` carries a light `lastInstall` (with the `installId` the client sent), and the new `altium_install_receipt` returns the full reply. The wiki page waits through this path (git-wiki e1c66a1), so a timed-out install still ends in the success line and the proof shot.
## 0.2.11 — 2026-09-07
- **Fresh-workspace install fixed** (adom/altium-bridge#10, found by the Hydrogen pre-production test: the dock's Altium Dashboard tile failed on every virgin box since 0.2.10). The 0.2.10 lint fix moved `altium-bridge-dev` out of the shipping tarball but `install.sh` still copied it, so `cp` failed and the whole install script was skipped, taking `altium-dashboard` with it. `install.sh` now installs only user skills; `altium-bridge-publish` moved to `publish-skills/` in the same pass (publish skills are source-only, same doctrine); `package.json` files[] and `page.json` scrubbed of both stale paths. `uninstall.sh` still removes previously-installed copies. Runtime unchanged.
## 0.2.10 — 2026-09-02
- **Two-tier (insiders) publish adopted** (adom/altium-bridge#9, John's ask; pattern from adom/pup-bridge which did it first). `src/bridge.json` declares `insidersManifestUrl`; a desktop on ab 2.0.99+ whose login can read the org's early-tier page follows the insiders manifest, everyone else follows the public one and never learns the field means anything. `scripts/release.py --publish` now ships to INSIDERS by default (zip on this page, manifest + release-log entry on the early-tier page, source stamps, skills pkg in lockstep per adom/wiki#170); the public manifest moves ONLY through `--promote <ver> --attest "<the human's exact words>" --by "<name>"`, which refuses placeholders and records the words in the manifest. `--rollback` re-points the public tier at an archived build, also attested. No runtime changes beyond the manifest field.
## 0.2.9 — 2026-09-02
The dedicated freeze-fix release (adom/altium-bridge#6, #7, #8). Every mechanism here traces to the evidence gathered on a colleague's machine on 2026-09-02: three hard desktop freezes, all enabled by a resident agent loop the per-instance views could not see.
- **The workspace split-brain is closed.** The agent REFUSES to bootstrap without a resolved instance (with a 12s resolve retry for just-launched windows) instead of silently landing in the legacy root where status and teardown were blind to it. `altium_close` now stops EVERY workspace's live loop (legacy included) before its WM_CLOSE; readiness/agent_status report `agentResidentAnywhere` + `liveWorkspaces`; `altium_state` (#8) answers from the union too, naming the workspace its `logical` read came from. `install_library_bundle` re-resolves the instance after its own auto-launch.
- **Close watchdog v2 detects natively and can end a wedged shutdown.** 0.2.8's watchdog missed both freezes it watched because its detection ran through ab, whose enumeration hangs or returns empty exactly mid-freeze. v2 asks Win32 directly (non-messaging calls only; "cannot answer" is distinguished from "none"), drops stop files into every live workspace the instant the main window vanishes, and after 45s of "window gone, X2 alive, our loop was involved" it warns by toast and terminates the wedged process, recording the seat consequence. A ten-second blip instead of a bricked desktop.
- **The one-click install lands the user on their library (#7).** After registering, the bundle opens the installed SchLib, selects the SCH Library panel (UIA SelectionItemPattern - a plain click does not switch panel tabs), captures the proof frame, and reports an honest `shown: true|false` plus real component counts (`symbolCount`; the old `symbols`/`footprints` file counts remain one release). The agent is STOPPED when the install lane finishes, so the user's later X-press finds an idle engine. The wrapper folder inside export zips is flattened (no more `UI\UI\`).
- **`altium_close_document {path}`** (new verb): closes one open document through its document handle, never saving. The menu processes are broken for this on ADDevelop: RunProcess `Client:CloseDocument` no-ops and `Client:CloseAllOpenDocuments` unregisters documents while their views stay open (both measured).
- **Agent template hardening**: `Application.Terminated` joins the loop's exit checks (`Client.IsQuitting` demonstrably never fired in time), idle auto-stop cut from 30 to 5 minutes (residency IS the freeze window), and the `close_document` command. Per the probe campaign on #6: no interception design is possible in this engine (timers die at script return, the VCL context is a sandbox, runtime event assignment is illegal in every spelling, TApplicationEvents is unknown to the dfm loader), and `Client.StopServer` rejects both plausible signatures, so server hygiene is deliberately NOT attempted.
- **Dialog catalog**: "Error Reading Form" (dfm class-not-found; Cancel is the clean dismissal) and a body-text fallback that reads Text-role control names, so the script debugger's "Continue execution?" prompt (title "X2", previously empty body) classifies instead of evading the reaper.
## 0.2.8 — 2026-09-02
- **User-initiated-close watchdog, ported from Colby Knox's PR 74 (adom/altium-bridge#4) to the Rust runtime.** A standing service polls every 2 s; for any per-instance workspace with a live heartbeat whose main window has vanished while an X2 process still lives, it drops the agent's stop file that instant, so the loop exits during shutdown instead of wedging under the engine teardown (the freeze that hard-locked a desktop). Cold starts never trigger it (a workspace must have been SEEN with a window). Each firing is logged in `altium_dialog_log`. Honest limit, as in the PR: a 2 s race, not a guarantee — the complete fix is the agent noticing shutdown itself.
- **`altium_install_library_bundle` auto-launches a closed Altium** (Colby, #5): one-click parity with the KiCad/Fusion lanes; cold start runs with the measured taskbar bar, the agent bootstraps on the first call, and the reply carries `launched` with the cold-start time.
## 0.2.7 — 2026-09-02
- **The reaper pre-empts Altium's licence dialogs too** (John, with two of them on his screen: "you should be handling this automatically for the user" / "you should pre-empt all of these dialog boxes btw"). Three gaps closed:
- the scan no longer gates lone-OK dialogs on a window-class allowlist — any titled, visible, non-main window on an Altium pid whose only UIA button is OK is an acknowledgement, whatever its class;
- the **licence chooser** (`TThemedConfirmDialog`, "You are not currently using an Altium Designer license", painted text) is recognised by its BUTTON SET (Choose a license | Continue): the reaper presses Choose a license and then Use License on that instance's License Management page via UIA — Continue would leave Altium in Viewer mode, which is never right for a bridge user;
- **License Renewal Failed** (`TFailedToRenewWarningForm`) exposes NO buttons to UIA enumeration yet its OK clicks by name, so it is admitted by class and cleared by a TITLE rule; a title that matches the catalog now qualifies a window on its own.
- Honest limit, stated in the reaper log's `followup`: a claim against a STRANDED seat (leased to a killed Altium) still fails with "maximum usage reached" — the bridge keeps that dialog off the screen, but only a sign-out/sign-in or the lease expiring frees the seat.
## 0.2.6 — 2026-09-02
- **The standing reaper was blind since 0.2.0 — fixed.** The VaultExplorer startup warning was still reaching the user's screen. Root cause: ab's `desktop_find_controls` returns buttons under `matches`; the Rust port read `controls`, so every dialog scanned as `buttons: []` and the lone-OK acknowledgement rule could never fire. Parser fixed with a regression test built from the exact reply ab gave for that warning; title-bar controls are dropped so a lone OK still reads as lone. Verified minutes later: two startup warnings reaped before anyone saw them.
## 0.2.5 — 2026-09-02
- **`altium_foreground` verifies instead of assuming**: after the raise it reads the z-order and reports `visiblyOnTop` — a raise can "succeed" while another fullscreen window still covers Altium (John watched an entire show that never became visible because his fullscreen Claude window sat on top, and his own typing kept it there — which the foreground-once rule correctly refuses to fight). `demo {live}` surfaces the verdict as `foregroundNote` so the narrating AI can TELL the user the show is running behind their current window rather than silently losing to it.
- **The taskbar progress bar is visible during live-demo stages**: pacing used the 210 s full-tour "demo" seed, so a ~12 s stage filled 0→7% — invisible. Live stages now pace against their own `demo_live_stage` phase (12 s seed), and every driving verb with a bar records its actual duration into the SAME key its estimate reads, so bars converge on this machine's measured truth instead of staying seeded forever.
## 0.2.4 — 2026-09-02
- **The live demo foregrounds Altium ONCE, at stop one — never again mid-show** (John: "just foreground altium once. then don't try to foreground again for the rest of the demo just to make sure you strike a balance for the user"). The opening raise is an invitation; re-raising every stop would drag back a user who deliberately glanced at something else. Documents keep opening in Altium either way, so a watching user misses nothing.
## 0.2.3 — 2026-09-02
- **The punch_in preset stops flying the view into blank canvas.** Altium zooms TOWARD THE CURSOR, so the 4-step zoom dive compounded drift toward wherever the user's mouse sat and scrolled the content off screen (John, watching take three: "you are like scrolling the content off the screen ... the screen going blank sometimes"). Per his fix: one gentle zoom-in/zoom-out breath, reframed with zoom_all — single-step drift is invisible.
- **The demo/live hints now teach the adom-tts cue lane** (1.9.47's `load`/`cue --confirm`/`cue-status`, built from this thread's spec on adom/adom-tts#4): pre-cache the narration as markers, cue each stop in the background as the stage call starts, advance when both finish. Prefetched cues start instantly, which closes the last sync gap the say --play lane had.
## 0.2.2 — 2026-09-02
- **The live show is code now, not a hand-rolled AI script** (review feedback: everything the demo needs should be a verb, and the sync jitter came from driving the show step by step from the AI instead of from code). Driven from a container, every camera step paid the full CLI→ab→bridge→agent round trip (~1.4 s, jittery); in-bridge it is one agent file-drop (~40 ms), so choreography holds its timing.
- **`altium_animate_view`** — camera/zoom choreography on the current document from NATIVE Altium camera commands (SetCamera/ResetCamera/FlipBoard/Zoom; Altium animates the transitions itself — no synthetic mouse). Steps (orbit_left/right/up/down, isometric, flip, zoom_in/out, pans, rolls…) or presets (punch_in, showcase3d, orbit, flip, finale), each with a pauseMs.
- **`altium_foreground`** — the sanctioned exception to the never-foreground rule, for a human who asked to WATCH. Refuses without a human-readable `reason` (>=10 chars), mirroring ab's caption rule.
- **`altium_demo {live:true}`** — the narrated, animated, foreground tour: per stop it paints the ab caption, opens the document, holds Altium in front, runs the choreography in-bridge, and returns `narrate` + `nextNarrate` so the caller's TTS speaks over exactly the motion described (fire nextNarrate detached, then make nextCall). When Altium is cold it narrates the launch too: caption + a spoken line + a background launch, `status: "launching"` until up.
- **Fixed the zoom that never happened**: schematic zoom-in is `Sch:Zoom Action=ZoomIn` — `Action=In` is silently IGNORED by Altium (John watched a whole stop where "pushing in" never moved). The rcs catalog carried the truth all along; the step map now encodes it.
## 0.2.1 — 2026-09-02
- **A finished verb can no longer leave a stale partial progress bar on Altium's taskbar icon.** John spotted it minutes after the 0.2.0 swap. Root cause: every progress paint is an ab round trip, and the clear was painted from the dispatch thread — so a tick still in flight from the ticker thread could land AFTER the clear and park a "47%" frame on the icon forever (a race the Python bridge carried latently; the Rust timing exposed it). The terminal frames (error flash, clear, flash:stop) are now painted by the ticker thread itself, where paints are sequential, so the clear is guaranteed to be last; the ticker also sleeps in 100 ms slices so the clear lands promptly.
## 0.2.0 — 2026-09-02
- **The bridge is Rust, end to end.** John: *"let's finish getting everything into the rust code. i don't like that we're half rust half python now."* All 112 verbs now dispatch from `bridge-rs` (`altium-bridge.exe`, `spawn.kind: "exe"` per ab's SDK); the Python runtime no longer ships. What crossed over, at behavioural parity with 0.1.41:
- the **instance layer** (`instance:<pid|key|word>`, `ambiguous_instance` refusal, per-instance agent workspaces `agent/<key>`, instance-aware licence gate incl. the Develop absent-tail rule);
- **background proof capture** (restore-minimized-into-background → capture → hand the foreground back; sub-400px slivers refused as `proof_unavailable`);
- the **standing dialog reaper** (catalog safe-actions only, lone-OK acknowledgement rule, never a save prompt, `dialog_log` ring) and the in-flight `-R` modal watcher;
- **taskbar signals** (driving badge, measured-estimate progress bar, 5-minute idle auto-clear) and the **measured-progress store** — same on-disk paths/JSON as the Python bridge, so each machine's history survives the engine swap;
- the **seat rules** (force-close refuses without `acceptSeatLoss`, seat hints on every licence-shaped failure, UIA-first licence page driving via `UseLicenseButton`);
- all **surfaces, library authoring, `import_part`** (zero-model attach still fails loudly), `install_library_bundle` (hardened unzip in Rust), **outputs** (compile/ERC, OutJobs, STEP via the extension, the DRC modal dance), **review_board** (all 14 checks as pure functions, now fixture-tested), **cross_check**, the **rcs command catalog**, and the **Altium 365 OAuth/GraphQL lane** (PKCE per RFC 7636, verified against its test vector).
- The verb lint now reads the RUST dispatch (`verbs.rs` KNOWN) against `bridge.json`, and the release also refuses on version drift across `bridge.json` / `BRIDGE_VERSION` / `package.json` / `main.rs`. `cargo test` (38 tests, incl. the agent-generation digest interop test that keeps Rust and the DelphiScript template in lockstep) runs before every build.
- Two Rust-side views that existed but were never declared — `altium_board_summary`, `altium_rules_by_kind` — are now in `bridge.json` (declared-vs-dispatchable is exactly what the lint exists for).
- Still intentionally not Rust: the DelphiScript agent template (it runs INSIDE Altium; that lane is Pascal by nature) and the container-side converters (`tools/kicad2altium.py`, `tools/symbol_modes.py`), which never ship in the bridge zip.
## 0.1.41 — 2026-09-01
- **The tab favicon actually reaches the tab now.** 0.1.40 shipped the correct line-mark `favicon.svg`, but the dashboard's icon route aliased EVERY icon path — `/favicon.svg` included — to `dock-altium.svg`, which is a 60 KB SVG wrapping an embedded PNG raster. Hydrogen's webview reads the page's favicon after load (hydrogen-tab-icons) and renders nothing for that blob, which is precisely the icon-less tab John reported twice. The favicon route now serves `ui/favicon.svg` (the monochrome #e6edf3 line mark, same file as the header icon); `/dock-altium.svg` keeps serving the dock card art.
- **`install.sh` now installs `favicon.svg`.** Its asset list was explicit, so 0.1.40's new file never landed in `~/.local/share/adom/altium-bridge/` — the pkg carried it and the installer dropped it.
- Verified end to end this time: the live server returns the 340-byte mark for `/favicon.svg`, and a Hydrogen workspace capture shows the restored header (mark, subtitle, pill chip with LED, ghost buttons) live in the dock tab.
## 0.1.40 — 2026-09-01
- **The dashboard header is whole again, and the dock tab has a favicon.** John, from one glance at the dock: *"why don't i see a favicon ... your header looks wonky ... almost like you're missing a css."* Both right. `index.html` linked `favicon.svg` but the file did not exist (the route 404s honestly since the 2026-08-25 route-table sweep, so the tab showed the generic mark), and an earlier CSS deletion had taken the header's visual rules — `.iconbtn`, `.vsep`, the target-chip pill, the brand font — while the markup and a `.iconbtn:focus-visible` rule survived, so the buttons rendered as UA-default white boxes on the dark bar.
- Restored per dash-header's numbers: 28x28 ghost buttons with 18px glyphs (muted at rest, teal on hover), mono pill target chip with the LED inside, 1px vsep, name in Familjen Grotesk 600 15px `var(--text)` — NOT the accent, per the 2026-08-24 ruling, deliberately unlike fusion's current teal name.
- `favicon.svg` is the SAME line-drawing mark as the header SVG, as the icons skill requires — one mark, tab and header.
## 0.1.39 — 2026-09-01
- **0.1.38's idle-watcher never actually started.** The boot-site call used `taskbar` unimported (dispatch imports it lazily inside the handler), so boot logged `name 'taskbar' is not defined` — and logged it as a DIALOG REAPER failure, because the watcher call sat inside the reaper's try-block. Fixed the import, and each boot service now has its own try so a failure names the thing that failed. Verified by the boot log this time, not by assumption.
## 0.1.38 — 2026-09-01
- **The taskbar overlay badge auto-clears after 5 minutes without a driving verb.** John, seeing the Adom mark still parked on Altium's taskbar icon: *"do you auto-remove that overlay after 5 minutes of verb inactivity?"* The answer was no — the badge was set on the first driving verb and never cleared until the bridge died, while this module's own header PROMISED "cleared when the bridge goes idle". A permanent overlay stops meaning "an AI is driving right now" and starts meaning nothing. An idle watcher (started at boot beside the dialog reaper) now clears it at 5 idle minutes; the next driving verb re-badges instantly.
- For the record, what already worked: every driving verb with a measured estimate ≥3 s runs a REAL taskbar progress bar on Altium's button (percent paced by history, marquee when unmeasured, red flash + clear on failure) and stops it when the verb returns — progress rides the taskbar icon, never toasts, per the house rule.
## 0.1.37 — 2026-09-01
- **Altium's dialog text is unreadable — so the reaper stops relying on it.** Measured: the VaultExplorer warning's entire UIA subtree is `Window "Warning" -> panes -> Button "OK"`. The message is PAINTED pixels, absent from `WM_GETTEXT` and from the accessibility tree, so no amount of text classification will ever see it.
- **The rule that does work: a dialog whose ONLY button is OK carries no decision.** Dismissing it can acknowledge, never choose. Everything that asks something of the user has two or more buttons — a save prompt (Save / Don't Save / Cancel), a licence chooser (Choose a license / Continue), a confirm (Yes / No). The standing reaper now clears lone-OK dialogs even when their text is unreadable, logging them as `single_ok_acknowledgement`.
- Multi-button dialogs are still never improvised on: if the catalog's safe action is not literally one of the dialog's buttons, the reaper leaves it alone.
- This is what finally makes `Could not start Vault Explorer` — and any future painted, single-OK Altium warning — invisible to the user.
## 0.1.36 — 2026-09-01
- **The bridge can finally READ Altium's dialog messages.** Altium PAINTS its dialog text instead of using Static controls, so `WM_GETTEXT` returned nothing but the BUTTON labels — `Could not start Vault Explorer…` came back as a body of just `"OK"`. Every such dialog therefore classified as `unknown`, which is why the standing reaper (correctly, per its own rules) refused to touch the exact warning it was built for.
- `_body_of()` now strips the button labels and, when what is left is too short to be a message, asks UIA (`desktop_ui_text`) for the painted text. Classification, the reaper, `check_dialogs` and every `_hint` that quotes a dialog all get the real message.
- This was the missing half of 0.1.35: the reaper was correct, its input was blind.
## 0.1.35 — 2026-09-01
- **A standing dialog reaper, running from bridge start.** John: *"and always pre-empt these from ever showing up."* Clearing a dialog after a verb notices it is not pre-emption — the user has already seen it. The reaper polls every 1.5 s and presses the catalog's SAFE action the instant a KNOWN nuisance appears, whether or not any verb is in flight. That is the only thing that can catch Altium's STARTUP warnings (`Could not start Vault Explorer`, `File type not recognised`), which appear with nothing running.
- **Hard rules, so it can never answer for the user:** only dialogs whose catalog entry declares a safe action; never `unknown`; never a save prompt (those name documents that may belong to another AI thread and are surfaced for a human decision). Every press is recorded and readable via the new `altium_dialog_log`.
- **Catalog additions with safe actions:** `Could not start Vault Explorer` (Altium's own extension failing to load — NOT the bridge: the extension is correctly registered, its payload is complete at 115 files including VaultExplorer.dll, and Altium's private .NET 8.0.15 runtime loads fine), `File type not recognised` (at startup it is Altium restoring a document it can no longer open).
- 110 verbs.
## 0.1.34 — 2026-09-01
- **The force-close guard now fires on INTENT, not on a blocked dialog.** 0.1.33 put it inside the blocked-by-dialog branch, so `altium_close {force:true}` sailed straight through whenever nothing happened to block — exactly the path that strands the seat. It is now checked at the top of the verb: `force:true` on a licensed instance is refused with `would_strand_licence_seat` unless `acceptSeatLoss:true`.
- **`altium_instances` applies the Develop licence rule too.** 0.1.33 fixed only `licence_state()`, so the instance list still reported Altium Designer Develop as unlicensed while its agent was running and importing.
## 0.1.33 — 2026-09-01
- **Three new skills, because AI reads verb hints and skills, not tribal memory** (John: *"ai hints are way more valuable to help the ai know how to navigate the stuff you didn't know how to"*):
- **altium-licensing** — one On-Demand seat serves EVERY Altium app (Designer and Designer Develop share it); killing an instance strands the seat and the licence page then reports it FREE while refusing every claim; the only recovery is a polite `altium_close` then `use_license`. Also: Develop writes NO licence tail in its title when it HOLDS the seat.
- **altium-instances** — addressing one Altium of several, why `ambiguous_instance` is a feature, the per-instance agent workspace, and the three places single-instance assumptions hid.
- **altium-wiki-import** — wiki part page or Cloud Library → Altium, the Cloud Library's Altium gap (phase 3) and the pipeline that closes it, plus the background proof captures.
- **AI hints where the knowledge bites**, not only in the skills: `altium_instances` and every `not_licensed` refusal now carry `_licenceHint` / `_seatHint` explaining the one-seat rule, the stranded-seat signature (Seats 0/1 while claims fail) and the exact fix.
- **`altium_close {force:true}` refuses** unless `acceptSeatLoss: true` — a forced kill strands the seat, and this used to be one keystroke away. The blocked-by-dialog reply now also warns that an Unsaved Changes prompt NAMES documents that may belong to another AI thread.
- **Dialog catalog** gained `License maximum usage reached`, `License Renewal Failed` (re-fires on a timer; clearing it is not the fix), `Cannot focus a disabled or invisible window` (Altium's crash reporter — its only button SENDS data) and a sharper `Unsaved Changes` entry.
- **Title parser: an absent licence tail on Altium Designer Develop now means LICENSED**, not unknown. `altium_instances` reported ADD unlicensed for an entire test while its agent imported five parts.
## 0.1.32 — 2026-09-01
- **Instance-awareness now reaches the licence gate.** `main_window()` returned "the first Altium main window" regardless of the selected instance, and `licence_state()` → `_need_licensed()` are built on it — so with two Altiums running, every verb targeting one instance was judged against the OTHER instance's licence. Measured live: `altium_license_status {instance:"develop"}` correctly reported `Trial`, while `altium_agent_start {instance:"develop"}` was refused `not_licensed` because Designer held no seat.
- Verified with a real second instance (Altium Designer Develop): the layer distinguishes `AD26/Designer` from `ADDevelop/Develop`, gives each its own agent workspace, and REFUSES a verb with `ambiguous_instance` when no selector is given — rather than acting inside the other AI thread's Altium.
## 0.1.31 — 2026-09-01
- **Systemic fix: `ad_callback` now unwraps ab's reply envelope.** ab's direct API answers `{success, status, error, output}` with the verb's REAL result JSON-encoded as a STRING in `output`. The Python bridge never unwrapped it, so EVERY field read back from an ab callback — `hwnd`, `windows`, `processes`, `controls` — was silently `None`. Calls that only ACTED (click, kill) worked, which is why this hid for so long; anything that tried to READ a result quietly saw nothing.
- Found because 0.1.30's background-capture helper decided a minimized window was not minimized: it was reading `rect` out of an envelope it could not see into. The Rust bridge has always unwrapped (`ab.rs::unwrap_output`) — this brings Python in line.
## 0.1.30 — 2026-09-01
- **Proof captures work with Altium minimized, without ever moving the window.** Windows does not render a minimized window, so a capture of one returned a ~237x39 title-bar sliver — which the bridge handed back as "proof" of a successful import, and which the wiki front-end would have displayed as evidence. Now every capture first makes the window CAPTURABLE: restore it if minimized, push it to the BOTTOM of the z-order, and hand the user's foreground straight back. Measured end to end at ~0.6 s; the capture then reads the occluded window at full size.
- **The rejected alternative, recorded so nobody rebuilds it:** parking the window off-screen. Rejected in review on 2026-09-01: a user who brings Altium forward by hand would not find it. Never move a window the user might reach for.
- **A capture narrower than 400 px is refused, not returned.** `_shot_of_main` reports `proofUnavailable` with the reason, and the surface verbs (`show_symbol` / `show_footprint` / `show_3d_chip` / …) return `success: false, errorCode: "proof_unavailable"` rather than a confident nothing. The frame also carries `prepared` — what the bridge had to do to the window — so a caller can say honestly whether the user's screen was touched.
## 0.1.29 — 2026-09-01
- **`import_part` no longer reports a silent half-import.** The `add_models` step recorded a bare boolean, so attaching ZERO footprint models looked identical to attaching one — and a symbol with no model places on a schematic with no footprint. It now surfaces the agent's own counts and FAILS with `failedAt: "add_models"` when the count is zero.
- **It also sanitises the model spec the way `add_models` always did**: the agent splits models on `;` and fields on `|`, so an unescaped separator inside a DESCRIPTION silently truncated the spec. `import_part` passed the description raw.
- Known defect, recorded not fixed: **`add_models` is not idempotent** — each call appends another implementation, so calling it twice leaves a symbol with duplicate footprint models. Removing a model needs an implementation-read API this agent has not proven yet, so the safe repair today is `remove_symbol` + re-import.
- Verified end to end against a real wiki component (`adom/stm32f303cbt6`, published by ray): kicad_sym → 48-pin symbol with 10 display modes, kicad_mod → LQFP-48 land pattern with courtyard/fab/pin-1 marker, STEP → 3D body, all three vendor part numbers attached, **zero dialogs, 7.75 s, and the user's foreground window never changed**.
## 0.1.28 — 2026-09-01
- **Four design-rule hygiene checks**, all from the rules the board read already carries:
- `disabled_design_rules` — a rule that exists but is switched OFF. Worse than a missing rule: the board LOOKS ruled, DRC passes, and the constraint was never applied. Raised to error for Clearance / Width / HoleSize / ShortCircuit.
- `missing_critical_rules` (error) — no ENABLED rule of a kind a fabricator relies on.
- `duplicate_rule_priority` (info) — two enabled rules of one kind sharing a priority. Altium resolves the tie internally, so which one wins is not readable from the board; usually a copy-paste never re-prioritised.
- `components_without_footprint` (warning) — a placed part with an empty footprint pattern name, so it cannot be matched back to a library or checked for updates.
- Rule kinds are named from the ordinal table read off a live default rule set; an unknown ordinal renders as `kind <n>` rather than being invented.
- 14 checks.
## 0.1.27 — 2026-09-01
- **`review_board` switches documents and returns PARTIAL results.** Altium holds one current document, so a board read and a sheet read cannot both be "now" — with a schematic focused, the previous version failed the whole review because `read_board` had nothing current. It now takes optional `board` and `sheet` paths and activates each in turn (like `cross_check`), and when one document cannot be read it still runs the checks that CAN run.
- **`skipped` names every check that did not run and why** — "no board read", "no schematic read — pass includeSchematic:true". A review that silently drops half its checks reads exactly like a clean board, which is the failure mode worth engineering against.
## 0.1.26 — 2026-09-01
- **The Adom house rule is now a check.** `vendor_part_numbers` (warning) flags any purchased component missing Mouser / DigiKey / LCSC part numbers — Mouser named first because it is the preferred vendor, so if only one gets filled in, that is the one. Test points, fiducials and mounting holes are exempt: they are not purchased parts, and flagging them would train people to ignore the check.
- `missing_datasheets` (info) and `schematic_placeholder_values` (error) — the latter catches default Comments at the SOURCE, where the BOM is actually generated from, rather than only on placed parts.
- **The schematic read now carries component parameters** (`params: "1"`), using the same `MkSet(eParameter)` iterator the parameter WRITER has always used — so three checks arrived with no new Altium API surface. Capped at 40 parameters per component.
- The three schematic checks return nothing unless `includeSchematic: true`, since without a sheet read they have nothing to judge.
- 10 checks.
## 0.1.25 — 2026-09-01
- **`checks:[...]` now filters on the ID you actually see.** The selector was derived from the Python function name (`_chk_components_outside_board`) while findings report `components_outside_board_outline`, so filtering by the visible name silently ran ZERO checks and returned a clean review. Each check now declares its own ID, the reply lists `availableChecks`, and an unrecognised name comes back as `unknownChecks` instead of being ignored.
- **New verb `altium_read_board`** — the raw read (components, nets, layers, copper, rules, occupancy, outline) in one round trip. It existed only in the Rust bridge, which meant nobody could see what a check saw; a review's silence had to be taken on trust. Now it can be inspected directly.
## 0.1.24 — 2026-09-01
- **`read_board` gains an `outline` section** — the board extent, read through the SAME guarded pattern the library reader already uses, because a library's internal board has a nil `BoardOutline` and touching `BoundingRectangle` on it halts the engine (measured 2026-08-30). It reports which source it used: `boardOutline` (the real outline's bounding box) or `boardBounds` (everything on the board, including strays outside it).
- **Two review checks unlocked by it**, both named for what they actually measure:
- `components_outside_board_outline` (error) — a part outside the board's bounding box.
- `components_near_board_edge` (warning) — a component ORIGIN within a margin of the edge (default 20 mil). Not called `pads_near_board_edge`: per-pad geometry is not in the board read, and the catalog's name would claim a precision this does not have.
- **Both go silent when the extent came from `boardBounds`.** That fallback includes stray primitives outside the real outline, so using it to declare a part "off-board" would produce confident false errors. A weaker source yields no finding rather than a wrong one.
- `notImplemented` now names the bounding-box limitation explicitly: on an L-shaped or slotted board, a part inside the box but outside the real outline is NOT caught.
## 0.1.23 — 2026-09-01
- **`altium_review_board`: design review over ONE board read.** Phase 2 of the plan, from the lint catalog. Five checks, all pure functions over data the bridge already reads — so adding them cost no new Altium API surface, which is the whole point: a wrong identifier in the agent halts the script engine behind a modal on the user's screen.
- `placeholder_values` (error) — a component still carrying Altium's default Comment. It reaches the BOM as "Comment".
- `duplicate_designators` (error) — the same designator twice on a sheet or board.
- `single_pin_nets` (warning) — a net with one pad connects to nothing; usually a typo'd label.
- `unused_mechanical_layers` (info) — an enabled mechanical layer carrying nothing. It travels into fab outputs and is invisible in the editor precisely because it is empty.
- `off_grid_components` (info) — a placed part off the grid (configurable; a metric board legitimately fails a 5 mil test).
- **A clean result is not claimed to be a complete one.** The reply carries `notImplemented` naming the checks this review cannot see yet and why — board outline, ratsnest, silkscreen geometry, via topology, decoupling. Silence is not a pass.
- Findings name the exact designator, net or layer and say what is wrong; never "verify X". Sorted error → warning → info, with counts.
## 0.1.22 — 2026-09-01
- **`altium_cross_check` reads the two documents in TURN, because Altium cannot hold both current.** `PCBServer.GetCurrentPCBBoard` follows the FOCUSED document, so focusing the schematic clears the current board — the first cut of this verb read the sheet, then failed on the board every time. It now activates the board, reads it, activates the sheet, reads it, and reports the document the user had in front of them. Takes `board` and `sheet` paths, or uses what is already open.
- The two caveats still travel with the answer: one sheet only (a hierarchical parent holds sheet symbols, not parts), and a folder mismatch between sheet and board is reported as `sameProject: false` with a warning, because comparing two unrelated documents makes every difference meaningless.
## 0.1.21 — 2026-09-01
- **"File type not recognised." pre-empted at the cause.** The agent opened every non-project file with `ObjectKind='Document'`, which Altium rejects with a modal on the user's screen even for a valid .PcbDoc. It now maps the extension to the kind Altium actually uses (PCB, SCH, PCBLIB, SCHLIB, OUTPUTJOB, PCB3DLIB, TEXT, Project) and opens through `Client.OpenDocument` — the sibling of `ShowDocument`, which this unit already relied on — falling back to the process lane only if that raises.
- **The bridge refuses unsupported types before Altium can complain.** An extension outside the openable set returns `unsupported_document` with the supported list, instead of handing Altium something it will answer with a dialog.
- **A slow open is no longer reported as a failure.** ab gives a verb 60 s; a cold document open can exceed it. `open_document` and `activate_document` now wait just under the budget and return `status: "in_progress"` with a poll hint, rather than timing out while Altium is still working.
## 0.1.20 — 2026-09-01
- **`altium_read_schematic` and `altium_cross_check` in the shipped runtime** (they existed only in the Rust bridge). `cross_check` compares the current sheet against the current board — what is drawn but not placed, and placed but not drawn — which neither read answers alone. Two caveats travel WITH the answer because both make it wrong to act on: the schematic side is ONE sheet (a hierarchical parent holds sheet symbols, not parts), and Altium's SchServer and PCBServer each hold their own "current document", so the two can belong to different projects — detected by comparing folders and reported as `sameProject: false` with a loud warning.
- **Manifest drift fixed, in both directions.** `bridge.json` declared three verbs with no handler (`close_document`, `export_pdf`, `screenshot_all`) — ab routed them and the bridge answered "Unknown command" — and omitted three that work (`ensure_licensed`, `license_status`, `setup_trial`), leaving them unreachable through ab. The phantoms are gone (they never worked; implementing them is separate work) and the three real ones are now routable.
- **The release refuses to build on drift.** `scripts/release.py` lints `bridge.json` against what `server.py` can actually dispatch and fails with both lists rather than shipping a manifest that lies. 106 verbs declared and dispatchable.
## 0.1.19 — 2026-09-01
- **Pre-emption, properly.** Two changes so a script modal cannot reach the user:
1. **A script lane now refuses when ANY resident loop is alive, in ANY workspace** — not just the current instance's. That gap is exactly how "Another script executing now" got on screen after per-instance workspaces landed: the pre-upgrade loop was still resident under the legacy path, `is_alive()` looked only at `agent/<key>`, saw nothing, and fired a bootstrap into an engine that was already running a script. The refusal names the workspace and heartbeat age so the fix is obvious.
2. **A watcher runs FOR THE DURATION of every `-R` lane** and presses OK the instant one of our own modals appears, so it exists for a few hundred milliseconds instead of sitting on screen until someone goes looking. Cleared modals come back as `modalsPreempted` rather than being hidden. The watcher only ever touches modals this bridge CAUSES (script-busy, another-instance, scripting-system); a save prompt or a licence question is the user's and is never pressed.
- Detection was always the weaker half: the engine is single-threaded, so the modal it raises is also what freezes it — which is precisely why watching during the operation works.
## 0.1.18 — 2026-09-01
- **Instances.** More than one Altium can run at once — John runs Altium Designer Develop beside Altium Designer so two AI threads can drive both — and the bridge assumed one in every place it mattered. New verb `altium_instances` lists each running Altium with the pid/key to address it; every other verb takes an optional `instance` (a pid, a key like `ADDevelop`, or a word from the edition/title). One Altium running: omit it, nothing changes. Several: verbs REFUSE with `ambiguous_instance` rather than guess, because guessing means acting inside the other thread's Altium.
- **Each instance gets its own agent workspace** (`bridges-cache/altium/agent/<key>`), keyed by install folder so it survives a restart. Two resident loops sharing one `request.txt` is the failure that does not announce itself: whichever loop notices first eats the other's request and answers it from the wrong board.
- **The -R lane spawns the selected instance's own exe**, not a hardcoded AD26 path — Designer and Designer Develop live in different install folders.
- **Dialog scanning is scoped to the selected instance**, so one thread can no longer see, or `dismiss_dialogs` clear, the other thread's modal.
- **The stray-helper reaper can no longer kill a real Altium.** It now requires `-R…ScriptingSystem` in the process's own command line (ab reports it). The old rule — "any X2 that is not the main window's process" — killed John's Develop instance on 2026-09-01 while it was hosting another thread's agent.
- 97 verbs.
## 0.1.17 — 2026-09-01
- **New verb `altium_layers_in_use`: how many layers the board ACTUALLY has.** Until now the only layer answer was "48", which is every copper layer Altium CAN address, not a property of the board. Altium's in-stack flag has no identifier proven on AD26 and a wrong guess halts the engine behind a modal, so the agent's new `layer_occupancy` counts primitives by layer in ONE pass (using only identifiers already shipped in this unit) and the bridge filters that tally to the copper layers. Verified live: LAN7800_test reports **1 copper layer in use (Top Layer)**, 7 layers carrying primitives, against 48 addressable.
- Read the result as a FLOOR: an inner plane carrying no primitives is invisible to a primitive count. The reply says so, and `copperLayersAddressable` is labelled as a capability rather than a board property.
- `read_board` gains the `occupancy` section (default on).
- The first cut of this view forgot to filter to copper and cheerfully reported a MECHANICAL layer (296 primitives) as the board's busiest layer. Now covered by a unit test against that exact shape.
- 96 verbs.
## Unreleased — bridge-rs only (no change to the shipped Python runtime)
- **`check_dialogs` no longer sees other applications' windows.** The first cut matched window TITLES containing "Error"/"Warning" and picked up four *"RemoteApp security warning"* windows on John's desktop that had nothing to do with Altium — which `dismiss_dialogs` would then have clicked OK on. Pressing buttons in someone else's application is exactly what this bridge must never do. Now scoped by PID: `process_list` for Altium's pids, `desktop_enum_windows {pids}` for only those windows, then class + "not the main window". Also 300× faster (one enumeration instead of six title searches): **1 ms**.
- Note for the Python bridge: it was never affected — `handlers/dialogs.py` has always attributed dialogs by pid.
## 0.1.16 — 2026-09-01
- **Renamed `dump_board` → `read_board`** (agent command and Rust verb), before anything depended on it. John, on the 0.1.15 naming: *"that's not a great verb name ... we should probably pick a better verb thats more like a read* or write*"*. He is right — "dump" names the implementation rather than the caller's intent, and it has no counterpart, so it breaks the symmetry the moment writes arrive. The convention is now recorded in the dev skill and the Rust README: **`read_*` to get data out, `write_*` / `apply_*` to put it back.** Rust module `dump.rs` → `board.rs`, `BoardDump` → `BoardData`, fixture `read_board.json`.
- For the record, since it came up: the name was mine, not inherited — no verb names were copied from any third-party Altium MCP server.
## 0.1.15 — 2026-09-01
- Agent gains `dump_board`: components + nets + layers + copper + rules in ONE round trip, composed from the existing readers so it adds no new Altium API surface (and therefore cannot introduce an engine-halting identifier). This is the primitive the Rust bridge shapes its views from; the Python verbs are unchanged.
- Rust bridge (`bridge-rs/`): **cold bootstrap through the `-R` lane proven live — 1.59 s, zero dialogs**, which was the last unverified path. Dump-derived views land with it (`board_summary` 232 ms, `rules_by_kind` 152 ms), plus `fixtures/dump_board.json` — a recorded live dump that lets `cargo test` exercise all the shaping logic with no Altium, no Windows and no agent. 8 tests; they already caught a dispatch bug and one wrong assumption about the Fanout rule group.
- `rule_kind_name()`: Altium emits `TRuleKind` as a bare ordinal; 31 ordinals are now mapped to names read off a live default rule set. Unknown ordinals render as `kind <n>` rather than being invented, and a test fails loudly if a board carries one we do not know.
## 0.1.14 — 2026-09-01
- `altium_activate_document` re-implemented over the AGENT instead of UIA. 0.1.13 shipped it as a UIA document-bar click; measured on a live box that call takes **>100 s** (ab's budget is 60 s) because Altium's main-window UIA tree is huge. The agent runs IN-PROCESS: ~4 s, no cursor, no OS foreground steal, and Altium activates an already-open document rather than reloading it. Verified: title switched to the target .PcbDoc and board verbs answered in 0.2 s.
- altium-uia skill corrected accordingly — the rule has two halves: **UIA for dialogs and WebView pages** (small trees, fast), **the resident agent for anything inside the application**. `desktop_click` only when neither can reach it.
## 0.1.13 — 2026-09-01
- **UIA first, cursor last.** John, watching the bridge drive his box: *"you keep turning on the cursor, but you could maybe use UIA to do those clicks"*. He was right — `altium_license` had been clicking hardcoded pixel coordinates since 0.1.x. Altium DOES expose a UIA tree, including the WebView2 License Management page (`automationId: UseLicenseButton`, invokable). The licence lane now invokes programmatically (no foreground, no cursor movement) and falls back to the image-space click only on a genuine UIA miss; the reply says which lane ran (`lane`, `windowEtiquette`).
- **New verb `altium_activate_document {name}`** — every open document is an invokable button in Altium's document bar (`GUIBinderView.TGUIDocBarButton`, accessible name = the tab caption). Making a .PcbDoc current no longer needs `open_document` (which raises Altium's window) or a pixel click on the tab. 95 verbs.
- New DEVELOPER skill **altium-uia**: the measured catalog of what AD26.9.1 exposes to UI Automation (dialogs, main menus, title bar, document bar, licence page) plus what it does NOT (the canvas — that is the agent's scripting API), the two costs (UIA searches over Altium's tree are slow; nothing reaches an app while a higher-integrity window such as a screensaver owns the foreground), and the honest list of what still legitimately foregrounds.
- Diagnosed the "Could not start Vault Explorer" warning users see on every launch: the extension is correctly registered (`ExtensionsRegistry.xml`) with a complete payload, and Altium's private .NET 8.0.15 runtime loads fine, so it is neither a broken registry nor a runtime mismatch. Investigation continues.
## 0.1.12 — 2026-08-31
- **14× faster agent round trip: 450–600 ms → 31–47 ms.** Root cause: `Break` inside a `Try..Except` in this script engine is implemented as an exception, so the `Except` arm swallowed it — every file write ran all 20 retry iterations (`Sleep(10)` each, ~220–310 ms) even though the FIRST write had already succeeded. Both write primitives now use a `Done` flag with a `While` guard instead. Verified across the whole template: no other `Break`-inside-`Try` site exists.
- `hb.txt` keeps tmp+rename (readers hold it open, and a direct rewrite raced them into `EFCreateError`); `response_<id>.json` writes direct — one file op instead of save+delete+rename.
- Responses carry `agentPhases` — `{readMs, dispatchMs, detectTick, sleepMs, hbSaveMs, hbSaveTries, hbSaveExcept, directMs, directTries, directExcept}`. `*Tries: 1, *Except: 0` is the healthy shape; anything higher means the box is contending for the file and the retry loop is earning its keep.
- New agent-side `io_bench` command (not a bridge verb): times SaveToFile new/overwrite/delete against the workspace, %TEMP% and Documents, plus rename/Sleep(1)/loop cost. This is what localized the stall — keep it for the next latency regression.
- Measured after the fix (LAN7800_test.PcbDoc, live): list_designators 44 ms, list_nets 57 ms, read_rules 68 ms (35 rules), layers 47 ms (28), copper_layers 67 ms (48), component_data 87 ms — agent round trip, dialogs 0.
## 0.1.11 — 2026-08-31
- Phase 1 of the parity plan: 11 read-only board/schematic introspection verbs — list_designators, component_data, component_pins, list_nets, net_connections, layers, copper_layers, schematic_data (pins + part IDs), read_rules, set_layer_visibility, footprint_primitives. All verified live on Bluetooth Sentinel (16–350 ms), zero dialogs.
- Busy-notice reaper rewritten to go through ab's desktop verbs (find_window → find_control → ui_click) with a 4 s watch, after the X2-pid scan missed the notice twice on a live box.
- Parked: create_net_class (IPCB_ObjectClass.SuperClass/.Name are undeclared in AD26 scripting) — function kept, not dispatched.
- Last Python release of the bridge process: the next major moves the bridge to a Rust exe that calls ab's verbs and shrinks the DelphiScript agent to generic dump/apply primitives (ab#987 requests the binary runtime kind).
## 0.1.10 — 2026-08-31
- #3 (Colby) root-cause FIX: the agent registers the PCB/SCH scripting APIs at startup (`ServersUp`: `Client.StartServer('PCB')`/`('SCH')` before the first `PCBServer` reference) — on a fresh session that never opened a PCB document the API unit is unregistered and the old template halted with an "Undeclared identifier: PostProcess" modal on the user's screen.
- #3 follow-up: `altium_agent_recover` now closes the scratch `AdomAgent_<digest>.PrjScr` out of the Projects panel after freeing the engine (only there — after a successful start the resident loop runs from it).
## 0.1.9 — 2026-08-31
- #2/PR #64 (Colby): detect the ADDevelop build — _TITLE_RE now accepts an optional edition word (Develop/Viewer/…) and optional version parens, with a guard so a bare mid-string "Altium Designer" in a dialog cannot pose as the main window. Proven against six title shapes.
- #3 (Colby, partial): the -R helpers now reap Altium's single-instance "another instance is busy" notice in the background (both RunScript/RunScriptText lanes and recover), so a stray helper never leaves that modal on the user's screen.
- Repo now carries the FULL source tree (handlers/, agent/, extension/, tools/) — previously only src/server.py was pushed, forcing PRs against the release zip.
## 0.1.8 — 2026-08-31
- FIX: bridge.json verbs[] must be plain strings — 0.1.5–0.1.7 shipped four entries as objects, which ab's bridge_registry could not parse (the bridge vanished from the registry after its next reap while bridge_install still reported ok). Release assets are now uploaded explicitly (release create --binary-path does not attach).
## 0.1.7 — 2026-08-31
- altium_install_library_bundle: the no-AI web-control lane (kicad-parity envelope, zip-slip hardened, restart contract, proof frame). Acceptance test: docs/webcontrol-altium-test.html.
## 0.1.6 — 2026-08-30
- Full skill slate (10): added altium-install (the install recipe, finally packed), altium-libraries, altium-boards, altium-outputs, altium-bridge-dev, altium-bridge-publish — matching the kicad-bridge fleet depth.
## 0.1.5 — 2026-08-30
- Barrett consolidation complete: cleanup_libs, set_visibility, xcopy_footprint (cross-library), and import_part (his fat verb on the resident lane) — full verb parity with barrett-land/adom-desktop-altium-bridge.
- Skillpack: the pkg now ships altium-part-import (his 3-path catalog-part workflow, rewritten on these verbs, with the tri-vendor rule: Mouser preferred + DigiKey + LCSC part numbers on every component) and altium-kicad-import (his measured KiCad-10 findings + the vendored v10→v9 downgrader + the native no-importer lane).
- Tools: kicad_v10_to_v9.py vendored from barrett-land/kicad10-to-altium.
## 0.1.4 — 2026-08-30
- Barrett-parity library manipulation: clone_symbol (all display modes, 555 primitives verified), rename_symbol, read_symbol (pins/params/modes read-back), copy_footprint (pads+bodies+drawing), rename_footprint, remove_footprint. Smoke-tested on the LAN7800 libraries with zero dialogs.
## 0.1.3 — 2026-08-30
- Symbol quality pass (John caught colliding 4-side names): tools/symbol_modes.py sizes bodies from measured pin-name lengths, std = CCW datasheet wrap (distinct from 2-column), die outline is a centred square skipped when it cannot fit; all ten modes frame-checked.
- set_params/add_models parameters are hidden metadata (visible ones piled over the symbol).
- New verb: altium_remove_symbol.
## 0.1.2 — 2026-08-30
- LAN7800 creative-import lane: build_footprint (spec from tools/kicad2altium.py: via pads by duplicate designator, roundrect %, named mech layers incl. Adom.SolderBalls), build_symbol_modes (10 display modes on one component) + symbol_mode (editor-driven), add_models (N footprint models + params), normalize_bodies, test_board, board_shape (PCB:PlaceBoardOutline), swap_footprint (nets kept by designator), pcb_report with pads/bodies/nets/tracks.
- Dialog pre-emption: ReadAll probes with FileOpen (the debugger breaks on any exception), TxClear on agent start / before save / after recover (dangling PreProcess = "a command is currently active").
- Library canvas offset understood: bodies added through the canvas realign onto the pads automatically.
## 0.1.0 — 2026-08-30
- Schematic placement fixed: place_on_sch copies each lib primitive onto a fresh sheet component (Replicate alone drops pins/body); proven on the CH340G (17 primitives render).
- 3D pipeline fixed end-to-end: bodies persist (added via the PcbLib editor CANVAS with the footprint current), STEP + extruded both render on a real board; place_on_pcb builds a fresh eComponentObject, copies children BEFORE the board add, normalizes the pad cluster and each body to the target separately. Ray's CH340G STEP renders in 3D on a board.
- New diagnostics: altium_pcb_report (per-component pads/bodies with layer+bbox, board census by ObjectId), place/agent stage breadcrumbs.
- Agent lifecycle: agent_start now detects a STALE resident generation (gen stamp in the heartbeat) and auto-restarts it — redeployed agent code can no longer silently not load.
## 0.1.0 — 2026-08-29
- Barrett merge: set_params (LCSC/MPN + footprint link), add_body (STEP/extruded), install_library, compile_intlib, extract_intlib; vendored AdomBridge extension (export_step/import_kicad — not loading on AD 26.9.1 yet).
- Taskbar: Adom overlay badge while driving, measured progress bar on slow verbs, no AUMID stamping.
- Phase 5: signin_status, license (WebView2 page driver), connect_workspace, a365_configure/status/query.
- First cut (phases 0-4): readiness/status/diagnostics, resident DelphiScript agent (eval, run_command, state, open), dialogs by PID with screenshots, agent_recover, the eight show_* surfaces + demo, compile/ERC with Messages panel, DRC with violations, OutJob runner (Gerber/ODB++/NC drill/BOM proven on the Bluetooth Sentinel example), create_footprint / create_symbol / save.