← Commit history
dist/linux/adom-bridge
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {
   "slug": "adom-bridge",
-  "version": "2.1.160",
+  "version": "2.1.161",
   "type": "app",
   "brief": "Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-only permission system keeps you in charge of every side-effecting command.",
   "description": "Install this package in a cloud or Hydrogen Desktop container, not on the PC. It delivers two things: the Linux adom-bridge-cli (linked onto PATH) and the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation). The CLI drives the Adom Bridge app running on your Windows PC over the relay; the Windows app itself is the separate installer in the Download box, not part of this package. Run adom-wiki pkg update to keep the CLI and skills current.",
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-bridge",-  "version": "2.1.160",+  "version": "2.1.161",   "type": "app",   "description": "Install this package in a cloud or Hydrogen Desktop container, not on the PC. It delivers two things: the Linux adom-bridge-cli (linked onto PATH) and the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation). The CLI drives the Adom Bridge app running on your Windows PC over the relay; the Windows app itself is the separate installer in the Download box, not part of this package. Run adom-wiki pkg update to keep the CLI and skills current.",   "keywords": [
skills/adom-bridge-bridges-guide/SKILL.md+1−1
@@ -209,7 +209,7 @@ A verb routed to a bridge that is not up never has to end in a bare timeout. ab 
 - `errorCode:"bridge_installing_deps"`, `stillRunning:true`, `installElapsedSec` - a source-only node bridge's ONE-TIME detached `npm install` is running (minutes on first use). Wait and re-issue the verb; do not restart anything.
 - `errorCode:"bridge_spawn_failed"`, `stillRunning:false`, `lastSpawnError`, `failedAttempts`, `retryBackoffRemainingMs` - the spawn (or its dependency install) failed and ab is in backoff, so it answers immediately instead of re-attempting on every call. `lastSpawnError` is the cause; for an npm failure the real `npm error` lines are in it and the FULL npm output is in `bridge_log_read {"name":"<bridge>"}` -> `npmLog` (also `~/.adom/bridge-logs/<bridge>.npm.log`). Fix the cause, then `bridge_resume {"name":"<bridge>"}` clears the backoff and the next verb retries.
-- `errorCode:"blocked_by_windows_app_control"`, `blockedFile`, `smartAppControl` (`on` | `evaluation` | `off` | `unknown`) - Windows refused to START the bridge's file (2.1.160, wiki #219). On Windows 11 this is almost always Smart App Control blocking an exe that is not code-signed; the user sees a Windows Security notice "Part of this app has been blocked" naming the exe. It is not a crash and retrying cannot help, so ab stops trying (every attempt would show that notice again) and answers this at once. The fix is a signed build from the bridge's author: ab spawns it again by itself as soon as a new file lands, or after `bridge_resume {"name":"<bridge>"}`. `bridge_list` shows the same fields on the bridge's row. Never advise turning Smart App Control off: Windows cannot turn it back on without a reinstall.
+- `errorCode:"blocked_by_windows_app_control"`, `blockedFile`, `smartAppControl` (`on` | `evaluation` | `off` | `unknown`) - Windows refused to START the bridge's file (2.1.160, wiki #219). On Windows 11 this is almost always Smart App Control blocking an exe that is not code-signed; the user sees a Windows Security notice "Part of this app has been blocked" naming the exe. It is not a crash and retrying cannot help, so ab stops trying (every attempt would show that notice again) and answers this at once. The fix is a signed build from the bridge's author: ab spawns it again by itself as soon as a new file lands, or after `bridge_resume {"name":"<bridge>"}`. `bridge_list` shows the same fields on the bridge's row. Never advise turning Smart App Control off: on many Windows 11 versions it cannot be switched back on without reinstalling Windows.
 
 `bridge_detail` / `bridge_list` carry the same fields (`lastSpawnError`, `failedAttempts`, `retryBackoffRemainingMs`, `depsInstallingSec`) so you can tell "never tried" from "tried and died" in one read.
 
