---
name: adom-desktop-bridge-sdk
description: Author, PUBLISH, and make-discoverable an adom-desktop bridge — a small local server that adds a namespace of verbs (kicad_, fusion_, browser_, your_prefix_) to adom-desktop. Use when building, publishing, updating, prewarming, or debugging a bridge, or deciding what skills to ship. Covers the AD-core-vs-author boundary; the #1 rule (rich hints in EVERY verb's OUTPUT, because the AI reads CLI output, never your on-disk skill); bridge.json; where every file lives (the Release-zip-vs-skills-pkg split, the TWO package.json files [server deps in the zip; the pkg declares dependencies:{adom/adom-desktop}], manifest + runtime-asset placement); the pkg carries ONLY user skills while dev + publish skills live in your source repo (not the pkg); the THREE things you publish (runtime Release zip + manifest, container skills pkg, wiki discovery_triggers); prewarm; and the auto-discover → install-check → use flow.
---

# Bridge SDK — build, publish, and surface a bridge for Adom Desktop

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:

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 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.)

---

## Where every file lives — the two artifacts, their deps, and which skills go where

**Two DISTINCT artifacts, two DIFFERENT homes, two DIFFERENT consumers. Crossing them is the #1 inconsistency across bridge repos — pin it here before anything else.** (Live audit 2026-07-06: fusion + pup do this right; kicad had **61** loose `adom-bridge-kicad-v*.zip` on `/files` + a `mesa-llvmpipe-x64.zip`; blender dumped 2 loose zips too.)

| | **RUNTIME artifact** (Artifact 1) | **SKILLS pkg** (Artifact 2) |
|---|---|---|
| **What it is** | your bridge's actual CODE | your consumer `SKILL.md` docs |
| **Runs on** | the user's **DESKTOP** — AD spawns it | **nothing runs it** — docs the CONTAINER reads |
| **Published as** | a versioned **Release** (`release create` + `release upload --platform any`) | an **`adom-wiki pkg`** (`pkg publish --org adom`) |
| **Installed by** | `bridge_install {manifestUrl}` → AD streams, unzips, spawns | `pkg install adom/<slug>` / `sync_skills` → `~/.claude/skills` + `~/.codex/skills` |
| **Contains** | server code + `bridge.json` at the zip **ROOT** + assets. **Node: SOURCE ONLY, no `node_modules`.** | ONLY: ROOT `SKILL.md`, `skills/<name>/SKILL.md` (user skills), `package.json`, `install.sh`, `uninstall.sh`. **Text only — the whole tarball should be a few KB.** |
| **NEVER contains** | the skills pkg | **ANY image (hero / screenshots — those are WIKI PAGE assets, see below), any binary, the Release zip, `src/`, `node_modules`, dev/publish skills, or the server's npm deps.** If `tar tzf` shows a `.png`/`.jpg`/`.zip`/`.exe`, you bundled bloat — remove it. |
| **its `package.json`** | (node) the **bridge SERVER's runtime deps** AD installs at spawn: `dependencies:{ puppeteer, … }` | the **pkg manifest**: `files[]` (user skills), `discovery_triggers`, scripts, **AND `dependencies:{"adom/adom-desktop":"^1.9.x"}`** (the container needs the AD CLI + core skills to drive you) — but NOT the server's npm deps |

**Yes, a node bridge has TWO `package.json` files and they are NOT interchangeable.** One rides inside the Release zip and lists the **bridge server's** runtime deps (puppeteer, sharp, ...) that AD installs on the *desktop* at spawn. The other is the **pkg manifest** in the skills tarball: it lists your user-skill `files[]` + `discovery_triggers` AND declares **`dependencies:{"adom/adom-desktop":"^1.9.x"}`** — `pkg install` resolves that npm-style, so installing your bridge's skill pkg also pulls the adom-desktop CLI + core skills the container needs to drive you. What the pkg manifest must NOT list is the **server's** npm deps (those are desktop-side, in the zip) — put them there and you bloat every container install, since the container never runs your server.

