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. Runtimebridge/ (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.mds, published via adom-wiki pkg publish --org adom, installed into the container's ~/.claude/skills + ~/.codex/skills.
  3. Discoverydiscovery_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:
    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.)
  1. 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}.
  2. 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).
  3. 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.)
  4. 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 installs 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."