---
name: publishing-the-bridge
description: "PUBLISH skill for maintainers of the adom-native-browser (nb/nbe) bridge - NOT for driving it. The exact, proven recipe to cut a release: bump versions in lockstep, build the zip, publish it as a Release ASSET (release create + upload --platform any), point the manifest url at the /download/ path (never a /files blob), push source, and land it with refresh_bridges. Covers the two-artifact split (Release zip vs skills pkg), the pkg's package.json declaring dependencies:{adom/adom-bridge:^2.x} (so pkg install pulls the ab CLI + core skills), the pkg shipping ONLY user skills while dev/publish skills live in the source repo (not the pkg, not files[], not user-invocable:false), the bridge.json contracts we declare (timeouts) and one we deliberately don't (detect), and the standing show-in-pup rule. Everyday users never need this."
---
Parent skill: **adom-native-browser**. Maintainer-only skill (not user-invocable). Bridge SDK:
https://wiki.adom.inc/adom/adom-bridge-sdk

# Publishing the native-browser bridge

## What "the bridge" actually is (3 artifacts, 3 places)
1. **Runtime** — `bridge/` (server.js + bridge.json) zipped and published as a **Release asset**
   (`release create` + `release upload --platform any`), with a manifest on `/files` whose `url` points
   at the Release `/download/` path. ab streams it to `bridges-cache/native-browser/` and spawns it.
   (Node runtime is source-only; ab provisions Node — see the SDK "Runtime contract".)
2. **Skills pkg** — the `skills/<name>/SKILL.md`s, published via `adom-wiki pkg publish --org adom`,
   installed into the container's `~/.claude/skills` + `~/.codex/skills`.
3. **Discovery** — `discovery_triggers` + `discovery_pitch` on `page.json`.

## bridge.json contracts we declare (and one we deliberately don't)
- **`timeouts`** (ab v1.9.79+): ab hard-caps every bridge verb at 60s unless you declare per-verb HTTP
  budgets. Ours match the bridge's own `timeoutFor()` (`screenshot|record|fetch_url|input_dispatch` →
  120s) plus `wait` (blocks up to its 120s cap): `verbs {wait:125, fetch_url:120, input_dispatch:120}`
  + `prefixes [{screenshot:120},{record:120}]`, `default:60`, and `statusVerb:"nbrowser_status"` (ab
  returns `{stillRunning:true,statusVerb}` on its own timeout → caller polls). Keep the describe
  `timeoutSeconds` in `server.js` aligned. Note: Bridge's **CLI has its own ~30s client
  timeout**, separate from ab's bridge budget — you can't observe a >30s verb via a bare CLI call.
- **`detect` — intentionally OMITTED.** `detect` is for a HOST APP ab detects/gates. Native-browser
  drives the user's REAL browser via the extension, so the authoritative "is it usable" signal is
  **extension-connected** (`nbrowser_readiness`), not a browser binary on disk — and our read-only
  verbs are always forwarded regardless. A single `hostApp:"Google Chrome"` would MISREPORT the common
  Edge-only machine (verified real: ADOMBASELINE runs fine on Edge with no Chrome), and Edge is
  universal on Windows so "browser installed" is near-zero signal. So we do NOT declare `detect`.

## Release the RUNTIME — the exact, proven sequence
Deps: none (core `http`/`net`), so the payload is source-only (no `node_modules`).