**Where each file physically lives (this is the map that ends the inconsistency):**
- **`bridge.json`** → the **root of the Release zip** (AD reads it post-unzip). Carries `version` + `updateManifestUrl`.
- **The Release `.zip`** → upload it as a **Release asset** (`release upload … --platform any`) and point the manifest `url` at its **release-download path**: `https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>.zip` (verified to serve anon 206). **fusion + pup do exactly this — copy them.** A loose `/files` blob technically serves too (206), so this is a HYGIENE + right-home rule, not "it won't work": the `/files` store is your git **page repo** (for the manifest, text, images), so a binary there accumulates forever (kicad has **61** stale `-v*.zip`), isn't pinned to a Release, and the `/blob/...` URL is just an **alias of the same blob** (same sha) — you can't tidy it. **⚠ Migrating off `/files` is a SEQUENCE, not a delete:** (1) `release upload` the zip, (2) repoint the manifest `url` at the `/download/` path, (3) verify it serves anon, (4) *only then* `repo rm` the old `/files` copies. **NEVER `repo rm` a zip while your manifest still points at it** (or at its `/blob` alias) — that breaks `bridge_install` + auto-update instantly.
- **The manifest** (`adom-bridge-<name>-manifest.json`, this naming is the consistent one — keep it) → POSTed to your page **`/files`**. Its `url` points at the **Release download** (`https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>.zip`), base-relative + extension-agnostic. `updateManifestUrl` (in the manifest AND in bridge.json) points back at this manifest.
- **A big RUNTIME ASSET** (a headless-GL pack like Mesa, a browser download) → declare it in **`prewarm.assets`** so AD downloads + manages it, or bundle it in your seed. **NOT a loose `/files` blob** — kicad's `mesa-llvmpipe-x64.zip` on `/files` is the anti-pattern.
- **Your hero image + screenshots** → **WIKI PAGE assets** — set them on the page (the page's hero / `repo push` the image to the page), **NEVER in the skills tarball.** A container that `pkg install`s your skills never renders a hero, so a bundled hero (a real one was **1 MB**) is pure download bloat on every install. **Do NOT put `hero: docs/hero.png` + `docs/**` in the pkg `package.json`** — that is exactly what stuffs the image into the tarball.

**The three kinds of skill — where each goes (this IS the demarcation, one table):**

| Skill kind | Who reads it | Lives in | In the pkg tarball? | Public? |
|---|---|---|---|---|
| **USER** (the public / consumer skill) | a consumer AI driving your verbs | your **pkg** | **YES** — that is the pkg's whole job | yes (the pkg is public) |
| **DEV** | a maintainer editing the bridge | your **source repo** | **NO, never** | only if your repo is public |
| **PUBLISH** | a maintainer releasing the bridge | your **source repo** | **NO, never** | only if your repo is public |

**The one rule:** the pkg tarball carries **USER skills only**; **DEV + PUBLISH skills live in the source repo.** Do NOT "scope" dev/publish into the pkg with `user-invocable:false` — that still ships them (pure bloat). Open-vs-closed source only sets your repo's *visibility*; it never changes what's in the pkg. adom-desktop core does exactly this: user/integration skills in the pkg; `RELEASE` / `WIKI_PUBLISH` / `SIGNING` / `AD_RELAUNCH` live only in the source repo.

---

## The boundary — what AD owns vs what YOU own

The #1 mistake is filing AD bugs for bridge issues. The split:

**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).

**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**.

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.

---

## Calling AD back (outbound) — you reach the FULL AD verb set (v1.9.84)

Your callback channel is AD's loopback direct API: `POST <ADOM_DIRECT_API_URL>/command` (env var; fallback `~/.adom/direct-api-port` = `host:port`). As of **AD v1.9.84** this reaches **every dispatchable AD verb**, not just `desktop_*` — same dispatcher the CLI uses, identical JSON back. `GET <ADOM_DIRECT_API_URL>/commands` lists the full set so you can capability-probe (don't hardcode).

