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

## Caller provenance: knowing WHICH AI thread asked

A typical user has ~20 AI conversations pointed at one desktop. Since AD v1.9.180 every relayed command
carries the identity of the conversation that made it, and AD hands that identity down to your bridge.
(Full contract: AD's `adom-desktop-caller-identity` skill.)

**1. What you receive for free.** AD stamps four headers on every request it dispatches to your bridge.
No manifest field, no opt-in, nothing to declare:

| Header | Carries |
|---|---|
| `X-Adom-Caller-Thread` | the AI conversation/tab name, e.g. `chip-fetcher tab 3` |
| `X-Adom-Caller-Container` | the container that thread runs in |
| `X-Adom-Caller-Reason` | the per-call justification, when the caller supplied one |
| `X-Adom-Caller-Delegate` | present when the call itself came second-hand (another bridge or Hydrogen relayed it) |

Read them at the top of your request handler, and **always code for absent** (`|| 'unidentified'`): a local
CLI call, an older caller, or your own internal timer will have none.

**2. Log the thread.** Highest value per line of code in this whole section, and it is three lines:

```js
const thread = req.headers['x-adom-caller-thread'] || 'unidentified';
log(`[verb] <prefix>_eval session=${sessionId} caller="${thread}"`);
```

Why it matters: `[verb] <prefix>_eval session=rev caller="chip-fetcher tab 3"` makes an interference report
reconstructable (two tabs were driving one session, and the log says which did what, when). A bare
`[verb] <prefix>_eval session=rev` never can, no matter how carefully anyone reads it afterwards.

**3. Arbitrate, but only if your bridge owns sessions or resources an agent can own.** If two threads can
fight over the same window, session, document, or device:

- Record `ownerThread` at creation time (the thread header on the verb that created it).
- When a **different** thread arrives, warn loudly in the response (`_hint` / a `warning` field). Do not
  fail silently, and do not quietly let the second thread stomp the first.
- **Refuse** only for destructive ops (close, reset, delete, overwrite). Reads and ordinary drive verbs
  should warn and proceed.
- **Always name the current owner** in the response. The second agent can then go coordinate ("session
  `rev` belongs to `chip-fetcher tab 3`") instead of guessing or forcing.

**4. REQUIRED: forward the chain on your own AD callbacks.** When your bridge calls an AD verb
(`desktop_screenshot_window`, `desktop_set_window_identity`, `notify_user`, `desktop_taskbar`) while
carrying out a verb an AI asked for, you are acting **on that thread's behalf**. Echo the three headers you
received and **add your own `X-Adom-Caller-Delegate: <bridge>`**. If you don't, the chain dies at your
doorstep: the user's Activity Log credits your bridge for work someone else asked for, and an approval toast
asks them to authorize a nameless component. Forward it and the user sees
`chip-fetcher tab 3 (via <bridge>)`, which is the whole point.

```js
// node. `incoming` is the request AD dispatched to your verb handler.
const FORWARD = ['thread', 'container', 'reason'];
const headers = {
  'Content-Type': 'application/json',
  'X-Adom-Bridge-Token': process.env.ADOM_BRIDGE_TOKEN,
  'X-Adom-Caller-Delegate': '<bridge>',     // name YOURSELF; never forward this one
};
for (const part of FORWARD) {
  const v = incoming.headers[`x-adom-caller-${part}`];
  if (v) headers[`X-Adom-Caller-${part[0].toUpperCase()}${part.slice(1)}`] = v;
}
await fetch(`${process.env.ADOM_DIRECT_API_URL}/command`, {
  method: 'POST', headers,
  body: JSON.stringify({ command: 'desktop_screenshot_window', args: { hwnd } }),
});
```

```python
# python, same shape
FORWARD = ("thread", "container", "reason")
headers = {
    "X-Adom-Bridge-Token": os.environ["ADOM_BRIDGE_TOKEN"],
    "X-Adom-Caller-Delegate": "<bridge>",   # name YOURSELF
}
for part in FORWARD:
    value = incoming_headers.get(f"x-adom-caller-{part}")
    if value:
        headers[f"X-Adom-Caller-{part.capitalize()}"] = value

requests.post(
    f"{os.environ['ADOM_DIRECT_API_URL']}/command",
    headers=headers,
    json={"command": "notify_user", "args": {"title": "<Bridge Name>: export finished"}},
)
```

When you act on your **own** behalf (a health poll, a timer, a cleanup sweep), do NOT forward a stale
identity from some earlier request. Send an explicit caller block in the args instead, which always beats
the forwarded headers:

```json
{ "caller": { "aiThread": "<bridge> bridge (self)", "containerName": "local" } }
```

**5. The trust rule, stated plainly: these values are SELF-ASSERTED.** They are for attribution, logging,
UX, and arbitration hints. They are **never** authorization and never a security boundary. AD records and
displays them; it cannot verify them, and neither can you. `if (thread === "admin-tab") allow(...)` is a
hole, not a check.

## 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.
- Don't gate any bridge behavior on a caller's `aiThread` / `X-Adom-Caller-Delegate` value, and don't drop
  the `X-Adom-Caller-*` headers on your own AD callbacks. The first is a security hole; the second breaks
  the user's attribution chain at your bridge.
