← Commit history

Add bridge-author + 3 fill-in-the-blanks templates as sub-skills (consolidate bridge-author)

John Lauer ·ac34ade8bb ·4mo ago ·parent 430d029
4 files changed +800
skills/bridge-author/SKILL.mdadded+264
@@ -0,0 +1,264 @@+---
+name: bridge-author
+description: Use this skill when the user says they want to write/author/build a third-party bridge for adom-desktop, integrate a new desktop app (Altium, MATLAB, SolidWorks, an oscilloscope, etc.) with cloud Claude, fork the hello sample bridge, ship a bridge to the wiki, or asks how custom CLI verbs get routed into adom-desktop. Walks them through scaffolding a fresh bridge from the canonical template, wiring `bridge.json`, writing the HTTP server, packaging the zip, hosting it, and verifying install via `adom-desktop bridge_install`.
+---
+
+# Authoring an Adom Desktop bridge
+
+A bridge is a small HTTP server (Python, Node, or any executable) that adom-desktop spawns on demand and proxies cloud CLI requests to. Once installed, every `adom-desktop <verb> '<json>'` whose verb starts with your bridge's prefix routes to your server. Adom Desktop v1.8.0+ supports dynamic bridges installed at runtime from a wiki manifest URL — no fork of adom-desktop required.
+
+## When to use this skill
+
+The user wants to write a bridge if they say any of:
+- "I want to integrate Altium / MATLAB / Eagle / SolidWorks / OrCAD / my oscilloscope / our internal CAD tool with adom-desktop"
+- "Help me build a third-party adom-desktop bridge"
+- "How do I add a new CLI verb to adom-desktop?" (the answer is almost always "ship it as a bridge, not by forking adom-desktop")
+- "Fork the hello sample bridge"
+- "Make my bridge installable via the install modal"
+
+If they want to USE existing bridges (kicad, fusion, puppeteer), that's the `adom-desktop` skill, not this one.
+
+## Prerequisites
+
+The user needs adom-desktop installed + running on their laptop and reachable from this container:
+
+```bash
+adom-desktop ping
+```
+
+If that fails, walk them through the install flow at https://wiki.adom.inc/wiki/apps/adom-desktop first.
+
+## Workflow
+
+### Step 1 — pick a bridge name + verb prefix
+
+Ask the user:
+- What's the host app this bridge integrates? (e.g. "Altium Designer")
+- What lowercase one-word name should the bridge use? (e.g. `altium`) — this is the cache directory name + the manifest key
+- What verb prefix should they own? Conventionally `<name>_*`, e.g. `altium_open_project`, `altium_export_step`. Prefixes are first-come-first-served across the registry; check existing bridges with `adom-desktop bridge_list` so you don't collide.
+
+Reserved prefixes that are already taken:
+- `kicad_*` (kicad bridge)
+- `browser_*`, `desktop_recorder_*`, `desktop_record_*` (puppeteer bridge)
+- `fusion_*` (fusion360 bridge)
+- `hello_*` (sample bridge)
+
+Reserved bridge ports: 8770–8779 (Adom-owned), 8851 (puppeteer). Pick something in 8800–9000 for new bridges.
+
+### Step 2 — scaffold from the hello sample
+
+The hello sample bridge is the minimum viable example — a ~80-line stdlib-Python HTTP server. Have the user download + extract it:
+
+```bash
+# On the user's laptop (run via adom-desktop shell_execute or have them run it themselves):
+curl -L https://wiki.adom.inc/static/apps/adom-desktop/hello-bridge-v1.0.0.zip \
+  -o /tmp/hello-bridge.zip
+mkdir -p ~/my-bridge && cd ~/my-bridge
+python3 -c "import zipfile; zipfile.ZipFile('/tmp/hello-bridge.zip').extractall()"
+ls -la
+```
+
+They now have `bridge.json`, `BRIDGE_VERSION`, `server.py`, `README.md`. Rename the directory + edit each file to match the new bridge name/verbs.
+
+For Node-based bridges, look at the puppeteer bridge zip instead:
+`https://wiki.adom.inc/static/apps/adom-desktop/puppeteer-bridge-v1.0.0.zip`
+
+For app-with-plugin-API bridges (Altium / SolidWorks / etc.), the fusion360 bridge is the right reference — it has both a bridge process AND an in-host add-in:
+`https://wiki.adom.inc/static/apps/adom-desktop/fusion360-bridge-v1.0.0.zip`
+
+### Step 3 — edit `bridge.json`
+
+The schema is documented at https://wiki.adom.inc/wiki/apps/adom-desktop-bridges#bridgejson-schema. Minimum required fields:
+
+```json
+{
+  "manifest_version": 1,
+  "name": "<lowercase-name>",
+  "displayName": "<human-readable>",
+  "version": "1.0.0",
+  "description": "One-line description that shows up in bridge_list.",
+  "spawn": {
+    "kind": "python",
+    "entrypoint": "server.py",
+    "port": 8XXX,
+    "healthEndpoint": "http://127.0.0.1:8XXX/status",
+    "stopMethod": "kill",
+    "killImageName": "python.exe"
+  },
+  "verbPrefixes": ["<name>_"],
+  "verbs": ["<name>_verb1", "<name>_verb2"]
+}
+```
+
+Keep `verbs` accurate — `bridge_list` advertises this to discovery. Don't list verbs you haven't implemented.
+
+If your bridge supports project-watching (file watcher → auto-resync to Docker), add a `watcher` block:
+```json
+"watcher": {
+  "supports": true,
+  "displayName": "<Watcher name>",
+  "defaultGlobs": ["*.your-ext"],
+  "configKey": "project_watch"
+}
+```
+
+The GUI renders an eye-pip on the chip when watching is active.
+
+### Recommended: `docs` URL + `platforms` map (v1.8.14+)
+
+These two optional fields make your bridge a first-class citizen of the GUI:
+
+```json
+"docs": "https://wiki.adom.inc/wiki/apps/your-bridge-name",
+"platforms": {
+  "windows": { "supported": true },
+  "macos":   { "supported": false, "reason": "Mac port pending — Win32 deps in handlers/window_automation.py need a Quartz equivalent." },
+  "linux":   { "supported": false, "reason": "Same as macOS." }
+}
+```
+
+- **`docs`**: surfaces in the chip's right-click context menu as "View on wiki" (top item). Use it. If you don't have a wiki page yet, publish one first (see `adompkg publish` via the `adompkg` skill).
+- **`platforms`**: an honest declaration of which OSes the bridge actually works on. Keys are `windows` / `macos` / `linux`. The GUI shows the support status in the tooltip + warns (non-blocking) in the install modal if the user is on a `supported: false` OS. Provide a `reason` string when something doesn't work — the user sees it verbatim, so explain why.
+
+Tell the user **don't lie in `platforms`**. If they haven't tested macOS, leave it as `{"supported": false, "reason": "Untested on macOS so far"}` rather than claiming support. The whole point of the field is honesty — better to set expectations than burn a user who installs a bridge that silently fails.
+
+### Step 4 — implement the server
+
+The HTTP contract is two endpoints:
+
+- `GET /status` → `{"ok": true, "version": "..."}` — used by adom-desktop to confirm the bridge is alive
+- `POST /command` body `{"verb": "<name>_<verb>", "args": {...}}` → `{"success": true, "output": "..."}` or `{"success": false, "error": "..."}`
+
+The hello sample's `server.py` is the canonical shape — fork it and replace the verb handlers with yours.
+
+When implementing handlers:
+- Validate `args` strictly. Return `{"success": false, "error": "..."}` for invalid input; don't 500.
+- Keep responses small. If returning large payloads (file contents, binary blobs), write to disk first and return the path; Docker will use `pull_file` to fetch.
+- Long-running operations (>5s) should return immediately with a job handle, then expose a `<name>_status` verb for polling.
+
+### Step 5 — test locally via a localhost manifest
+
+Before publishing to the wiki, test with a local HTTP server:
+
+```bash
+# Build the zip
+cd ~/my-bridge
+python3 -c "
+import zipfile, os, fnmatch
+EXCLUDE = ['*/__pycache__/*', '*.pyc', '.history/*']
+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 + size
+SHA=$(sha256sum ../my-bridge-v1.0.0.zip | awk '{print $1}')
+SIZE=$(stat -c%s ../my-bridge-v1.0.0.zip)
+
+# Write the manifest
+cat > ../my-bridge-manifest.json <<EOF
+{
+  "manifest_version": 1,
+  "version": "1.0.0",
+  "url": "http://127.0.0.1:9999/my-bridge-v1.0.0.zip",
+  "sha256": "$SHA",
+  "size": $SIZE,
+  "released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+}
+EOF
+
+# Serve them on a local HTTP port
+cd .. && python3 -m http.server 9999 &
+LOCAL_SERVE_PID=$!
+
+# Install through the localhost URL
+adom-desktop bridge_install '{"manifestUrl":"http://127.0.0.1:9999/my-bridge-manifest.json"}'
+
+# Try a verb
+adom-desktop <name>_<verb> '{...}'
+
+# Stop the local server when done
+kill $LOCAL_SERVE_PID
+```
+
+This roundtrip exercises the entire install path (download + sha256 verify + extract + registry rescan + spawn-on-first-call) without uploading anything yet.
+
+### Step 6 — ship to the wiki (recommended) or any HTTPS host
+
+Two options:
+
+**Option A — Adom Wiki** (recommended for bridges they want the community to find):
+
+Publish the bridge zip and its manifest as a wiki package with `adompkg publish` (list both files in `package.json`'s `files` array, then run the publish from the package directory). The manifest should point at the zip's wiki static URL.
+
+Suggest the user also write a dedicated `apps/<my-bridge>-bridge` wiki page documenting their bridge — same shape as the [reference bridges](https://wiki.adom.inc/wiki/apps/adom-desktop-bridges#reference-implementations). Use the `adompkg` skill for the publish flow.
+
+**Option B — any HTTPS host** (GitHub Releases, your own CDN, S3):
+
+```bash
+# Upload both files anywhere reachable by HTTPS, then:
+adom-desktop bridge_install '{"manifestUrl":"https://your-host/my-bridge-manifest.json"}'
+```
+
+Cloud users can install from any reachable URL — they're not locked to the wiki.
+
+### Step 7 — verify end-to-end + iterate
+
+```bash
+# See it in the registry
+adom-desktop bridge_list
+
+# Trigger your verbs
+adom-desktop <name>_<verb> '{...}'
+
+# Inspect the install dir (right-click the chip in the GUI → Open install folder)
+# OR programmatically:
+adom-desktop shell_execute '{"command":"powershell -NoProfile -Command \"Get-ChildItem $env:LOCALAPPDATA\\\\\\\"Adom Desktop\\\\\\\"\\\\bridges-cache\\\\<name>\"","timeout_secs":5}'
+
+# After edits, bump bridge.json's version + BRIDGE_VERSION, rebuild the zip, re-upload, then:
+adom-desktop refresh_bridges
+```
+
+To ship a breaking change without leaving users on the old version: bump the manifest's `version` field. On next launch + refresh, adom-desktop fetches the new manifest, compares versions, downloads + verifies the new zip, atomically swaps the cache dir.
+
+## Lifecycle controls available to your bridge users
+
+These verbs are built into adom-desktop — you don't implement them, just know they exist so you can mention them when users ask:
+
+| Verb | What it does |
+|---|---|
+| `bridge_list` | List all installed bridges + their metadata |
+| `bridge_install` | Install from a manifest URL |
+| `bridge_uninstall` | Remove from cache (returns to the bundled version if there was one) |
+| `bridge_pause` | Stop routing verbs (process stays running, verbs return "paused") |
+| `bridge_resume` | Resume routing |
+| `refresh_bridges` | Re-poll all known manifest URLs for updates |
+
+From the Adom Desktop GUI: right-click any chip in the bridge bar for Pause / Resume / Open install folder / Uninstall.
+
+## Reference pages on the wiki
+
+These pages contain full architecture writeups for the three bundled bridges. Use them when the user is stuck on a pattern you've already seen one of these solve:
+
+- **[Bridge SDK guide](https://wiki.adom.inc/wiki/apps/adom-desktop-bridges)** — schema, packaging, lifecycle, trust model
+- **[KiCad bridge reference](https://wiki.adom.inc/wiki/apps/adom-desktop-kicad-bridge)** — multi-instance, reverse-bridge into the host app
+- **[Puppeteer bridge reference](https://wiki.adom.inc/wiki/apps/adom-desktop-puppeteer-bridge)** — single-instance Node + session-profile isolation + recording
+- **[Fusion 360 bridge reference](https://wiki.adom.inc/wiki/apps/adom-desktop-fusion-bridge)** — passthrough-to-add-in pattern (use for Altium, SolidWorks, Eagle)
+
+## Trust + security gotchas to warn users about
+
+- The trust model is SHA256-verification of the zip against the manifest. **Users install with their own privileges** — bridges can read/write any file the user can. Tell them to install only from sources they trust.
+- If you publish to the wiki, anyone in the org can see it. For internal-only bridges, host the manifest behind auth (HTTPS basic, signed URLs, etc.) — adom-desktop's `bridge_install` honors HTTP redirects + auth headers passed via `--auth-header`.
+- Don't put secrets in the bridge zip. The unpacked cache dir is readable by anyone on the same machine. Read secrets at runtime from env vars / OS keychain / a `.env` next to the bridge cache that the user populates manually.
+
+## When something goes wrong
+
+1. **`bridge_install` says SHA mismatch** — the zip you uploaded doesn't match the manifest's `sha256`. Recompute and re-upload the manifest.
+2. **Verb returns "bridge not found"** — the prefix you declared doesn't match the verb. Check `adom-desktop bridge_list` to see what prefixes you actually registered.
+3. **Bridge process won't start** — adom-desktop reports the spawn failure. Common causes: wrong `entrypoint`, `spawn.kind` mismatch (Python file as `node`), missing dependencies. Check the GUI's activity log.
+4. **Bridge runs but verbs return "process exited"** — your handler probably crashed. Add `try/except` around your dispatcher and log to stderr; adom-desktop forwards stderr lines to the GUI log.
+5. **Open install folder shows a "Location is not available" dialog** — the cache dir doesn't exist on the user's real filesystem (probably installed from a sandboxed context). Uninstall + reinstall from a clean session. See https://wiki.adom.inc/wiki/apps/adom-desktop-bridges for the full troubleshooting.
skills/bridge-dev-template/SKILL.mdadded+150
@@ -0,0 +1,150 @@+---
+name: bridge-dev-template
+user-invocable: false
+description: "TEMPLATE / reference for building, publishing, and maintaining ANY Adom Desktop bridge (the cloud Claude/Codex thread that owns a bridge copies this into its own dev-skills/ and fills it in). NOT a user-facing skill — general users want the bridge's own user skill. Read this when editing bridge code, cutting a new version, publishing to the wiki, wiring a runtime/heavy-dependency cold-start self-heal, or understanding the pkg-vs-release-vs-bundled artifact model. Trigger words: bridge dev, build adom-desktop bridge, publish bridge, ship bridge, bridge_install, adom-desktop-<bridge>-bridge, cold-start self-heal, bridge readiness internals, bridge release, bridge manifest, updateManifestUrl, pkg vs release vs bundled."
+---
+
+<!-- Template — copy into your bridge's own dev-skills/, replace <bridge>/<prefix>/<Bridge Name>, keep the structure. -->
+
+# bridge-dev-template — building & shipping an Adom Desktop bridge
+
+The canonical source of truth for the `<bridge>` bridge is **its wiki repo**
+(`wiki.adom.inc/adom/adom-desktop-<bridge>-bridge`), cloud-owned (like the kicad/fusion/puppeteer
+bridges) so bridge iteration is decoupled from adom-desktop core. Read `CLAUDE.md` (ownership boundary)
+and `PUBLISHING.md` (step-by-step recipe) in this repo alongside this skill.
+
+## The THREE artifacts — know which is which (this is the #1 confusion)
+
+A user installs TWO different things from this one page. They are not the same:
+
+| Artifact | What it is | Who installs it | How |
+|---|---|---|---|
+| **pkg tarball** (Packages tab) | the **container-side Claude/Codex skills** (`SKILL.md` + `skills/*`) — docs that teach the cloud AI how to drive `<bridge>`. **No bridge runtime.** | a cloud **container** | `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge`, or auto via AD's `sync_skills` |
+| **release zip** (Releases tab) | the **bridge RUNTIME** (entrypoint + bridge code + `bridge.json` + `BRIDGE_VERSION`; heavy deps reconstructed on spawn) — the code that actually runs on the desktop | **Adom Desktop**, streamed into `bridges-cache\<bridge>` | `bridge_install {manifestUrl}` / `refresh_bridges` (via `updateManifestUrl`) |
+| **bundled seed** (inside the AD installer) | a copy of the same runtime, shipped in the NSIS for first-run/offline | every AD install | ships with Adom Desktop; superseded by a newer cache copy (cache-over-bundled, numeric version) |
+
+So: **the pkg is skills (container). The bridge is the zip (desktop) + the bundled seed.** Keep the
+pkg description saying exactly that, or people think the tarball contains the bridge — it does not.
+
+## How `<bridge>` reaches a container — auto-discovery → auto-install → verbs
+
+The full chain when a user on a fresh container says *"<the phrase that triggers your bridge>"*:
+
+1. **Discovery.** The wiki's `discover` index (fed by this page's `discovery_triggers` — which should lead
+   with your bridge's natural phrases, e.g. its short name + the top user actions) matches the phrase. The
+   container's regenerated `adom-wiki-discover` skill surfaces this page, so the AI proposes it.
+2. **Auto-install the pkg.** The AI runs `adom-wiki skills install adom/adom-desktop-<bridge>-bridge`
+   (a.k.a. `pkg install`). That extracts the tarball and runs `install.sh`, which drops `SKILL.md` +
+   `skills/*` into **both `~/.claude/skills/` and `~/.codex/skills/`**. The `<bridge>` user skill is now loaded.
+3. **The skill calls the bridge.** The user skill's `SKILL.md` tells the AI to run `adom-desktop <prefix>_*`
+   verbs. AD routes every `<prefix>_*` verb to the `<bridge>` bridge.
+4. **The bridge runtime.** It's already on the desktop: AD ships `<bridge>` as a **bundled seed** (and
+   supersedes it from this page's release zip via `updateManifestUrl` / `bridge_install`). On a truly fresh
+   PC the cold-start self-heal installs `<your runtime / heavy dependency>` on first use.
+
+**Two install paths, keep BOTH healthy:**
+- **Discovery path (above)** — works once `discovery_triggers` include the user phrases + the discover
+  index is regenerated. This is the "user said `<bridge>` → AI installs the skill" path.
+- **AD `sync_skills` path** — AD's daily `maybe_background_skill_sync` reads `bridge_list` → each bridge's
+  `skillPkg` → `pkg install`s it into the container. For this to work AD must report
+  `skillPkg = adom/adom-desktop-<bridge>-bridge` for your bridge. AD now derives that slug from the bridge's
+  `docs` field automatically, **except** for any bridge hardcoded into `AD_CORE_SKILL_BRIDGES` in
+  `bridge_registry.rs` (that list forces `skillPkg = null` for bridges whose skill historically lived in
+  the AD-core pkg). If your bridge's skill ships from THIS page (not core), make sure your name is NOT in
+  that list — if it is, removing it is an AD-core handoff item.
+
+## Repo layout
+
+- `src/` — the runnable bridge (what the release zip contains, at zip-root). Entrypoint + bridge modules,
+  `bridge.json`, `BRIDGE_VERSION`, dependency manifest/lockfile, any html/icons the runtime needs.
+- `SKILL.md` (root) — the **user** skill (`name: <bridge>`). Deployed to `~/.claude/skills/adom-desktop-<bridge>-bridge/`.
+- `dev-skills/<bridge>-bridge-dev/` — THIS dev skill. `dev-skills/<bridge>-bridge-publish/` — the publish recipe.
+  Both are **source-only** (in `dev-skills/`, NOT `skills/`): NOT in the installable pkg and NOT on the
+  wiki Skills tab — you get them via `adom-wiki repo clone`. Everyday users only get the `<bridge>` user skill.
+- `adom-bridge-<bridge>-manifest.json` (root) — the streaming manifest AD's `updateManifestUrl` points at.
+- `package.json` (root) — the **adompkg descriptor** (slug/version/type/description) for `pkg publish`.
+  NOT a language/runtime package descriptor (e.g. not the node `package.json`) — keep them distinct.
+- `install.sh` / `uninstall.sh` — deploy `SKILL.md` + `skills/*` into the container's `~/.claude/skills/`.
+- `page.json`, `README.md`, `*.png` heroes — the wiki page presentation (the heroes are gitignored so the
+  container pkg stays lean — they're already uploaded as page assets).
+
+## Cold-start self-heal — the architecture pattern (the reason a cloud-owned repo earns its keep)
+
+On a fresh PC the installer pre-installs neither your bridge's language runtime nor its heavy dependency.
+Without a self-heal, the first `<prefix>_<verb>` fails with a generic launch error + a dev string. The
+pattern that fixes it — adapt the names to your runtime:
+
+- **A detect/install/ready module** — `detectDependency()` (validate the ACTUAL artifact, not "a dir
+  exists"), `installDependency()` (programmatic, **pinned to the exact version your code expects** — never
+  a floating 'latest'/'stable', or the launch path won't find what it pinned against), an install-state
+  singleton (dedup concurrent installs + report progress), `ensureReady()` (the gate — kicks off a
+  background install and returns immediately), `readiness()` (powers the verb + the status chip).
+- **The first action verb gates on readiness:** missing dependency → kick off a **non-blocking** install +
+  return `{errorCode:"<dependency>_installing", statusVerb:"<prefix>_readiness", _hint:"poll then retry"}`.
+  Install fail → a specific `<dependency>_install_failed` / `<dependency>_download_failed` error — **never
+  silently continue into a doomed launch.**
+- **A `<prefix>_readiness` verb** (poll) + a **`<prefix>_prewarm` verb** (install/warm without doing the
+  user-visible action).
+- **`/status` (or your `healthEndpoint`)** emits `led`/`summary`/`tooltip` truthfully (yellow installing,
+  red failed, green ready). AD paints these verbatim.
+- **The language runtime itself is handled by AD core BEFORE the bridge spawns** (e.g. a `node_not_found` →
+  `desktop_install_node` flow) — NOT your bridge's job. The bridge can't bootstrap the very runtime it
+  needs to run; AD installs the runtime, then spawns you.
+- **The one thing the bridge can't do:** pre-warm before first use (it isn't running until the first call).
+  AD/HD must call `<prefix>_prewarm` on embedded first-run — track that as a handoff item.
+
+> **Worked example (pup's case):** the puppeteer bridge's runtime dependency is Chrome for Testing. Its
+> module is `src/chrome.js` (`detectChrome` / `installChrome` via the bundled `@puppeteer/browsers` pinned
+> to puppeteer's expected buildId / `ensureChromeReady` / `readiness`); `browser_open_window` is the gated
+> first verb (CfT path only; its native-drive path skips the gate), returning
+> `chrome_for_testing_installing`; and it exposes `browser_readiness` + `browser_prewarm`. Node is the
+> language runtime AD core installs first (`node_not_found` → `desktop_install_node`). That's the concrete
+> shape of every bullet above.
+
+## Publishing (the wiki has TWO storage layers — don't mix them)
+
+Full recipe in `PUBLISHING.md`. The load-bearing rules:
+
+1. **The bridge `.zip` → a RELEASE asset** (`adom-wiki release create` + `release upload`). NEVER
+   `repo push` a `.zip` — it's gitignored and silently skipped; the manifest URL would 404.
+2. **The manifest JSON + source → the git repo** (`adom-wiki repo push`). The manifest's `url` is the
+   RELEASE download URL; its `sha256`/`size` MUST match the served asset (releases preserve your sha;
+   re-download to confirm or `bridge_install` rejects it).
+3. **The skills → a PKG** (`adom-wiki pkg publish`). The pkg description (root `package.json`) must say
+   "container-side skills." `pkg pack` honors `.gitignore` for exclusion (no separate ignore file) — that's
+   why the heroes are gitignored, to keep the tarball lean. Set per-version notes with `adom-wiki pkg notes`.
+4. **Version display gotcha:** a RELEASE shows up in `adom-wiki pkg info` as a row with `type:null` (a
+   wiki UI conflation). The real pkg rows have `type:"app"`. Use `release list` for true releases.
+5. **`bridge.json` + `BRIDGE_VERSION` lockstep**, and keep the version **>** the AD bundled seed so
+   cache-over-bundled wins.
+6. **Declare `updateManifestUrl` in your `bridge.json`** (the full URL of this page's manifest) so a
+   user's AD re-polls THIS page for updates — and the same value lives in the bundled seed AD ships, so a
+   fresh install auto-updates with no user opt-in. A republish that drops it makes AD fall back to the
+   legacy convention and stop seeing your updates.
+
+## Testing a new version on a real desktop
+
+```bash
+adom-desktop targets    # multiple desktops → --target <name> is required
+adom-desktop --target <name> bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-<bridge>-bridge/files/adom-bridge-<bridge>-manifest.json","force":true}'
+adom-desktop --target <name> <prefix>_readiness '{}'   # proves the new code is live
+```
+
+- **The language runtime is missing on the box** → `<prefix>_*` returns the runtime-not-found error from
+  AD core *before* the bridge spawns (correct). That's a genuine cold-start demo of the FIRST link, but it
+  can't exercise your self-heal module (there's no runtime to run it).
+- **A box WITH the runtime but WITHOUT the heavy dependency** is the only place to see the full dependency
+  self-install live (`<prefix>_<verb>` → `<dependency>_installing` → poll `<prefix>_readiness` → succeeds).
+  Most dev boxes already have the dependency, so `readiness()` just returns `ready:true`.
+- The streamed zip is **source-only**; AD reconstructs heavy deps in the cache on first spawn (ship
+  prebuilt native deps where possible). The bundled seed already has its deps materialized. **Don't put
+  reconstructable dependency trees (node_modules, venvs, vendored build output) in the zip.**
+
+## Don't
+
+- Don't edit the bundled seed in the adom-desktop repo (`plugins/<bridge>`) as if it's live, and don't run
+  `release-bridge.sh <bridge>` from the adom-desktop repo — that clobbers this cloud-owned page. (The
+  adom-desktop release script hard-refuses the cloud-owned bridges; the rule is "don't try.")
+- Don't ship the bridge runtime in the pkg tarball or the skills in the release zip. Two artifacts, two homes.
+- Don't pin your heavy dependency to a floating channel ('latest'/'stable') — pin to the exact version your
+  code launches against, or the install and the launch disagree.
skills/bridge-publish-template/SKILL.mdadded+243
@@ -0,0 +1,243 @@+---
+name: bridge-publish-template
+user-invocable: false
+description: "TEMPLATE — the canonical, battle-tested recipe for publishing a new version of ANY Adom Desktop bridge to wiki.adom.inc, plus every gotcha hit live. Copy into your own bridge's dev-skills/, replace the <bridge>/<prefix> placeholders, and follow it. NOT for general users. Read before cutting a release: which artifact goes where (pkg=skills, release=bridge zip, repo=manifest+source), version-lockstep (kept above the AD bundled seed), the `files` allowlist + `skills/**` glob trap, keeping the tarball lean (NO zip / NO src / NO heroes), keeping dev/publish skills SOURCE-ONLY (not SKILL.md), deploying skills to .claude AND .codex, scrubbing retired-wiki host URLs, user-first discovery triggers, sha matching, and verify steps. Trigger words: publish bridge, ship bridge, release bridge, pkg publish, repo push, adom-wiki release, bridge manifest, bridge version bump, no zip in tarball, dev skill source only, scrub old wiki url, codex skills, discovery triggers, adom-desktop bridge publish template."
+---
+
+# bridge-publish-template — shipping a new `<bridge>` bridge version
+
+> **Template — copy into your bridge's own `dev-skills/`, replace the `<bridge>`/`<prefix>` placeholders, follow the steps.**
+
+This is the step-by-step ship recipe for a cloud-owned Adom Desktop bridge living at
+`wiki.adom.inc/adom/adom-desktop-<bridge>-bridge`, plus the traps that cost real time. Companion docs in
+your repo: a `*-dev.md` dev skill (architecture / ownership), a terse `PUBLISHING.md`, and `CLAUDE.md`
+(ownership boundary).
+
+Placeholder legend — replace consistently throughout before you ship:
+
+| Placeholder | Meaning | Example (Puppeteer bridge) |
+|---|---|---|
+| `<bridge>` | lowercase bridge id | `puppeteer` |
+| `<Bridge Name>` | display name | `Puppeteer` |
+| `<prefix>_` | verb prefix | `browser_` |
+| `adom-desktop-<bridge>-bridge` | wiki slug | `adom-desktop-puppeteer-bridge` |
+| `adom-bridge-<bridge>-manifest.json` | streaming manifest filename | `adom-bridge-puppeteer-manifest.json` |
+| `adom-bridge-<bridge>-v<version>.zip` | release zip filename | `adom-bridge-puppeteer-v1.1.1.zip` |
+| `<your bridge's files>` | the runtime source files | `server.js`, `chrome.js`, `bridge.json` |
+| `<your runtime>` | the language/runtime AD reconstructs | `node` + `node_modules` |
+
+## What goes WHERE — three artifacts, three homes (memorize this)
+
+| Artifact | Home | Command | Carries |
+|---|---|---|---|
+| **bridge runtime `.zip`** | a **Release** asset | `adom-wiki release upload` | `src/` at zip-root (`<your bridge's files>` — the thing AD runs). |
+| **streaming manifest JSON** | the **git repo** | `adom-wiki repo push` | points at the release zip URL + its sha256/size |
+| **container skills tarball** | a **Package** | `adom-wiki pkg publish` | `SKILL.md` + `skills/*` (the Claude/Codex skills). NO runtime. |
+
+> The `.zip` is a RELEASE, never a git file: `*.zip` is gitignored and `repo push --files X.zip`
+> reports `ok` but silently skips it (the URL then 404s). Binaries → `release upload`.
+
+## Auth (once per container)
+
+```bash
+export ADOM_WIKI_TOKEN=$(cat /var/run/adom/api-key 2>/dev/null)   # container default; or session.json's login session_token
+adom-wiki whoami    # MUST show a real user (e.g. [email protected]). Strict-auth rejects the shared "Developer" token.
+```
+`ADOM_WIKI_TOKEN` is the user's **login `session_token`** — the container default lives at
+`/var/run/adom/api-key`; otherwise pull it from `~/.adom/session.json`. Every mutation needs a real
+`--changelog` / `-m` (≥10 chars, ≥2 words) or the server 400s.
+
+## The recipe
+
+### 1. Bump version IN LOCKSTEP, and keep it > the AD bundled seed
+
+```bash
+printf '1.1.1' > src/BRIDGE_VERSION
+sed -i 's/"version": "1.1.0"/"version": "1.1.1"/' src/bridge.json    # bridge runtime
+sed -i 's/"version": "1.1.0"/"version": "1.1.1"/' package.json       # adompkg descriptor (the pkg)
+```
+`src/BRIDGE_VERSION` and `src/bridge.json` MUST match (AD uses `BRIDGE_VERSION` to detect a stale running
+process). Keep the number **above** the seed in `adom-desktop/plugins/<bridge>` so cache-over-bundled wins.
+
+### 2. Build the bridge zip (source-only, `src/` at zip ROOT — no top-level dir)
+
+`zip` may be absent → Python. Your `<your runtime>` deps (e.g. `node_modules`) are NEVER included (AD
+reconstructs them on spawn; the seed ships them; a fat zip would blow wiki limits):
+
+```python
+import zipfile, os, hashlib
+root, out = "src", "adom-bridge-<bridge>-v1.1.1.zip"
+with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
+    for dp, dirs, files in os.walk(root):
+        for skip in ("node_modules","screenshots","profiles"):   # your runtime's heavy/generated dirs
+            if skip in dirs: dirs.remove(skip)
+        for f in files: z.write(os.path.join(dp,f), os.path.relpath(os.path.join(dp,f), root))
+print("sha256:", hashlib.sha256(open(out,"rb").read()).hexdigest(), "size:", os.path.getsize(out))
+# SANITY: assert your changed file(s) are inside before shipping.
+```
+
+> **Concrete pup example (so the commands aren't abstract):** the Puppeteer bridge skips
+> `node_modules`/`screenshots`/`profiles`, and its sanity check asserts `chrome.js` + the change are
+> inside the zip before upload. `node_modules` is excluded because AD `npm install`s it on spawn (an
+> 88 MB zip would exceed wiki limits).
+
+### 3. Publish the zip as a Release (releases PRESERVE your sha)
+
+```bash
+adom-wiki release create adom/adom-desktop-<bridge>-bridge 1.1.1 --title "..." --changelog "<≥2 words>"
+adom-wiki release upload adom/adom-desktop-<bridge>-bridge 1.1.1 adom-bridge-<bridge>-v1.1.1.zip --json
+# the response's assets[].sha256/size are what the manifest MUST carry. Then VERIFY the PUBLIC url:
+curl -sL https://wiki.adom.inc/download/adom/adom-desktop-<bridge>-bridge/1.1.1/adom-bridge-<bridge>-v1.1.1.zip | sha256sum
+```
+
+### 4. Point the manifest at the release + push it (git repo)
+
+Edit `adom-bridge-<bridge>-manifest.json` → `version`, `url` (the release download URL), `sha256`,
+`size` (from step 3 — **must match the served asset or `bridge_install` rejects it**):
+
+```json
+{ "manifest_version": 1, "name": "<bridge>", "version": "1.1.1",
+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-<bridge>-bridge/1.1.1/adom-bridge-<bridge>-v1.1.1.zip",
+  "sha256": "<the release asset's sha256>", "size": <bytes>,
+  "verbPrefixes": ["<prefix>_"],
+  "healthEndpoint": "/status", "statusVerb": "<prefix>_readiness",
+  "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-<bridge>-bridge/files/adom-bridge-<bridge>-manifest.json",
+  "released_at": "<UTC ISO8601>" }
+```
+
+> Declare `updateManifestUrl` (the full URL of THIS manifest on your own page) in both the manifest and
+> `src/bridge.json` so `refresh_bridges` / the auto-poll reach YOUR page instead of the legacy
+> AD-owned convention. Then push:
+
+```bash
+adom-wiki repo push adom/adom-desktop-<bridge>-bridge \
+  --files adom-bridge-<bridge>-manifest.json src/bridge.json src/BRIDGE_VERSION <your bridge's files> SKILL.md \
+  -m "v1.1.1: <what changed>"
+```
+
+> `sha256`/`size` MUST match the served asset or `bridge_install` rejects it. Re-download to confirm:
+> `curl -sL <download_url> | sha256sum`.
+
+### 5. Publish the container skills as a pkg — keep it LEAN, and MIND THE TRAPS
+
+```bash
+adom-wiki pkg pack            # ALWAYS pack + inspect BEFORE publish
+tar tzf *.tgz                 # confirm: SKILL.md + skills/*/SKILL.md + install.sh, and NOTHING heavy
+adom-wiki pkg publish --org adom --version 1.1.1
+adom-wiki pkg notes adom/adom-desktop-<bridge>[email protected] -m "Container skills: <what changed>"
+```
+
+The pkg description lives in the root **`package.json`** `description` — keep it explicit that this is
+**container-side skills only; the bridge runtime is the Release zip + bundled in Adom Desktop.**
+
+## Traps hit live (don't relearn these)
+
+- **`pkg pack` includes EVERYTHING by default** → a fat tarball (all the hero PNGs + `src/` runtime).
+  Fix: the heroes are gitignored (`*.png`) since they're already page assets, AND `package.json` has a
+  **`files` allowlist**. With both, the tarball drops to ~12 KB. (Pup's was 16 MB before the guards.)
+- **`files` directory entries need a GLOB.** Listing `"skills"` (bare dir) silently includes ZERO skill
+  files. Use **`"skills/**"`** (and/or the explicit `"skills/<name>/SKILL.md"`). Always `tar tzf` to
+  confirm your skills actually made it in — a missing skill = an empty Skills tab.
+- **`pkg pack` honors `.gitignore`, not a separate npmignore.** So you can't gitignore something you
+  need in git (e.g. `src/`) to drop it from the pkg — use the `files` allowlist for that.
+- **A RELEASE shows up in `adom-wiki pkg info` as a row with `type:null`** (wiki UI conflation). The real
+  pkg rows are `type:"app"`. Use `adom-wiki release list` for true releases; don't think your pkg shipped
+  just because a version appears in `pkg info`.
+- **sha/size mismatch → `bridge_install` rejects the manifest.** Releases preserve the uploaded sha, so
+  use the `release upload` response values verbatim and re-download to confirm.
+- **The Skills tab + `skill_count` + `adom-wiki skills install` come from the GIT REPO, not the pkg.**
+  The wiki indexes **every `SKILL.md`** in the page repo (root = main skill; `<dir>/SKILL.md` = sub-skills)
+  — `page.skills` is null; it's a repo scan. So a file named `SKILL.md` ANYWHERE in the repo becomes an
+  indexed, tab-listed, `skills install`-able skill. (This is *separate* from `pkg install`, which installs
+  the pkg tarball governed by the `files` allowlist.) Corollary → see "Keep dev/publish skills source-only".
+
+## Verify (every ship)
+
+```bash
+# manifest serves at the updateManifestUrl path:
+curl -s https://wiki.adom.inc/api/v1/pages/adom-desktop-<bridge>-bridge/files/adom-bridge-<bridge>-manifest.json | jq .sha256
+# anon-fetch the PUBLIC release zip URL and confirm the sha matches the manifest (curl 200 is NOT enough):
+curl -sL https://wiki.adom.inc/download/adom/adom-desktop-<bridge>-bridge/1.1.1/adom-bridge-<bridge>-v1.1.1.zip | sha256sum
+# live install + new verbs on a real desktop (multi-desktop → --target; `adom-desktop targets` lists them):
+adom-desktop --target <name> bridge_install '{"manifestUrl":".../files/adom-bridge-<bridge>-manifest.json","force":true}'
+adom-desktop --target <name> <prefix>_readiness '{}'
+# bridge_list shows the new version + skillPkg; bridge_check_updates reaches your page; sync_skills pulls the user skill:
+adom-desktop --target <name> bridge_list '{}'            # confirm version + skillPkg (adom/adom-desktop-<bridge>-bridge)
+adom-desktop --target <name> bridge_check_updates '{}'   # wikiHosted:true, latest = your new version
+adom-desktop --target <name> sync_skills '{}'            # installs the user skill pkg into the container
+# render the page in pup + READ the screenshot. Confirm version + Skills tab.
+```
+> Server-only change → AD reaps + respawns the bridge from the new cache on the next `<prefix>_*` call.
+
+## Keep dev/publish skills SOURCE-ONLY (not on the Skills tab, not installed)
+
+Everyday users should get ONLY the `<bridge>` user skill — not the dev/publish skills. But the wiki indexes
+every `SKILL.md` in the repo (above), so a dev skill named `SKILL.md` would show on the Skills tab AND
+install via `skills install`. The fix: keep dev/publish skills as **source-only docs that are NOT named
+`SKILL.md`** — they live in `dev-skills/` as `<bridge>-bridge-dev.md` / `<bridge>-bridge-publish.md`. Then
+they are:
+
+- **not indexed** (`skill_count` stays 1), **not on the Skills tab**, **not `skills install`-able**;
+- still in git → available via `adom-wiki repo clone` (and the Files tab);
+- excluded from the pkg too (the `files` allowlist lists only `SKILL.md` + `skills/**`).
+
+`dev-skills/README.md` tells a developer how to load one as a live skill (copy it to
+`~/.claude/skills/<name>/SKILL.md`). **Verify after publish:** `skill_count` should be **1** —
+`curl -s https://wiki.adom.inc/api/pages/adom/adom-desktop-<bridge>-bridge | jq '.page.skill_count'`.
+
+## Keep the pkg tarball LEAN — NO zip, NO src/, NO heroes
+
+The pkg tarball must be the `<bridge>` user skill ONLY — never the bridge runtime zip, `src/`, or hero PNGs.
+Guards: the `package.json` `files` allowlist (`SKILL.md`, `skills/**`, `install.sh`, `uninstall.sh`,
+`README.md`, `package.json`) **plus** `.gitignore` (`*.zip`, `*.png`, `*.bundle`, `*.tgz`, the old
+`*-source.tar.gz`). **ALWAYS** `adom-wiki pkg pack` then `tar tzf *.tgz` and confirm there is **no
+`*.zip`, no `src/`, no `*.png`**. A lean tarball is ~10 KB; if it's megabytes, something heavy leaked in
+(it was 16 MB before the guards). `rm -f *.tgz` after inspecting.
+
+## Skills install to BOTH .claude AND .codex
+
+`install.sh` deploys the user skill into **both `~/.claude/skills/` and `~/.codex/skills/`** (Codex only
+if `~/.codex` exists); `uninstall.sh` removes from both. Codex is a real second agent skills home now —
+don't ship an install.sh that only does `.claude`.
+
+## Scrub retired-wiki URLs (the #1 fork/extract gotcha)
+
+**The only wiki host is `wiki.adom.inc`.** Any `wiki-*.adom.cloud` host is RETIRED — as are the
+`/static/apps/...` and `/wiki/apps/...` path formats. When you fork/extract a page, the inherited
+`README.md` and the `page.json` `readme` body carry OLD URLs (this bit pup live — a dead `static/apps`
+zip link). Before every publish:
+
+```bash
+grep -rnI 'adom\.cloud\|/static/apps\|/wiki/apps' . --exclude-dir=node_modules
+```
+
+Fix every hit (README + the `page.json` `readme` body are the usual culprits), then VERIFY the **live**
+entity (not just local):
+
+```bash
+curl -s https://wiki.adom.inc/api/pages/adom/adom-desktop-<bridge>-bridge | grep -c '\.adom\.cloud'   # want 0
+```
+
+Also `repo rm` any stale source-mirror artifacts left on the page (old `*-source.tar.gz` / `*.bundle`).
+(`https://adom.inc/fonts/...` is fine — that's the brand font CDN, not a wiki host.)
+
+## Canonical wiki.adom.inc URL formats
+
+- **Page:** `https://wiki.adom.inc/adom/<slug>`
+- **File (served, anon):** `https://wiki.adom.inc/api/v1/pages/<slug>/files/<name>`
+- **Release download:** `https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>`
+- **Sibling pages:** `/adom/adom-desktop-bridges`, `/adom/adom-desktop-kicad-bridge`,
+  `/adom/adom-desktop-fusion-bridge`, `/adom/adom-desktop`
+
+## Discovery triggers — lead with everyday USER phrases
+
+Lead `discovery_triggers` with everyday phrases, not dev jargon — the single most important is the bare
+bridge name users actually say. The wiki **auto-generates** the discover snippet from `discovery_triggers`
++ `discovery_pitch` (there is no separate "upload a snippet" — those two fields ARE the snippet). Set them
+via `adom-wiki discover triggers <ref> --add ...` OR `page.json` `discovery_triggers`/`discovery_pitch`
++ push. **Verify:** `adom-wiki discover preview "<a natural user phrase for your bridge>"` should rank this
+page #1.
+
+> **Concrete pup example:** the most important trigger is the bare word **`pup`**, plus `open in pup`,
+> `open my app in pup`, `screenshot the page`, `record a browser window`. `adom-wiki discover preview
+> "open my app in pup"` must rank the Puppeteer page #1.
skills/bridge-user-skill-template/SKILL.mdadded+143
@@ -0,0 +1,143 @@+---
+name: bridge-user-skill-template
+user-invocable: true
+description: "TEMPLATE for an Adom Desktop bridge's CONSUMER skill — copy it, then replace <bridge>/<Bridge Name>/<prefix> and the verb glosses. It teaches a cloud Claude/Codex AI how to DRIVE <Bridge Name> on the user's desktop via the adom-desktop CLI: <one-line value prop — what the bridge controls and why>. Includes a FIRST-USE / COLD-START warm-up section so the AI doesn't panic on a fresh machine. Keep a trigger-words line like this one so the skill activates. Trigger words: <bridge>, <Bridge Name>, <prefix>_<verb1>, <prefix>_<verb2>, <prefix>_readiness, <prefix>_prewarm, <2-3 plain-English phrases a user would say, e.g. 'open in <bridge>', '<bridge> screenshot'>."
+---
+
+> Template — copy into your bridge's pkg as `skills/<bridge>/SKILL.md`, replace `<bridge>`/`<prefix>`.
+
+# <Bridge Name> — <one-line capability summary> (with first-use warm-up)
+
+Drive <Bridge Name> on the user's desktop from the cloud via the `adom-desktop` CLI. All verbs are
+Bash commands (NOT MCP tools):
+
+```bash
+adom-desktop <command> '<json_args>'
+```
+
+## ⭐ FIRST-TIME / COLD-START — read this BEFORE you panic that "<prefix>_<verb> did nothing"
+
+On a brand-new machine (e.g. the user just installed the app that bundles Adom Desktop),
+**something <Bridge Name> needs may NOT be pre-installed by the installer** — a runtime, a large
+dependency, an external app, or a one-time download. The bridge handles this gracefully — your job is
+to **read the structured response and follow it, not to give up.**
+
+When you run a `<prefix>_<verb>` on a cold machine, one of these happens — handle each:
+
+| Response | What it means | What you do |
+|---|---|---|
+| `errorCode: "<dependency>_not_found"` | A required runtime/dependency isn't installed (the bridge can't start). | Run the bridge's documented install verb (e.g. `adom-desktop <prefix>_install_<dep> '{}'`), wait for it, then retry the original verb. |
+| `errorCode: "<resource>_installing"` (`installing:true`) | **A needed resource is missing; the bridge is auto-downloading/installing it now (one time).** This is EXPECTED on a fresh machine — NOT a failure. | **Poll `<prefix>_readiness` until `ready:true`**, THEN re-issue the exact same verb. You do NOT install anything yourself. Do NOT report failure to the user. |
+| `errorCode: "<resource>_install_failed"` / `<resource>_download_failed` | The download/install itself failed. | Read `readiness.lastError`. It's usually no network / a proxy / low disk — NOT your fault. Retry `<prefix>_prewarm`; if it persists, surface `lastError` to the user, and use any documented fallback mode. |
+| `ok:true` / a real session | Everything was already warm. | Proceed normally. |
+
+### The clean cold-start recipe
+
+```bash
+# 1) (optional but nice) Warm the bridge WITHOUT doing real work first:
+adom-desktop <prefix>_prewarm '{}'             # returns fast; kicks off any first-use install
+
+# 2) Poll readiness until ready (the bridge sets itself up):
+adom-desktop <prefix>_readiness '{}'           # → {ready:false, installing:true, installProgressPct:42, ...}
+#    ...wait a few seconds, poll again until → {ready:true, ...}
+
+# 3) Now do the real work — it's instant:
+adom-desktop <prefix>_<verb> '{"sessionId":"<id>", ...}'
+```
+
+`<prefix>_prewarm` and `<prefix>_readiness` are the two cold-start verbs. `<prefix>_prewarm '{"wait":true}'`
+blocks until setup finishes instead of returning immediately. **Always surface a verb's `_hint`
+field verbatim to the user when something isn't ready** — it tells you (and them) exactly what's happening.
+
+> **Concrete example (the Puppeteer/pup bridge):** on a fresh PC, `browser_open_window` returns
+> `errorCode: "chrome_for_testing_installing"` (`installing:true`) because Chrome for Testing (~150 MB)
+> is auto-downloading. That is NOT a failure — you poll `browser_readiness` until `{ready:true}`, then
+> re-issue the same `browser_open_window`. Before the bridge handled this, a cold PC failed with a
+> generic "Failed to launch after 3 attempts" and the AI flailed. Don't reintroduce the panic; read
+> the structured response and follow the table.
+
+## How to Run Commands
+
+```bash
+adom-desktop <prefix>_<verb>      '{"sessionId":"myjob", ...}'   # do a unit of work
+adom-desktop <prefix>_screenshot  '{"sessionId":"myjob"}'        # capture current state (lossless PNG)
+adom-desktop <prefix>_status      '{}'                           # list sessions + readiness
+```
+
+**These are Bash commands, not MCP tools.** If your verbs are session-scoped, always pass `sessionId` —
+`'{}'` targets the *active* session, which may not be the one you mean.
+
+## Background by default — don't disrupt the user
+
+**<Bridge Name> is the AI's workspace, not the user's. Keep its windows/output OUT of the user's way unless they asked to watch.**
+
+- After opening/driving something for automation (a screenshot, a scrape, a debug loop), send it to the
+  back / leave it un-focused so it doesn't sit on top of what the user is doing (e.g. a
+  `<prefix>_lower_os_window` or equivalent).
+- **Never** force your window to the foreground unless the user must SEE it — a recording, a demo, or a
+  finished result you're explicitly showing them.
+- Use a **non-intrusive nudge** (flash the taskbar via a `<prefix>_alert_window`-style verb, or just
+  report in chat) for "it's ready / done" instead of stealing focus.
+- For long-running output the user WILL look at, a dedicated window is fine — but still don't re-raise
+  it on every update; nudge instead of grabbing focus.
+
+```bash
+adom-desktop <prefix>_<verb>          '{"sessionId":"job", ...}'
+adom-desktop <prefix>_lower_os_window '{"sessionId":"job"}'   # get it off the user's screen; keep driving headlessly
+# ...do work as needed (works while lowered/backgrounded)...
+adom-desktop <prefix>_alert_window    '{"sessionId":"job"}'   # non-intrusive "done" when you want their attention
+```
+
+## Core verbs
+
+| Command | Description | Key args |
+|---|---|---|
+| `<prefix>_readiness` | Cold-start probe: is the bridge set up / still installing? | none |
+| `<prefix>_prewarm` | Do first-use setup WITHOUT starting real work | `wait` (optional bool) |
+| `<prefix>_open_window` *(or your "start a session" verb)* | Spawn a new session/window | `sessionId`, `profile`, ... |
+| `<prefix>_<do-work>` | The bridge's primary action verb(s) | `sessionId`, ... |
+| `<prefix>_screenshot` | Capture current state (lossless PNG) | `sessionId`, `fullPage?` |
+| `<prefix>_status` | All sessions + readiness | none |
+| `<prefix>_lower_os_window` | Send the window to the back (keep it off the user's screen) | `sessionId` |
+| `<prefix>_raise_os_window` / `<prefix>_focus_window` | Bring to foreground (use sparingly — only to show the user) | `sessionId` |
+| `<prefix>_alert_window` | Flash the taskbar (non-intrusive attention) | `sessionId` |
+| `<prefix>_close_window` | Close one session + its window (destructive) | `sessionId` |
+| `<prefix>_close` | Close ALL sessions | none |
+
+Run `adom-desktop <prefix>_describe '{}'` for the full machine-readable verb catalog.
+
+## Sessions & persistence (if your bridge has sessions)
+
+If one session = one long-lived context (a window, a connection, a document), say so here, and tell the
+AI **how to reuse instead of re-open**:
+
+- New context entirely → `<prefix>_open_window` (or your start verb).
+- Reuse an existing one → drive it directly (a `<prefix>_navigate`/equivalent), don't open a second.
+
+**Run `<prefix>_status '{}'` before opening a new session** at the start of a conversation (especially
+after a context compaction) — if a session already matches what you're about to do, reuse it. Opening a
+duplicate leaves the user staring at two identical windows. Pick a **stable `profile`/identifier across
+runs** so any persisted state (logins, cookies, caches) survives — the user sets up once, reuses for
+months.
+
+## Verify what you did — `ok:true` is not enough
+
+Many start/action verbs return `ok:true` as soon as the operation *begins*, not when it *finished* /
+rendered. Sanity-check with the cheapest signal: a `<prefix>_eval`/read verb that reflects real state
+(fastest), or a `<prefix>_screenshot` when a value isn't discriminating. Don't report success to the
+user on `ok:true` alone for anything that can silently land on an error/empty state.
+
+## Key rules
+
+- **Always pass `sessionId`** for session-scoped verbs. Never `'{}'` when you mean a specific session.
+- **It's `adom-desktop <prefix>_<cmd> <json>`** — there is no top-level `<bridge>` subcommand.
+- **Surface `_hint` verbatim** whenever a verb reports it isn't ready or failed — it's written for both
+  you and the user.
+- **Reuse, don't reflexively close+reopen** — persisted state is keyed by `profile`/identifier; prefer a
+  reload/navigate over a destroy+recreate.
+- **Never broadly kill the underlying app/process** — use the bridge's per-session close
+  (`<prefix>_close_window`) so you don't nuke unrelated work.
+- **Document any per-verb gotchas** here (escaping rules, arg quirks, timeouts) — e.g. pup's
+  "no semicolons in `browser_eval` — use the comma operator or an IIFE."
+
+---