- **`app` is OPTIONAL** — inferred from the command. Just `POST {"command":"<verb>","args":{…}}`. (Pass `app` to override the inference.)
- **Top-level verbs you can now call:** `notify_user` (walk the user through something — returns `{action:"displayed"}`), `notify_response` (poll a toast button/input), `targets` (list the OTHER ADs on the relay), `ping`, `bridge_list`, `runtimes`, `status`, … — plus every `desktop_*` and every *other* bridge's verbs.
- **Cross-AD (`target`):** add `"target":"<clientName>"` (from `targets`) or `"all"` to route the call to a PEER AD via the relay — e.g. **you run on a VM but the user is on their laptop**: `notify_user` with `target:"<laptop clientName>"` reaches them where they are. `"attended"` isn't resolvable yet (use a concrete clientName).
- **`X-Adom-Bridge-Token` (optional, attribution).** AD injects `ADOM_BRIDGE_TOKEN` into your spawn env. Send it as the `X-Adom-Bridge-Token` header and AD badges your calls "bridge" in its Activity Log — nice for the user's transparency. It is **NOT** an approval gate: you're trusted by transport (AD binds 127.0.0.1 only + the user consented by installing you), so your calls — including `write_file`/`run_script` — run ungated. A stale token → 403 (re-read it from your env after an AD restart); omitting it is fine.
- **`cliRequired`** verbs (`pull_file`/`send_files`/`shell_execute`) return `412 cli_required` — they need the streaming/approval CLI path, not this HTTP channel.

```bash
# Discover, then call — no hardcoding.
curl -s "$ADOM_DIRECT_API_URL/commands"
curl -s -X POST "$ADOM_DIRECT_API_URL/command" -H 'Content-Type: application/json' \
  -H "X-Adom-Bridge-Token: $ADOM_BRIDGE_TOKEN" \
  -d '{"command":"notify_user","args":{"title":"Heads up","body":"…"}}'
```

---

## Branding + driving a window you borrowed (window identity + taskbar, v1.9.106–1.9.153)

If your bridge drives a window another process owns — the classic case is a **Chrome for Testing** window that otherwise reads *"Test"* with Chrome's icon — AD gives you a generic verb family to make it present as YOUR app across every Windows shell surface, and to show live status on its taskbar button. AD stays generic: it applies whatever name/icon/command you pass; it knows nothing about your app. **Full guide: the `adom-desktop-window-identity` skill** (`https://wiki.adom.inc/api/v1/pages/adom-desktop/files/docs/WINDOW_IDENTITY.md` or the repo `skills/WINDOW_IDENTITY.md`). The essentials:

- **Two halves, you need both:** `desktop_register_app_identity {appId, displayName, iconPath, shortcut}` teaches Windows what an AUMID means (HKCU only, no UAC; `shortcut:false` for a per-session id with no Start Menu spam), and `desktop_set_window_identity {hwnd, appId, iconPath, displayName, relaunchCommand}` stamps it on the live window. Stamp without register → Alt-Tab / taskbar fall back to the owning exe's icon.
- **Per-session identity:** register `shortcut:false` per window (e.g. `Adom.Pup.<sessionId>`) so each keeps its own taskbar button; `desktop_unregister_app_identity {appId}` on close.
- **Jump list + header:** `desktop_set_window_jumplist {appId, tasks, hwnd?, headerIcon?, headerCommand?}` adds the right-click menu AND (with `hwnd`+`headerIcon`) brands the header row (the app tile above Pin/Close) with no Start Menu shortcut.
- **Taskbar status:** `desktop_taskbar {hwnd, progress?, overlay?, flash?, thumbnailTooltip?, thumbnailClip?}` paints glanceable state on any window's button. **New in v1.9.153:** `thumbnailTooltip` is the ONLY uncapped hover-text surface (the window title truncates ~25-30 chars; this does not, and takes newlines; `""` clears it), and `thumbnailClip:{x,y,w,h}` crops the live hover preview to a region (`null` resets). Overlay compositing (`overlay.avatar`/`profileDir`) preserves a browser profile face while adding your mark.
- **Icon format — read the `iconCheck` in every response.** The identity verbs inspect any icon you pass and return `iconCheck {kind,sizes,issues,ok,hint}`. The traps it catches: a **256px `.ico` size that is not PNG-compressed** renders as the generic white-document icon (Vista+ rule); **SVG can't be loaded** by the Win32 shell; the AUMID registry icon wants a `.ico`/`.png`, not an `.exe`. If `iconCheck.ok` is false, fix the file and re-stamp — don't ship the broken icon. Ship a multi-size `.ico` (16/24/32/48/256, the 256 PNG-compressed).
- **The live-tile trap:** re-stamping the SAME appId with a new icon updates the title bar + Alt-Tab but NOT the taskbar tile (Win11 bakes it at button creation). To change a live tile, register + stamp a DIFFERENT appId; the button is recreated in ~1s.