skills/adom-bridge-runtimes/SKILL.md+152−148
@@ -1,148 +1,152 @@-----name: adom-bridge-runtimes-description: >-  Managed Node and Python runtimes for Adom Bridge bridges. Use this skill when a-  node or python bridge fails to spawn, when you need to check runtime status with the-  runtimes verb, when a bridge needs Node.js or Python and the box may not have one,-  when npm dependencies for a source-only node bridge are missing, or when you want to-  understand the no-UAC portable node/python install, the prewarm on first launch, the-  Runtimes panel in the GUI, or the loopback-only bind rule for bridge processes.------# Managed runtimes: how ab provisions Node and Python for bridges--Adom Bridge owns a global, portable, no-UAC Node and Python that every bridge-shares. A bridge never installs its own interpreter and never pops a UAC or-firewall prompt. Core logic lives in `src-tauri/src/runtime_cache.rs`.--## What the managed runtimes are--Resolution order for `ensure_runtime(app, Node|Python)`, race-safe via a-per-kind install lock (two bridges asking at once never double-download):--1. Already `Ready` in the in-memory state map: return the exe.-2. A **system runtime on PATH** is used as-is (`source: system`), no download.-3. A prior **portable copy** under `~/.adom/adom-runtimes/<kind>-<ver>/`-   (`source: cache`).-4. **Download a portable copy** (Windows): stream-download, extract, atomic-   rename into the runtime dir, emitting `runtime-status` progress events.--Pinned portable versions (one place to bump, in `runtime_cache.rs`):--- Node 22.11.0 (official nodejs.org win-x64 zip)-- Python 3.12.13 (astral-sh python-build-standalone `install_only` tar.gz,-  full CPython with `venv` and `pip`)--The two hard guarantees:--1. **Never a UAC prompt.** The portable runtimes are a zip extract (Node) and-   a tar.gz extract (Python) into `~/.adom/adom-runtimes/`. No MSI, no-   installer, no elevation, no winget.-2. **Never a firewall prompt by default.** ab passes-   `ADOM_BIND_HOST=127.0.0.1` to every bridge process, and bridges must bind-   it. A bare `server.listen(PORT)` binds `0.0.0.0` and triggers the Windows-   Firewall dialog; loopback binds never do.--One known trap the resolver already handles: the Windows Store `python.exe`-app-execution-alias stub (a fake python that prints an install message-instead of a version). `detect_system` accepts a found exe only if it reports-a parseable version; the stub falls through to the portable download.--## The `runtimes` verb (read + on-demand ensure)--```bash-adom-bridge runtimes-# v2.1.14+: kick a provision NOW instead of waiting for a spawn to need it --# useful before installing an exe-kind bridge whose scripts want Python/Node:-adom-bridge runtimes '{"action":"ensure","runtime":"python"}'-```--Available over the relay and the direct API. Returns a per-runtime snapshot:--- `state`: `absent | installing | ready | failed`-- `source`: `system | cache`-- `version`, plus `pct`/`phase` while a download is in flight--This is the machine-readable "what is installed and at which version" for-cloud AIs. The human-facing mirror is the **Runtimes panel** in the GUI's-Bridges sidebar: a state dot, version, and source per runtime, with live-updates from the `runtime-status` event.--## Prewarm on first launch--`runtime_cache::prewarm_all` fires about 3 seconds after boot on **every**-launch (standalone and embedded), gated by config `prewarm_runtimes`-(default true). It kicks off both `ensure_runtime` calls fire-and-forget:-a system or cached runtime resolves in milliseconds; only a runtime-less box-actually downloads. Turning `prewarm_runtimes` off means runtimes fetch-lazily on the first bridge spawn instead.--Because prewarm is async, a bridge can need Node or Python before it-finishes. The bridge auto-start path calls a non-blocking `runtime_gate`:--- `Ready` -> spawn now (the common case).-- `Warming` -> the verb returns a non-fatal `runtime_warming` result with-  `stillRunning: true` and `statusVerb: "runtimes"`. Poll and retry; this is-  not a failure.-- `Failed` -> a `runtime_failed` result; a retry re-attempts the install.--```bash-# A bridge verb returned runtime_warming? Poll until ready, then retry:-adom-bridge runtimes-# ... wait for state=ready on the needed runtime ...-adom-bridge browser_open_window '{"url": "https://example.com"}'-```--## npm installs for source-only node bridges (`ensure_node_modules`)--A node bridge's wiki zip rightly ships source-only, so its cache dir has no-`node_modules`. At spawn, `ensure_node_modules` resolves dependencies in-this order:--1. The cache dir already has `node_modules`: use it, nothing to do.-2. A **bundled seed** of the same bridge name has `node_modules` AND the-   seed's `bridge.json` version is `>=` the cache version: reuse the seed's-   modules via `NODE_PATH` for the spawn. Instant, no copy, and the seed's-   native bindings are already built for this platform. The version gate is-   the guardrail: a cache strictly newer than the seed may have different-   deps, so stale bundled modules are never reused.-3. Otherwise run `npm install --include=optional` in the cache dir.--npm runs through the **managed runtime's own npm**-(`node <node_modules/npm/bin/npm-cli.js>`), because a portable Node is not-on PATH so a bare `npm` would fail. Bare npm is used only for a system Node.--## When a node or python bridge fails to spawn: what to check--1. **Runtime state first.**-   ```bash-   adom-bridge runtimes-   ```-   - `state: installing`: the portable download is in flight. Poll and retry-     the bridge verb; do not treat `runtime_warming` as an error.-   - `state: failed`: retry the bridge verb (a retry re-attempts the-     install), or check network reachability to nodejs.org / the-     python-build-standalone release host.-   - `state: absent` with `prewarm_runtimes` off: the first bridge spawn-     will fetch lazily; expect a one-time delay.-2. **MODULE_NOT_FOUND from a node bridge** means its dependencies did not-   resolve: verify `ensure_node_modules` ran (cache `node_modules` present,-   or a same-or-newer bundled seed exists), or that the npm install in the-   cache dir succeeded.-3. **A Windows Firewall prompt appeared**: the bridge bound `0.0.0.0`-   instead of honoring `ADOM_BIND_HOST`. That is a bridge bug; bridges must-   bind `process.env.ADOM_BIND_HOST || '127.0.0.1'` (Node) or-   `os.environ.get('ADOM_BIND_HOST', '127.0.0.1')` (Python).-4. **Python shows a strange version or none**: on old ab builds the Windows-   Store python stub could be pinned as a dead system runtime; current-   builds reject it and download the portable Python instead.--## Bridge author contract (summary)--1. Bind `ADOM_BIND_HOST` (loopback), never `0.0.0.0`.-2. Do not bootstrap your own Node or Python. ab provisions the interpreter-   and spawns your entrypoint by absolute path, with no UAC. Shipping a-   source-only node bridge is fine; ab runs npm for you at spawn.--Dev internals and the do-not-regress rules: `skills/MANAGED_RUNTIMES.md`.-Launch-order timeline of every background prewarm task: `skills/PREWARM.md`.+---
+name: adom-bridge-runtimes
+description: >
+  Managed Node and Python runtimes for Adom Bridge bridges. Use this skill when a
+  node or python bridge fails to spawn, when you need to check runtime status with the
+  runtimes verb, when a bridge needs Node.js or Python and the box may not have one,
+  when npm dependencies for a source-only node bridge are missing, or when you want to
+  understand the no-UAC portable node/python install, the prewarm on first launch, the
+  Runtimes panel in the GUI, or the loopback-only bind rule for bridge processes.
+---
+
+# Managed runtimes: how ab provisions Node and Python for bridges
+
+Adom Bridge owns a global, portable, no-UAC Node and Python that every bridge
+shares. A bridge never installs its own interpreter and never pops a UAC or
+firewall prompt. Core logic lives in `src-tauri/src/runtime_cache.rs`.
+
+## What the managed runtimes are
+
+Resolution order for `ensure_runtime(app, Node|Python)`, race-safe via a
+per-kind install lock (two bridges asking at once never double-download):
+
+1. Already `Ready` in the in-memory state map: return the exe.
+2. A **system runtime on PATH** is used as-is (`source: system`), no download.
+3. A prior **portable copy** under `~/.adom/adom-runtimes/<kind>-<ver>/`
+   (`source: cache`).
+4. **Download a portable copy** (Windows): stream-download, extract, atomic
+   rename into the runtime dir, emitting `runtime-status` progress events.
+
+Pinned portable versions (one place to bump, in `runtime_cache.rs`):
+
+- Node 22.11.0 (official nodejs.org win-x64 zip)
+- Python 3.13.15 (the official python.org build as published on nuget.org, a
+  plain zip: full CPython with `venv`, `pip` and `ensurepip`, no Tk). Since 2.1.161
+  its interpreter, DLLs and modules are signed by the Python Software Foundation, so
+  Windows Smart App Control does not block it. Run pip and tools as `python -m pip`
+  / `python -m <module>`: the `pip.exe`-style wrappers pip generates are unsigned in
+  every Python distribution and can be blocked.
+
+The two hard guarantees:
+
+1. **Never a UAC prompt.** The portable runtimes are a zip extract (Node) and
+   a tar.gz extract (Python) into `~/.adom/adom-runtimes/`. No MSI, no
+   installer, no elevation, no winget.
+2. **Never a firewall prompt by default.** ab passes
+   `ADOM_BIND_HOST=127.0.0.1` to every bridge process, and bridges must bind
+   it. A bare `server.listen(PORT)` binds `0.0.0.0` and triggers the Windows
+   Firewall dialog; loopback binds never do.
+
+One known trap the resolver already handles: the Windows Store `python.exe`
+app-execution-alias stub (a fake python that prints an install message
+instead of a version). `detect_system` accepts a found exe only if it reports
+a parseable version; the stub falls through to the portable download.
+
+## The `runtimes` verb (read + on-demand ensure)
+
+```bash
+adom-bridge runtimes
+# v2.1.14+: kick a provision NOW instead of waiting for a spawn to need it -
+# useful before installing an exe-kind bridge whose scripts want Python/Node:
+adom-bridge runtimes '{"action":"ensure","runtime":"python"}'
+```
+
+Available over the relay and the direct API. Returns a per-runtime snapshot:
+
+- `state`: `absent | installing | ready | failed`
+- `source`: `system | cache`
+- `version`, plus `pct`/`phase` while a download is in flight
+
+This is the machine-readable "what is installed and at which version" for
+cloud AIs. The human-facing mirror is the **Runtimes panel** in the GUI's
+Bridges sidebar: a state dot, version, and source per runtime, with live
+updates from the `runtime-status` event.
+
+## Prewarm on first launch
+
+`runtime_cache::prewarm_all` fires about 3 seconds after boot on **every**
+launch (standalone and embedded), gated by config `prewarm_runtimes`
+(default true). It kicks off both `ensure_runtime` calls fire-and-forget:
+a system or cached runtime resolves in milliseconds; only a runtime-less box
+actually downloads. Turning `prewarm_runtimes` off means runtimes fetch
+lazily on the first bridge spawn instead.
+
+Because prewarm is async, a bridge can need Node or Python before it
+finishes. The bridge auto-start path calls a non-blocking `runtime_gate`:
+
+- `Ready` -> spawn now (the common case).
+- `Warming` -> the verb returns a non-fatal `runtime_warming` result with
+  `stillRunning: true` and `statusVerb: "runtimes"`. Poll and retry; this is
+  not a failure.
+- `Failed` -> a `runtime_failed` result; a retry re-attempts the install.
+
+```bash
+# A bridge verb returned runtime_warming? Poll until ready, then retry:
+adom-bridge runtimes
+# ... wait for state=ready on the needed runtime ...
+adom-bridge browser_open_window '{"url": "https://example.com"}'
+```
+
+## npm installs for source-only node bridges (`ensure_node_modules`)
+
+A node bridge's wiki zip rightly ships source-only, so its cache dir has no
+`node_modules`. At spawn, `ensure_node_modules` resolves dependencies in
+this order:
+
+1. The cache dir already has `node_modules`: use it, nothing to do.
+2. A **bundled seed** of the same bridge name has `node_modules` AND the
+   seed's `bridge.json` version is `>=` the cache version: reuse the seed's
+   modules via `NODE_PATH` for the spawn. Instant, no copy, and the seed's
+   native bindings are already built for this platform. The version gate is
+   the guardrail: a cache strictly newer than the seed may have different
+   deps, so stale bundled modules are never reused.
+3. Otherwise run `npm install --include=optional` in the cache dir.
+
+npm runs through the **managed runtime's own npm**
+(`node <node_modules/npm/bin/npm-cli.js>`), because a portable Node is not
+on PATH so a bare `npm` would fail. Bare npm is used only for a system Node.
+
+## When a node or python bridge fails to spawn: what to check
+
+1. **Runtime state first.**
+   ```bash
+   adom-bridge runtimes
+   ```
+   - `state: installing`: the portable download is in flight. Poll and retry
+     the bridge verb; do not treat `runtime_warming` as an error.
+   - `state: failed`: retry the bridge verb (a retry re-attempts the
+     install), or check network reachability to nodejs.org / api.nuget.org
+     (where the official Python build is fetched from).
+   - `state: absent` with `prewarm_runtimes` off: the first bridge spawn
+     will fetch lazily; expect a one-time delay.
+2. **MODULE_NOT_FOUND from a node bridge** means its dependencies did not
+   resolve: verify `ensure_node_modules` ran (cache `node_modules` present,
+   or a same-or-newer bundled seed exists), or that the npm install in the
+   cache dir succeeded.
+3. **A Windows Firewall prompt appeared**: the bridge bound `0.0.0.0`
+   instead of honoring `ADOM_BIND_HOST`. That is a bridge bug; bridges must
+   bind `process.env.ADOM_BIND_HOST || '127.0.0.1'` (Node) or
+   `os.environ.get('ADOM_BIND_HOST', '127.0.0.1')` (Python).
+4. **Python shows a strange version or none**: on old ab builds the Windows
+   Store python stub could be pinned as a dead system runtime; current
+   builds reject it and download the portable Python instead.
+
+## Bridge author contract (summary)
+
+1. Bind `ADOM_BIND_HOST` (loopback), never `0.0.0.0`.
+2. Do not bootstrap your own Node or Python. ab provisions the interpreter
+   and spawns your entrypoint by absolute path, with no UAC. Shipping a
+   source-only node bridge is fine; ab runs npm for you at spawn.
+
+Dev internals and the do-not-regress rules: `skills/MANAGED_RUNTIMES.md`.
+Launch-order timeline of every background prewarm task: `skills/PREWARM.md`.
skills/adom-bridge-sdk-audit/SKILL.md+1
@@ -36,6 +36,7 @@ When a maintainer says *"audit your code, skills, README, and wiki page against - [ ] **`timeouts`** declared if ANY verb can run longer than 60s (a big export/render/cloud-open) — with a matching **`statusVerb`** to poll. Third-party bridges were 60s-capped before v1.9.79; this is how you raise it. *(→ bridge.json)*  **Artifacts & deps** (the two-artifact split — the #1 inconsistency) *(→ Where every file lives)*:+- [ ] **SIGNED: every Windows PE file in your zip (`.exe`, `.dll`, `.node`, `.pyd`) carries a timestamped signature from a trusted publisher.** Unzip your published release on Windows and run `Get-ChildItem -Recurse -Include *.exe,*.dll,*.node,*.pyd | Get-AuthenticodeSignature | Format-Table Status,Path`; every row must read `Valid`. An unsigned exe is blocked outright by Windows Smart App Control on many new PCs (ab answers `blocked_by_windows_app_control`), and you will never see it on a developer machine. Self-signed does not count. *(→ Sign everything you ship)* - [ ] Your Release `.zip` is a **Release asset** (`release upload`), NOT a loose `/files` blob — and old version zips are NOT piling up on `/files` (audit for stray `*-v*.zip`). - [ ] The **server's** runtime deps live in the **ZIP's** `package.json` (node) / `requirements.txt` (python). The **pkg's** `package.json` declares `dependencies:{"adom/adom-bridge":"^1.9.x"}` (so `pkg install` pulls the ab CLI + core skills) and does NOT list the server's npm deps. - [ ] A big runtime asset (Mesa, a browser) is a **`prewarm.asset`** or seed-bundled — NOT a loose `/files` blob.
skills/adom-bridge-sdk-publish/SKILL.md+23−1
@@ -29,6 +29,28 @@ This is the code ab streams onto the user's machine. Two files on your page: 
 **Users install/update** with ab's `bridge_install {manifestUrl}` → ab downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\Adom Bridge\bridges-cache\<name>\`, rescans, and remembers the URL for auto-update.
 
