← Commit history

v0.1.34: force-close guard fires on intent; instances reads Develop licence

John Lauer ·d8056778c7 ·1mo ago ·parent 6934e55
8 files changed +33−8
CHANGELOG.md+4
@@ -1,5 +1,9 @@ # Changelog +## 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.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.33",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.33/adom-bridge-altium-v0.1.33.zip",-  "sha256": "7c53c2a5f79036f65f431145af27a412965e4a4c2384ca8e0a14d217ab22db00",-  "size": 132257,+  "version": "0.1.34",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.34/adom-bridge-altium-v0.1.34.zip",+  "sha256": "3dc30ed5d47d1a9dd95dda049ab0e3aa3884c11a938d9f79068a56e57ec6e613",+  "size": 132603,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.34.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.33",+  "version": "0.1.34",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.33",+  "version": "0.1.34",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.33+0.1.34
src/handlers/altium_detect.py+7−1
@@ -322,13 +322,19 @@ def instances() -> list[dict]:         seen.add(w["pid"])         exe = _exe_of_pid(w["pid"])         st = w.get("licenceState")+        licensed = (st.lower() not in ("no license", "no licence", "viewer")) if st else None+        if st is None and (w.get("edition") == "Develop"):+            # Develop writes NO licence tail when it HOLDS the seat (only "- Viewer" when it does+            # not) — reading the absence as "unknown" reported ADD unlicensed for a whole test while+            # its agent imported five parts (2026-09-01).+            licensed, st = True, "Licensed (Develop reports no licence tail)"         out.append({             "pid": w["pid"],             "key": _key_for(exe, w["pid"]),             "edition": w.get("edition") or "Designer",             "adVersion": w.get("adVersion"),             "licenceState": st,-            "licensed": (st.lower() not in ("no license", "no licence", "viewer")) if st else None,+            "licensed": licensed,             "exe": exe,             "hwnd": w["hwnd"],             "document": w.get("document"),
src/server.py+15
@@ -234,6 +234,21 @@ def h_close(args):     lic = altium_detect.licence_state()     if not lic.get("running"):         return {"success": True, "wasRunning": False}+    # The INTENT to force is the risk, not whether a dialog happens to block: force:true KILLS the+    # process, which strands the On-Demand seat (the licence page then shows Seats 0/1 while every+    # claim fails with "License maximum usage reached"). Refuse up front — a polite close releases+    # the seat and is almost always what the caller actually wants.+    if args.get("force") and not args.get("acceptSeatLoss") and lic.get("licensed") is not False:+        return _seat_hints({"success": False, "errorCode": "would_strand_licence_seat",+            "licenceState": lic.get("state"),+            "error": "force:true kills Altium and STRANDS its licence seat — no other Altium will be "+                     "able to claim it afterwards, and the licence page will report it as free while "+                     "refusing every claim.",+            "_hint": "Try altium_close with no force first: a polite WM_CLOSE releases the seat. If it "+                     "reports blocked_by_dialog, READ the dialog (an Unsaved Changes prompt names the "+                     "documents — on a shared machine one may belong to another AI thread) and answer "+                     "it. Only if you truly must kill: repeat with acceptSeatLoss:true."},+            lic.get("state"), len(altium_detect.instances()))     if agent.is_alive():         agent.stop()     # polite first: WM_CLOSE on the main window (prompts for unsaved docs → dialogs), then taskkill