---

## Runtime contract — AD provisions Node/Python; you bind loopback (v1.9.63 / v1.9.69)

**The single most important change for a Node or Python bridge.** As of AD **v1.9.63** you no longer bootstrap your own interpreter, and you MUST bind loopback. Four rules:

1. **Don't bootstrap your own Node/Python.** AD owns a global, portable, **no-UAC** runtime under `~/.adom/adom-runtimes/`: a system install if one's on PATH, else a portable copy it downloads + prewarms in the background on first launch. AD spawns your `entrypoint` with that interpreter **by absolute path**. So **delete every `winget install node` / `desktop_install_node` / "please install Python" / self-download step** from your bridge and its skills. `spawn.kind: "node"|"python"` IS your runtime declaration — AD reads it and provisions the runtime. Anyone can check status with the **`runtimes`** verb (`state ∈ absent|installing|ready|failed`, `source ∈ system|cache`, version). AD pins the version (Node 22.11.0 / Python 3.12.13 today) — need a different major? ask the AD-core thread; don't ship your own.

2. **Bind `ADOM_BIND_HOST`, never `0.0.0.0`.** AD passes an **`ADOM_BIND_HOST`** env var (always `127.0.0.1`) to every bridge it spawns. A bare listen that defaults to `0.0.0.0` pops a **Windows Firewall "allow access?" dialog** on first run — and AD's hard guarantee to users is **no firewall prompt by default**.
   - Node: `server.listen(PORT, process.env.ADOM_BIND_HOST || '127.0.0.1', cb)`
   - Python: `HTTPServer((os.environ.get('ADOM_BIND_HOST', '127.0.0.1'), PORT), Handler)`

3. **Ship the Release zip source-only — AD installs your deps, and v1.9.69 makes it bulletproof.** A Node bridge's zip carries SOURCE only (no `node_modules`). AD reuses the bundled seed's `node_modules` via `NODE_PATH` when versions match, else runs **`npm install --include=optional`** for you with the managed runtime's own npm. **As of v1.9.69 AD puts the managed runtime's dir on the child PATH for that install AND for your spawned process** — so a dep's postinstall / `node-gyp` / `prebuild-install` that shells out to a **bare** `node`/`npm`/`npx` (puppeteer's own `node install.mjs`, `sharp`, `keytar`, `better-sqlite3`, `bcrypt`) resolves even on a machine with **no system Node**. **You do NOT add the runtime to PATH yourself** — AD guarantees it. (Python: the interpreter dir + `Scripts/` are on PATH too, so pip can build wheels.)

4. **Bundle deps in your SEED for an instant first spawn.** Source-only zip + a bundled-seed `node_modules` at the same version = AD reuses the seed via `NODE_PATH` (instant); a wiki version newer than the seed forces one cold `npm install` (now PATH-correct, but slower). Bump the seed in lockstep with each Release.

**Net:** declare `spawn.kind`, bind `ADOM_BIND_HOST`, ship source-only. AD does the rest — no UAC, no firewall prompt, no `'node' is not recognized`.

---

## Staying current — catch up on SDK changes yourself (no prompt needed)