+### ⭐ REQUIRED for Windows binaries: SIGN everything you ship, or Windows will refuse to run it
+
+**Why.** On Windows 11 a feature called **Smart App Control** refuses to run a program that is not code-signed by a trusted publisher. The user sees a Windows Security notice, *"Part of this app has been blocked ... we can't confirm who published `<your exe>`"*, and your bridge simply never starts. It is ON by default on many new PCs used for everyday work, and OFF on almost every developer machine, so **you will not see this on your own box**. The users who hit it are exactly the ones you most want: someone with a new laptop who installed Adom Bridge and nothing else. (Adom's own bridges shipped unsigned exes until a user hit this: adom/adom-bridge#219.)
+
+**What ab does about it (2.1.160+).** When Windows refuses to start your file, ab stops retrying (each retry would show the user that notice again). Your verbs answer `errorCode: "blocked_by_windows_app_control"` with `blockedFile` and `smartAppControl`, and `bridge_list` shows the same on your row. The moment a NEW file replaces the blocked one (your next release, signed), ab starts it again by itself. ab cannot sign your binary for you, and it never will: the signature has to say who published it, and that is you.
+
+**What to sign.** Every Windows PE file in your payload: your main `.exe` (`spawn.kind: exe`), any helper `.exe`, and every `.dll`, `.node` or `.pyd` your process loads. Smart App Control checks the DLLs a process loads too, not only the exe ab starts. Python and Node bridges with only `.py`/`.js` source are fine as far as their own files go; any native module they ship is not.
+
+**How to get a signing identity** (pick one; check each provider's current requirements and pricing):
+- **Microsoft Artifact Signing** (formerly Azure Trusted Signing): cloud signing where Microsoft verifies your identity and holds the key. No hardware to manage, and signatures are trusted by Smart App Control. Eligibility depends on where you and your organization are based.
+- **A code-signing certificate from a public certificate authority** (DigiCert, Sectigo, SSL.com, GlobalSign and others). Since 2023 the private key must live in hardware: a USB token, or the CA's cloud signing service.
+- **Open-source projects** may qualify for free signing from programs such as the SignPath Foundation.
+- ⛔ A **self-signed** certificate does NOT help. Windows only honours signatures that chain to a trusted authority.
+
+**How to sign.** On Windows: `signtool sign` (Windows SDK), or Microsoft's `sign` CLI for Artifact Signing. On Linux or macOS: `osslsigncode` with your certificate or hardware token, or any signer your provider supports. Always add an **RFC 3161 timestamp** (for example `/tr <your CA's timestamp URL> /td sha256` with signtool). A timestamped signature stays valid after your certificate expires; an untimestamped one breaks the day it does.
+
+**Where it goes in your flow:** build -> **sign every PE file** -> verify -> zip -> sha256/size into the manifest -> publish. Sign BEFORE you hash: signing changes the bytes, so a manifest hashed before signing will fail ab's sha256 check.
+
+**Verify before you publish.** On a Windows machine, `(Get-AuthenticodeSignature <file>).Status` must be `Valid`, `SignerCertificate` must name you, and `TimeStamperCertificate` must be present. `signtool verify /pa /v <file>` is the stricter check. You cannot easily make Smart App Control block you on a dev machine, so a Valid, timestamped signature on every PE file IS the test. After you publish, install your release through ab (`bridge_install`) and check the INSTALLED file the same way.
+
+**Keep signing out of your public source.** Signing is a release step you run with your own credential. Your build scripts, CI config and repo must never contain a key, a token, or a private signing endpoint. Someone building your bridge from source gets an unsigned binary, which is correct. Your README can say "official releases are signed by <you>".
+
 **To ship a new version:** bump `version` in BOTH `bridge.json` (in the zip) and the manifest, re-`release upload` the zip, re-POST the manifest. Users auto-get it on the next 4h poll, or immediately via `refresh_bridges {name}`. **A bundled SEED** (a `plugins/<name>/` folder shipped in ab's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware). ⚠ **A `release upload` REPLACES a same-platform asset ONLY when the FILENAME is IDENTICAL** — a different name (a version bump `-v1.2.0` → `-v1.3.0`) ADDS a second asset, leaving a stale zip on the release. Keep ONE asset per release: overwrite the same filename, or `adom-wiki release delete-asset <owner/slug> <ver> "<old-filename>"` after a rename. (A stale extra won't break the manifest-URL install, but it clutters the release.)
 
 ---
@@ -317,7 +339,7 @@ These eight patterns are what make a cloud AI actually succeed with your bridge ## Publish checklist (the whole modern flow)
 
 1. **Page** — `adom-wiki page create adom/adom-bridge-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.
-2. **Runtime** — `release create` + `release upload` the versioned `.zip`; POST the manifest to `/files`; set `updateManifestUrl` in BOTH the zip's `bridge.json` and the manifest. **Before uploading: `unzip -l <zip> | sort -k1 -n -r | head` — runtime only, no media.** *(→ Payload rules)*
+2. **Runtime** — **sign every Windows PE file first** (`.exe`/`.dll`/`.node`/`.pyd`, timestamped; `Get-AuthenticodeSignature` reads Valid), then `release create` + `release upload` the versioned `.zip`; POST the manifest to `/files`; set `updateManifestUrl` in BOTH the zip's `bridge.json` and the manifest. **Before uploading: `unzip -l <zip> | sort -k1 -n -r | head` — runtime only, no media.** *(→ Sign everything you ship; Payload rules)*
 3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (**`tar tzf` it — skills only, zero media/binaries**) → `pkg publish --org adom` (mind the `.gitignore` trap). Your `description` carries the one-sentence "this installs SKILLS, not the runtime" line. *(→ Artifact 2)*
 4. **Discovery** — `discover triggers` to confirm; `discover preview "<a real user phrase>"` to confirm you surface.
 5. **Re-seed ab's bundled copy** (ab-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.
skills/adom-bridge-sdk-runtime/SKILL.md+123−121
@@ -1,121 +1,123 @@-----name: adom-bridge-sdk-runtime-description: >--  The RUNTIME contract for a bridge: Adom Bridge provisions Node/Python for you and you MUST-  bind loopback (ADOM_BIND_HOST) so no firewall prompt appears; plus prewarm, the cold-start-  tiers, host-app detection and the bridge_readiness probe, the health endpoint and status--  chip contract (led/summary/tooltip), the lifecycle verbs users drive, and the logging split-  (ab owns the file, you own the volume). Use when your bridge fails to spawn, shows the wrong-  LED, is slow on first call, or needs a runtime it cannot assume exists.-------## Runtime contract — ab provisions Node/Python; you bind loopback (v1.9.63 / v1.9.69)--**The single most important change for a Node or Python bridge.** As of ab **v1.9.63** you no longer bootstrap your own interpreter, and you MUST bind loopback. Four rules:--1. **Don't bootstrap your own Node/Python.** ab owns a global, portable, **no-UAC** runtime under `~/.adom/adom-runtimes/`: a system install if one's on PATH, else a portable copy it downloads + prewarms in the background on first launch. ab spawns your `entrypoint` with that interpreter **by absolute path**. So **delete every `winget install node` / `desktop_install_node` / "please install Python" / self-download step** from your bridge and its skills. `spawn.kind: "node"|"python"` IS your runtime declaration — ab reads it and provisions the runtime. Anyone can check status with the **`runtimes`** verb (`state ∈ absent|installing|ready|failed`, `source ∈ system|cache`, version). ab pins the version (Node 22.11.0 / Python 3.12.13 today) — need a different major? ask the ab-core thread; don't ship your own.--2. **Bind `ADOM_BIND_HOST`, never `0.0.0.0`.** ab passes an **`ADOM_BIND_HOST`** env var (always `127.0.0.1`) to every bridge it spawns. A bare listen that defaults to `0.0.0.0` pops a **Windows Firewall "allow access?" dialog** on first run — and ab's hard guarantee to users is **no firewall prompt by default**.-   - Node: `server.listen(PORT, process.env.ADOM_BIND_HOST || '127.0.0.1', cb)`-   - Python: `HTTPServer((os.environ.get('ADOM_BIND_HOST', '127.0.0.1'), PORT), Handler)`--3. **Ship the Release zip source-only — ab installs your deps, and v1.9.69 makes it bulletproof.** A Node bridge's zip carries SOURCE only (no `node_modules`). ab reuses the bundled seed's `node_modules` via `NODE_PATH` when versions match, else runs **`npm install --include=optional`** for you with the managed runtime's own npm. **As of v1.9.69 ab puts the managed runtime's dir on the child PATH for that install AND for your spawned process** — so a dep's postinstall / `node-gyp` / `prebuild-install` that shells out to a **bare** `node`/`npm`/`npx` (puppeteer's own `node install.mjs`, `sharp`, `keytar`, `better-sqlite3`, `bcrypt`) resolves even on a machine with **no system Node**. **You do NOT add the runtime to PATH yourself** — ab guarantees it. (Python: the interpreter dir + `Scripts/` are on PATH too, so pip can build wheels.)--4. **Bundle deps in your SEED for an instant first spawn.** Source-only zip + a bundled-seed `node_modules` at the same version = ab reuses the seed via `NODE_PATH` (instant); a wiki version newer than the seed forces one cold `npm install` (now PATH-correct, but slower). Bump the seed in lockstep with each Release.--5. **`ADOM_SESSION_TOKEN` (ab v2.1.57, optional): your bridge's OWN Adom session token, when the user configured one.** If the user set `bridge_session_tokens: {"<your-bridge>": "<token>"}` in ab (via `settings_set`; the key is your bridge's runtime name, the same one `bridge_log_read` uses), ab injects it into your spawn env as `ADOM_SESSION_TOKEN`. Use it for calls that must be attributed to your bridge's own identity on wiki.adom.inc / Adom Anywhere (`adom-wiki pkg install`, wiki writes, app runs) instead of borrowing the human's token. Rules: it is a SECRET - never log it, never put it in a `_hint` or any verb output, never write it to disk. Absent means the user did not configure one: degrade gracefully (public/unauthenticated behavior, or report `session_token_required` in your output so the AI can ask the user to set one). ab only DELIVERS the token; the identity behind it (for example a "Grok Bot" account) is created by a human in Carbon.--**Net:** declare `spawn.kind`, bind `ADOM_BIND_HOST`, ship source-only. ab does the rest — no UAC, no firewall prompt, no `'node' is not recognized`.-------## Prewarm — make the first call instant--A cold first call that pays multi-minute setup (npm install, downloading a runtime, a heavy import) blows the caller's timeout. Pay that cost in the background BEFORE the user asks:--- **Bundle deps in your SEED.** Pup ships a bundled `node_modules/` in its NSIS seed so a fresh PC can spawn with no `npm install`. When you re-seed from a Release zip, keep the bundled deps.-- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.-- **ab prefetches for you on first launch — standalone AND embedded (Hydrogen-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of ab v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent "ensure ready" path ab/your seed can trigger, and surface a clean `errorCode` (e.g. `node_not_found`) when a prereq is missing so the AI can install it (`desktop_install_node`) instead of hanging.-- **`pup_readiness`-style verb.** Offer a fast verb that reports `{ready:true|false, …}` so a caller (or a fresh-PC test) can confirm warmth without triggering work.-------## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)--A bridge has TWO kinds of dependency, and **ab treats them OPPOSITELY.** Get this right or a first-install probe (e.g. Hydrogen's "what tools do I have") triggers a slow install at the worst moment.--**Tier 1 — your RUNTIME (Node, Python, your own browser download): ab prewarms + auto-installs it.** This is your plumbing. On first boot ab **provisions** Node/Python in the background — a **portable, no-UAC** copy under `~/.adom/adom-runtimes/` (or a system install if one's on PATH), **NOT winget/MSI** (see **Runtime contract** above) — and prewarms heavy assets you declare. You may also download your own runtime asset (pup downloads Chrome-for-Testing) — fine, it's yours. **But do it in the BACKGROUND: a `*_status` / `*_readiness` / ANY probe verb must NEVER block on a heavy download.** Spawn your server fast (it usually doesn't need the asset to *start*), kick the download off detached, and report `state:"warming"` until ready. A synchronous-download-on-first-verb is the exact footgun this release fixed (pup's 150 MB Chrome fetch used to block the probe).--**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): ab DETECTS it by default and installs it ON REQUEST — never pre-emptively, and NEVER by telling the user to do it themselves.** Two rules that sound opposed but aren't:-- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default ab only DETECTS presence (a machine with KiCad belongs to a KiCad user; EDA users are mono-tool — a Fusion user isn't a KiCad user). ab won't prewarm/spawn your bridge while the app is absent — a resource decision, not a "go away."-- **But the cardinal Adom rule overrides everything: the AI does EVERYTHING for the user — it must NEVER tell the user to do something the AI can do itself.** So your bridge MUST know how to install the host app (`desktop_install_kicad` / winget / your own install verb) and just DO it the instant the user wants it. **Your "not installed" hint must OFFER to install it** ("KiCad isn't installed — want me to install it for you?"), then do it on a yes — it must NEVER say *"download it from kicad.org."* Pointing the user at a manual download is the breach.--**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**--| Level | Bridge(s) | Behavior |-|---|---|---|-| **0 · Built-in / MUST** | Hydrogen (`hd`) | The platform itself — the editor ab lives in. Always present; not "installed" by ab. |-| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. ab pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |-| **2 · Recommended — proactive offer** | native-browser (the browser EXTENSION half) | The user's REAL logged-in browser (login/forms). Since v1.9.142 the DESKTOP BRIDGE half ships bundled in the ab installer (0.2 MB, zero deps, `persistent:true` — its extension-free verbs like `nbrowser_open_os_window`/`nbrowser_profiles`/`nbrowser_os_windows` work day one). What remains level-2 is the in-browser EXTENSION: ab/the bridge proactively SUGGESTS it and it installs on a yes (a browser-extension step the user confirms), unlocking full in-page drive. |-| **3 · On-request host app** | KiCad, Fusion, Altium, … | Detect by default; NEVER pre-install (huge app, rude unasked). The moment the user wants it, the AI installs it AUTOMATICALLY (winget / `desktop_install_*`) — NEVER tells the user to do it manually. |-| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |--The through-line: **the AI never makes the user do something it can do itself.** The only knob is *when* — pre-emptive (pup), proactively-offered (native-browser), or on-request (host apps).--**Declare both in `bridge.json`:**-```json-"detect": {                          // Tier 2 — how ab finds your HOST APP (detection only)-  "hostApp": "KiCad",-  "appPathsExe": "kicad.exe",        // optional: Windows App Paths registry lookup (most reliable)-  "paths": {                         // optional: candidate paths, %VAR% expanded, one * = a version dir-    "windows": ["%ProgramFiles%\\KiCad\\*\\bin\\kicad.exe"],-    "macos":   ["/Applications/KiCad/KiCad.app"],-    "linux":   ["/usr/bin/kicad"]-  }-},-"prewarm": { "assets": ["chrome-for-testing"] }   // Tier 1 — heavy runtime assets ab prewarms + reports-```-A bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. ab infers the runtime from `spawn.kind` (node→Node, python→Python).--**The read-only probe: `bridge_readiness`.** ab aggregates all of this into ONE side-effect-free verb the setup AI calls — it **never spawns a bridge, never installs/downloads**. Per bridge: `{hostApp, hostAppInstalled, runtime, runtimeReady, assets, state}` where `state` ∈ **`ready`** (use it) | **`warming`** (runtime ok, a heavy asset still downloading in the background — re-poll, don't trigger it) | **`needs-runtime`** (node/python installing) | **`no-app`** (host app not installed → not relevant here). Plus a top-level `edaToolsInstalled` list — the one-line answer to "what EDA tools do I have." **Tell your users (in your skill): for a "what do I have / is it ready" question call `bridge_readiness`, NOT your `*_status` verb** — a `*_status` may spawn your bridge; readiness never does.--Your two own cold-start verbs (above) still apply: `<prefix>_readiness` (read-only `{ready, installing, installProgressPct, lastError}`) + `<prefix>_prewarm` (`{wait}`-able). Those drive YOUR bridge specifically; `bridge_readiness` is the ab-level aggregate that detects + summarizes across all bridges without spawning any.-------## Health + status chip--Your `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). ab owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.--## Lifecycle verbs (ab gives these to USERS — know them to guide users)--`bridge_install {manifestUrl}` · `bridge_list` · `bridge_info`/`bridge_detail {name}` · `bridge_log_read {name, sinceOffset?}` · `bridge_kill {name}` · `bridge_pause {name}` · `refresh_bridges {name?}` · `bridge_check_updates`. A bridge installed via `bridge_install` auto-updates with no further opt-in; `updateManifestUrl` in `bridge.json` makes it durable across republish.--**"My bridge keeps exiting and I can't see why" (read this).** Your own stdout log truncates on every respawn, so a crash-then-restart erases its own cause. `bridge_log_read` therefore also tails **ab's lifecycle audit log** for your bridge, a separate append-only file (`~/.adom/bridge-logs/<name>.ad.log`) that survives respawns, shown under an `===== ab bridge lifecycle =====` header. It records every spawn, reap, duplicate-collapse, and `bridge_kill` with the reason, so you can tell "ab reaped it as a duplicate" or "the `bridge_kill` verb stopped it" or "it was reaped before a fresh spawn" apart from "it crashed on its own." If the audit log shows ab never stopped it, the exit is inside your process.--## Logging: ab owns the FILE, you own the VOLUME--Every bridge writes to stdout, ab captures it, and nobody asks the volume question until someone's log is a firehose. The split:--- **ab owns the FILE.** ab spawns you and captures stdout/stderr, so the file and its rotation are ab's. A bridge cannot rotate a file another process holds open, and bounded logs have to be a platform guarantee rather than per-author boilerplate.-- **You own the VOLUME.** How much you emit is the half a tidy platform cannot fix for you.--*(READING the log is the section immediately above: `bridge_log_read` plus ab's separate lifecycle audit. This section is about what you WRITE.)*--**Do NOT write your own parallel log file.** Two log files means two sources of truth and double the disk for the same bytes. pup built one (a rotated `pup.log`) and deliberately reverted it. If you think you need your own log, what you probably want is for ab to scope `bridge_log_read` to your bridge: file that against `adom/adom-bridge` (see "Filing issues against ab" above) instead of forking the log.--**The five patterns that keep a bridge out of firehose territory:**--1. **Never log per-item inside a loop over live state.** This is the single biggest source of volume. A drag-detection check logged one line PER TAB per check, all of them saying "(same)", so a 12-tab window emitted 12 useless lines every time the check fired. **Log the exception (the tab that moved), never the roll call.**-2. **Collapse consecutive identical lines.** Wrap `console.log` once, hold the last message, and emit `^ last line repeated Nx` when it changes. A retry loop then costs 2 lines instead of 200. Collapse only EXACT consecutive repeats, so no distinct event is lost. About 15 lines of code.-3. **Log static facts once per session, not per call.** "no favicon on this page" was emitted on every taskbar-badge update. It is a property of the page, not an event, and because the repeats were not consecutive the collapser could not catch them. Gate it behind a per-session flag.-4. **DO log every incoming verb, one compact line.** This is the one place to ADD volume. A log that records what the bridge DID (park, identity, overlay) but never WHAT WAS ASKED of it cannot answer "what foregrounded this window?" after the fact. Include only the args that change behavior, and skip your own log-read verb so reading the log does not pollute it. Put the caller thread on this line too (the shape is in "Caller provenance" above):--   ```-   [verb] pup_open_window session=parkedge url=https://example.com owner=parktest-   ```--5. **Never let logging throw.** Wrap the whole path in try/catch. A logging failure must not take the bridge down.-----+---
+name: adom-bridge-sdk-runtime
+description: >-
+  The RUNTIME contract for a bridge: Adom Bridge provisions Node/Python for you and you MUST
+  bind loopback (ADOM_BIND_HOST) so no firewall prompt appears; plus prewarm, the cold-start
+  tiers, host-app detection and the bridge_readiness probe, the health endpoint and status-
+  chip contract (led/summary/tooltip), the lifecycle verbs users drive, and the logging split
+  (ab owns the file, you own the volume). Use when your bridge fails to spawn, shows the wrong
+  LED, is slow on first call, or needs a runtime it cannot assume exists.
+---
+
+
+## Runtime contract — ab provisions Node/Python; you bind loopback (v1.9.63 / v1.9.69)
+
+**The single most important change for a Node or Python bridge.** As of ab **v1.9.63** you no longer bootstrap your own interpreter, and you MUST bind loopback. Four rules:
+
+1. **Don't bootstrap your own Node/Python.** ab owns a global, portable, **no-UAC** runtime under `~/.adom/adom-runtimes/`: a system install if one's on PATH, else a portable copy it downloads + prewarms in the background on first launch. ab spawns your `entrypoint` with that interpreter **by absolute path**. So **delete every `winget install node` / `desktop_install_node` / "please install Python" / self-download step** from your bridge and its skills. `spawn.kind: "node"|"python"` IS your runtime declaration — ab reads it and provisions the runtime. Anyone can check status with the **`runtimes`** verb (`state ∈ absent|installing|ready|failed`, `source ∈ system|cache`, version). ab pins the version (Node 22.11.0 / Python 3.12.13 today) — need a different major? ask the ab-core thread; don't ship your own.
+
+   **Python bridges: run pip and tools as `python -m pip ...` / `python -m <module>`**, never a generated `Scripts\pip.exe` or console-script `.exe`. ab's managed Python is signed (Python Software Foundation), so Windows Smart App Control lets it run, but the wrapper exes pip generates are unsigned in every Python distribution and can be blocked on a new PC. The same goes for any native module you ship (`.pyd`, `.node`, `.dll`): sign it (`adom-bridge-sdk-publish`, "Sign everything you ship").
+
+2. **Bind `ADOM_BIND_HOST`, never `0.0.0.0`.** ab passes an **`ADOM_BIND_HOST`** env var (always `127.0.0.1`) to every bridge it spawns. A bare listen that defaults to `0.0.0.0` pops a **Windows Firewall "allow access?" dialog** on first run — and ab's hard guarantee to users is **no firewall prompt by default**.
+   - Node: `server.listen(PORT, process.env.ADOM_BIND_HOST || '127.0.0.1', cb)`
+   - Python: `HTTPServer((os.environ.get('ADOM_BIND_HOST', '127.0.0.1'), PORT), Handler)`
+
+3. **Ship the Release zip source-only — ab installs your deps, and v1.9.69 makes it bulletproof.** A Node bridge's zip carries SOURCE only (no `node_modules`). ab reuses the bundled seed's `node_modules` via `NODE_PATH` when versions match, else runs **`npm install --include=optional`** for you with the managed runtime's own npm. **As of v1.9.69 ab puts the managed runtime's dir on the child PATH for that install AND for your spawned process** — so a dep's postinstall / `node-gyp` / `prebuild-install` that shells out to a **bare** `node`/`npm`/`npx` (puppeteer's own `node install.mjs`, `sharp`, `keytar`, `better-sqlite3`, `bcrypt`) resolves even on a machine with **no system Node**. **You do NOT add the runtime to PATH yourself** — ab guarantees it. (Python: the interpreter dir + `Scripts/` are on PATH too, so pip can build wheels.)
+
+4. **Bundle deps in your SEED for an instant first spawn.** Source-only zip + a bundled-seed `node_modules` at the same version = ab reuses the seed via `NODE_PATH` (instant); a wiki version newer than the seed forces one cold `npm install` (now PATH-correct, but slower). Bump the seed in lockstep with each Release.
+
+5. **`ADOM_SESSION_TOKEN` (ab v2.1.57, optional): your bridge's OWN Adom session token, when the user configured one.** If the user set `bridge_session_tokens: {"<your-bridge>": "<token>"}` in ab (via `settings_set`; the key is your bridge's runtime name, the same one `bridge_log_read` uses), ab injects it into your spawn env as `ADOM_SESSION_TOKEN`. Use it for calls that must be attributed to your bridge's own identity on wiki.adom.inc / Adom Anywhere (`adom-wiki pkg install`, wiki writes, app runs) instead of borrowing the human's token. Rules: it is a SECRET - never log it, never put it in a `_hint` or any verb output, never write it to disk. Absent means the user did not configure one: degrade gracefully (public/unauthenticated behavior, or report `session_token_required` in your output so the AI can ask the user to set one). ab only DELIVERS the token; the identity behind it (for example a "Grok Bot" account) is created by a human in Carbon.
+
+**Net:** declare `spawn.kind`, bind `ADOM_BIND_HOST`, ship source-only. ab does the rest — no UAC, no firewall prompt, no `'node' is not recognized`.
+
+---
+
+## Prewarm — make the first call instant
+
+A cold first call that pays multi-minute setup (npm install, downloading a runtime, a heavy import) blows the caller's timeout. Pay that cost in the background BEFORE the user asks:
+
+- **Bundle deps in your SEED.** Pup ships a bundled `node_modules/` in its NSIS seed so a fresh PC can spawn with no `npm install`. When you re-seed from a Release zip, keep the bundled deps.
+- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.
+- **ab prefetches for you on first launch — standalone AND embedded (Hydrogen-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of ab v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent "ensure ready" path ab/your seed can trigger, and surface a clean `errorCode` (e.g. `node_not_found`) when a prereq is missing so the AI can install it (`desktop_install_node`) instead of hanging.
+- **`pup_readiness`-style verb.** Offer a fast verb that reports `{ready:true|false, …}` so a caller (or a fresh-PC test) can confirm warmth without triggering work.
+
+---
+
+## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)
+
+A bridge has TWO kinds of dependency, and **ab treats them OPPOSITELY.** Get this right or a first-install probe (e.g. Hydrogen's "what tools do I have") triggers a slow install at the worst moment.
+
+**Tier 1 — your RUNTIME (Node, Python, your own browser download): ab prewarms + auto-installs it.** This is your plumbing. On first boot ab **provisions** Node/Python in the background — a **portable, no-UAC** copy under `~/.adom/adom-runtimes/` (or a system install if one's on PATH), **NOT winget/MSI** (see **Runtime contract** above) — and prewarms heavy assets you declare. You may also download your own runtime asset (pup downloads Chrome-for-Testing) — fine, it's yours. **But do it in the BACKGROUND: a `*_status` / `*_readiness` / ANY probe verb must NEVER block on a heavy download.** Spawn your server fast (it usually doesn't need the asset to *start*), kick the download off detached, and report `state:"warming"` until ready. A synchronous-download-on-first-verb is the exact footgun this release fixed (pup's 150 MB Chrome fetch used to block the probe).
+
+**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): ab DETECTS it by default and installs it ON REQUEST — never pre-emptively, and NEVER by telling the user to do it themselves.** Two rules that sound opposed but aren't:
+- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default ab only DETECTS presence (a machine with KiCad belongs to a KiCad user; EDA users are mono-tool — a Fusion user isn't a KiCad user). ab won't prewarm/spawn your bridge while the app is absent — a resource decision, not a "go away."
+- **But the cardinal Adom rule overrides everything: the AI does EVERYTHING for the user — it must NEVER tell the user to do something the AI can do itself.** So your bridge MUST know how to install the host app (`desktop_install_kicad` / winget / your own install verb) and just DO it the instant the user wants it. **Your "not installed" hint must OFFER to install it** ("KiCad isn't installed — want me to install it for you?"), then do it on a yes — it must NEVER say *"download it from kicad.org."* Pointing the user at a manual download is the breach.
+
+**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**
+
+| Level | Bridge(s) | Behavior |
+|---|---|---|
+| **0 · Built-in / MUST** | Hydrogen (`hd`) | The platform itself — the editor ab lives in. Always present; not "installed" by ab. |
+| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. ab pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |
+| **2 · Recommended — proactive offer** | native-browser (the browser EXTENSION half) | The user's REAL logged-in browser (login/forms). Since v1.9.142 the DESKTOP BRIDGE half ships bundled in the ab installer (0.2 MB, zero deps, `persistent:true` — its extension-free verbs like `nbrowser_open_os_window`/`nbrowser_profiles`/`nbrowser_os_windows` work day one). What remains level-2 is the in-browser EXTENSION: ab/the bridge proactively SUGGESTS it and it installs on a yes (a browser-extension step the user confirms), unlocking full in-page drive. |
+| **3 · On-request host app** | KiCad, Fusion, Altium, … | Detect by default; NEVER pre-install (huge app, rude unasked). The moment the user wants it, the AI installs it AUTOMATICALLY (winget / `desktop_install_*`) — NEVER tells the user to do it manually. |
+| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |
+
+The through-line: **the AI never makes the user do something it can do itself.** The only knob is *when* — pre-emptive (pup), proactively-offered (native-browser), or on-request (host apps).
+
+**Declare both in `bridge.json`:**
+```json
+"detect": {                          // Tier 2 — how ab finds your HOST APP (detection only)
+  "hostApp": "KiCad",
+  "appPathsExe": "kicad.exe",        // optional: Windows App Paths registry lookup (most reliable)
+  "paths": {                         // optional: candidate paths, %VAR% expanded, one * = a version dir
+    "windows": ["%ProgramFiles%\\KiCad\\*\\bin\\kicad.exe"],
+    "macos":   ["/Applications/KiCad/KiCad.app"],
+    "linux":   ["/usr/bin/kicad"]
+  }
+},
+"prewarm": { "assets": ["chrome-for-testing"] }   // Tier 1 — heavy runtime assets ab prewarms + reports
+```
+A bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. ab infers the runtime from `spawn.kind` (node→Node, python→Python).
+
+**The read-only probe: `bridge_readiness`.** ab aggregates all of this into ONE side-effect-free verb the setup AI calls — it **never spawns a bridge, never installs/downloads**. Per bridge: `{hostApp, hostAppInstalled, runtime, runtimeReady, assets, state}` where `state` ∈ **`ready`** (use it) | **`warming`** (runtime ok, a heavy asset still downloading in the background — re-poll, don't trigger it) | **`needs-runtime`** (node/python installing) | **`no-app`** (host app not installed → not relevant here). Plus a top-level `edaToolsInstalled` list — the one-line answer to "what EDA tools do I have." **Tell your users (in your skill): for a "what do I have / is it ready" question call `bridge_readiness`, NOT your `*_status` verb** — a `*_status` may spawn your bridge; readiness never does.
+
+Your two own cold-start verbs (above) still apply: `<prefix>_readiness` (read-only `{ready, installing, installProgressPct, lastError}`) + `<prefix>_prewarm` (`{wait}`-able). Those drive YOUR bridge specifically; `bridge_readiness` is the ab-level aggregate that detects + summarizes across all bridges without spawning any.
+
+---
+
+## Health + status chip
+
+Your `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). ab owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.
+
+## Lifecycle verbs (ab gives these to USERS — know them to guide users)
+
+`bridge_install {manifestUrl}` · `bridge_list` · `bridge_info`/`bridge_detail {name}` · `bridge_log_read {name, sinceOffset?}` · `bridge_kill {name}` · `bridge_pause {name}` · `refresh_bridges {name?}` · `bridge_check_updates`. A bridge installed via `bridge_install` auto-updates with no further opt-in; `updateManifestUrl` in `bridge.json` makes it durable across republish.
+
+**"My bridge keeps exiting and I can't see why" (read this).** Your own stdout log truncates on every respawn, so a crash-then-restart erases its own cause. `bridge_log_read` therefore also tails **ab's lifecycle audit log** for your bridge, a separate append-only file (`~/.adom/bridge-logs/<name>.ad.log`) that survives respawns, shown under an `===== ab bridge lifecycle =====` header. It records every spawn, reap, duplicate-collapse, and `bridge_kill` with the reason, so you can tell "ab reaped it as a duplicate" or "the `bridge_kill` verb stopped it" or "it was reaped before a fresh spawn" apart from "it crashed on its own." If the audit log shows ab never stopped it, the exit is inside your process.
+
+## Logging: ab owns the FILE, you own the VOLUME
+
+Every bridge writes to stdout, ab captures it, and nobody asks the volume question until someone's log is a firehose. The split:
+
+- **ab owns the FILE.** ab spawns you and captures stdout/stderr, so the file and its rotation are ab's. A bridge cannot rotate a file another process holds open, and bounded logs have to be a platform guarantee rather than per-author boilerplate.
+- **You own the VOLUME.** How much you emit is the half a tidy platform cannot fix for you.
+
+*(READING the log is the section immediately above: `bridge_log_read` plus ab's separate lifecycle audit. This section is about what you WRITE.)*
+
+**Do NOT write your own parallel log file.** Two log files means two sources of truth and double the disk for the same bytes. pup built one (a rotated `pup.log`) and deliberately reverted it. If you think you need your own log, what you probably want is for ab to scope `bridge_log_read` to your bridge: file that against `adom/adom-bridge` (see "Filing issues against ab" above) instead of forking the log.
+
+**The five patterns that keep a bridge out of firehose territory:**
+
+1. **Never log per-item inside a loop over live state.** This is the single biggest source of volume. A drag-detection check logged one line PER TAB per check, all of them saying "(same)", so a 12-tab window emitted 12 useless lines every time the check fired. **Log the exception (the tab that moved), never the roll call.**
+2. **Collapse consecutive identical lines.** Wrap `console.log` once, hold the last message, and emit `^ last line repeated Nx` when it changes. A retry loop then costs 2 lines instead of 200. Collapse only EXACT consecutive repeats, so no distinct event is lost. About 15 lines of code.
+3. **Log static facts once per session, not per call.** "no favicon on this page" was emitted on every taskbar-badge update. It is a property of the page, not an event, and because the repeats were not consecutive the collapser could not catch them. Gate it behind a per-session flag.
+4. **DO log every incoming verb, one compact line.** This is the one place to ADD volume. A log that records what the bridge DID (park, identity, overlay) but never WHAT WAS ASKED of it cannot answer "what foregrounded this window?" after the fact. Include only the args that change behavior, and skip your own log-read verb so reading the log does not pollute it. Put the caller thread on this line too (the shape is in "Caller provenance" above):
+
+   ```
+   [verb] pup_open_window session=parkedge url=https://example.com owner=parktest
+   ```
+
+5. **Never let logging throw.** Wrap the whole path in try/catch. A logging failure must not take the bridge down.
+
+---