1. **Bump the version in THREE places, together** (they drift if you don't):
   - `bridge/bridge.json` `version` (what ab reads + reports),
   - `bridge/package.json` `version`,
   - `bridge/BRIDGE_VERSION`.
   `server.js` reads `VERSION` from `bridge.json` (authoritative) so it can't drift — keep it that way.
   ab reads that same `version` off your `/status` JSON as `liveVersion`, so keep `/status` accurate.
1b. **Regenerate `bridge.json` `verbs[]` from `VERB_META`. NEVER hand-edit it.** `verbs[]` is what ab
   can see; a verb missing from it is invisible to every caller even though the code works, with no
   error anywhere. Twelve verbs (incl. `nbrowser_events`) silently rotted out of it this way. Run:
   ```bash
   node -e 'const fs=require("fs");const s=fs.readFileSync("bridge/server.js","utf8");
   const i=s.indexOf("const VERB_META = {");let d=0,e=-1;
   for(let j=s.indexOf("{",i);j<s.length;j++){const c=s[j];if(c==="{")d++;else if(c==="}"){d--;if(!d){e=j;break}}}
   const k=[...s.slice(i,e+1).matchAll(/^ {2}(nbrowser_[a-z0-9_]+):\s*\{/gm)].map(m=>m[1]);
   const b=JSON.parse(fs.readFileSync("bridge/bridge.json","utf8"));b.verbs=k;
   fs.writeFileSync("bridge/bridge.json",JSON.stringify(b,null,2)+"\n");console.log("verbs:",k.length)'
   ```
1c. **Run the gates before anything else.** `node --check bridge/server.js` (a syntax error has shipped
   twice and takes the bridge down) and `node tools/check-guard-coverage.mjs`, which now fails on verb
   drift, a dead declared verb, an out-of-order `verbs[]`, or `bridge.json` version != `BRIDGE_VERSION`.
2. **Build the zip with files at the ROOT** (there is no `zip` binary — use Python `zipfile`):
   members = `bridge.json`, `package.json`, `server.js`, `BRIDGE_VERSION`, `adom-favicon.png`. Name it
   `native-browser-bridge-v<ver>.zip` (as of 0.1.29 — it always WAS a zip; the old `.bin` naming was a
   scar from the retired `/files` base64 API which rejected `.zip`. Release assets are extension-agnostic
   and ab only follows the manifest `url`, so no ab-side change was needed for the rename; old `.bin`
   releases stay valid at their existing URLs).
3. **The runtime is a RELEASE ASSET, not a `/files` blob** (SDK "two-artifact" rule — fusion + pup do
   exactly this). Cut a Release for the bridge version + upload the zip:
   - `adom-wiki release create adom/nb-bridge <ver> --title "native-browser bridge runtime v<ver>"`
   - `adom-wiki release upload adom/nb-bridge <ver> native-browser-bridge-v<ver>.zip --platform any`
   The download then serves anon at `https://wiki.adom.inc/download/adom/nb-bridge/<ver>/native-browser-bridge-v<ver>.zip` (verify: `curl -sI` → 200). (The pkg's own release stays 0.3.x; the bridge release is 0.0.x — separate version streams on the same page, that's expected.)
4. **sha256 + byte size** → write `native-browser-bridge-manifest.json` with the `url` pointing at that
   **`/download/` path** (NOT a `/files` blob):
   `{manifest_version:1, version, url:"https://wiki.adom.inc/download/adom/nb-bridge/<ver>/native-browser-bridge-v<ver>.zip", sha256, size, released_at}`.
5. **Push manifest + source as TEXT** with `adom-wiki repo push adom-native-browser --files native-browser-bridge-manifest.json bridge/bridge.json bridge/package.json bridge/server.js bridge/BRIDGE_VERSION ...` (the manifest lives on `/files`; only the binary is off it).
6. **Land it:** `refresh_bridges {name:"native-browser"}` → verify `nbrowser_readiness` routes and
   `bridge_check_updates`/`bridge_list` show `liveVersion==<ver>` (no `staleProcess`). *(bridge_check_updates
   intermittently omits native-browser after a Bridge self-update — trust readiness + bridge_list.)*
7. **NEVER `repo rm` a runtime zip while the manifest still points at it.** The old `/files` `.bin` uploads
   are the dead anti-pattern (they piled up to 22 stale versions once) — do NOT re-add them. If any linger,
   migrate as a SEQUENCE: release-upload → repoint manifest → verify `/download/` serves → THEN delete the
   `/files` copies. The **pkg's** `package.json` (the skills tarball, not the zip) declares
   `dependencies:{"adom/adom-bridge":"^2.x"}` (so `pkg install` pulls the ab CLI + core skills the
   container needs) — NOT the server's npm deps (the bridge has none anyway). The **zip's**
   `bridge/package.json` holds the server's runtime deps (none for us — core `http`/`net`).

## Reaping is fixed (ab v1.9.76) — the old orphan trap is gone
Earlier, `refresh_bridges` could leave a stale PID serving old code (a node bridge doesn't lock the
cache dir, so a clean swap didn't reap). **ab v1.9.76 fixed this:** `refresh_bridges` now force-restarts
a running bridge after an update, `restart_bridge {name}` is a real verb (kill tracked+port-orphan +
respawn on the stable port), and `bridge_check_updates` surfaces `liveVersion` + `staleProcess`
directly. So: after a release, just `refresh_bridges` (or `restart_bridge` for an explicit restart) and
confirm `liveVersion` matches — no more digging in `host.json` or manual `taskkill`.
(`bridge_pause`/`bridge_resume` are a ROUTING gate only; they never kill the process.)

## Publish the SKILLS pkg — USER skills ONLY (dev/publish live in the repo)
`adom-wiki pkg publish --org adom`. **The pkg ships ONLY your USER skills** — the ones a general Adom
user's agent needs to DRIVE the bridge. That's the pkg's whole job: a container `pkg install`s it to USE
you. Each is a real `skills/<name>/SKILL.md` with a `Parent skill:` first line; list **every user-skill
path EXPLICITLY** (no globs) in `package.json` `files[]` AND `install.sh`/`uninstall.sh`.
- **DEV + PUBLISH skills do NOT go in the pkg — they live in your source REPO** (this skill, and
  `developing-the-bridge`). A developer who clones the source to EDIT the bridge gets them as repo files;
  the normal user never needs them. So: **not in `files[]`, not in `install.sh`, and do NOT scope them
  with `user-invocable:false`** — that convention still SHIPS them and bloats every container install.
  Keep them under `dev-skills/` + `publish-skills/` in the repo (the wiki Skills page reads those folders
  for its Dev/Publish tabs). **Open-vs-closed source is irrelevant** — it only sets whether the repo is
  publicly readable; either way dev/publish stay in the repo, never the pkg. (Bridge core does
  exactly this: its RELEASE/WIKI_PUBLISH/SIGNING skills live only in source, never in the published pkg.)
- **The pkg's `package.json` declares `dependencies:{"adom/adom-bridge":"^2.x"}`** — `pkg install`
  resolves it npm-style, so installing our skill pkg also pulls the ab CLI + core skills the container
  needs to drive us. (Do NOT put the SERVER's npm deps here — those ride in the zip's `bridge/package.json`.)

## Discovery
Edit `page.json` `discovery_triggers` (30+ user-task phrasings, login/forms niche — pup can't do
logins) + `discovery_pitch` (an INSTRUCTION). Push `page.json` **LAST** (a pkg publish clobbers it and
drops the hero). Verify: `adom-wiki discover preview --json "<7 real user queries>"` → land top-3.

## After ANY wiki change: show John in pup (standing rule)
Open the live page in a maximized pup window + shotlog + hand him the phone URL. Never just report
"published."

## After ANY release: prove the USER-VISIBLE result, unprompted (standing rule)
Verifying the pipeline is not verifying the feature. "The verb returned ok / the file exists / a test
window rendered" is engineer-proof; the user cares what THEY will see. So after landing a release,
automatically demonstrate the actual user-visible outcome on EVERY surface the change touches (e.g. a
taskbar-badge change = show every browser+profile it will decorate, not one throwaway Notepad button)
and put it in shotlog with the phone URL - before the user has to ask. If a live demo would touch
something sensitive (a banking window, a signed-in session), don't skip the demo - build the same
proof a safe way (generate from the real inputs off to the side) and say why. The user should never
have to say "prove it."