This SDK evolves. When it does, AD posts a re-audit notice to **YOUR bridge's wiki discussion** and re-publishes this billboard. You do NOT need a human to hand you a prompt — self-serve at the start of a work session:

1. **Check your discussion for a notice.** `adom-wiki discussion list <your-slug>` (or open your page's Discussions tab) and read any "SDK update" / "re-audit" thread. A notice can be EDITED after posting (a correction), so always read the **current** body, not a cached earlier version.
2. **Re-read this billboard**, especially "Where every file lives" (above) and the "Self-audit checklist" (below): `https://wiki.adom.inc/adom/adom-desktop-bridges` (or `curl -s https://wiki.adom.inc/api/v1/pages/adom-desktop-bridges/files/README.md`).
3. **Run the Self-audit checklist** (next section) against your code, `bridge.json`, skills, README, and wiki page. Fix every miss.
4. **Reply on the discussion** with what you changed or any blockers. That thread is the ONLY channel AD uses to reach you, so closing the loop there is how AD knows you are current.

---

## Self-audit checklist — run this when asked to "audit against the SDK"

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.

**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)*
- [ ] **`timeouts`** declared if ANY verb can run longer than 60s (a big export/render/cloud-open) — with a matching **`statusVerb`** to poll. Third-party bridges were 60s-capped before v1.9.79; this is how you raise it. *(→ bridge.json)*

**Artifacts & deps** (the two-artifact split — the #1 inconsistency) *(→ Where every file lives)*:
- [ ] Your Release `.zip` is a **Release asset** (`release upload`), NOT a loose `/files` blob — and old version zips are NOT piling up on `/files` (audit for stray `*-v*.zip`).
- [ ] The **server's** runtime deps live in the **ZIP's** `package.json` (node) / `requirements.txt` (python). The **pkg's** `package.json` declares `dependencies:{"adom/adom-desktop":"^1.9.x"}` (so `pkg install` pulls the AD CLI + core skills) and does NOT list the server's npm deps.
- [ ] A big runtime asset (Mesa, a browser) is a **`prewarm.asset`** or seed-bundled — NOT a loose `/files` blob.
- [ ] Your **manifest** (`adom-bridge-<name>-manifest.json`) is on `/files`; its `url` points at the **Release download**, not at a `/files` blob.

**Runtime** (Node/Python bridges) *(→ Runtime contract)*:
- [ ] You bind **`ADOM_BIND_HOST`** (loopback default), never `0.0.0.0`.
- [ ] You do **NOT** bootstrap your own Node/Python — no `winget`/`desktop_install_node`/self-download in code or skills. `spawn.kind` is the declaration; AD provisions it.
- [ ] Release zip is **source-only** (no `node_modules`); you rely on AD's `npm install` (PATH-correct as of v1.9.69) + a lockstep bundled seed for instant spawn. You do NOT put the runtime on PATH yourself.

**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)*

**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.

**Skills & docs** (what the container + humans read):
- [ ] Your pkg ships **ONLY your USER skill(s)** — a real `skills/<name>/SKILL.md` (core user skill + user sub-skills) with a `Parent skill:` first body line; every path listed EXPLICITLY (no globs) in `files[]` + install.sh/uninstall.sh. *(→ Artifact 2 + The SKILL SET)*
- [ ] **DEV + PUBLISH skills are in your source REPO, NOT the pkg** — not in `files[]`, not scoped in via `user-invocable:false`. A developer gets them by cloning the source; the normal user never needs them. Open-vs-closed source is irrelevant (it only sets repo visibility). *(→ Where every file lives)*
- [ ] Your pkg's `package.json` declares `dependencies:{"adom/adom-desktop":"^1.9.x"}` so `pkg install` pulls the AD CLI + core skills the container needs. *(→ Where every file lives)*
- [ ] 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).

---

