app
DEPRECATED: Bridge SDK (moved into Adom Bridge)
Public Made by Adomby adom
Bridge SDK guide: bridge.json schema, kind:python/node/exe, hello-python + hello-rust reference templates, packaging + lifecycle commands.
← Commit history
Billboard = the current SDK (self-audit checklist + cold-start + schema) — was stale; authors read the billboard, not SKILL.md
3 files changed
+479−423
README.md+229−237@@ -1,310 +1,302 @@-# Adom Desktop - Bridge SDK +# Bridge SDK — build, publish, and surface a bridge for Adom Desktop -## Build your own bridge +A **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge: -A **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere \u2014 including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge. +1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it. +2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs. +3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *"open my app in pup"* surfaces YOU via `adom-wiki discover find`. -The fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below. +> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.** +> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.** +> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.) -### Starting prompt (paste into a cloud Claude session) +--- -``` -I want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>. - -A bridge is a small local HTTP server described by a bridge.json; AD spawns it on a -stable port and relays verbs to it. Before writing anything, study the reference -bridges for design ideas and copy the conventions you see: - - - START SIMPLE \u2014 read the two "hello" reference bridges first (one Rust, one - Python). They are the minimal, correct shape: a /command POST handler, a /status - health endpoint that self-reports its LED, and a bridge.json. Mirror them. - - THEN study an advanced bridge for the patterns you need: a long-running native - app (KiCad / Fusion 360), a process you supervise, or an external client that - dials back in (native-browser). Steal their bridge.json fields and verb shapes. - -My bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>. - -Follow the Adom Desktop Bridge SDK conventions on this page: - - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure - renderer \u2014 it never invents your status). - - Return rich, structured JSON with an _hint on every response so the calling AI - knows what to do next; on a slow operation, return a statusVerb to poll. - - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will - relay it so a timeout reads as "still running, poll this", not "failed"). - -When it works locally, package it with release-bridge.sh and tell me the -bridge_install command so I can install it on my Adom Desktop from here. -``` +## The boundary — what AD owns vs what YOU own -Claude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time. +The #1 mistake is filing AD bugs for bridge issues. The split: -### Reference bridges \u2014 copy these +**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them). -Start with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts. +**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**. -| Bridge | Language | Why read it | -|---|---|---| -| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. | -| **hello-rust** | Rust | Same minimal shape in Rust \u2014 single-binary, no runtime deps. | -| **native-browser** | \u2014 | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. | -| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. | +If a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD. -The hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index). +--- -### The whole dev loop is remote \u2014 install & uninstall from anywhere +## Self-audit checklist — run this when asked to "audit against the SDK" -This is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI: +When a maintainer says *"audit your code, skills, README, and wiki page against the Bridge SDK,"* **this is the checklist.** Each item names its section below; fix every miss, then re-check. -```bash -# Install a bridge from its published wiki manifest (canonical owner-scoped URL): -adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/pages/<owner>/<your-bridge-page>/files/<your>-bridge-manifest.json"}' +**bridge.json** (the manifest in your zip): +- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)* +- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.** +- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)* -# It appears in AD's Bridges panel immediately and starts on first verb call. -adom-desktop bridge_list # see it registered + its LED -adom-desktop <your_verb> '{...}' # call it - AD spawns + relays +**Verbs & output** (what the AI actually reads): +- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)* +- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)* +- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)* +- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)* -# Iterate: re-publish a higher version, then refresh - AD reaps the old instance and -# streams the new one onto the SAME stable port (no port churn, no manual kill; AD 1.9.27+). -adom-desktop refresh_bridges '{"name":"<your-bridge>"}' +**Discovery** (can a plain request find you?): +- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)* +- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each. +- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION. -# Remove it cleanly when done: -adom-desktop bridge_uninstall '{"name":"<your-bridge>"}' -``` +**Skills & docs** (what the container + humans read): +- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)* +- [ ] Your USER skill leads with the cold-start "read before you panic" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)* +- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted). -In the AD GUI, the same flows are one click: **"+ Bridge"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below). +--- -That's the full toolchain \u2014 author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop. +## bridge.json — the manifest that ships INSIDE your zip + +```jsonc +{ + "manifest_version": 1, + "name": "blender", // registry name → verb prefix "blender_" + "displayName": "Blender", // in-app chip label (the tool's real name) + "version": "1.2.0", // numeric; a newer CACHE copy supersedes the bundled seed + "description": "Drive Blender headless + GUI", + "author": "you", + "license": "MIT", + "docs": "https://wiki.adom.inc/adom/adom-desktop-blender-bridge", // your OWN page (drives skillPkg + hero) + "hero": "https://.../hero.png", // optional; else AD uses your wiki page hero, else a monogram + "spawn": { + "kind": "python", // python | node | exe | external-http (NOT "process") + "entrypoint": "server.py", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv + "port": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>. + "persistent": true, // AD's supervisor respawns it if it dies + "healthEndpoint": "/health", // INSIDE spawn. AD probes it for the chip (default /health) + "stopMethod": "kill", // kill | sigterm | graceful_endpoint + "killImageName": "python.exe" // image name for taskkill on stop (kind:python/node) + }, + "verbPrefixes": ["blender_"], // any verb with these prefixes routes to you + "verbs": ["blender_render", "blender_status", "blender_describe"], // every verb (routing + Verbs tab) + "statusVerb": "blender_status", // verb a caller polls after a long op times out + "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json", + + // v1.9.47 cold-start (see "Cold-start tiers" below): + "detect": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it. + "hostApp": "Blender", // omit the whole `detect` block if your bridge brings its own runtime + "appPathsExe": "blender.exe", // (e.g. pup ships its own browser → no host app) + "paths": { "windows": ["%ProgramFiles%\\Blender Foundation\\*\\blender.exe"] } + }, + "prewarm": { "assets": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. ["chrome-for-testing"]) +} +``` + +- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `"process"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.) +- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md). +- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`. +- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.) +- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page. +- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.) +- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in "Cold-start tiers" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser. --- +## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest - +This is the code AD streams onto the user's machine. Two files on your page: - +1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob: + ```bash + adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title "..." --changelog "..." + adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any + ``` + (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.) +2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`: + ```jsonc + { "name":"blender", "version":"1.2.0", + "url":"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip", // base-relative, extension-agnostic (.bin works where wiki blocks .zip) + "sha256":"…", "size":123456, "verbPrefixes":["blender_"], "healthEndpoint":"/health", + "updateManifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json" } + ``` - +**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\Adom Desktop\bridges-cache\<name>\`, rescans, and remembers the URL for auto-update. -> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's "+ Bridge" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`. +**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware). + +--- -v1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything \u2014 without coordinating with the Adom Desktop release cycle. +## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`) -## Try it now \u2014 the `hello-python` and `hello-rust` sample bridges +Your runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries): -Two parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports \u2014 v1.8.31+) so no collision is possible. +```bash +# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and +# package.json {name:"adom-desktop-<bridge>-bridge", version, discovery_triggers, files:[...]}. +# THREE traps that bite — verify each with `tar tzf` before publishing: +# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE, +# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH +# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else +# every user who installs your pkg also installs your dev/publish docs. +# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files` +# allowlist needs the GLOB: a bare "skills" entry ships ZERO skill files — use "skills/**". +# (Set .gitignore aside across pack+publish, then restore.) +# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB +# before guards). Skills + docs only. +adom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present, + # ZERO binaries, ZERO dev/publish SKILL.md +adom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge +``` -| Flavor | Manifest URL | Verb prefix | Port | When to fork | -|---|---|---|---|---| -| **Python** (stdlib only, ~80 lines) | `\u2026/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work | -| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `\u2026/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety | +A container gets it **without anyone asking**: +- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it. +- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill. +- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them. -Both manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`. +**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh. -### Install from the GUI +--- -1. Click **+ Bridge** in Adom Desktop's bridge bar -2. Paste one (or both) of the manifest URLs into the dialog, click **Install** -3. New chips appear ~2 seconds later with a small \u2726 marker (third-party indicator) +## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you -### Install both from the CLI +The wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs. ```bash -# Install the Python flavor -adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json"}' -adom-desktop hellopy_ping -# \u2192 {"success":true,"output":"Hello from the Python sample bridge v1.1.0!","language":"python",...} - -# Install the Rust flavor (can coexist with hello-python) -adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json"}' -adom-desktop hellors_ping -# \u2192 {"success":true,"output":"Hello from the Rust sample bridge v1.1.0!","language":"rust",...} - -# Echo through both -adom-desktop hellopy_echo '{"message":"world"}' -# \u2192 {"success":true,"output":"echo (python): world",...} -adom-desktop hellors_echo '{"message":"world"}' -# \u2192 {"success":true,"output":"echo (rust): world",...} - -# Both appear in bridge_list -adom-desktop bridge_list -# \u2192 bundled three + "hello-python" + "hello-rust" +adom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add "open my pcb" "export gerbers" # add triggers +adom-wiki discover preview --json "open my pcb design" # what would surface + the SCORE + WHY +adom-wiki discover find "screenshot my website" # the live find an AI runs ``` -### Uninstall +**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):** +- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it. +- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title ("Adom Desktop - Blender Bridge") + a matching tag scored **ZERO** for the literal query *"blender"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.) +- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query. -```bash -adom-desktop bridge_uninstall '{"name":"hello-python"}' -adom-desktop bridge_uninstall '{"name":"hello-rust"}' -``` +**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *"open my pcb design"* / *"export gerber files"* / *"design a circuit board"* — NOT *"reverse bridge"* / *"kicad_bridge_call"* / *"fork the kicad bridge"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy "pcb"/"export" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end. -Each bridge's cache dir + registry entry are removed; the chips disappear from the bar. +**VET it — never ship triggers you didn't test:** +1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`). +2. `adom-wiki discover preview --json "<each query>"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.) +3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on "pcb"), add MORE exact phrasings so your 3×-per-trigger total wins. -The sources live at: -- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) \u2014 `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`) -- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) \u2014 `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB) +**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. "for logged-in form-filling, install the native-browser extension") — rather than claim it and do it badly. -Fork whichever flavor matches your target's natural ecosystem. +- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline. +- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`)."* -## Directory layout +### The end-to-end flow your three artifacts enable -``` -my-bridge/ -\u251c\u2500\u2500 bridge.json \u2190 REQUIRED \u2014 schema below -\u251c\u2500\u2500 BRIDGE_VERSION \u2190 optional, semver line (kept in sync with bridge.json's version field) -\u251c\u2500\u2500 server.py \u2190 or server.js, or my-bridge.exe \u2014 your bridge's entrypoint -\u251c\u2500\u2500 handlers/ \u2190 your code -\u2502 \u251c\u2500\u2500 ... -\u2514\u2500\u2500 README.md \u2190 optional, recommended -``` +User says *"show me my app in pup"* → the AI: +1. `adom-wiki discover find "show me my app in pup"` → your bridge surfaces (Artifact 3). +2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1). +3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2). +4. Drive your verbs — guided by the **rich hints you return** (the principle at the top). -## `bridge.json` schema +--- -```json -{ - "manifest_version": 1, - "name": "altium", - "displayName": "Altium Designer", - "version": "1.0.0", - "description": "One-line description shown in bridge_list output.", - "homepage": "https://github.com/your-org/adom-bridge-altium", - "author": "Your Org", - "license": "MIT", - "spawn": { - "kind": "python", - "entrypoint": "server.py", - "port": 0, - "healthEndpoint": "/status", - "stopMethod": "kill", - "killImageName": "python.exe" - }, - "verbPrefixes": ["altium_"], - "verbs": [ - "altium_open_project", - "altium_export_step", - "altium_run_drc" - ], - "dependencies": { - "python": ">=3.11", - "altium-designer": ">=24" - }, - "category": "EDA", - "tags": ["pcb", "altium", "commercial"] -} -``` +## Prewarm — make the first call instant -### Field reference +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: -- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry. -- **`displayName`** \u2014 human-friendly shown in `bridge_list` + future GUI bridge panel. -- **`version`** (required, semver). Each ship bumps this. -- **`spawn.kind`** \u2014 `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge. - - `python` \u2192 Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings. - - `node` \u2192 Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.). - - `exe` \u2192 Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. \u2014 no runtime install required on the user's machine. -- **`spawn.entrypoint`** \u2014 relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`). -- **`spawn.port`** \u2014 **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated \u2014 anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge "won't start" bugs). Bridge ports are internal plumbing \u2014 callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly. -- **`spawn.healthEndpoint`** \u2014 path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse \u2014 the host+port get replaced at runtime. -- **`spawn.stopMethod`** \u2014 `kill` (process-handle Child::kill, NOT `taskkill /im` \u2014 that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint). -- **`spawn.killImageName`** \u2014 legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name). -- **`verbPrefixes`** \u2014 list of CLI verb prefixes this bridge owns (e.g. `["altium_"]`). All `altium_*` verbs route here. -- **`verbs`** \u2014 explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing. +- **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. +- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent "ensure ready" path AD/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. +- **`browser_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. -## Packaging + shipping +--- -Use the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties: +## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47) -```bash -# In your bridge repo: -python3 -c " -import zipfile, os, fnmatch -EXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*'] -with zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf: - for root, dirs, files in os.walk('.'): - for f in files: - rel = os.path.relpath(os.path.join(root, f), '.') - if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE): - continue - zf.write(os.path.join(root, f), rel) -" - -# Compute SHA256 of the zip -sha256sum my-bridge-v1.0.0.zip - -# Write the manifest the GUI polls -cat > my-bridge-manifest.json <<EOF -{ - "manifest_version": 1, - "version": "1.0.0", - "url": "https://your-host/my-bridge-v1.0.0.zip", - "sha256": "<sha256 from above>", - "size": <bytes>, - "released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" -} -EOF -``` +A bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's "what tools do I have") triggers a slow install at the worst moment. -Host both files anywhere reachable by HTTPS \u2014 GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry. +**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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). -## Installing +**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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 AD 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). AD 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. -Users install your bridge with one command: +**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:** -```bash -adom-desktop bridge_install '{"manifestUrl":"https://your-host/my-bridge-manifest.json"}' +| Level | Bridge(s) | Behavior | +|---|---|---| +| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not "installed" by AD. | +| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. | +| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). | +| **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 AD 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 AD prewarms + reports ``` +A bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python). -The GUI: -1. Fetches `my-bridge-manifest.json` -2. Downloads the referenced zip -3. SHA256-verifies against the manifest -4. Extracts to `%LOCALAPPDATA%\Adom Desktop\bridges-cache\<name>\` (the `name` comes from your `bridge.json`) -5. Rescans the runtime registry \u2014 your bridge is now discoverable via `bridge_list` +**The read-only probe: `bridge_readiness`.** AD 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. -The next CLI call to one of your `verbPrefixes` auto-spawns the bridge process. +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 AD-level aggregate that detects + summarizes across all bridges without spawning any. -## Updating +--- -Bump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users: +## The SKILL SET you should ship (not one SKILL.md — a small library) -```bash -adom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from -``` +Don't cram everything into one file. Ship **three kinds** of skill, each with a different reader: -(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work \u2014 tracking third-party manifest URLs in user state so they auto-refresh \u2014 lands in v1.8.x.) +1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong. +2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill. +3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.) -## Lifecycle controls (from cloud Docker, or local GUI) +Publish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1). -```bash -adom-desktop bridge_list # see everything -adom-desktop bridge_pause '{"name":"altium"}' # stop routing verbs -adom-desktop bridge_resume '{"name":"altium"}' -adom-desktop bridge_uninstall '{"name":"altium"}' # remove from cache -``` +**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply. -## Trust + security +--- -For v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust. +## USER-skill conventions — what every consumer skill must teach (from pup's shape) -Future work (v1.9.0+): -- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for "verified" status -- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field) -- Community submission flow on the wiki so contributors don't need a GitHub account +These eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship: -## Reference implementations +1. **Open with a "FIRST-TIME / COLD-START — read before you panic" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose). +2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{"wait":true}` to block. Gives every bridge a uniform "poll until ready, then proceed" loop. (Complements the runtime-side Prewarm section above.) +3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type ("take a screenshot of", "log into"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3). +4. **Include a short "Core verbs" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb. +5. **"Background by default — don't disrupt the user."** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`. +6. **Teach "`ok:true` is not enough" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently. +7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun. +8. **"Surface `_hint` verbatim."** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI. -Every bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a "fork this" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory \u2014 both humans browse it and the GUI's Browse tab parses it. +--- + +## Verb contract (quick reference) + +- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you. +- **Request:** AD POSTs `{ "command":"<verb>", "args":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`. +- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`. +- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI. + +## Health + status chip -**Hello-world starter templates** \u2014 fork these for a 5-minute "does this thing work?" loop: -- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` \u2014 ~80 lines, stdlib only, `kind:python` -- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` \u2014 ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary +Your `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline. -**Adom-shipped bridges** \u2014 canonical full-complexity examples; fork their layout for a real vendor integration: -- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` \u2014 Python, complex, multi-instance (one bridge per KiCad GUI exe) -- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` \u2014 Node, single-instance, recording-capable -- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` \u2014 Python, simpler, command-passthrough to Fusion add-in +## Lifecycle verbs (AD 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. + +--- -## Get listed +## Publish checklist (the whole modern flow) -Once your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** \u2014 that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths: +1. **Page** — `adom-wiki page create adom/adom-desktop-<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. +3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap). +4. **Discovery** — `discover triggers` to confirm; `discover preview "<a real user phrase>"` to confirm you surface. +5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback. +6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find "<phrase>"` surfaces you with an instructional brief. -1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the "Community bridges" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria. -2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert.+**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.
bridges-SDK.md+244−180@@ -1,238 +1,302 @@-# Writing a third-party Adom Desktop bridge +# Bridge SDK — build, publish, and surface a bridge for Adom Desktop -> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's "+ Bridge" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`. +A **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge: -v1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle. +1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it. +2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs. +3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *"open my app in pup"* surfaces YOU via `adom-wiki discover find`. -## Try it now — the `hello-python` and `hello-rust` sample bridges +> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.** +> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.** +> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.) -Two parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible. +--- -| Flavor | Manifest URL | Verb prefix | Port | When to fork | -|---|---|---|---|---| -| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work | -| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety | +## The boundary — what AD owns vs what YOU own -Both manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`. +The #1 mistake is filing AD bugs for bridge issues. The split: -### Install from the GUI +**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them). -1. Click **+ Bridge** in Adom Desktop's bridge bar -2. Paste one (or both) of the manifest URLs into the dialog, click **Install** -3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator) +**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**. -### Install both from the CLI +If a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD. -```bash -# Install the Python flavor -adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json"}' -adom-desktop hellopy_ping -# → {"success":true,"output":"Hello from the Python sample bridge v1.1.0!","language":"python",...} - -# Install the Rust flavor (can coexist with hello-python) -adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json"}' -adom-desktop hellors_ping -# → {"success":true,"output":"Hello from the Rust sample bridge v1.1.0!","language":"rust",...} - -# Echo through both -adom-desktop hellopy_echo '{"message":"world"}' -# → {"success":true,"output":"echo (python): world",...} -adom-desktop hellors_echo '{"message":"world"}' -# → {"success":true,"output":"echo (rust): world",...} - -# Both appear in bridge_list -adom-desktop bridge_list -# → bundled three + "hello-python" + "hello-rust" -``` +--- -### Uninstall +## Self-audit checklist — run this when asked to "audit against the SDK" -```bash -adom-desktop bridge_uninstall '{"name":"hello-python"}' -adom-desktop bridge_uninstall '{"name":"hello-rust"}' -``` +When a maintainer says *"audit your code, skills, README, and wiki page against the Bridge SDK,"* **this is the checklist.** Each item names its section below; fix every miss, then re-check. -Each bridge's cache dir + registry entry are removed; the chips disappear from the bar. +**bridge.json** (the manifest in your zip): +- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)* +- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.** +- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)* -The sources live at: -- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`) -- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB) +**Verbs & output** (what the AI actually reads): +- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)* +- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)* +- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)* +- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)* -Fork whichever flavor matches your target's natural ecosystem. +**Discovery** (can a plain request find you?): +- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)* +- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each. +- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION. -## Directory layout +**Skills & docs** (what the container + humans read): +- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)* +- [ ] Your USER skill leads with the cold-start "read before you panic" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)* +- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted). -``` -my-bridge/ -├── bridge.json ← REQUIRED — schema below -├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field) -├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint -├── handlers/ ← your code -│ ├── ... -└── README.md ← optional, recommended -``` +--- -## `bridge.json` schema +## bridge.json — the manifest that ships INSIDE your zip -```json +```jsonc { "manifest_version": 1, - "name": "altium", - "displayName": "Altium Designer", - "version": "1.0.0", - "description": "One-line description shown in bridge_list output.", - "homepage": "https://github.com/your-org/adom-bridge-altium", - "author": "Your Org", + "name": "blender", // registry name → verb prefix "blender_" + "displayName": "Blender", // in-app chip label (the tool's real name) + "version": "1.2.0", // numeric; a newer CACHE copy supersedes the bundled seed + "description": "Drive Blender headless + GUI", + "author": "you", "license": "MIT", + "docs": "https://wiki.adom.inc/adom/adom-desktop-blender-bridge", // your OWN page (drives skillPkg + hero) + "hero": "https://.../hero.png", // optional; else AD uses your wiki page hero, else a monogram "spawn": { - "kind": "python", - "entrypoint": "server.py", - "port": 0, - "healthEndpoint": "/status", - "stopMethod": "kill", - "killImageName": "python.exe" + "kind": "python", // python | node | exe | external-http (NOT "process") + "entrypoint": "server.py", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv + "port": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>. + "persistent": true, // AD's supervisor respawns it if it dies + "healthEndpoint": "/health", // INSIDE spawn. AD probes it for the chip (default /health) + "stopMethod": "kill", // kill | sigterm | graceful_endpoint + "killImageName": "python.exe" // image name for taskkill on stop (kind:python/node) }, - "verbPrefixes": ["altium_"], - "verbs": [ - "altium_open_project", - "altium_export_step", - "altium_run_drc" - ], - "dependencies": { - "python": ">=3.11", - "altium-designer": ">=24" + "verbPrefixes": ["blender_"], // any verb with these prefixes routes to you + "verbs": ["blender_render", "blender_status", "blender_describe"], // every verb (routing + Verbs tab) + "statusVerb": "blender_status", // verb a caller polls after a long op times out + "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json", + + // v1.9.47 cold-start (see "Cold-start tiers" below): + "detect": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it. + "hostApp": "Blender", // omit the whole `detect` block if your bridge brings its own runtime + "appPathsExe": "blender.exe", // (e.g. pup ships its own browser → no host app) + "paths": { "windows": ["%ProgramFiles%\\Blender Foundation\\*\\blender.exe"] } }, - "category": "EDA", - "tags": ["pcb", "altium", "commercial"] + "prewarm": { "assets": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. ["chrome-for-testing"]) } ``` -### Field reference +- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `"process"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.) +- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md). +- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`. +- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.) +- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page. +- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.) +- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in "Cold-start tiers" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser. -- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry. -- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel. -- **`version`** (required, semver). Each ship bumps this. -- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge. - - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings. - - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.). - - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine. -- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`). -- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge "won't start" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly. -- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime. -- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint). -- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name). -- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `["altium_"]`). All `altium_*` verbs route here. -- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing. +--- -## Packaging + shipping +## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest -Use the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties: +This is the code AD streams onto the user's machine. Two files on your page: -```bash -# In your bridge repo: -python3 -c " -import zipfile, os, fnmatch -EXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*'] -with zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf: - for root, dirs, files in os.walk('.'): - for f in files: - rel = os.path.relpath(os.path.join(root, f), '.') - if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE): - continue - zf.write(os.path.join(root, f), rel) -" - -# Compute SHA256 of the zip -sha256sum my-bridge-v1.0.0.zip - -# Write the manifest the GUI polls -cat > my-bridge-manifest.json <<EOF -{ - "manifest_version": 1, - "version": "1.0.0", - "url": "https://your-host/my-bridge-v1.0.0.zip", - "sha256": "<sha256 from above>", - "size": <bytes>, - "released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" -} -EOF -``` +1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob: + ```bash + adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title "..." --changelog "..." + adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any + ``` + (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.) +2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`: + ```jsonc + { "name":"blender", "version":"1.2.0", + "url":"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip", // base-relative, extension-agnostic (.bin works where wiki blocks .zip) + "sha256":"…", "size":123456, "verbPrefixes":["blender_"], "healthEndpoint":"/health", + "updateManifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json" } + ``` -Host both files anywhere reachable by HTTPS - GitHub Pages, your own CDN, or the Adom Wiki. +**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\Adom Desktop\bridges-cache\<name>\`, rescans, and remembers the URL for auto-update. -> ⚠️ **Hosting on the Adom Wiki: the files API silently rejects `.zip`** (the upload returns `ok:true` -> but the file never lands; a `.png` through the same path commits fine). Host the zip **bytes under a -> `.bin` name** (`my-bridge-v1.0.0.bin`) and point the manifest `url` at that - AD GETs the `url` and -> unzips it regardless of extension. And give AD the **canonical owner-scoped file URL**, -> `https://wiki.adom.inc/api/pages/<owner>/<slug>/files/<file>`, NOT the legacy `/api/v1/pages/<slug>/...` -> alias (it still resolves but is fragile). Note `bridge.json` must sit at the **zip root**, so build the -> zip from your bridge dir's contents, not a parent folder. The full Adom-Wiki publish flow (with the -> `.bin` upload via `adom-wiki api`) is the **shipping-a-bridge** skill on this page. +**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware). -## Installing +--- -Users install your bridge with one command: +## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`) + +Your runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries): ```bash -adom-desktop bridge_install '{"manifestUrl":"https://your-host/my-bridge-manifest.json"}' +# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and +# package.json {name:"adom-desktop-<bridge>-bridge", version, discovery_triggers, files:[...]}. +# THREE traps that bite — verify each with `tar tzf` before publishing: +# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE, +# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH +# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else +# every user who installs your pkg also installs your dev/publish docs. +# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files` +# allowlist needs the GLOB: a bare "skills" entry ships ZERO skill files — use "skills/**". +# (Set .gitignore aside across pack+publish, then restore.) +# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB +# before guards). Skills + docs only. +adom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present, + # ZERO binaries, ZERO dev/publish SKILL.md +adom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge ``` -The GUI: -1. Fetches `my-bridge-manifest.json` -2. Downloads the referenced zip -3. SHA256-verifies against the manifest -4. Extracts to `%LOCALAPPDATA%\Adom Desktop\bridges-cache\<name>\` (the `name` comes from your `bridge.json`) -5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list` +A container gets it **without anyone asking**: +- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it. +- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill. +- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them. + +**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh. -The next CLI call to one of your `verbPrefixes` auto-spawns the bridge process. +--- -## Updating +## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you -Bump `bridge.json`'s `version` + `BRIDGE_VERSION` (and `package.json` `version` if you ship one), rebuild -the zip, re-upload, update the manifest. Users pick it up with: +The wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs. ```bash -adom-desktop refresh_bridges '{"name":"<your-bridge>"}' # omit name to refresh all +adom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add "open my pcb" "export gerbers" # add triggers +adom-wiki discover preview --json "open my pcb design" # what would surface + the SCORE + WHY +adom-wiki discover find "screenshot my website" # the live find an AI runs ``` -**Third-party bridges auto-update from their OWN manifest URL (AD 1.9.26+).** `bridge_install` persists the -`manifestUrl` you installed from, so `refresh_bridges` (and AD's on-launch + 4-hourly auto-sync) polls -*your* page too, not just the bundled core bridges. And **AD 1.9.27+ self-reaps the running bridge** and -respawns it on the new code, so **no manual `bridge_kill` is needed** - the result is -`action:"updated", from, to`, and the bridge respawns on the next verb call. Verify with -`bridge_check_updates` (your bridge shows `wikiHosted:true`, `current` == the new version). If it shows -`wikiHosted:false` / `latest:null`, AD has no tracked URL for it yet - re-run `bridge_install` once from -the canonical manifest URL to register it. +**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):** +- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it. +- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title ("Adom Desktop - Blender Bridge") + a matching tag scored **ZERO** for the literal query *"blender"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.) +- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query. -## Lifecycle controls (from cloud Docker, or local GUI) +**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *"open my pcb design"* / *"export gerber files"* / *"design a circuit board"* — NOT *"reverse bridge"* / *"kicad_bridge_call"* / *"fork the kicad bridge"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy "pcb"/"export" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end. -```bash -adom-desktop bridge_list # see everything -adom-desktop bridge_pause '{"name":"altium"}' # stop routing verbs -adom-desktop bridge_resume '{"name":"altium"}' -adom-desktop bridge_uninstall '{"name":"altium"}' # remove from cache +**VET it — never ship triggers you didn't test:** +1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`). +2. `adom-wiki discover preview --json "<each query>"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.) +3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on "pcb"), add MORE exact phrasings so your 3×-per-trigger total wins. + +**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. "for logged-in form-filling, install the native-browser extension") — rather than claim it and do it badly. + +- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline. +- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`)."* + +### The end-to-end flow your three artifacts enable + +User says *"show me my app in pup"* → the AI: +1. `adom-wiki discover find "show me my app in pup"` → your bridge surfaces (Artifact 3). +2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1). +3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2). +4. Drive your verbs — guided by the **rich hints you return** (the principle at the top). + +--- + +## 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. +- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent "ensure ready" path AD/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. +- **`browser_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 **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's "what tools do I have") triggers a slow install at the worst moment. + +**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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, …): AD 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 AD 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). AD 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 Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not "installed" by AD. | +| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. | +| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). | +| **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 AD 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 AD prewarms + reports ``` +A bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python). + +**The read-only probe: `bridge_readiness`.** AD 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 AD-level aggregate that detects + summarizes across all bridges without spawning any. + +--- + +## The SKILL SET you should ship (not one SKILL.md — a small library) + +Don't cram everything into one file. Ship **three kinds** of skill, each with a different reader: + +1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong. +2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill. +3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.) + +Publish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1). + +**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply. + +--- + +## USER-skill conventions — what every consumer skill must teach (from pup's shape) + +These eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship: + +1. **Open with a "FIRST-TIME / COLD-START — read before you panic" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose). +2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{"wait":true}` to block. Gives every bridge a uniform "poll until ready, then proceed" loop. (Complements the runtime-side Prewarm section above.) +3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type ("take a screenshot of", "log into"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3). +4. **Include a short "Core verbs" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb. +5. **"Background by default — don't disrupt the user."** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`. +6. **Teach "`ok:true` is not enough" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently. +7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun. +8. **"Surface `_hint` verbatim."** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI. + +--- + +## Verb contract (quick reference) + +- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you. +- **Request:** AD POSTs `{ "command":"<verb>", "args":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`. +- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`. +- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI. -## Trust + security +## Health + status chip -For v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust. +Your `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline. -Future work (v1.9.0+): -- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for "verified" status -- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field) -- `bridges-registry.json` on the Adom Wiki listing community-vetted bridges +## Lifecycle verbs (AD gives these to USERS — know them to guide users) -## Reference implementations +`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. -**Hello-world starter templates** — fork these for a 5-minute "does this thing work?" loop: -- **hello-python**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python` -- **hello-rust**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary +--- -**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration: -- **kicad**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe) -- **puppeteer**: `plugins/puppeteer/` — Node, single-instance, recording-capable -- **fusion360**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in +## Publish checklist (the whole modern flow) -## Get listed +1. **Page** — `adom-wiki page create adom/adom-desktop-<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. +3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap). +4. **Discovery** — `discover triggers` to confirm; `discover preview "<a real user phrase>"` to confirm you surface. +5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback. +6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find "<phrase>"` surfaces you with an instructional brief. -Once your bridge is stable: open a PR adding an entry to `plugins/BRIDGES.md` (this file) so other users find it. Or once we set up the wiki `bridges-registry.json`, add yours there. +**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.
page.json+6−6@@ -4,7 +4,7 @@ "slug": "adom-desktop-bridges", "title": "Adom Desktop - Bridge SDK", "brief": "Bridge SDK guide: bridge.json schema, kind:python/node/exe, hello-python + hello-rust reference templates, packaging + lifecycle commands.", - "version": "1.1.3", + "version": "1.1.4", "tags": [], "license": "MIT", "discovery_triggers": [ @@ -56,7 +56,7 @@ "install_hint": "", "version_cmd": "" }, - "readme": "# Adom Desktop - Bridge SDK\n\n## Build your own bridge\n\nA **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere — including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge.\n\nThe fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below.\n\n### Starting prompt (paste into a cloud Claude session)\n\n```\nI want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>.\n\nA bridge is a small local HTTP server described by a bridge.json; AD spawns it on a\nstable port and relays verbs to it. Before writing anything, study the reference\nbridges for design ideas and copy the conventions you see:\n\n - START SIMPLE — read the two \"hello\" reference bridges first (one Rust, one\n Python). They are the minimal, correct shape: a /command POST handler, a /status\n health endpoint that self-reports its LED, and a bridge.json. Mirror them.\n - THEN study an advanced bridge for the patterns you need: a long-running native\n app (KiCad / Fusion 360), a process you supervise, or an external client that\n dials back in (native-browser). Steal their bridge.json fields and verb shapes.\n\nMy bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>.\n\nFollow the Adom Desktop Bridge SDK conventions on this page:\n - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure\n renderer — it never invents your status).\n - Return rich, structured JSON with an _hint on every response so the calling AI\n knows what to do next; on a slow operation, return a statusVerb to poll.\n - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will\n relay it so a timeout reads as \"still running, poll this\", not \"failed\").\n\nWhen it works locally, package it with release-bridge.sh and tell me the\nbridge_install command so I can install it on my Adom Desktop from here.\n```\n\nClaude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time.\n\n### Reference bridges — copy these\n\nStart with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts.\n\n| Bridge | Language | Why read it |\n|---|---|---|\n| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. |\n| **hello-rust** | Rust | Same minimal shape in Rust — single-binary, no runtime deps. |\n| **native-browser** | — | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. |\n| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. |\n\nThe hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index).\n\n### The whole dev loop is remote — install & uninstall from anywhere\n\nThis is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI:\n\n```bash\n# Install (or update) a bridge from its published wiki manifest:\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/<your-bridge-page>/files/<your>-bridge-manifest.json\"}'\n\n# It appears in AD's Bridges panel immediately and starts on first verb call.\nadom-desktop bridge_list # see it registered + its LED\nadom-desktop <your_verb> '{...}' # call it — AD spawns + relays\n\n# Iterate: re-publish a higher version, then re-install — AD reaps the old\n# instance and streams the new one onto the SAME stable port (no port churn).\nadom-desktop bridge_install '{\"manifestUrl\":\"...\",\"force\":true}'\n\n# Remove it cleanly when done:\nadom-desktop bridge_uninstall '{\"name\":\"<your-bridge>\"}'\n```\n\nIn the AD GUI, the same flows are one click: **\"+ Bridge\"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below).\n\nThat's the full toolchain — author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop.\n\n---\n\n\n\n\n\n\n\n\n> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's \"+ Bridge\" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`.\n\nv1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle.\n\n## Try it now — the `hello-python` and `hello-rust` sample bridges\n\nTwo parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible.\n\n| Flavor | Manifest URL | Verb prefix | Port | When to fork |\n|---|---|---|---|---|\n| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work |\n| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety |\n\nBoth manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`.\n\n### Install from the GUI\n\n1. Click **+ Bridge** in Adom Desktop's bridge bar\n2. Paste one (or both) of the manifest URLs into the dialog, click **Install**\n3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator)\n\n### Install both from the CLI\n\n```bash\n# Install the Python flavor\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json\"}'\nadom-desktop hellopy_ping\n# → {\"success\":true,\"output\":\"Hello from the Python sample bridge v1.1.0!\",\"language\":\"python\",...}\n\n# Install the Rust flavor (can coexist with hello-python)\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json\"}'\nadom-desktop hellors_ping\n# → {\"success\":true,\"output\":\"Hello from the Rust sample bridge v1.1.0!\",\"language\":\"rust\",...}\n\n# Echo through both\nadom-desktop hellopy_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (python): world\",...}\nadom-desktop hellors_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (rust): world\",...}\n\n# Both appear in bridge_list\nadom-desktop bridge_list\n# → bundled three + \"hello-python\" + \"hello-rust\"\n```\n\n### Uninstall\n\n```bash\nadom-desktop bridge_uninstall '{\"name\":\"hello-python\"}'\nadom-desktop bridge_uninstall '{\"name\":\"hello-rust\"}'\n```\n\nEach bridge's cache dir + registry entry are removed; the chips disappear from the bar.\n\nThe sources live at:\n- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`)\n- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB)\n\nFork whichever flavor matches your target's natural ecosystem.\n\n## Directory layout\n\n```\nmy-bridge/\n├── bridge.json ← REQUIRED — schema below\n├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field)\n├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint\n├── handlers/ ← your code\n│ ├── ...\n└── README.md ← optional, recommended\n```\n\n## `bridge.json` schema\n\n```json\n{\n \"manifest_version\": 1,\n \"name\": \"altium\",\n \"displayName\": \"Altium Designer\",\n \"version\": \"1.0.0\",\n \"description\": \"One-line description shown in bridge_list output.\",\n \"homepage\": \"https://github.com/your-org/adom-bridge-altium\",\n \"author\": \"Your Org\",\n \"license\": \"MIT\",\n \"spawn\": {\n \"kind\": \"python\",\n \"entrypoint\": \"server.py\",\n \"port\": 0,\n \"healthEndpoint\": \"/status\",\n \"stopMethod\": \"kill\",\n \"killImageName\": \"python.exe\"\n },\n \"verbPrefixes\": [\"altium_\"],\n \"verbs\": [\n \"altium_open_project\",\n \"altium_export_step\",\n \"altium_run_drc\"\n ],\n \"dependencies\": {\n \"python\": \">=3.11\",\n \"altium-designer\": \">=24\"\n },\n \"category\": \"EDA\",\n \"tags\": [\"pcb\", \"altium\", \"commercial\"]\n}\n```\n\n### Field reference\n\n- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry.\n- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel.\n- **`version`** (required, semver). Each ship bumps this.\n- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge.\n - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings.\n - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.).\n - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine.\n- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`).\n- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge \"won't start\" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly.\n- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime.\n- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint).\n- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name).\n- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `[\"altium_\"]`). All `altium_*` verbs route here.\n- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing.\n\n## Packaging + shipping\n\nUse the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties:\n\n```bash\n# In your bridge repo:\npython3 -c \"\nimport zipfile, os, fnmatch\nEXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*']\nwith zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk('.'):\n for f in files:\n rel = os.path.relpath(os.path.join(root, f), '.')\n if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE):\n continue\n zf.write(os.path.join(root, f), rel)\n\"\n\n# Compute SHA256 of the zip\nsha256sum my-bridge-v1.0.0.zip\n\n# Write the manifest the GUI polls\ncat > my-bridge-manifest.json <<EOF\n{\n \"manifest_version\": 1,\n \"version\": \"1.0.0\",\n \"url\": \"https://your-host/my-bridge-v1.0.0.zip\",\n \"sha256\": \"<sha256 from above>\",\n \"size\": <bytes>,\n \"released_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n}\nEOF\n```\n\nHost both files anywhere reachable by HTTPS — GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry.\n\n## Installing\n\nUsers install your bridge with one command:\n\n```bash\nadom-desktop bridge_install '{\"manifestUrl\":\"https://your-host/my-bridge-manifest.json\"}'\n```\n\nThe GUI:\n1. Fetches `my-bridge-manifest.json`\n2. Downloads the referenced zip\n3. SHA256-verifies against the manifest\n4. Extracts to `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\` (the `name` comes from your `bridge.json`)\n5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list`\n\nThe next CLI call to one of your `verbPrefixes` auto-spawns the bridge process.\n\n## Updating\n\nBump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users:\n\n```bash\nadom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from\n```\n\n(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work — tracking third-party manifest URLs in user state so they auto-refresh — lands in v1.8.x.)\n\n## Lifecycle controls (from cloud Docker, or local GUI)\n\n```bash\nadom-desktop bridge_list # see everything\nadom-desktop bridge_pause '{\"name\":\"altium\"}' # stop routing verbs\nadom-desktop bridge_resume '{\"name\":\"altium\"}'\nadom-desktop bridge_uninstall '{\"name\":\"altium\"}' # remove from cache\n```\n\n## Trust + security\n\nFor v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust.\n\nFuture work (v1.9.0+):\n- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for \"verified\" status\n- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field)\n- Community submission flow on the wiki so contributors don't need a GitHub account\n\n## Reference implementations\n\nEvery bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a \"fork this\" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory — both humans browse it and the GUI's Browse tab parses it.\n\n**Hello-world starter templates** — fork these for a 5-minute \"does this thing work?\" loop:\n- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python`\n- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary\n\n**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration:\n- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe)\n- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` — Node, single-instance, recording-capable\n- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in\n\n## Get listed\n\nOnce your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** — that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths:\n\n1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the \"Community bridges\" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria.\n2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert.", + "readme": "# Bridge SDK — build, publish, and surface a bridge for Adom Desktop\n\nA **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:\n\n1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it.\n2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs.\n3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *\"open my app in pup\"* surfaces YOU via `adom-wiki discover find`.\n\n> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.**\n> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.**\n> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)\n\n---\n\n## The boundary — what AD owns vs what YOU own\n\nThe #1 mistake is filing AD bugs for bridge issues. The split:\n\n**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them).\n\n**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**.\n\nIf a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.\n\n---\n\n## Self-audit checklist — run this when asked to \"audit against the SDK\"\n\nWhen a maintainer says *\"audit your code, skills, README, and wiki page against the Bridge SDK,\"* **this is the checklist.** Each item names its section below; fix every miss, then re-check.\n\n**bridge.json** (the manifest in your zip):\n- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)*\n- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.**\n- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)*\n\n**Verbs & output** (what the AI actually reads):\n- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)*\n- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)*\n- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)*\n- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)*\n\n**Discovery** (can a plain request find you?):\n- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)*\n- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each.\n- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION.\n\n**Skills & docs** (what the container + humans read):\n- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)*\n- [ ] Your USER skill leads with the cold-start \"read before you panic\" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)*\n- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).\n\n---\n\n## bridge.json — the manifest that ships INSIDE your zip\n\n```jsonc\n{\n \"manifest_version\": 1,\n \"name\": \"blender\", // registry name → verb prefix \"blender_\"\n \"displayName\": \"Blender\", // in-app chip label (the tool's real name)\n \"version\": \"1.2.0\", // numeric; a newer CACHE copy supersedes the bundled seed\n \"description\": \"Drive Blender headless + GUI\",\n \"author\": \"you\",\n \"license\": \"MIT\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop-blender-bridge\", // your OWN page (drives skillPkg + hero)\n \"hero\": \"https://.../hero.png\", // optional; else AD uses your wiki page hero, else a monogram\n \"spawn\": {\n \"kind\": \"python\", // python | node | exe | external-http (NOT \"process\")\n \"entrypoint\": \"server.py\", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv\n \"port\": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.\n \"persistent\": true, // AD's supervisor respawns it if it dies\n \"healthEndpoint\": \"/health\", // INSIDE spawn. AD probes it for the chip (default /health)\n \"stopMethod\": \"kill\", // kill | sigterm | graceful_endpoint\n \"killImageName\": \"python.exe\" // image name for taskkill on stop (kind:python/node)\n },\n \"verbPrefixes\": [\"blender_\"], // any verb with these prefixes routes to you\n \"verbs\": [\"blender_render\", \"blender_status\", \"blender_describe\"], // every verb (routing + Verbs tab)\n \"statusVerb\": \"blender_status\", // verb a caller polls after a long op times out\n \"updateManifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\",\n\n // v1.9.47 cold-start (see \"Cold-start tiers\" below):\n \"detect\": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.\n \"hostApp\": \"Blender\", // omit the whole `detect` block if your bridge brings its own runtime\n \"appPathsExe\": \"blender.exe\", // (e.g. pup ships its own browser → no host app)\n \"paths\": { \"windows\": [\"%ProgramFiles%\\\\Blender Foundation\\\\*\\\\blender.exe\"] }\n },\n \"prewarm\": { \"assets\": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. [\"chrome-for-testing\"])\n}\n```\n\n- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `\"process\"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.)\n- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).\n- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`.\n- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.)\n- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page.\n- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.)\n- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in \"Cold-start tiers\" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser.\n\n---\n\n## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest\n\nThis is the code AD streams onto the user's machine. Two files on your page:\n\n1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob:\n ```bash\n adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title \"...\" --changelog \"...\"\n adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any\n ```\n (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.)\n2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`:\n ```jsonc\n { \"name\":\"blender\", \"version\":\"1.2.0\",\n \"url\":\"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip\", // base-relative, extension-agnostic (.bin works where wiki blocks .zip)\n \"sha256\":\"…\", \"size\":123456, \"verbPrefixes\":[\"blender_\"], \"healthEndpoint\":\"/health\",\n \"updateManifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\" }\n ```\n\n**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\`, rescans, and remembers the URL for auto-update.\n\n**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware).\n\n---\n\n## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`)\n\nYour runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries):\n\n```bash\n# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and\n# package.json {name:\"adom-desktop-<bridge>-bridge\", version, discovery_triggers, files:[...]}.\n# THREE traps that bite — verify each with `tar tzf` before publishing:\n# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE,\n# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH\n# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else\n# every user who installs your pkg also installs your dev/publish docs.\n# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files`\n# allowlist needs the GLOB: a bare \"skills\" entry ships ZERO skill files — use \"skills/**\".\n# (Set .gitignore aside across pack+publish, then restore.)\n# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB\n# before guards). Skills + docs only.\nadom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present,\n # ZERO binaries, ZERO dev/publish SKILL.md\nadom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge\n```\n\nA container gets it **without anyone asking**:\n- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it.\n- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.\n- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them.\n\n**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh.\n\n---\n\n## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you\n\nThe wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs.\n\n```bash\nadom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add \"open my pcb\" \"export gerbers\" # add triggers\nadom-wiki discover preview --json \"open my pcb design\" # what would surface + the SCORE + WHY\nadom-wiki discover find \"screenshot my website\" # the live find an AI runs\n```\n\n**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):**\n- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it.\n- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title (\"Adom Desktop - Blender Bridge\") + a matching tag scored **ZERO** for the literal query *\"blender\"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.)\n- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.\n\n**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *\"open my pcb design\"* / *\"export gerber files\"* / *\"design a circuit board\"* — NOT *\"reverse bridge\"* / *\"kicad_bridge_call\"* / *\"fork the kicad bridge\"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy \"pcb\"/\"export\" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end.\n\n**VET it — never ship triggers you didn't test:**\n1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`).\n2. `adom-wiki discover preview --json \"<each query>\"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.)\n3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on \"pcb\"), add MORE exact phrasings so your 3×-per-trigger total wins.\n\n**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. \"for logged-in form-filling, install the native-browser extension\") — rather than claim it and do it badly.\n\n- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline.\n- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *\"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`).\"*\n\n### The end-to-end flow your three artifacts enable\n\nUser says *\"show me my app in pup\"* → the AI:\n1. `adom-wiki discover find \"show me my app in pup\"` → your bridge surfaces (Artifact 3).\n2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1).\n3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2).\n4. Drive your verbs — guided by the **rich hints you return** (the principle at the top).\n\n---\n\n## Prewarm — make the first call instant\n\nA 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:\n\n- **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.\n- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.\n- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent \"ensure ready\" path AD/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.\n- **`browser_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.\n\n---\n\n## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)\n\nA bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's \"what tools do I have\") triggers a slow install at the worst moment.\n\n**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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).\n\n**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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:\n- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default AD 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). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a \"go away.\"\n- **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.\n\n**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**\n\n| Level | Bridge(s) | Behavior |\n|---|---|---|\n| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not \"installed\" by AD. |\n| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |\n| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). |\n| **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. |\n| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |\n\nThe 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).\n\n**Declare both in `bridge.json`:**\n```json\n\"detect\": { // Tier 2 — how AD finds your HOST APP (detection only)\n \"hostApp\": \"KiCad\",\n \"appPathsExe\": \"kicad.exe\", // optional: Windows App Paths registry lookup (most reliable)\n \"paths\": { // optional: candidate paths, %VAR% expanded, one * = a version dir\n \"windows\": [\"%ProgramFiles%\\\\KiCad\\\\*\\\\bin\\\\kicad.exe\"],\n \"macos\": [\"/Applications/KiCad/KiCad.app\"],\n \"linux\": [\"/usr/bin/kicad\"]\n }\n},\n\"prewarm\": { \"assets\": [\"chrome-for-testing\"] } // Tier 1 — heavy runtime assets AD prewarms + reports\n```\nA bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python).\n\n**The read-only probe: `bridge_readiness`.** AD 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.\n\nYour 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 AD-level aggregate that detects + summarizes across all bridges without spawning any.\n\n---\n\n## The SKILL SET you should ship (not one SKILL.md — a small library)\n\nDon't cram everything into one file. Ship **three kinds** of skill, each with a different reader:\n\n1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong.\n2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill.\n3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.)\n\nPublish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1).\n\n**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply.\n\n---\n\n## USER-skill conventions — what every consumer skill must teach (from pup's shape)\n\nThese eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:\n\n1. **Open with a \"FIRST-TIME / COLD-START — read before you panic\" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose).\n2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{\"wait\":true}` to block. Gives every bridge a uniform \"poll until ready, then proceed\" loop. (Complements the runtime-side Prewarm section above.)\n3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type (\"take a screenshot of\", \"log into\"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3).\n4. **Include a short \"Core verbs\" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb.\n5. **\"Background by default — don't disrupt the user.\"** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`.\n6. **Teach \"`ok:true` is not enough\" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently.\n7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun.\n8. **\"Surface `_hint` verbatim.\"** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.\n\n---\n\n## Verb contract (quick reference)\n\n- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you.\n- **Request:** AD POSTs `{ \"command\":\"<verb>\", \"args\":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`.\n- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`.\n- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI.\n\n## Health + status chip\n\nYour `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.\n\n## Lifecycle verbs (AD gives these to USERS — know them to guide users)\n\n`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.\n\n---\n\n## Publish checklist (the whole modern flow)\n\n1. **Page** — `adom-wiki page create adom/adom-desktop-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.\n2. **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.\n3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap).\n4. **Discovery** — `discover triggers` to confirm; `discover preview \"<a real user phrase>\"` to confirm you surface.\n5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.\n6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find \"<phrase>\"` surfaces you with an instructional brief.\n\n**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.\n", "author": { "id": "695820315b5f1e4db2fcf602", "name": "Kyle Bergstedt", @@ -75,8 +75,8 @@ "skills": [], "author_name": "John Lauer", "changelog": "Set descriptive title to the Adom Desktop - <name> naming convention for app-index consistency", - "content": "# Adom Desktop - Bridge SDK\n\n## Build your own bridge\n\nA **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere — including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge.\n\nThe fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below.\n\n### Starting prompt (paste into a cloud Claude session)\n\n```\nI want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>.\n\nA bridge is a small local HTTP server described by a bridge.json; AD spawns it on a\nstable port and relays verbs to it. Before writing anything, study the reference\nbridges for design ideas and copy the conventions you see:\n\n - START SIMPLE — read the two \"hello\" reference bridges first (one Rust, one\n Python). They are the minimal, correct shape: a /command POST handler, a /status\n health endpoint that self-reports its LED, and a bridge.json. Mirror them.\n - THEN study an advanced bridge for the patterns you need: a long-running native\n app (KiCad / Fusion 360), a process you supervise, or an external client that\n dials back in (native-browser). Steal their bridge.json fields and verb shapes.\n\nMy bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>.\n\nFollow the Adom Desktop Bridge SDK conventions on this page:\n - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure\n renderer — it never invents your status).\n - Return rich, structured JSON with an _hint on every response so the calling AI\n knows what to do next; on a slow operation, return a statusVerb to poll.\n - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will\n relay it so a timeout reads as \"still running, poll this\", not \"failed\").\n\nWhen it works locally, package it with release-bridge.sh and tell me the\nbridge_install command so I can install it on my Adom Desktop from here.\n```\n\nClaude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time.\n\n### Reference bridges — copy these\n\nStart with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts.\n\n| Bridge | Language | Why read it |\n|---|---|---|\n| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. |\n| **hello-rust** | Rust | Same minimal shape in Rust — single-binary, no runtime deps. |\n| **native-browser** | — | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. |\n| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. |\n\nThe hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index).\n\n### The whole dev loop is remote — install & uninstall from anywhere\n\nThis is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI:\n\n```bash\n# Install (or update) a bridge from its published wiki manifest:\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/<your-bridge-page>/files/<your>-bridge-manifest.json\"}'\n\n# It appears in AD's Bridges panel immediately and starts on first verb call.\nadom-desktop bridge_list # see it registered + its LED\nadom-desktop <your_verb> '{...}' # call it — AD spawns + relays\n\n# Iterate: re-publish a higher version, then re-install — AD reaps the old\n# instance and streams the new one onto the SAME stable port (no port churn).\nadom-desktop bridge_install '{\"manifestUrl\":\"...\",\"force\":true}'\n\n# Remove it cleanly when done:\nadom-desktop bridge_uninstall '{\"name\":\"<your-bridge>\"}'\n```\n\nIn the AD GUI, the same flows are one click: **\"+ Bridge\"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below).\n\nThat's the full toolchain — author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop.\n\n---\n\n\n\n\n\n\n\n\n> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's \"+ Bridge\" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`.\n\nv1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle.\n\n## Try it now — the `hello-python` and `hello-rust` sample bridges\n\nTwo parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible.\n\n| Flavor | Manifest URL | Verb prefix | Port | When to fork |\n|---|---|---|---|---|\n| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work |\n| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety |\n\nBoth manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`.\n\n### Install from the GUI\n\n1. Click **+ Bridge** in Adom Desktop's bridge bar\n2. Paste one (or both) of the manifest URLs into the dialog, click **Install**\n3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator)\n\n### Install both from the CLI\n\n```bash\n# Install the Python flavor\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json\"}'\nadom-desktop hellopy_ping\n# → {\"success\":true,\"output\":\"Hello from the Python sample bridge v1.1.0!\",\"language\":\"python\",...}\n\n# Install the Rust flavor (can coexist with hello-python)\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json\"}'\nadom-desktop hellors_ping\n# → {\"success\":true,\"output\":\"Hello from the Rust sample bridge v1.1.0!\",\"language\":\"rust\",...}\n\n# Echo through both\nadom-desktop hellopy_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (python): world\",...}\nadom-desktop hellors_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (rust): world\",...}\n\n# Both appear in bridge_list\nadom-desktop bridge_list\n# → bundled three + \"hello-python\" + \"hello-rust\"\n```\n\n### Uninstall\n\n```bash\nadom-desktop bridge_uninstall '{\"name\":\"hello-python\"}'\nadom-desktop bridge_uninstall '{\"name\":\"hello-rust\"}'\n```\n\nEach bridge's cache dir + registry entry are removed; the chips disappear from the bar.\n\nThe sources live at:\n- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`)\n- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB)\n\nFork whichever flavor matches your target's natural ecosystem.\n\n## Directory layout\n\n```\nmy-bridge/\n├── bridge.json ← REQUIRED — schema below\n├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field)\n├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint\n├── handlers/ ← your code\n│ ├── ...\n└── README.md ← optional, recommended\n```\n\n## `bridge.json` schema\n\n```json\n{\n \"manifest_version\": 1,\n \"name\": \"altium\",\n \"displayName\": \"Altium Designer\",\n \"version\": \"1.0.0\",\n \"description\": \"One-line description shown in bridge_list output.\",\n \"homepage\": \"https://github.com/your-org/adom-bridge-altium\",\n \"author\": \"Your Org\",\n \"license\": \"MIT\",\n \"spawn\": {\n \"kind\": \"python\",\n \"entrypoint\": \"server.py\",\n \"port\": 0,\n \"healthEndpoint\": \"/status\",\n \"stopMethod\": \"kill\",\n \"killImageName\": \"python.exe\"\n },\n \"verbPrefixes\": [\"altium_\"],\n \"verbs\": [\n \"altium_open_project\",\n \"altium_export_step\",\n \"altium_run_drc\"\n ],\n \"dependencies\": {\n \"python\": \">=3.11\",\n \"altium-designer\": \">=24\"\n },\n \"category\": \"EDA\",\n \"tags\": [\"pcb\", \"altium\", \"commercial\"]\n}\n```\n\n### Field reference\n\n- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry.\n- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel.\n- **`version`** (required, semver). Each ship bumps this.\n- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge.\n - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings.\n - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.).\n - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine.\n- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`).\n- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge \"won't start\" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly.\n- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime.\n- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint).\n- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name).\n- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `[\"altium_\"]`). All `altium_*` verbs route here.\n- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing.\n\n## Packaging + shipping\n\nUse the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties:\n\n```bash\n# In your bridge repo:\npython3 -c \"\nimport zipfile, os, fnmatch\nEXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*']\nwith zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk('.'):\n for f in files:\n rel = os.path.relpath(os.path.join(root, f), '.')\n if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE):\n continue\n zf.write(os.path.join(root, f), rel)\n\"\n\n# Compute SHA256 of the zip\nsha256sum my-bridge-v1.0.0.zip\n\n# Write the manifest the GUI polls\ncat > my-bridge-manifest.json <<EOF\n{\n \"manifest_version\": 1,\n \"version\": \"1.0.0\",\n \"url\": \"https://your-host/my-bridge-v1.0.0.zip\",\n \"sha256\": \"<sha256 from above>\",\n \"size\": <bytes>,\n \"released_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n}\nEOF\n```\n\nHost both files anywhere reachable by HTTPS — GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry.\n\n## Installing\n\nUsers install your bridge with one command:\n\n```bash\nadom-desktop bridge_install '{\"manifestUrl\":\"https://your-host/my-bridge-manifest.json\"}'\n```\n\nThe GUI:\n1. Fetches `my-bridge-manifest.json`\n2. Downloads the referenced zip\n3. SHA256-verifies against the manifest\n4. Extracts to `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\` (the `name` comes from your `bridge.json`)\n5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list`\n\nThe next CLI call to one of your `verbPrefixes` auto-spawns the bridge process.\n\n## Updating\n\nBump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users:\n\n```bash\nadom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from\n```\n\n(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work — tracking third-party manifest URLs in user state so they auto-refresh — lands in v1.8.x.)\n\n## Lifecycle controls (from cloud Docker, or local GUI)\n\n```bash\nadom-desktop bridge_list # see everything\nadom-desktop bridge_pause '{\"name\":\"altium\"}' # stop routing verbs\nadom-desktop bridge_resume '{\"name\":\"altium\"}'\nadom-desktop bridge_uninstall '{\"name\":\"altium\"}' # remove from cache\n```\n\n## Trust + security\n\nFor v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust.\n\nFuture work (v1.9.0+):\n- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for \"verified\" status\n- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field)\n- Community submission flow on the wiki so contributors don't need a GitHub account\n\n## Reference implementations\n\nEvery bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a \"fork this\" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory — both humans browse it and the GUI's Browse tab parses it.\n\n**Hello-world starter templates** — fork these for a 5-minute \"does this thing work?\" loop:\n- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python`\n- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary\n\n**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration:\n- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe)\n- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` — Node, single-instance, recording-capable\n- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in\n\n## Get listed\n\nOnce your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** — that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths:\n\n1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the \"Community bridges\" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria.\n2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert.", - "body_md": "# Adom Desktop - Bridge SDK\n\n## Build your own bridge\n\nA **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere — including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge.\n\nThe fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below.\n\n### Starting prompt (paste into a cloud Claude session)\n\n```\nI want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>.\n\nA bridge is a small local HTTP server described by a bridge.json; AD spawns it on a\nstable port and relays verbs to it. Before writing anything, study the reference\nbridges for design ideas and copy the conventions you see:\n\n - START SIMPLE — read the two \"hello\" reference bridges first (one Rust, one\n Python). They are the minimal, correct shape: a /command POST handler, a /status\n health endpoint that self-reports its LED, and a bridge.json. Mirror them.\n - THEN study an advanced bridge for the patterns you need: a long-running native\n app (KiCad / Fusion 360), a process you supervise, or an external client that\n dials back in (native-browser). Steal their bridge.json fields and verb shapes.\n\nMy bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>.\n\nFollow the Adom Desktop Bridge SDK conventions on this page:\n - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure\n renderer — it never invents your status).\n - Return rich, structured JSON with an _hint on every response so the calling AI\n knows what to do next; on a slow operation, return a statusVerb to poll.\n - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will\n relay it so a timeout reads as \"still running, poll this\", not \"failed\").\n\nWhen it works locally, package it with release-bridge.sh and tell me the\nbridge_install command so I can install it on my Adom Desktop from here.\n```\n\nClaude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time.\n\n### Reference bridges — copy these\n\nStart with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts.\n\n| Bridge | Language | Why read it |\n|---|---|---|\n| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. |\n| **hello-rust** | Rust | Same minimal shape in Rust — single-binary, no runtime deps. |\n| **native-browser** | — | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. |\n| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. |\n\nThe hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index).\n\n### The whole dev loop is remote — install & uninstall from anywhere\n\nThis is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI:\n\n```bash\n# Install (or update) a bridge from its published wiki manifest:\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/<your-bridge-page>/files/<your>-bridge-manifest.json\"}'\n\n# It appears in AD's Bridges panel immediately and starts on first verb call.\nadom-desktop bridge_list # see it registered + its LED\nadom-desktop <your_verb> '{...}' # call it — AD spawns + relays\n\n# Iterate: re-publish a higher version, then re-install — AD reaps the old\n# instance and streams the new one onto the SAME stable port (no port churn).\nadom-desktop bridge_install '{\"manifestUrl\":\"...\",\"force\":true}'\n\n# Remove it cleanly when done:\nadom-desktop bridge_uninstall '{\"name\":\"<your-bridge>\"}'\n```\n\nIn the AD GUI, the same flows are one click: **\"+ Bridge\"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below).\n\nThat's the full toolchain — author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop.\n\n---\n\n\n\n\n\n\n\n\n> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's \"+ Bridge\" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`.\n\nv1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle.\n\n## Try it now — the `hello-python` and `hello-rust` sample bridges\n\nTwo parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible.\n\n| Flavor | Manifest URL | Verb prefix | Port | When to fork |\n|---|---|---|---|---|\n| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work |\n| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety |\n\nBoth manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`.\n\n### Install from the GUI\n\n1. Click **+ Bridge** in Adom Desktop's bridge bar\n2. Paste one (or both) of the manifest URLs into the dialog, click **Install**\n3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator)\n\n### Install both from the CLI\n\n```bash\n# Install the Python flavor\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json\"}'\nadom-desktop hellopy_ping\n# → {\"success\":true,\"output\":\"Hello from the Python sample bridge v1.1.0!\",\"language\":\"python\",...}\n\n# Install the Rust flavor (can coexist with hello-python)\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json\"}'\nadom-desktop hellors_ping\n# → {\"success\":true,\"output\":\"Hello from the Rust sample bridge v1.1.0!\",\"language\":\"rust\",...}\n\n# Echo through both\nadom-desktop hellopy_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (python): world\",...}\nadom-desktop hellors_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (rust): world\",...}\n\n# Both appear in bridge_list\nadom-desktop bridge_list\n# → bundled three + \"hello-python\" + \"hello-rust\"\n```\n\n### Uninstall\n\n```bash\nadom-desktop bridge_uninstall '{\"name\":\"hello-python\"}'\nadom-desktop bridge_uninstall '{\"name\":\"hello-rust\"}'\n```\n\nEach bridge's cache dir + registry entry are removed; the chips disappear from the bar.\n\nThe sources live at:\n- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`)\n- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB)\n\nFork whichever flavor matches your target's natural ecosystem.\n\n## Directory layout\n\n```\nmy-bridge/\n├── bridge.json ← REQUIRED — schema below\n├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field)\n├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint\n├── handlers/ ← your code\n│ ├── ...\n└── README.md ← optional, recommended\n```\n\n## `bridge.json` schema\n\n```json\n{\n \"manifest_version\": 1,\n \"name\": \"altium\",\n \"displayName\": \"Altium Designer\",\n \"version\": \"1.0.0\",\n \"description\": \"One-line description shown in bridge_list output.\",\n \"homepage\": \"https://github.com/your-org/adom-bridge-altium\",\n \"author\": \"Your Org\",\n \"license\": \"MIT\",\n \"spawn\": {\n \"kind\": \"python\",\n \"entrypoint\": \"server.py\",\n \"port\": 0,\n \"healthEndpoint\": \"/status\",\n \"stopMethod\": \"kill\",\n \"killImageName\": \"python.exe\"\n },\n \"verbPrefixes\": [\"altium_\"],\n \"verbs\": [\n \"altium_open_project\",\n \"altium_export_step\",\n \"altium_run_drc\"\n ],\n \"dependencies\": {\n \"python\": \">=3.11\",\n \"altium-designer\": \">=24\"\n },\n \"category\": \"EDA\",\n \"tags\": [\"pcb\", \"altium\", \"commercial\"]\n}\n```\n\n### Field reference\n\n- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry.\n- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel.\n- **`version`** (required, semver). Each ship bumps this.\n- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge.\n - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings.\n - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.).\n - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine.\n- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`).\n- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge \"won't start\" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly.\n- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime.\n- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint).\n- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name).\n- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `[\"altium_\"]`). All `altium_*` verbs route here.\n- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing.\n\n## Packaging + shipping\n\nUse the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties:\n\n```bash\n# In your bridge repo:\npython3 -c \"\nimport zipfile, os, fnmatch\nEXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*']\nwith zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk('.'):\n for f in files:\n rel = os.path.relpath(os.path.join(root, f), '.')\n if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE):\n continue\n zf.write(os.path.join(root, f), rel)\n\"\n\n# Compute SHA256 of the zip\nsha256sum my-bridge-v1.0.0.zip\n\n# Write the manifest the GUI polls\ncat > my-bridge-manifest.json <<EOF\n{\n \"manifest_version\": 1,\n \"version\": \"1.0.0\",\n \"url\": \"https://your-host/my-bridge-v1.0.0.zip\",\n \"sha256\": \"<sha256 from above>\",\n \"size\": <bytes>,\n \"released_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n}\nEOF\n```\n\nHost both files anywhere reachable by HTTPS — GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry.\n\n## Installing\n\nUsers install your bridge with one command:\n\n```bash\nadom-desktop bridge_install '{\"manifestUrl\":\"https://your-host/my-bridge-manifest.json\"}'\n```\n\nThe GUI:\n1. Fetches `my-bridge-manifest.json`\n2. Downloads the referenced zip\n3. SHA256-verifies against the manifest\n4. Extracts to `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\` (the `name` comes from your `bridge.json`)\n5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list`\n\nThe next CLI call to one of your `verbPrefixes` auto-spawns the bridge process.\n\n## Updating\n\nBump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users:\n\n```bash\nadom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from\n```\n\n(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work — tracking third-party manifest URLs in user state so they auto-refresh — lands in v1.8.x.)\n\n## Lifecycle controls (from cloud Docker, or local GUI)\n\n```bash\nadom-desktop bridge_list # see everything\nadom-desktop bridge_pause '{\"name\":\"altium\"}' # stop routing verbs\nadom-desktop bridge_resume '{\"name\":\"altium\"}'\nadom-desktop bridge_uninstall '{\"name\":\"altium\"}' # remove from cache\n```\n\n## Trust + security\n\nFor v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust.\n\nFuture work (v1.9.0+):\n- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for \"verified\" status\n- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field)\n- Community submission flow on the wiki so contributors don't need a GitHub account\n\n## Reference implementations\n\nEvery bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a \"fork this\" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory — both humans browse it and the GUI's Browse tab parses it.\n\n**Hello-world starter templates** — fork these for a 5-minute \"does this thing work?\" loop:\n- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python`\n- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary\n\n**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration:\n- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe)\n- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` — Node, single-instance, recording-capable\n- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in\n\n## Get listed\n\nOnce your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** — that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths:\n\n1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the \"Community bridges\" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria.\n2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert.", - "markdown": "# Adom Desktop - Bridge SDK\n\n## Build your own bridge\n\nA **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere — including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge.\n\nThe fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below.\n\n### Starting prompt (paste into a cloud Claude session)\n\n```\nI want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>.\n\nA bridge is a small local HTTP server described by a bridge.json; AD spawns it on a\nstable port and relays verbs to it. Before writing anything, study the reference\nbridges for design ideas and copy the conventions you see:\n\n - START SIMPLE — read the two \"hello\" reference bridges first (one Rust, one\n Python). They are the minimal, correct shape: a /command POST handler, a /status\n health endpoint that self-reports its LED, and a bridge.json. Mirror them.\n - THEN study an advanced bridge for the patterns you need: a long-running native\n app (KiCad / Fusion 360), a process you supervise, or an external client that\n dials back in (native-browser). Steal their bridge.json fields and verb shapes.\n\nMy bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>.\n\nFollow the Adom Desktop Bridge SDK conventions on this page:\n - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure\n renderer — it never invents your status).\n - Return rich, structured JSON with an _hint on every response so the calling AI\n knows what to do next; on a slow operation, return a statusVerb to poll.\n - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will\n relay it so a timeout reads as \"still running, poll this\", not \"failed\").\n\nWhen it works locally, package it with release-bridge.sh and tell me the\nbridge_install command so I can install it on my Adom Desktop from here.\n```\n\nClaude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time.\n\n### Reference bridges — copy these\n\nStart with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts.\n\n| Bridge | Language | Why read it |\n|---|---|---|\n| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. |\n| **hello-rust** | Rust | Same minimal shape in Rust — single-binary, no runtime deps. |\n| **native-browser** | — | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. |\n| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. |\n\nThe hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index).\n\n### The whole dev loop is remote — install & uninstall from anywhere\n\nThis is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI:\n\n```bash\n# Install (or update) a bridge from its published wiki manifest:\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/<your-bridge-page>/files/<your>-bridge-manifest.json\"}'\n\n# It appears in AD's Bridges panel immediately and starts on first verb call.\nadom-desktop bridge_list # see it registered + its LED\nadom-desktop <your_verb> '{...}' # call it — AD spawns + relays\n\n# Iterate: re-publish a higher version, then re-install — AD reaps the old\n# instance and streams the new one onto the SAME stable port (no port churn).\nadom-desktop bridge_install '{\"manifestUrl\":\"...\",\"force\":true}'\n\n# Remove it cleanly when done:\nadom-desktop bridge_uninstall '{\"name\":\"<your-bridge>\"}'\n```\n\nIn the AD GUI, the same flows are one click: **\"+ Bridge\"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below).\n\nThat's the full toolchain — author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop.\n\n---\n\n\n\n\n\n\n\n\n> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's \"+ Bridge\" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`.\n\nv1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle.\n\n## Try it now — the `hello-python` and `hello-rust` sample bridges\n\nTwo parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible.\n\n| Flavor | Manifest URL | Verb prefix | Port | When to fork |\n|---|---|---|---|---|\n| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work |\n| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety |\n\nBoth manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`.\n\n### Install from the GUI\n\n1. Click **+ Bridge** in Adom Desktop's bridge bar\n2. Paste one (or both) of the manifest URLs into the dialog, click **Install**\n3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator)\n\n### Install both from the CLI\n\n```bash\n# Install the Python flavor\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json\"}'\nadom-desktop hellopy_ping\n# → {\"success\":true,\"output\":\"Hello from the Python sample bridge v1.1.0!\",\"language\":\"python\",...}\n\n# Install the Rust flavor (can coexist with hello-python)\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json\"}'\nadom-desktop hellors_ping\n# → {\"success\":true,\"output\":\"Hello from the Rust sample bridge v1.1.0!\",\"language\":\"rust\",...}\n\n# Echo through both\nadom-desktop hellopy_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (python): world\",...}\nadom-desktop hellors_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (rust): world\",...}\n\n# Both appear in bridge_list\nadom-desktop bridge_list\n# → bundled three + \"hello-python\" + \"hello-rust\"\n```\n\n### Uninstall\n\n```bash\nadom-desktop bridge_uninstall '{\"name\":\"hello-python\"}'\nadom-desktop bridge_uninstall '{\"name\":\"hello-rust\"}'\n```\n\nEach bridge's cache dir + registry entry are removed; the chips disappear from the bar.\n\nThe sources live at:\n- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`)\n- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB)\n\nFork whichever flavor matches your target's natural ecosystem.\n\n## Directory layout\n\n```\nmy-bridge/\n├── bridge.json ← REQUIRED — schema below\n├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field)\n├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint\n├── handlers/ ← your code\n│ ├── ...\n└── README.md ← optional, recommended\n```\n\n## `bridge.json` schema\n\n```json\n{\n \"manifest_version\": 1,\n \"name\": \"altium\",\n \"displayName\": \"Altium Designer\",\n \"version\": \"1.0.0\",\n \"description\": \"One-line description shown in bridge_list output.\",\n \"homepage\": \"https://github.com/your-org/adom-bridge-altium\",\n \"author\": \"Your Org\",\n \"license\": \"MIT\",\n \"spawn\": {\n \"kind\": \"python\",\n \"entrypoint\": \"server.py\",\n \"port\": 0,\n \"healthEndpoint\": \"/status\",\n \"stopMethod\": \"kill\",\n \"killImageName\": \"python.exe\"\n },\n \"verbPrefixes\": [\"altium_\"],\n \"verbs\": [\n \"altium_open_project\",\n \"altium_export_step\",\n \"altium_run_drc\"\n ],\n \"dependencies\": {\n \"python\": \">=3.11\",\n \"altium-designer\": \">=24\"\n },\n \"category\": \"EDA\",\n \"tags\": [\"pcb\", \"altium\", \"commercial\"]\n}\n```\n\n### Field reference\n\n- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry.\n- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel.\n- **`version`** (required, semver). Each ship bumps this.\n- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge.\n - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings.\n - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.).\n - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine.\n- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`).\n- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge \"won't start\" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly.\n- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime.\n- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint).\n- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name).\n- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `[\"altium_\"]`). All `altium_*` verbs route here.\n- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing.\n\n## Packaging + shipping\n\nUse the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties:\n\n```bash\n# In your bridge repo:\npython3 -c \"\nimport zipfile, os, fnmatch\nEXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*']\nwith zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk('.'):\n for f in files:\n rel = os.path.relpath(os.path.join(root, f), '.')\n if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE):\n continue\n zf.write(os.path.join(root, f), rel)\n\"\n\n# Compute SHA256 of the zip\nsha256sum my-bridge-v1.0.0.zip\n\n# Write the manifest the GUI polls\ncat > my-bridge-manifest.json <<EOF\n{\n \"manifest_version\": 1,\n \"version\": \"1.0.0\",\n \"url\": \"https://your-host/my-bridge-v1.0.0.zip\",\n \"sha256\": \"<sha256 from above>\",\n \"size\": <bytes>,\n \"released_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n}\nEOF\n```\n\nHost both files anywhere reachable by HTTPS — GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry.\n\n## Installing\n\nUsers install your bridge with one command:\n\n```bash\nadom-desktop bridge_install '{\"manifestUrl\":\"https://your-host/my-bridge-manifest.json\"}'\n```\n\nThe GUI:\n1. Fetches `my-bridge-manifest.json`\n2. Downloads the referenced zip\n3. SHA256-verifies against the manifest\n4. Extracts to `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\` (the `name` comes from your `bridge.json`)\n5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list`\n\nThe next CLI call to one of your `verbPrefixes` auto-spawns the bridge process.\n\n## Updating\n\nBump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users:\n\n```bash\nadom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from\n```\n\n(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work — tracking third-party manifest URLs in user state so they auto-refresh — lands in v1.8.x.)\n\n## Lifecycle controls (from cloud Docker, or local GUI)\n\n```bash\nadom-desktop bridge_list # see everything\nadom-desktop bridge_pause '{\"name\":\"altium\"}' # stop routing verbs\nadom-desktop bridge_resume '{\"name\":\"altium\"}'\nadom-desktop bridge_uninstall '{\"name\":\"altium\"}' # remove from cache\n```\n\n## Trust + security\n\nFor v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust.\n\nFuture work (v1.9.0+):\n- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for \"verified\" status\n- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field)\n- Community submission flow on the wiki so contributors don't need a GitHub account\n\n## Reference implementations\n\nEvery bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a \"fork this\" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory — both humans browse it and the GUI's Browse tab parses it.\n\n**Hello-world starter templates** — fork these for a 5-minute \"does this thing work?\" loop:\n- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python`\n- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary\n\n**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration:\n- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe)\n- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` — Node, single-instance, recording-capable\n- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in\n\n## Get listed\n\nOnce your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** — that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths:\n\n1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the \"Community bridges\" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria.\n2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert.", - "body": "# Adom Desktop - Bridge SDK\n\n## Build your own bridge\n\nA **bridge** is any small local program that exposes a few verbs over a tiny HTTP server. Adom Desktop (AD) discovers it from a `bridge.json`, spawns it, and relays your verbs to it from anywhere — including a cloud Claude session. If you can write a 50-line HTTP server in any language, you can ship a bridge.\n\nThe fastest path is to **let Claude build it for you**. Open a cloud Claude (or Claude Code) session that has the `adom-desktop` CLI installed, and paste a prompt like the one below.\n\n### Starting prompt (paste into a cloud Claude session)\n\n```\nI want to build an Adom Desktop bridge for <YOUR APP / DEVICE / SERVICE>.\n\nA bridge is a small local HTTP server described by a bridge.json; AD spawns it on a\nstable port and relays verbs to it. Before writing anything, study the reference\nbridges for design ideas and copy the conventions you see:\n\n - START SIMPLE — read the two \"hello\" reference bridges first (one Rust, one\n Python). They are the minimal, correct shape: a /command POST handler, a /status\n health endpoint that self-reports its LED, and a bridge.json. Mirror them.\n - THEN study an advanced bridge for the patterns you need: a long-running native\n app (KiCad / Fusion 360), a process you supervise, or an external client that\n dials back in (native-browser). Steal their bridge.json fields and verb shapes.\n\nMy bridge should expose these verbs: <list them, e.g. foo_status, foo_do_thing>.\n\nFollow the Adom Desktop Bridge SDK conventions on this page:\n - Self-report {led, summary, tooltip} from the health endpoint (AD is a pure\n renderer — it never invents your status).\n - Return rich, structured JSON with an _hint on every response so the calling AI\n knows what to do next; on a slow operation, return a statusVerb to poll.\n - Declare statusVerb in bridge.json if any verb can run longer than ~60s (AD will\n relay it so a timeout reads as \"still running, poll this\", not \"failed\").\n\nWhen it works locally, package it with release-bridge.sh and tell me the\nbridge_install command so I can install it on my Adom Desktop from here.\n```\n\nClaude will read the reference bridges, scaffold your `bridge.json` + server, run it locally, and hand you a one-line install command. You stay in the cloud session the whole time.\n\n### Reference bridges — copy these\n\nStart with a **hello** bridge (minimal + correct), then borrow from an **advanced** one for the hard parts.\n\n| Bridge | Language | Why read it |\n|---|---|---|\n| **hello-python** | Python | The minimal bridge: `/command` + `/status` self-report + `bridge.json`. The shape every bridge follows. |\n| **hello-rust** | Rust | Same minimal shape in Rust — single-binary, no runtime deps. |\n| **native-browser** | — | Advanced: an **external client** (a Chrome extension) dials back into the bridge; shows the stable-port + reconnect pattern. |\n| **kicad** / **fusion360** | Python | Advanced: supervising a **long-running native app**, multi-screenshot arrays, `statusVerb` for slow cloud operations. |\n\nThe hello bridges live in the AD repo under `scripts/sample-bridges/`; each advanced bridge has its own wiki page (open it from AD's Bridges panel, or the apps index).\n\n### The whole dev loop is remote — install & uninstall from anywhere\n\nThis is the part that makes bridge development easy: **you never touch the Windows box by hand.** From any cloud Claude session with the `adom-desktop` CLI:\n\n```bash\n# Install (or update) a bridge from its published wiki manifest:\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/<your-bridge-page>/files/<your>-bridge-manifest.json\"}'\n\n# It appears in AD's Bridges panel immediately and starts on first verb call.\nadom-desktop bridge_list # see it registered + its LED\nadom-desktop <your_verb> '{...}' # call it — AD spawns + relays\n\n# Iterate: re-publish a higher version, then re-install — AD reaps the old\n# instance and streams the new one onto the SAME stable port (no port churn).\nadom-desktop bridge_install '{\"manifestUrl\":\"...\",\"force\":true}'\n\n# Remove it cleanly when done:\nadom-desktop bridge_uninstall '{\"name\":\"<your-bridge>\"}'\n```\n\nIn the AD GUI, the same flows are one click: **\"+ Bridge\"** (top of the Bridges panel) installs from a manifest URL, and each bridge card has **Uninstall** in its pop-out. Publishing the manifest + zip is a single `release-bridge.sh <name>` run (see the SDK reference below).\n\nThat's the full toolchain — author in a cloud prompt, publish once, `bridge_install` from anywhere, iterate by re-installing. No RDP, no copying files to the desktop.\n\n---\n\n\n\n\n\n\n\n\n> **Public version of this guide:** [`apps/adom-desktop-bridges` on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). The wiki page is the canonical user-facing copy and is what the GUI's \"+ Bridge\" install modal links to (the adom-inc GitHub repo is private). When you update this file with breaking schema changes or new lifecycle commands, also republish the wiki page via `adom-wiki page edit apps/adom-desktop-bridges --field content --body-md <this file>`.\n\nv1.8.0+ supports dynamic bridges: any zip with a `bridge.json` at its root can be installed via `adom-desktop bridge_install --manifestUrl <wiki-or-github-url>`. This lets community authors ship bridges for Altium, MATLAB, Rohde & Schwarz oscilloscopes, Keysight, OrCAD, anything — without coordinating with the Adom Desktop release cycle.\n\n## Try it now — the `hello-python` and `hello-rust` sample bridges\n\nTwo parallel reference bridges are hosted on the Adom Wiki, one per language. Both expose the same two verbs (`*_ping` / `*_echo`) under different prefixes (`hellopy_` / `hellors_`), so they can be installed simultaneously for a side-by-side demo. Both use `port: 0` (dynamic OS-assigned ports — v1.8.31+) so no collision is possible.\n\n| Flavor | Manifest URL | Verb prefix | Port | When to fork |\n|---|---|---|---|---|\n| **Python** (stdlib only, ~80 lines) | `…/hello-python-bridge-manifest.json` | `hellopy_` | 8890 | Vendor has Python bindings, fast dev iter, I/O-bound work |\n| **Rust** (`tiny_http` + serde, single static binary, ~120 lines) | `…/hello-rust-bridge-manifest.json` | `hellors_` | 8891 | No Python runtime on user's machine, CPU-bound, type safety |\n\nBoth manifest URLs are under `https://wiki.adom.inc/api/v1/pages/<bridge-slug>/files/`.\n\n### Install from the GUI\n\n1. Click **+ Bridge** in Adom Desktop's bridge bar\n2. Paste one (or both) of the manifest URLs into the dialog, click **Install**\n3. New chips appear ~2 seconds later with a small ✦ marker (third-party indicator)\n\n### Install both from the CLI\n\n```bash\n# Install the Python flavor\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-python-bridge/files/adom-bridge-hello-python-manifest.json\"}'\nadom-desktop hellopy_ping\n# → {\"success\":true,\"output\":\"Hello from the Python sample bridge v1.1.0!\",\"language\":\"python\",...}\n\n# Install the Rust flavor (can coexist with hello-python)\nadom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-hello-rust-bridge/files/adom-bridge-hello-rust-manifest.json\"}'\nadom-desktop hellors_ping\n# → {\"success\":true,\"output\":\"Hello from the Rust sample bridge v1.1.0!\",\"language\":\"rust\",...}\n\n# Echo through both\nadom-desktop hellopy_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (python): world\",...}\nadom-desktop hellors_echo '{\"message\":\"world\"}'\n# → {\"success\":true,\"output\":\"echo (rust): world\",...}\n\n# Both appear in bridge_list\nadom-desktop bridge_list\n# → bundled three + \"hello-python\" + \"hello-rust\"\n```\n\n### Uninstall\n\n```bash\nadom-desktop bridge_uninstall '{\"name\":\"hello-python\"}'\nadom-desktop bridge_uninstall '{\"name\":\"hello-rust\"}'\n```\n\nEach bridge's cache dir + registry entry are removed; the chips disappear from the bar.\n\nThe sources live at:\n- [`scripts/sample-bridges/hello-python/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-python) — `bridge.json` + `server.py` (~120 lines of `http.server.BaseHTTPRequestHandler`)\n- [`scripts/sample-bridges/hello-rust/`](https://github.com/adom-inc/adom-desktop/tree/main/scripts/sample-bridges/hello-rust) — `bridge.json` + `Cargo.toml` + `src/main.rs` (~140 lines of `tiny_http`) + pre-built `hello-rust.exe` (~300 KB)\n\nFork whichever flavor matches your target's natural ecosystem.\n\n## Directory layout\n\n```\nmy-bridge/\n├── bridge.json ← REQUIRED — schema below\n├── BRIDGE_VERSION ← optional, semver line (kept in sync with bridge.json's version field)\n├── server.py ← or server.js, or my-bridge.exe — your bridge's entrypoint\n├── handlers/ ← your code\n│ ├── ...\n└── README.md ← optional, recommended\n```\n\n## `bridge.json` schema\n\n```json\n{\n \"manifest_version\": 1,\n \"name\": \"altium\",\n \"displayName\": \"Altium Designer\",\n \"version\": \"1.0.0\",\n \"description\": \"One-line description shown in bridge_list output.\",\n \"homepage\": \"https://github.com/your-org/adom-bridge-altium\",\n \"author\": \"Your Org\",\n \"license\": \"MIT\",\n \"spawn\": {\n \"kind\": \"python\",\n \"entrypoint\": \"server.py\",\n \"port\": 0,\n \"healthEndpoint\": \"/status\",\n \"stopMethod\": \"kill\",\n \"killImageName\": \"python.exe\"\n },\n \"verbPrefixes\": [\"altium_\"],\n \"verbs\": [\n \"altium_open_project\",\n \"altium_export_step\",\n \"altium_run_drc\"\n ],\n \"dependencies\": {\n \"python\": \">=3.11\",\n \"altium-designer\": \">=24\"\n },\n \"category\": \"EDA\",\n \"tags\": [\"pcb\", \"altium\", \"commercial\"]\n}\n```\n\n### Field reference\n\n- **`name`** (required, lowercase, no spaces). The directory name + cache key. Must be unique across the registry.\n- **`displayName`** — human-friendly shown in `bridge_list` + future GUI bridge panel.\n- **`version`** (required, semver). Each ship bumps this.\n- **`spawn.kind`** — `python` | `node` | `exe`. Determines how Adom Desktop launches the bridge.\n - `python` → Adom Desktop finds Python in PATH and runs `python <entrypoint> --port <port>`. Good when the vendor's SDK has Python bindings.\n - `node` → Adom Desktop finds Node in PATH and runs `node <entrypoint> --port <port>`. Good for ecosystem reuse (Puppeteer, Playwright, etc.).\n - `exe` → Adom Desktop runs `<entrypoint> --port <port>` directly. The entrypoint must be a pre-built executable in the zip. Good for Rust, Go, .NET single-file builds, etc. — no runtime install required on the user's machine.\n- **`spawn.entrypoint`** — relative path inside the bridge dir (e.g. `server.py`, `server.js`, `my-bridge.exe`).\n- **`spawn.port`** — **set to `0`** (v1.8.31+ recommended). Adom Desktop picks an OS-assigned ephemeral port at spawn time and passes it to your `--port` arg. No collision possible between bridges. Hardcoded ports still work for back-compat but are deprecated — anything you pick will eventually collide with someone (Hydrogen Desktop's bridges already used 8772/8773/8851; static-port collisions are the #1 source of bridge \"won't start\" bugs). Bridge ports are internal plumbing — callers always route through `adom-desktop <prefix>_<verb>`, never reach a bridge directly.\n- **`spawn.healthEndpoint`** — path Adom Desktop polls to confirm the bridge is alive (e.g. `/status`, `/health`). v1.8.31+ accepts a path-only fragment (recommended) and constructs the URL using the runtime port. Legacy full URLs (`http://127.0.0.1:8881/status`) still parse — the host+port get replaced at runtime.\n- **`spawn.stopMethod`** — `kill` (process-handle Child::kill, NOT `taskkill /im` — that would nuke ALL Pythons on the box), `sigterm` (graceful), or `graceful_endpoint` (POST to an endpoint).\n- **`spawn.killImageName`** — legacy field, only used if Adom Desktop falls back to image-name kill. With v1.8.31's dynamic ports + process-handle kill this is rarely needed. Set to your process name for safety (`python.exe` / `node.exe` / your binary name).\n- **`verbPrefixes`** — list of CLI verb prefixes this bridge owns (e.g. `[\"altium_\"]`). All `altium_*` verbs route here.\n- **`verbs`** — explicit verb list. Used in `bridge_list` to advertise capabilities; doesn't affect routing.\n\n## Packaging + shipping\n\nUse the same `scripts/release-bridge.sh` flow Adom Desktop uses for its own bridges. Adapted for third parties:\n\n```bash\n# In your bridge repo:\npython3 -c \"\nimport zipfile, os, fnmatch\nEXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*', '*/node_modules/*']\nwith zipfile.ZipFile('my-bridge-v1.0.0.zip', 'w', zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk('.'):\n for f in files:\n rel = os.path.relpath(os.path.join(root, f), '.')\n if any(fnmatch.fnmatch(rel.replace(os.sep, '/'), g) for g in EXCLUDE):\n continue\n zf.write(os.path.join(root, f), rel)\n\"\n\n# Compute SHA256 of the zip\nsha256sum my-bridge-v1.0.0.zip\n\n# Write the manifest the GUI polls\ncat > my-bridge-manifest.json <<EOF\n{\n \"manifest_version\": 1,\n \"version\": \"1.0.0\",\n \"url\": \"https://your-host/my-bridge-v1.0.0.zip\",\n \"sha256\": \"<sha256 from above>\",\n \"size\": <bytes>,\n \"released_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n}\nEOF\n```\n\nHost both files anywhere reachable by HTTPS — GitHub Pages, your own CDN, the Adom Wiki if you contribute to the official registry.\n\n## Installing\n\nUsers install your bridge with one command:\n\n```bash\nadom-desktop bridge_install '{\"manifestUrl\":\"https://your-host/my-bridge-manifest.json\"}'\n```\n\nThe GUI:\n1. Fetches `my-bridge-manifest.json`\n2. Downloads the referenced zip\n3. SHA256-verifies against the manifest\n4. Extracts to `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\` (the `name` comes from your `bridge.json`)\n5. Rescans the runtime registry — your bridge is now discoverable via `bridge_list`\n\nThe next CLI call to one of your `verbPrefixes` auto-spawns the bridge process.\n\n## Updating\n\nBump `bridge.json`'s `version` + `BRIDGE_VERSION`, rebuild the zip, re-upload, update the manifest. Users:\n\n```bash\nadom-desktop refresh_bridges # picks up new versions from manifest URLs they previously installed from\n```\n\n(Currently `refresh_bridges` only polls the three core wiki manifests. The follow-up work — tracking third-party manifest URLs in user state so they auto-refresh — lands in v1.8.x.)\n\n## Lifecycle controls (from cloud Docker, or local GUI)\n\n```bash\nadom-desktop bridge_list # see everything\nadom-desktop bridge_pause '{\"name\":\"altium\"}' # stop routing verbs\nadom-desktop bridge_resume '{\"name\":\"altium\"}'\nadom-desktop bridge_uninstall '{\"name\":\"altium\"}' # remove from cache\n```\n\n## Trust + security\n\nFor v1.8.0 the trust model is SHA256-verification of the wiki-hosted zip against the manifest. Bridges run with the user's privileges; install from sources you trust.\n\nFuture work (v1.9.0+):\n- Optional Ed25519 signing of bridge zips with the Adom-Inc public key for \"verified\" status\n- Per-bridge sandboxing (limited filesystem / network access declared in manifest `permissions` field)\n- Community submission flow on the wiki so contributors don't need a GitHub account\n\n## Reference implementations\n\nEvery bridge below has its own dedicated wiki page with hero, install/uninstall recipes, and a \"fork this\" walkthrough. The **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** is the canonical directory — both humans browse it and the GUI's Browse tab parses it.\n\n**Hello-world starter templates** — fork these for a 5-minute \"does this thing work?\" loop:\n- **[hello-python](https://wiki.adom.inc/adom/adom-desktop-hello-python-bridge)**: `scripts/sample-bridges/hello-python/` — ~80 lines, stdlib only, `kind:python`\n- **[hello-rust](https://wiki.adom.inc/adom/adom-desktop-hello-rust-bridge)**: `scripts/sample-bridges/hello-rust/` — ~140 lines, `tiny_http` + serde, `kind:exe`, single 300-KB static binary\n\n**Adom-shipped bridges** — canonical full-complexity examples; fork their layout for a real vendor integration:\n- **[kicad](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge)**: `plugins/kicad/` — Python, complex, multi-instance (one bridge per KiCad GUI exe)\n- **[puppeteer](https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge)**: `plugins/puppeteer/` — Node, single-instance, recording-capable\n- **[fusion360](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)**: `plugins/fusion360/` — Python, simpler, command-passthrough to Fusion add-in\n\n## Get listed\n\nOnce your bridge is stable, add it to the **[bridges registry](https://wiki.adom.inc/adom/adom-desktop-bridges-registry)** — that's the canonical machine-readable directory the GUI's Browse tab parses to discover what's installable. Two paths:\n\n1. **PR-based** (preferred): open a PR against [`adom-inc/adom-desktop`](https://github.com/adom-inc/adom-desktop) adding your row to the \"Community bridges\" table in this file (`plugins/BRIDGES.md`). A maintainer mirrors it to the wiki on the next sync. See the registry page's [submission section](https://wiki.adom.inc/adom/adom-desktop-bridges-registry#how-to-submit-a-bridge) for the row format + review criteria.\n2. **Direct wiki edit**: if you have an Adom account, `adom-wiki page edit apps/adom-desktop-bridges-registry --field content --body-md <patch>`. Maintainers review and either keep or revert." + "content": "# Bridge SDK — build, publish, and surface a bridge for Adom Desktop\n\nA **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:\n\n1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it.\n2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs.\n3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *\"open my app in pup\"* surfaces YOU via `adom-wiki discover find`.\n\n> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.**\n> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.**\n> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)\n\n---\n\n## The boundary — what AD owns vs what YOU own\n\nThe #1 mistake is filing AD bugs for bridge issues. The split:\n\n**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them).\n\n**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**.\n\nIf a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.\n\n---\n\n## Self-audit checklist — run this when asked to \"audit against the SDK\"\n\nWhen a maintainer says *\"audit your code, skills, README, and wiki page against the Bridge SDK,\"* **this is the checklist.** Each item names its section below; fix every miss, then re-check.\n\n**bridge.json** (the manifest in your zip):\n- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)*\n- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.**\n- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)*\n\n**Verbs & output** (what the AI actually reads):\n- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)*\n- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)*\n- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)*\n- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)*\n\n**Discovery** (can a plain request find you?):\n- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)*\n- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each.\n- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION.\n\n**Skills & docs** (what the container + humans read):\n- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)*\n- [ ] Your USER skill leads with the cold-start \"read before you panic\" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)*\n- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).\n\n---\n\n## bridge.json — the manifest that ships INSIDE your zip\n\n```jsonc\n{\n \"manifest_version\": 1,\n \"name\": \"blender\", // registry name → verb prefix \"blender_\"\n \"displayName\": \"Blender\", // in-app chip label (the tool's real name)\n \"version\": \"1.2.0\", // numeric; a newer CACHE copy supersedes the bundled seed\n \"description\": \"Drive Blender headless + GUI\",\n \"author\": \"you\",\n \"license\": \"MIT\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop-blender-bridge\", // your OWN page (drives skillPkg + hero)\n \"hero\": \"https://.../hero.png\", // optional; else AD uses your wiki page hero, else a monogram\n \"spawn\": {\n \"kind\": \"python\", // python | node | exe | external-http (NOT \"process\")\n \"entrypoint\": \"server.py\", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv\n \"port\": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.\n \"persistent\": true, // AD's supervisor respawns it if it dies\n \"healthEndpoint\": \"/health\", // INSIDE spawn. AD probes it for the chip (default /health)\n \"stopMethod\": \"kill\", // kill | sigterm | graceful_endpoint\n \"killImageName\": \"python.exe\" // image name for taskkill on stop (kind:python/node)\n },\n \"verbPrefixes\": [\"blender_\"], // any verb with these prefixes routes to you\n \"verbs\": [\"blender_render\", \"blender_status\", \"blender_describe\"], // every verb (routing + Verbs tab)\n \"statusVerb\": \"blender_status\", // verb a caller polls after a long op times out\n \"updateManifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\",\n\n // v1.9.47 cold-start (see \"Cold-start tiers\" below):\n \"detect\": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.\n \"hostApp\": \"Blender\", // omit the whole `detect` block if your bridge brings its own runtime\n \"appPathsExe\": \"blender.exe\", // (e.g. pup ships its own browser → no host app)\n \"paths\": { \"windows\": [\"%ProgramFiles%\\\\Blender Foundation\\\\*\\\\blender.exe\"] }\n },\n \"prewarm\": { \"assets\": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. [\"chrome-for-testing\"])\n}\n```\n\n- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `\"process\"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.)\n- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).\n- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`.\n- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.)\n- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page.\n- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.)\n- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in \"Cold-start tiers\" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser.\n\n---\n\n## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest\n\nThis is the code AD streams onto the user's machine. Two files on your page:\n\n1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob:\n ```bash\n adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title \"...\" --changelog \"...\"\n adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any\n ```\n (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.)\n2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`:\n ```jsonc\n { \"name\":\"blender\", \"version\":\"1.2.0\",\n \"url\":\"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip\", // base-relative, extension-agnostic (.bin works where wiki blocks .zip)\n \"sha256\":\"…\", \"size\":123456, \"verbPrefixes\":[\"blender_\"], \"healthEndpoint\":\"/health\",\n \"updateManifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\" }\n ```\n\n**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\`, rescans, and remembers the URL for auto-update.\n\n**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware).\n\n---\n\n## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`)\n\nYour runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries):\n\n```bash\n# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and\n# package.json {name:\"adom-desktop-<bridge>-bridge\", version, discovery_triggers, files:[...]}.\n# THREE traps that bite — verify each with `tar tzf` before publishing:\n# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE,\n# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH\n# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else\n# every user who installs your pkg also installs your dev/publish docs.\n# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files`\n# allowlist needs the GLOB: a bare \"skills\" entry ships ZERO skill files — use \"skills/**\".\n# (Set .gitignore aside across pack+publish, then restore.)\n# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB\n# before guards). Skills + docs only.\nadom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present,\n # ZERO binaries, ZERO dev/publish SKILL.md\nadom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge\n```\n\nA container gets it **without anyone asking**:\n- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it.\n- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.\n- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them.\n\n**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh.\n\n---\n\n## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you\n\nThe wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs.\n\n```bash\nadom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add \"open my pcb\" \"export gerbers\" # add triggers\nadom-wiki discover preview --json \"open my pcb design\" # what would surface + the SCORE + WHY\nadom-wiki discover find \"screenshot my website\" # the live find an AI runs\n```\n\n**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):**\n- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it.\n- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title (\"Adom Desktop - Blender Bridge\") + a matching tag scored **ZERO** for the literal query *\"blender\"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.)\n- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.\n\n**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *\"open my pcb design\"* / *\"export gerber files\"* / *\"design a circuit board\"* — NOT *\"reverse bridge\"* / *\"kicad_bridge_call\"* / *\"fork the kicad bridge\"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy \"pcb\"/\"export\" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end.\n\n**VET it — never ship triggers you didn't test:**\n1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`).\n2. `adom-wiki discover preview --json \"<each query>\"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.)\n3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on \"pcb\"), add MORE exact phrasings so your 3×-per-trigger total wins.\n\n**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. \"for logged-in form-filling, install the native-browser extension\") — rather than claim it and do it badly.\n\n- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline.\n- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *\"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`).\"*\n\n### The end-to-end flow your three artifacts enable\n\nUser says *\"show me my app in pup\"* → the AI:\n1. `adom-wiki discover find \"show me my app in pup\"` → your bridge surfaces (Artifact 3).\n2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1).\n3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2).\n4. Drive your verbs — guided by the **rich hints you return** (the principle at the top).\n\n---\n\n## Prewarm — make the first call instant\n\nA 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:\n\n- **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.\n- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.\n- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent \"ensure ready\" path AD/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.\n- **`browser_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.\n\n---\n\n## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)\n\nA bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's \"what tools do I have\") triggers a slow install at the worst moment.\n\n**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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).\n\n**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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:\n- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default AD 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). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a \"go away.\"\n- **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.\n\n**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**\n\n| Level | Bridge(s) | Behavior |\n|---|---|---|\n| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not \"installed\" by AD. |\n| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |\n| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). |\n| **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. |\n| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |\n\nThe 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).\n\n**Declare both in `bridge.json`:**\n```json\n\"detect\": { // Tier 2 — how AD finds your HOST APP (detection only)\n \"hostApp\": \"KiCad\",\n \"appPathsExe\": \"kicad.exe\", // optional: Windows App Paths registry lookup (most reliable)\n \"paths\": { // optional: candidate paths, %VAR% expanded, one * = a version dir\n \"windows\": [\"%ProgramFiles%\\\\KiCad\\\\*\\\\bin\\\\kicad.exe\"],\n \"macos\": [\"/Applications/KiCad/KiCad.app\"],\n \"linux\": [\"/usr/bin/kicad\"]\n }\n},\n\"prewarm\": { \"assets\": [\"chrome-for-testing\"] } // Tier 1 — heavy runtime assets AD prewarms + reports\n```\nA bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python).\n\n**The read-only probe: `bridge_readiness`.** AD 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.\n\nYour 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 AD-level aggregate that detects + summarizes across all bridges without spawning any.\n\n---\n\n## The SKILL SET you should ship (not one SKILL.md — a small library)\n\nDon't cram everything into one file. Ship **three kinds** of skill, each with a different reader:\n\n1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong.\n2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill.\n3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.)\n\nPublish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1).\n\n**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply.\n\n---\n\n## USER-skill conventions — what every consumer skill must teach (from pup's shape)\n\nThese eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:\n\n1. **Open with a \"FIRST-TIME / COLD-START — read before you panic\" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose).\n2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{\"wait\":true}` to block. Gives every bridge a uniform \"poll until ready, then proceed\" loop. (Complements the runtime-side Prewarm section above.)\n3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type (\"take a screenshot of\", \"log into\"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3).\n4. **Include a short \"Core verbs\" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb.\n5. **\"Background by default — don't disrupt the user.\"** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`.\n6. **Teach \"`ok:true` is not enough\" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently.\n7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun.\n8. **\"Surface `_hint` verbatim.\"** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.\n\n---\n\n## Verb contract (quick reference)\n\n- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you.\n- **Request:** AD POSTs `{ \"command\":\"<verb>\", \"args\":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`.\n- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`.\n- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI.\n\n## Health + status chip\n\nYour `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.\n\n## Lifecycle verbs (AD gives these to USERS — know them to guide users)\n\n`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.\n\n---\n\n## Publish checklist (the whole modern flow)\n\n1. **Page** — `adom-wiki page create adom/adom-desktop-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.\n2. **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.\n3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap).\n4. **Discovery** — `discover triggers` to confirm; `discover preview \"<a real user phrase>\"` to confirm you surface.\n5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.\n6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find \"<phrase>\"` surfaces you with an instructional brief.\n\n**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.\n", + "body_md": "# Bridge SDK — build, publish, and surface a bridge for Adom Desktop\n\nA **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:\n\n1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it.\n2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs.\n3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *\"open my app in pup\"* surfaces YOU via `adom-wiki discover find`.\n\n> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.**\n> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.**\n> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)\n\n---\n\n## The boundary — what AD owns vs what YOU own\n\nThe #1 mistake is filing AD bugs for bridge issues. The split:\n\n**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them).\n\n**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**.\n\nIf a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.\n\n---\n\n## Self-audit checklist — run this when asked to \"audit against the SDK\"\n\nWhen a maintainer says *\"audit your code, skills, README, and wiki page against the Bridge SDK,\"* **this is the checklist.** Each item names its section below; fix every miss, then re-check.\n\n**bridge.json** (the manifest in your zip):\n- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)*\n- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.**\n- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)*\n\n**Verbs & output** (what the AI actually reads):\n- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)*\n- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)*\n- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)*\n- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)*\n\n**Discovery** (can a plain request find you?):\n- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)*\n- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each.\n- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION.\n\n**Skills & docs** (what the container + humans read):\n- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)*\n- [ ] Your USER skill leads with the cold-start \"read before you panic\" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)*\n- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).\n\n---\n\n## bridge.json — the manifest that ships INSIDE your zip\n\n```jsonc\n{\n \"manifest_version\": 1,\n \"name\": \"blender\", // registry name → verb prefix \"blender_\"\n \"displayName\": \"Blender\", // in-app chip label (the tool's real name)\n \"version\": \"1.2.0\", // numeric; a newer CACHE copy supersedes the bundled seed\n \"description\": \"Drive Blender headless + GUI\",\n \"author\": \"you\",\n \"license\": \"MIT\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop-blender-bridge\", // your OWN page (drives skillPkg + hero)\n \"hero\": \"https://.../hero.png\", // optional; else AD uses your wiki page hero, else a monogram\n \"spawn\": {\n \"kind\": \"python\", // python | node | exe | external-http (NOT \"process\")\n \"entrypoint\": \"server.py\", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv\n \"port\": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.\n \"persistent\": true, // AD's supervisor respawns it if it dies\n \"healthEndpoint\": \"/health\", // INSIDE spawn. AD probes it for the chip (default /health)\n \"stopMethod\": \"kill\", // kill | sigterm | graceful_endpoint\n \"killImageName\": \"python.exe\" // image name for taskkill on stop (kind:python/node)\n },\n \"verbPrefixes\": [\"blender_\"], // any verb with these prefixes routes to you\n \"verbs\": [\"blender_render\", \"blender_status\", \"blender_describe\"], // every verb (routing + Verbs tab)\n \"statusVerb\": \"blender_status\", // verb a caller polls after a long op times out\n \"updateManifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\",\n\n // v1.9.47 cold-start (see \"Cold-start tiers\" below):\n \"detect\": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.\n \"hostApp\": \"Blender\", // omit the whole `detect` block if your bridge brings its own runtime\n \"appPathsExe\": \"blender.exe\", // (e.g. pup ships its own browser → no host app)\n \"paths\": { \"windows\": [\"%ProgramFiles%\\\\Blender Foundation\\\\*\\\\blender.exe\"] }\n },\n \"prewarm\": { \"assets\": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. [\"chrome-for-testing\"])\n}\n```\n\n- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `\"process\"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.)\n- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).\n- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`.\n- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.)\n- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page.\n- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.)\n- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in \"Cold-start tiers\" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser.\n\n---\n\n## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest\n\nThis is the code AD streams onto the user's machine. Two files on your page:\n\n1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob:\n ```bash\n adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title \"...\" --changelog \"...\"\n adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any\n ```\n (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.)\n2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`:\n ```jsonc\n { \"name\":\"blender\", \"version\":\"1.2.0\",\n \"url\":\"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip\", // base-relative, extension-agnostic (.bin works where wiki blocks .zip)\n \"sha256\":\"…\", \"size\":123456, \"verbPrefixes\":[\"blender_\"], \"healthEndpoint\":\"/health\",\n \"updateManifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\" }\n ```\n\n**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\`, rescans, and remembers the URL for auto-update.\n\n**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware).\n\n---\n\n## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`)\n\nYour runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries):\n\n```bash\n# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and\n# package.json {name:\"adom-desktop-<bridge>-bridge\", version, discovery_triggers, files:[...]}.\n# THREE traps that bite — verify each with `tar tzf` before publishing:\n# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE,\n# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH\n# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else\n# every user who installs your pkg also installs your dev/publish docs.\n# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files`\n# allowlist needs the GLOB: a bare \"skills\" entry ships ZERO skill files — use \"skills/**\".\n# (Set .gitignore aside across pack+publish, then restore.)\n# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB\n# before guards). Skills + docs only.\nadom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present,\n # ZERO binaries, ZERO dev/publish SKILL.md\nadom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge\n```\n\nA container gets it **without anyone asking**:\n- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it.\n- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.\n- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them.\n\n**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh.\n\n---\n\n## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you\n\nThe wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs.\n\n```bash\nadom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add \"open my pcb\" \"export gerbers\" # add triggers\nadom-wiki discover preview --json \"open my pcb design\" # what would surface + the SCORE + WHY\nadom-wiki discover find \"screenshot my website\" # the live find an AI runs\n```\n\n**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):**\n- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it.\n- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title (\"Adom Desktop - Blender Bridge\") + a matching tag scored **ZERO** for the literal query *\"blender\"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.)\n- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.\n\n**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *\"open my pcb design\"* / *\"export gerber files\"* / *\"design a circuit board\"* — NOT *\"reverse bridge\"* / *\"kicad_bridge_call\"* / *\"fork the kicad bridge\"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy \"pcb\"/\"export\" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end.\n\n**VET it — never ship triggers you didn't test:**\n1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`).\n2. `adom-wiki discover preview --json \"<each query>\"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.)\n3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on \"pcb\"), add MORE exact phrasings so your 3×-per-trigger total wins.\n\n**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. \"for logged-in form-filling, install the native-browser extension\") — rather than claim it and do it badly.\n\n- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline.\n- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *\"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`).\"*\n\n### The end-to-end flow your three artifacts enable\n\nUser says *\"show me my app in pup\"* → the AI:\n1. `adom-wiki discover find \"show me my app in pup\"` → your bridge surfaces (Artifact 3).\n2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1).\n3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2).\n4. Drive your verbs — guided by the **rich hints you return** (the principle at the top).\n\n---\n\n## Prewarm — make the first call instant\n\nA 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:\n\n- **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.\n- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.\n- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent \"ensure ready\" path AD/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.\n- **`browser_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.\n\n---\n\n## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)\n\nA bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's \"what tools do I have\") triggers a slow install at the worst moment.\n\n**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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).\n\n**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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:\n- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default AD 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). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a \"go away.\"\n- **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.\n\n**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**\n\n| Level | Bridge(s) | Behavior |\n|---|---|---|\n| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not \"installed\" by AD. |\n| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |\n| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). |\n| **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. |\n| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |\n\nThe 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).\n\n**Declare both in `bridge.json`:**\n```json\n\"detect\": { // Tier 2 — how AD finds your HOST APP (detection only)\n \"hostApp\": \"KiCad\",\n \"appPathsExe\": \"kicad.exe\", // optional: Windows App Paths registry lookup (most reliable)\n \"paths\": { // optional: candidate paths, %VAR% expanded, one * = a version dir\n \"windows\": [\"%ProgramFiles%\\\\KiCad\\\\*\\\\bin\\\\kicad.exe\"],\n \"macos\": [\"/Applications/KiCad/KiCad.app\"],\n \"linux\": [\"/usr/bin/kicad\"]\n }\n},\n\"prewarm\": { \"assets\": [\"chrome-for-testing\"] } // Tier 1 — heavy runtime assets AD prewarms + reports\n```\nA bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python).\n\n**The read-only probe: `bridge_readiness`.** AD 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.\n\nYour 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 AD-level aggregate that detects + summarizes across all bridges without spawning any.\n\n---\n\n## The SKILL SET you should ship (not one SKILL.md — a small library)\n\nDon't cram everything into one file. Ship **three kinds** of skill, each with a different reader:\n\n1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong.\n2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill.\n3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.)\n\nPublish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1).\n\n**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply.\n\n---\n\n## USER-skill conventions — what every consumer skill must teach (from pup's shape)\n\nThese eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:\n\n1. **Open with a \"FIRST-TIME / COLD-START — read before you panic\" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose).\n2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{\"wait\":true}` to block. Gives every bridge a uniform \"poll until ready, then proceed\" loop. (Complements the runtime-side Prewarm section above.)\n3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type (\"take a screenshot of\", \"log into\"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3).\n4. **Include a short \"Core verbs\" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb.\n5. **\"Background by default — don't disrupt the user.\"** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`.\n6. **Teach \"`ok:true` is not enough\" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently.\n7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun.\n8. **\"Surface `_hint` verbatim.\"** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.\n\n---\n\n## Verb contract (quick reference)\n\n- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you.\n- **Request:** AD POSTs `{ \"command\":\"<verb>\", \"args\":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`.\n- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`.\n- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI.\n\n## Health + status chip\n\nYour `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.\n\n## Lifecycle verbs (AD gives these to USERS — know them to guide users)\n\n`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.\n\n---\n\n## Publish checklist (the whole modern flow)\n\n1. **Page** — `adom-wiki page create adom/adom-desktop-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.\n2. **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.\n3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap).\n4. **Discovery** — `discover triggers` to confirm; `discover preview \"<a real user phrase>\"` to confirm you surface.\n5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.\n6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find \"<phrase>\"` surfaces you with an instructional brief.\n\n**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.\n", + "markdown": "# Bridge SDK — build, publish, and surface a bridge for Adom Desktop\n\nA **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:\n\n1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it.\n2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs.\n3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *\"open my app in pup\"* surfaces YOU via `adom-wiki discover find`.\n\n> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.**\n> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.**\n> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)\n\n---\n\n## The boundary — what AD owns vs what YOU own\n\nThe #1 mistake is filing AD bugs for bridge issues. The split:\n\n**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them).\n\n**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**.\n\nIf a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.\n\n---\n\n## Self-audit checklist — run this when asked to \"audit against the SDK\"\n\nWhen a maintainer says *\"audit your code, skills, README, and wiki page against the Bridge SDK,\"* **this is the checklist.** Each item names its section below; fix every miss, then re-check.\n\n**bridge.json** (the manifest in your zip):\n- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)*\n- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.**\n- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)*\n\n**Verbs & output** (what the AI actually reads):\n- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)*\n- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)*\n- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)*\n- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)*\n\n**Discovery** (can a plain request find you?):\n- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)*\n- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each.\n- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION.\n\n**Skills & docs** (what the container + humans read):\n- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)*\n- [ ] Your USER skill leads with the cold-start \"read before you panic\" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)*\n- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).\n\n---\n\n## bridge.json — the manifest that ships INSIDE your zip\n\n```jsonc\n{\n \"manifest_version\": 1,\n \"name\": \"blender\", // registry name → verb prefix \"blender_\"\n \"displayName\": \"Blender\", // in-app chip label (the tool's real name)\n \"version\": \"1.2.0\", // numeric; a newer CACHE copy supersedes the bundled seed\n \"description\": \"Drive Blender headless + GUI\",\n \"author\": \"you\",\n \"license\": \"MIT\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop-blender-bridge\", // your OWN page (drives skillPkg + hero)\n \"hero\": \"https://.../hero.png\", // optional; else AD uses your wiki page hero, else a monogram\n \"spawn\": {\n \"kind\": \"python\", // python | node | exe | external-http (NOT \"process\")\n \"entrypoint\": \"server.py\", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv\n \"port\": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.\n \"persistent\": true, // AD's supervisor respawns it if it dies\n \"healthEndpoint\": \"/health\", // INSIDE spawn. AD probes it for the chip (default /health)\n \"stopMethod\": \"kill\", // kill | sigterm | graceful_endpoint\n \"killImageName\": \"python.exe\" // image name for taskkill on stop (kind:python/node)\n },\n \"verbPrefixes\": [\"blender_\"], // any verb with these prefixes routes to you\n \"verbs\": [\"blender_render\", \"blender_status\", \"blender_describe\"], // every verb (routing + Verbs tab)\n \"statusVerb\": \"blender_status\", // verb a caller polls after a long op times out\n \"updateManifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\",\n\n // v1.9.47 cold-start (see \"Cold-start tiers\" below):\n \"detect\": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.\n \"hostApp\": \"Blender\", // omit the whole `detect` block if your bridge brings its own runtime\n \"appPathsExe\": \"blender.exe\", // (e.g. pup ships its own browser → no host app)\n \"paths\": { \"windows\": [\"%ProgramFiles%\\\\Blender Foundation\\\\*\\\\blender.exe\"] }\n },\n \"prewarm\": { \"assets\": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. [\"chrome-for-testing\"])\n}\n```\n\n- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `\"process\"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.)\n- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).\n- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`.\n- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.)\n- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page.\n- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.)\n- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in \"Cold-start tiers\" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser.\n\n---\n\n## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest\n\nThis is the code AD streams onto the user's machine. Two files on your page:\n\n1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob:\n ```bash\n adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title \"...\" --changelog \"...\"\n adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any\n ```\n (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.)\n2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`:\n ```jsonc\n { \"name\":\"blender\", \"version\":\"1.2.0\",\n \"url\":\"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip\", // base-relative, extension-agnostic (.bin works where wiki blocks .zip)\n \"sha256\":\"…\", \"size\":123456, \"verbPrefixes\":[\"blender_\"], \"healthEndpoint\":\"/health\",\n \"updateManifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\" }\n ```\n\n**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\`, rescans, and remembers the URL for auto-update.\n\n**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware).\n\n---\n\n## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`)\n\nYour runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries):\n\n```bash\n# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and\n# package.json {name:\"adom-desktop-<bridge>-bridge\", version, discovery_triggers, files:[...]}.\n# THREE traps that bite — verify each with `tar tzf` before publishing:\n# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE,\n# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH\n# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else\n# every user who installs your pkg also installs your dev/publish docs.\n# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files`\n# allowlist needs the GLOB: a bare \"skills\" entry ships ZERO skill files — use \"skills/**\".\n# (Set .gitignore aside across pack+publish, then restore.)\n# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB\n# before guards). Skills + docs only.\nadom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present,\n # ZERO binaries, ZERO dev/publish SKILL.md\nadom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge\n```\n\nA container gets it **without anyone asking**:\n- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it.\n- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.\n- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them.\n\n**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh.\n\n---\n\n## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you\n\nThe wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs.\n\n```bash\nadom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add \"open my pcb\" \"export gerbers\" # add triggers\nadom-wiki discover preview --json \"open my pcb design\" # what would surface + the SCORE + WHY\nadom-wiki discover find \"screenshot my website\" # the live find an AI runs\n```\n\n**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):**\n- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it.\n- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title (\"Adom Desktop - Blender Bridge\") + a matching tag scored **ZERO** for the literal query *\"blender\"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.)\n- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.\n\n**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *\"open my pcb design\"* / *\"export gerber files\"* / *\"design a circuit board\"* — NOT *\"reverse bridge\"* / *\"kicad_bridge_call\"* / *\"fork the kicad bridge\"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy \"pcb\"/\"export\" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end.\n\n**VET it — never ship triggers you didn't test:**\n1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`).\n2. `adom-wiki discover preview --json \"<each query>\"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.)\n3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on \"pcb\"), add MORE exact phrasings so your 3×-per-trigger total wins.\n\n**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. \"for logged-in form-filling, install the native-browser extension\") — rather than claim it and do it badly.\n\n- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline.\n- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *\"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`).\"*\n\n### The end-to-end flow your three artifacts enable\n\nUser says *\"show me my app in pup\"* → the AI:\n1. `adom-wiki discover find \"show me my app in pup\"` → your bridge surfaces (Artifact 3).\n2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1).\n3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2).\n4. Drive your verbs — guided by the **rich hints you return** (the principle at the top).\n\n---\n\n## Prewarm — make the first call instant\n\nA 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:\n\n- **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.\n- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.\n- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent \"ensure ready\" path AD/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.\n- **`browser_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.\n\n---\n\n## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)\n\nA bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's \"what tools do I have\") triggers a slow install at the worst moment.\n\n**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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).\n\n**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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:\n- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default AD 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). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a \"go away.\"\n- **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.\n\n**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**\n\n| Level | Bridge(s) | Behavior |\n|---|---|---|\n| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not \"installed\" by AD. |\n| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |\n| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). |\n| **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. |\n| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |\n\nThe 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).\n\n**Declare both in `bridge.json`:**\n```json\n\"detect\": { // Tier 2 — how AD finds your HOST APP (detection only)\n \"hostApp\": \"KiCad\",\n \"appPathsExe\": \"kicad.exe\", // optional: Windows App Paths registry lookup (most reliable)\n \"paths\": { // optional: candidate paths, %VAR% expanded, one * = a version dir\n \"windows\": [\"%ProgramFiles%\\\\KiCad\\\\*\\\\bin\\\\kicad.exe\"],\n \"macos\": [\"/Applications/KiCad/KiCad.app\"],\n \"linux\": [\"/usr/bin/kicad\"]\n }\n},\n\"prewarm\": { \"assets\": [\"chrome-for-testing\"] } // Tier 1 — heavy runtime assets AD prewarms + reports\n```\nA bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python).\n\n**The read-only probe: `bridge_readiness`.** AD 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.\n\nYour 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 AD-level aggregate that detects + summarizes across all bridges without spawning any.\n\n---\n\n## The SKILL SET you should ship (not one SKILL.md — a small library)\n\nDon't cram everything into one file. Ship **three kinds** of skill, each with a different reader:\n\n1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong.\n2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill.\n3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.)\n\nPublish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1).\n\n**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply.\n\n---\n\n## USER-skill conventions — what every consumer skill must teach (from pup's shape)\n\nThese eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:\n\n1. **Open with a \"FIRST-TIME / COLD-START — read before you panic\" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose).\n2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{\"wait\":true}` to block. Gives every bridge a uniform \"poll until ready, then proceed\" loop. (Complements the runtime-side Prewarm section above.)\n3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type (\"take a screenshot of\", \"log into\"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3).\n4. **Include a short \"Core verbs\" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb.\n5. **\"Background by default — don't disrupt the user.\"** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`.\n6. **Teach \"`ok:true` is not enough\" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently.\n7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun.\n8. **\"Surface `_hint` verbatim.\"** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.\n\n---\n\n## Verb contract (quick reference)\n\n- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you.\n- **Request:** AD POSTs `{ \"command\":\"<verb>\", \"args\":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`.\n- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`.\n- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI.\n\n## Health + status chip\n\nYour `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.\n\n## Lifecycle verbs (AD gives these to USERS — know them to guide users)\n\n`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.\n\n---\n\n## Publish checklist (the whole modern flow)\n\n1. **Page** — `adom-wiki page create adom/adom-desktop-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.\n2. **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.\n3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap).\n4. **Discovery** — `discover triggers` to confirm; `discover preview \"<a real user phrase>\"` to confirm you surface.\n5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.\n6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find \"<phrase>\"` surfaces you with an instructional brief.\n\n**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.\n", + "body": "# Bridge SDK — build, publish, and surface a bridge for Adom Desktop\n\nA **bridge** is a small local program (Python, Node, Rust, anything) that AD spawns and routes a **namespace of verbs** to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own — get all three right and a cloud/WSL2 container can *discover* your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:\n\n1. **Runtime** — a versioned **Release `.zip`** + a **manifest** on your wiki page. AD's `bridge_install` streams it onto the user's machine and spawns it.\n2. **Skills** — an **`adom-wiki pkg`** tarball carrying your consumer SKILL.md(s). A container installs it into `~/.claude/skills/` (or `~/.codex/skills/`) so the AI *there* knows your verbs.\n3. **Discoverability** — **`discovery_triggers`** on your wiki page so a user saying *\"open my app in pup\"* surfaces YOU via `adom-wiki discover find`.\n\n> **THE ONE PRINCIPLE THAT MATTERS MOST — read this before anything else.**\n> **The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call.**\n> So your leverage is in the **`_hint` / `_next` / `related` / `pitfalls` you return on EVERY verb response.** A verb that returns `{success:true}` and nothing else leaves the AI guessing and retry-looping. A verb that returns *what just happened, the obvious next verb, the related verbs, and the pitfall to avoid* gets driven perfectly on the first try. AD relays every field of your response **verbatim** to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the *human* author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)\n\n---\n\n## The boundary — what AD owns vs what YOU own\n\nThe #1 mistake is filing AD bugs for bridge issues. The split:\n\n**AD core owns** (don't reimplement): the CLI/relay/`/command` passthrough (AD relays your JSON verbatim — every field); spawning/reaping your process, the **stable port**, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; `status`/`_hint` classification for timeouts/disconnects (AD authors those — you never stamp them).\n\n**You own**: your verbs + their behavior + their **rich hints**; your `/health` (+ optional chip fields); your `<prefix>describe` catalog; your wiki page (hero, brief, `discovery_triggers`, docs); your published **manifest + Release zip**; and your **skills pkg**.\n\nIf a *bridge verb* misbehaves → fix the bridge. If a *generic AD capability* is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.\n\n---\n\n## Self-audit checklist — run this when asked to \"audit against the SDK\"\n\nWhen a maintainer says *\"audit your code, skills, README, and wiki page against the Bridge SDK,\"* **this is the checklist.** Each item names its section below; fix every miss, then re-check.\n\n**bridge.json** (the manifest in your zip):\n- [ ] `spawn.kind` ∈ `python|node|exe|external-http`, with **`entrypoint`** (NOT `process`/`cmd`/`args`); `healthEndpoint` is **inside `spawn`**; `port:0` lets AD pick. *(→ bridge.json)*\n- [ ] `updateManifestUrl` → YOUR page's manifest (durable auto-update); `docs` → your own page; **`homepage` is a `wiki.adom.inc` URL, NEVER `github.com`.**\n- [ ] `detect` declared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup). `prewarm.assets` declared for heavy downloads (e.g. `chrome-for-testing`). *(→ Cold-start tiers)*\n\n**Verbs & output** (what the AI actually reads):\n- [ ] EVERY verb returns a rich **`_hint`** (+ `_next`/`related`/`pitfalls`). *(→ THE ONE PRINCIPLE)*\n- [ ] A `<prefix>_describe` verb lists your whole catalog. *(→ Verb contract)*\n- [ ] No verb blocks on a heavy install/download — background it, report `installing`/`warming`; `<prefix>_readiness` is read-only. *(→ Cold-start tiers)*\n- [ ] `/health` returns `led`/`summary`/`tooltip` truthfully. *(→ Health + status chip)*\n\n**Discovery** (can a plain request find you?):\n- [ ] **10+ user-task `discovery_triggers`** (everyday phrasings, NOT dev jargon). **No triggers = invisible.** *(→ Artifact 3)*\n- [ ] You ran `discover preview --json` on 5-7 real user queries and land **top-3** for each.\n- [ ] Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup). `discovery_pitch` is an INSTRUCTION.\n\n**Skills & docs** (what the container + humans read):\n- [ ] You ship the publish/dev/user **3-skill set**; only the USER skill is named `SKILL.md` (dev/publish are `dev-skills/*.md`, source-only). *(→ The SKILL SET)*\n- [ ] Your USER skill leads with the cold-start \"read before you panic\" section + the 8 conventions, and points the AI at `bridge_readiness`. *(→ USER-skill conventions)*\n- [ ] README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).\n\n---\n\n## bridge.json — the manifest that ships INSIDE your zip\n\n```jsonc\n{\n \"manifest_version\": 1,\n \"name\": \"blender\", // registry name → verb prefix \"blender_\"\n \"displayName\": \"Blender\", // in-app chip label (the tool's real name)\n \"version\": \"1.2.0\", // numeric; a newer CACHE copy supersedes the bundled seed\n \"description\": \"Drive Blender headless + GUI\",\n \"author\": \"you\",\n \"license\": \"MIT\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop-blender-bridge\", // your OWN page (drives skillPkg + hero)\n \"hero\": \"https://.../hero.png\", // optional; else AD uses your wiki page hero, else a monogram\n \"spawn\": {\n \"kind\": \"python\", // python | node | exe | external-http (NOT \"process\")\n \"entrypoint\": \"server.py\", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv\n \"port\": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.\n \"persistent\": true, // AD's supervisor respawns it if it dies\n \"healthEndpoint\": \"/health\", // INSIDE spawn. AD probes it for the chip (default /health)\n \"stopMethod\": \"kill\", // kill | sigterm | graceful_endpoint\n \"killImageName\": \"python.exe\" // image name for taskkill on stop (kind:python/node)\n },\n \"verbPrefixes\": [\"blender_\"], // any verb with these prefixes routes to you\n \"verbs\": [\"blender_render\", \"blender_status\", \"blender_describe\"], // every verb (routing + Verbs tab)\n \"statusVerb\": \"blender_status\", // verb a caller polls after a long op times out\n \"updateManifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\",\n\n // v1.9.47 cold-start (see \"Cold-start tiers\" below):\n \"detect\": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.\n \"hostApp\": \"Blender\", // omit the whole `detect` block if your bridge brings its own runtime\n \"appPathsExe\": \"blender.exe\", // (e.g. pup ships its own browser → no host app)\n \"paths\": { \"windows\": [\"%ProgramFiles%\\\\Blender Foundation\\\\*\\\\blender.exe\"] }\n },\n \"prewarm\": { \"assets\": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. [\"chrome-for-testing\"])\n}\n```\n\n- **`spawn.kind`** is `python` | `node` | `exe` | `external-http` — there is **no `\"process\"` kind**, and no `cmd`/`args`/`cwd`. AD launches `spawn.entrypoint` and **automatically appends `--port <port>`** to its argv; your server parses `--port`. (`external-http` = AD doesn't spawn, only probes a port you declare via a `discovery` block.)\n- **`spawn.port: 0`** → AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind `127.0.0.1:<port>`. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).\n- **`spawn.healthEndpoint`** lives **inside `spawn`** (not at the top level), default `/health`.\n- **`updateManifestUrl`** — the full URL of YOUR manifest on YOUR page. **Declare it in `bridge.json`** so it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in `~/.adom/bridge-install-urls.json` as a fallback, but the declared field is the durable, author-intended path.)\n- **`docs`** — point at YOUR dedicated wiki page. AD derives your **skillPkg** (`adom/<slug>`) and your card hero from this. Don't point it at a shared/structural page.\n- **`homepage`** — a **`wiki.adom.inc`** URL (your own page, or `https://wiki.adom.inc/adom/adom-desktop` for the platform). **NEVER `github.com/...`** — GitHub is source-backup only; the wiki is the canonical home. (A `homepage: github.com/adom-inc/adom-desktop` is a stale default to fix.)\n- **`detect` / `prewarm`** — the v1.9.47 cold-start declarations (full rules in \"Cold-start tiers\" below). `detect` = your HOST APP (AD detects, never installs); `prewarm` = your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works without `prewarm`; declare `prewarm.assets` only for big *downloaded* assets like a browser.\n\n---\n\n## Artifact 1 — the RUNTIME: publish your `.zip` as a RELEASE + a manifest\n\nThis is the code AD streams onto the user's machine. Two files on your page:\n\n1. **A versioned Release `.zip`** of your bridge dir (server + `bridge.json` at the zip ROOT + any assets). Ship it as a **Release** (the downloadable, version-pinned artifact), not a loose `/files` blob:\n ```bash\n adom-wiki release create adom/adom-desktop-<bridge>-bridge <version> --title \"...\" --changelog \"...\"\n adom-wiki release upload adom/adom-desktop-<bridge>-bridge <version> your-bridge-v<version>.zip --platform any\n ```\n (Node bridges: the zip carries SOURCE only — keep `node_modules` out; AD/the seed handle deps. See Prewarm.)\n2. **The manifest JSON** (e.g. `blender-manifest.json`) — identity + where the zip is — POSTed to your page `/files`:\n ```jsonc\n { \"name\":\"blender\", \"version\":\"1.2.0\",\n \"url\":\"https://wiki.adom.inc/download/adom/adom-desktop-blender-bridge/1.2.0/blender-v1.2.0.zip\", // base-relative, extension-agnostic (.bin works where wiki blocks .zip)\n \"sha256\":\"…\", \"size\":123456, \"verbPrefixes\":[\"blender_\"], \"healthEndpoint\":\"/health\",\n \"updateManifestUrl\":\"https://wiki.adom.inc/api/v1/pages/adom-desktop-blender-bridge/files/blender-manifest.json\" }\n ```\n\n**Users install/update** with AD's `bridge_install {manifestUrl}` → AD downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\<name>\\`, rescans, and remembers the URL for auto-update.\n\n**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 AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware).\n\n---\n\n## Artifact 2 — the SKILLS pkg: container-side docs (`.claude/skills` / `.codex/skills`)\n\nYour runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your **consumer SKILL.md** locally to know your verbs. Ship it as its own **`adom-wiki pkg`** (skills only, NO binaries):\n\n```bash\n# Stage dir: a ROOT SKILL.md (required) + USER skills under skills/<slug>/SKILL.md, and\n# package.json {name:\"adom-desktop-<bridge>-bridge\", version, discovery_triggers, files:[...]}.\n# THREE traps that bite — verify each with `tar tzf` before publishing:\n# 1. ⚠ The wiki indexes EVERY file named SKILL.md in the tarball as an INSTALLABLE,\n# Skills-tab-listed USER skill (it counts them as skill_count). So your DEV + PUBLISH\n# skills must NOT be named SKILL.md — put them in dev-skills/*.md, source-only — else\n# every user who installs your pkg also installs your dev/publish docs.\n# 2. ⚠ pkg pack/publish HONORS the stage dir's .gitignore AND the package.json `files`\n# allowlist needs the GLOB: a bare \"skills\" entry ships ZERO skill files — use \"skills/**\".\n# (Set .gitignore aside across pack+publish, then restore.)\n# 3. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB\n# before guards). Skills + docs only.\nadom-wiki pkg pack # then: tar tzf <tgz> → every USER SKILL.md present,\n # ZERO binaries, ZERO dev/publish SKILL.md\nadom-wiki pkg publish --org adom # publishes adom/adom-desktop-<bridge>-bridge\n```\n\nA container gets it **without anyone asking**:\n- `bridge_install` returns `skillPkg` + `installSkill` (AD derives `adom/<slug>` from your `docs`) and the `_hint` tells the AI to run it.\n- **`adom-desktop sync_skills`** installs every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.\n- The wiki page's **Skills tab** lists the skills your pkg ships, so the page itself advertises them.\n\n**Install to BOTH agent homes.** A container may run Claude OR Codex — your pkg's `install.sh` must deploy the user skill to **`~/.claude/skills/`** AND **`~/.codex/skills/`** (Codex is a real second skills home now). `adom-wiki pkg update` keeps both fresh.\n\n---\n\n## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you\n\nThe wiki has an **auto-discover** index, and it is the difference between a usable bridge and an **invisible** one. Declare `discovery_triggers` (the phrases a user actually says) on your page, and a plain request matches them via `adom-wiki discover find` / the in-app discovery an AI runs.\n\n```bash\nadom-wiki discover triggers adom/adom-desktop-<bridge>-bridge --add \"open my pcb\" \"export gerbers\" # add triggers\nadom-wiki discover preview --json \"open my pcb design\" # what would surface + the SCORE + WHY\nadom-wiki discover find \"screenshot my website\" # the live find an AI runs\n```\n\n**How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):**\n- `score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text)`. **Trigger matches dominate (3× each);** tags/title/brief barely move it.\n- **NO TRIGGERS = INVISIBLE.** A bridge with a perfect title (\"Adom Desktop - Blender Bridge\") + a matching tag scored **ZERO** for the literal query *\"blender\"* — it never appeared in `discover preview`. The scorer ignores title/tags when there are no triggers. **Every shipped bridge MUST carry `discovery_triggers` or it cannot be found by the discovery an AI uses.** (`discover search` full-texts the readme, but that is NOT the path discovery takes.)\n- **More good triggers = higher rank.** Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry **30-40** triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.\n\n**The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon.** A real user types *\"open my pcb design\"* / *\"export gerber files\"* / *\"design a circuit board\"* — NOT *\"reverse bridge\"* / *\"kicad_bridge_call\"* / *\"fork the kicad bridge\"*. A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the **Fusion** bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy \"pcb\"/\"export\" triggers). Put **10+ everyday task phrasings first**; technical aliases (verb names) are a small minority at the end.\n\n**VET it — never ship triggers you didn't test:**\n1. Write **5-7 queries a typical user would type** to get your tool (these double as your page `sample_prompts`).\n2. `adom-wiki discover preview --json \"<each query>\"` → read `data.scoring[]` (array order = rank). **You must land in the top 3.** (Rate-limited: space calls ~2 s; expect a 429 after ~25.)\n3. For every query where you're NOT top-3, **add that exact phrasing as a trigger** and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on \"pcb\"), add MORE exact phrasings so your 3×-per-trigger total wins.\n\n**Claim YOUR niche — don't poach a sibling's territory.** Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has *more* triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. **Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's.** Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for *non-login* automation (scraping, screenshots, testing, public pages). So pup must NOT carry `log in` / `fill a form on a site I'm signed into` triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your `discovery_pitch` *point at the bridge that does* — and tell the user to install it (e.g. \"for logged-in form-filling, install the native-browser extension\") — rather than claim it and do it badly.\n\n- **`discovery_pitch`** is the one-liner the snippet quotes back to the AI — auto-generated from `discovery_triggers` + `discovery_pitch`, **no separate upload step.** Make it an INSTRUCTION, not a tagline.\n- **Make your `brief` / `discovery_pitch` an instruction** — it's what the AI reads from the find result. Tell it the next moves: *\"…check `adom-desktop bridge_list` to confirm the bridge is installed; if it is, drive its `<prefix>_*` verbs. If the container lacks the skill, `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge` (or `adom-desktop sync_skills`).\"*\n\n### The end-to-end flow your three artifacts enable\n\nUser says *\"show me my app in pup\"* → the AI:\n1. `adom-wiki discover find \"show me my app in pup\"` → your bridge surfaces (Artifact 3).\n2. `adom-desktop bridge_list` → is your bridge installed in AD? If not, `bridge_install {your manifestUrl}` (Artifact 1).\n3. Is your skill pkg installed in THIS container (`~/.claude/skills/` or `~/.codex/skills/`)? If not, `adom-wiki pkg install adom/<slug>` / `sync_skills` (Artifact 2).\n4. Drive your verbs — guided by the **rich hints you return** (the principle at the top).\n\n---\n\n## Prewarm — make the first call instant\n\nA 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:\n\n- **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.\n- **Idempotent warm-up.** Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.\n- **AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability.** (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent \"ensure ready\" path AD/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.\n- **`browser_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.\n\n---\n\n## Cold-start tiers, host-app detection & the `bridge_readiness` probe (v1.9.47)\n\nA bridge has TWO kinds of dependency, and **AD treats them OPPOSITELY.** Get this right or a first-install probe (e.g. HD's \"what tools do I have\") triggers a slow install at the worst moment.\n\n**Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it.** This is your plumbing. On first boot AD installs Node/Python in the background (winget) 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).\n\n**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD 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:\n- **Don't install it unasked.** A multi-GB app nobody asked for is rude, so by default AD 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). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a \"go away.\"\n- **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.\n\n**Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:**\n\n| Level | Bridge(s) | Behavior |\n|---|---|---|\n| **0 · Built-in / MUST** | Hydrogen Desktop (`hd`) | The platform itself — the editor AD lives in. Always present; not \"installed\" by AD. |\n| **1 · Fundamental — pre-emptive** | Puppeteer (`pup`) | Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |\n| **2 · Recommended — proactive offer** | native-browser (the extension) | The user's REAL logged-in browser (login/forms). AD proactively SUGGESTS it and installs on a yes (it needs a browser-extension step the user confirms). |\n| **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. |\n| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |\n\nThe 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).\n\n**Declare both in `bridge.json`:**\n```json\n\"detect\": { // Tier 2 — how AD finds your HOST APP (detection only)\n \"hostApp\": \"KiCad\",\n \"appPathsExe\": \"kicad.exe\", // optional: Windows App Paths registry lookup (most reliable)\n \"paths\": { // optional: candidate paths, %VAR% expanded, one * = a version dir\n \"windows\": [\"%ProgramFiles%\\\\KiCad\\\\*\\\\bin\\\\kicad.exe\"],\n \"macos\": [\"/Applications/KiCad/KiCad.app\"],\n \"linux\": [\"/usr/bin/kicad\"]\n }\n},\n\"prewarm\": { \"assets\": [\"chrome-for-testing\"] } // Tier 1 — heavy runtime assets AD prewarms + reports\n```\nA bridge that brings its OWN runtime (pup ships its own browser) **omits `detect`** — it has no external host app. AD infers the runtime from `spawn.kind` (node→Node, python→Python).\n\n**The read-only probe: `bridge_readiness`.** AD 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.\n\nYour 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 AD-level aggregate that detects + summarizes across all bridges without spawning any.\n\n---\n\n## The SKILL SET you should ship (not one SKILL.md — a small library)\n\nDon't cram everything into one file. Ship **three kinds** of skill, each with a different reader:\n\n1. **A PUBLISH skill** (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the `.gitignore` trap), set `discovery_triggers`, and re-seed AD's bundled copy. This keeps *your* future self from getting the publish dance wrong.\n2. **A DEV skill** (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, **the rich-hints-in-output rule**: *the AI never reads this file; it reads your verb OUTPUT, so every verb must return `_hint`/`_next`/`related`/`pitfalls`.* Make that the loudest thing in the dev skill.\n3. **USER skill(s)** (shipped in your pkg → the container): how a cloud AI drives your verbs to get real work done. **If the verb surface is large, split it:** a small **core USER skill** (the 80% workflow + a pointer to the sub-skills) plus **sub-skills** by area (e.g. `…-recording`, `…-electronics`, `…-aps-search`). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + sub-skills.)\n\nPublish the PUBLISH + DEV skills on your page for your thread; ship the USER skills in your **pkg** so containers get them. **Critical naming rule:** only USER skills may be named `SKILL.md` — the wiki indexes every `SKILL.md` in your tarball as an installable user skill, so name your DEV/PUBLISH docs `dev-skills/*.md` (source-only) or every user installs them (Artifact 2, trap #1).\n\n**Start from the templates — don't write these from scratch.** `skills/bridge-templates/` in the AD repo has a fill-in-the-blanks file for each of the three skill kinds, generalized from pup's battle-tested set: **`bridge-dev-template.md`** (architecture + the rich-hints rule), **`bridge-publish-template.md`** (the whole release/pkg/discovery dance + every trap), **`bridge-user-skill-template.md`** (the consumer skill, already encoding all eight conventions below). Copy the one you need, swap `<bridge>`/`<prefix>` for your names, delete what doesn't apply.\n\n---\n\n## USER-skill conventions — what every consumer skill must teach (from pup's shape)\n\nThese eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:\n\n1. **Open with a \"FIRST-TIME / COLD-START — read before you panic\" section.** A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a **response → meaning → what-you-do** table that frames `*_installing` / `not_ready` as EXPECTED, keyed off `errorCode` (not prose).\n2. **Standardize two cold-start verbs: `<prefix>_readiness` and `<prefix>_prewarm`.** `readiness` → `{ready, installing, installProgressPct, lastError}`; `prewarm` kicks off setup without real work and supports `{\"wait\":true}` to block. Gives every bridge a uniform \"poll until ready, then proceed\" loop. (Complements the runtime-side Prewarm section above.)\n3. **End the `description:` frontmatter with a `Trigger words:` line** mixing verb names AND plain-English phrases a user would actually type (\"take a screenshot of\", \"log into\"). This is what makes a user-invocable skill activate — and it feeds your `discovery_triggers` (Artifact 3).\n4. **Include a short \"Core verbs\" table** (Command | Description | Key args) for the 80% path, and point at `<prefix>_describe '{}'` for the full machine-readable catalog. The AI reads the table to act fast; `describe` is the completeness escape hatch — don't bloat the skill with every verb.\n5. **\"Background by default — don't disrupt the user.\"** Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (`*_alert_window`) instead of stealing focus. Standardize `*_lower_os_window` / `*_raise_os_window` / `*_alert_window`.\n6. **Teach \"`ok:true` is not enough\" — verify what you did.** Verbs that return on operation *start* (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently.\n7. **Sessions: reuse-don't-reopen + stable identifiers.** Tell the AI to `*_status` before opening a new session (especially after compaction), reuse a match, and pick a stable `profile` so logins/state persist across runs. Prevents duplicate windows and the lost-login footgun.\n8. **\"Surface `_hint` verbatim.\"** Every verb returns an actionable `_hint` on not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stable `errorCode` (not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.\n\n---\n\n## Verb contract (quick reference)\n\n- **Naming:** every verb is `<name>_<verb>` (`blender_render`, `blender_status`). AD routes any verb with your prefix to you.\n- **Request:** AD POSTs `{ \"command\":\"<verb>\", \"args\":{…} }` to your port. **Response:** JSON with at least `success` (bool); human/AI output in `output`, errors in `error` (+ an `errorCode` for machine-actionable failures). AD relays everything else verbatim — so ALWAYS add `_hint`/`_next`/`related`/`pitfalls`.\n- **Long-running verbs:** return promptly with a job id + set `statusVerb`; the caller polls instead of blocking. On a timeout AD auto-returns `{stillRunning:true, statusVerb, timeoutSeconds}`.\n- **`<prefix>describe`:** return your full verb catalog (`{verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}`). AD caches it (`~/.adom/bridge-verbs/<name>.json`) and shows it in the GUI Verbs tab — but remember, `describe` is read rarely; the per-call hints are what carry the AI.\n\n## Health + status chip\n\nYour `/health` (200) makes the chip green. Optionally return `led` (`green`/`yellow`/`red`), `summary` (short label), `tooltip` (full hover). AD owns the **offline** (gray) state when your endpoint is unreachable — you can never assert offline.\n\n## Lifecycle verbs (AD gives these to USERS — know them to guide users)\n\n`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.\n\n---\n\n## Publish checklist (the whole modern flow)\n\n1. **Page** — `adom-wiki page create adom/adom-desktop-<bridge>-bridge` with a hero + an **instructional `brief`** + **`discovery_triggers`**.\n2. **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.\n3. **Skills pkg** — stage ROOT SKILL.md + `skills/<slug>/SKILL.md` (core USER skill + sub-skills) + `discovery_triggers`; `pkg pack` (inspect!) → `pkg publish --org adom` (mind the `.gitignore` trap).\n4. **Discovery** — `discover triggers` to confirm; `discover preview \"<a real user phrase>\"` to confirm you surface.\n5. **Re-seed AD's bundled copy** (AD-core thread, not you) — extract your Release zip over `plugins/<name>/` keeping bundled deps; ships in the next NSIS as the offline fallback.\n6. **Verify** — `bridge_install {manifestUrl}` on a desktop → `bridge_list` shows your `version` + `skillPkg`; `bridge_check_updates` reaches your page; a container `sync_skills` pulls your USER skill; `discover find \"<phrase>\"` surfaces you with an instructional brief.\n\n**Reference bridge = pup** (`adom/adom-desktop-puppeteer-bridge`): live `discovery_triggers`, a skills pkg with a core + sub-skills, a Release zip + manifest with `updateManifestUrl`, a bundled seed with prewarmed `node_modules`, and rich per-verb hints. Copy its shape. (`scripts/sample-bridges/hello-python` + `hello-rust` are the minimal skeletons.) KiCad/Fusion/Puppeteer are cloud-owned — don't edit them from the AD repo.\n" }