## 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. Ship it at 1.6 aspect (≈2000×1250) — see "Card hero + update cost"
  "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",

  "timeouts": {                             // v1.9.79 — per-verb HTTP budgets (seconds); see note below
    "default": 60,                          //   fallback for any verb not matched (absent → AD's global 60s)
    "verbs": { "render": 600, "export_step": 360 },        // exact BARE verb names (no prefix)
    "prefixes": [ { "prefix": "export_", "seconds": 120 } ] // fallback rules, first match wins
  },

  // 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.
- **`timeouts`** (v1.9.79) — per-verb HTTP budgets (seconds) AD honors so a SLOW verb (a big export, a cloud open, a long render) isn't cut off before it finishes. Resolution: exact `verbs` (BARE names, no prefix) → first matching `prefixes` → `default` → AD's global 60s. A timeout is **NON-terminal**: AD returns `{stillRunning:true, statusVerb, timeoutSeconds}` and the caller polls your `statusVerb` (so ALSO declare `statusVerb`). **⚠ Before v1.9.79 a third-party bridge was hard-capped at 60s with no way to raise it** — if any of your verbs can run longer, declare `timeouts` (this is the fix). AD reads it with ZERO AD change; the two bundled EDA bridges keep a labeled built-in table only until they declare their own.

---

## 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.

**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). ⚠ **A `release upload` REPLACES a same-platform asset ONLY when the FILENAME is IDENTICAL** — a different name (a version bump `-v1.2.0` → `-v1.3.0`) ADDS a second asset, leaving a stale zip on the release. Keep ONE asset per release: overwrite the same filename, or `adom-wiki release delete-asset <owner/slug> <ver> "<old-filename>"` after a rename. (A stale extra won't break the manifest-URL install, but it clutters the release.)

---

## 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):

**Layout** (reference: `adom/adom-wiki-skillpack`):

- **The pkg carries ONLY your USER skill(s).** Each is a real `skills/<name>/SKILL.md`. A ROOT `SKILL.md` (required by the registry) is the pack entry point: `user-invocable: true`, a `description` full of trigger words. Large surface → a core user skill + user sub-skills, but **every skill in the pkg is a *user* skill.**
- **DEV + PUBLISH skills do NOT go in the pkg — they live in your source repo** (a developer gets them by cloning to edit the bridge). Do NOT list them in `files[]`, and do NOT "scope" them in with `user-invocable:false` (that still ships them and bloats every container install).
- **Declare `dependencies:{"adom/adom-desktop":"^1.9.x"}`** in `package.json` — `pkg install` resolves it npm-style, so installing your bridge's skill pkg also pulls the AD CLI + core skills the container needs to drive you.
- **Each sub-skill's body opens with a `Parent skill:` line** (first line after the frontmatter): `Parent skill: **adom-desktop-<bridge>-bridge**`.
- **List every user-skill path EXPLICITLY — no globs — in `package.json` `files[]` AND `install.sh`/`uninstall.sh`** (one `install_skill <dest> <src>` line each, kept in sync). The publish linter REQUIRES explicit listing. (The old "use `skills/**`" advice is dead.)

```bash
# Stage dir: ROOT SKILL.md + skills/<name>/SKILL.md per USER skill (dev/publish stay in the repo),
# package.json {name:"adom-desktop-<bridge>-bridge", version, discovery_triggers,
#               dependencies:{"adom/adom-desktop":"^1.9.x"},
#               files:[ every USER-skill path, explicitly ], scripts:{install,uninstall}}.
# TWO traps that still bite — verify each with `tar tzf` before publishing:
#  1. ⚠ pkg pack/publish HONORS the stage dir's .gitignore — a gitignored skills/ ships
#     a 3 MB tarball with ZERO skills. Set .gitignore aside across pack+publish, restore after.
#  2. ⚠ 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 skills/<name>/SKILL.md
                                         #   listed in files[] present, ZERO binaries
adom-wiki pkg publish --org adom         # publishes adom/adom-desktop-<bridge>-bridge
```

**`skill_count` on your page counts the USER skills your published PKG carries** — so it should be SMALL (1 for a single-skill bridge, a handful for a core + sub-skills). If it looks inflated, you probably shipped dev/publish skills that belong in the repo instead. The metric is NOT "SKILL.md files in the repo," so a `type:skill` page can read `skill_count: 0` while its repo holds many — don't chase parity across page types (wiki-side quirk).

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.

---

## Artifact 3 — DISCOVERABILITY: `discovery_triggers` so a plain request finds you

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-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
```

**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.

**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.

**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.

---

## Card hero + update cost — two gotchas that bite

**Hero: ship a 1.6-aspect image, or AD crops the OG-card fallback.** AD's bridge card renders the hero in a banner that is `aspect-ratio: 1.6` with `object-fit: cover`. So:
- Ship a **1.6-aspect** hero (≈**2000×1250**), full-bleed — like pup & kicad. It fills the banner cleanly.
- If you ship **no** hero (`bridge.json` has no `hero` AND your wiki page has no `hero_path`), AD falls back to your page's **auto-generated OG card** (1200×630 = **1.90** aspect, the social-card size). `cover` scales that wider image to the banner's height and **clips the sides** — the left text panel gets cut off. (That cropped, off-center look is the tell of a hero-less bridge — it's not an AD bug.)
- Fix: publish a real hero at 1.6 (the `adom-wiki` page hero, or a `hero` URL in `bridge.json`). Verify by opening your card in AD — the title/text must not be clipped.

**Update cost: keep the Release zip source-only; re-seed the NSIS bundle in lockstep.** When a user clicks the card's "↑ v…" update badge, AD downloads your Release zip into the cache and (for a node bridge) provisions deps:
- **The Release zip MUST be source-only — no `node_modules`.** AD provisions node deps at spawn: it **reuses the bundled seed's `node_modules` via `NODE_PATH`** when the cache version ≤ the seed version (instant), and runs a real **`npm install`** only when the wiki version is *newer* than the bundled seed.
- So a wiki version much newer than your bundled seed forces a **cold `npm install`** on the next spawn — slow for a heavy tree. **When you publish a new Release, bump the bundled NSIS seed in lockstep** (same version) so most users get the instant NODE_PATH reuse, not a cold install.
- AD (**v1.9.52+**) never blocks the UI on this: the update downloads straight to cache (no wasteful seed re-copy — the old code deep-copied pup's 73 MB / 4698-file seed and then discarded it, the "updating… forever" hang), the badge shows live progress (`downloading…` → done) with a 30s "installing…" fallback, and your bridge **keeps serving the old version** until the new one is ready. A lean zip + a lockstep seed is what keeps it *fast*.

---

## 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 **provisions** Node/Python in the background — a **portable, no-UAC** copy under `~/.adom/adom-runtimes/` (or a system install if one's on PATH), **NOT winget/MSI** (see **Runtime contract** above) — and prewarms heavy assets you declare. You may also download your own runtime asset (pup downloads Chrome-for-Testing) — fine, it's yours. **But do it in the BACKGROUND: a `*_status` / `*_readiness` / ANY probe verb must NEVER block on a heavy download.** Spawn your server fast (it usually doesn't need the asset to *start*), kick the download off detached, and report `state:"warming"` until ready. A synchronous-download-on-first-verb is the exact footgun this release fixed (pup's 150 MB Chrome fetch used to block the probe).

**Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): 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 browser EXTENSION half) | The user's REAL logged-in browser (login/forms). Since v1.9.142 the DESKTOP BRIDGE half ships bundled in the AD installer (0.2 MB, zero deps, `persistent:true` — its extension-free verbs like `nbrowser_open_os_window`/`nbrowser_profiles`/`nbrowser_os_windows` work day one). What remains level-2 is the in-browser EXTENSION: AD/the bridge proactively SUGGESTS it and it installs on a yes (a browser-extension step the user confirms), unlocking full in-page drive. |
| **3 · On-request host app** | KiCad, Fusion, Altium, … | Detect by default; NEVER pre-install (huge app, rude unasked). The moment the user wants it, the AI installs it AUTOMATICALLY (winget / `desktop_install_*`) — NEVER tells the user to do it manually. |
| **4 · Explicit third-party** | any `bridge_install`'d bridge | The user opts in by installing it. |

The through-line: **the AI never makes the user do something it can do itself.** The only knob is *when* — pre-emptive (pup), proactively-offered (native-browser), or on-request (host apps).

**Declare both in `bridge.json`:**
```json
"detect": {                          // Tier 2 — how 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 **and a different home** — the USER skill goes in the pkg; the DEV and PUBLISH skills stay in your source repo:

1. **A PUBLISH skill — lives in your source REPO** (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 — lives in your source REPO** (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 + user sub-skills.)

**Only the USER skill(s) go in your pkg** (Artifact 2), each a real `skills/<name>/SKILL.md`. **The DEV + PUBLISH skills stay in your source repo** — a developer gets them by cloning to edit the bridge; they are NOT in the pkg's `files[]`, and you do NOT "scope" them into the pkg with `user-invocable:false` (that still ships them and bloats every container install). Open-vs-closed source doesn't change this — it only sets whether your repo's dev/publish files are publicly readable.

**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}`. If a verb genuinely blocks past 60s (a synchronous export/render), **also declare a `timeouts` block** (v1.9.79) so AD's HTTP budget matches — otherwise AD cuts the connection at 60s (third-party bridges were hard-capped there before v1.9.79).
- **`<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.
- **Keyboard input: route through AD's `desktop_press_key` — do NOT reimplement chords.** AD's key verb already does full modifier chords (`"shift+s"`, `"ctrl+shift+p"`, `"alt+f4"` — modifiers held via SendInput, key tapped, modifiers released in reverse), foregrounds the target window first (`{window|titleContains|hwnd}`) so CEF/Qt apps like Fusion receive it, and every bridge inherits it via the `/command` desktop passthrough (`{"command":"press_key", ...}` on the desktop namespace). A bridge-local `*_send_key` that parses only single keys (the Fusion bridge's `fusion_send_key` circa v1.6.x — `"shift+s"` → "Unknown key") duplicates AD's work and drifts; keep such a verb only for SendMessage-to-exact-hwnd cases AD's focus-first model can't cover, and say so in its `describe`.
- **Host-app-optional verbs (v1.9.76):** declare `detect.hostAppOptionalVerbs` (bare names, e.g. `["readiness","describe"]`) in `bridge.json` so AD forwards those verbs to your running bridge even when your host app is absent — AD's built-in read-only set {readiness, describe, get_app_state, status} is always forwarded. And declare **`detect.installVerb`** (e.g. `"kicad_upgrade"`) so `bridge_readiness` recommends YOUR installer over the generic winget fallback.

## Health + status chip

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.

## 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.

**"My bridge keeps exiting and I can't see why" (read this).** Your own stdout log truncates on every respawn, so a crash-then-restart erases its own cause. `bridge_log_read` therefore also tails **AD's lifecycle audit log** for your bridge, a separate append-only file (`~/.adom/bridge-logs/<name>.ad.log`) that survives respawns, shown under an `===== AD bridge lifecycle =====` header. It records every spawn, reap, duplicate-collapse, and `bridge_kill` with the reason, so you can tell "AD reaped it as a duplicate" or "the `bridge_kill` verb stopped it" or "it was reaped before a fresh spawn" apart from "it crashed on its own." If the audit log shows AD never stopped it, the exit is inside your process.

---

## Publish checklist (the whole modern flow)

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.

**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, Blender, Puppeteer, and the **native-browser** bridge (in the `adom-browser-extension` repo) are cloud-owned — don't edit them from the AD repo. When an AD release changes a bridge-facing contract **— or when THIS SDK doc itself changes —** AD posts a migration notice to **each** of their wiki discussions (`adom-desktop-{kicad,fusion,blender,puppeteer}-bridge` + `adom-browser-extension`). **Author-facing takeaway: watch your bridge's own wiki DISCUSSION/issues list — that's where an "SDK changed, please re-audit against the billboard" notice lands, and where you reply with your plan or blockers.** It's the only channel AD uses to reach you; AD never edits your repo.
