name: pup-bridge-publish description: "DEVELOPER skill — the exact, battle-tested recipe for publishing a new version of the Puppeteer (pup) bridge to wiki.adom.inc, plus every gotcha hit live. NOT for general users. Read before cutting a release: which artifact goes where (pkg=skills, release=bridge zip), version-lockstep, the files allowlist, keeping the tarball lean (NO zip / NO src / NO heroes / NO node deps), the pkg ships ONLY the pup USER skill (dev/publish skills are source-only in dev-skills/ + publish-skills/, never in the pkg — open-vs-closed source doesn't matter), the skillpack package.json declaring dependencies:{ adom/adom-bridge } to pull the AD CLI + core skills, deploying skills to .claude AND .codex, scrubbing stale retired-wiki (wiki-ufypy5dpx93o.adom.cloud) URLs, user-first discovery triggers, sha matching, and verify steps. Trigger words: publish pup bridge, ship pup bridge, release pup, pkg publish, repo push, adom-wiki release, bridge manifest, pup version bump, no zip in tarball, dev skill source only, scrub old wiki url, codex skills, discovery triggers, pup-bridge publish."

Parent skill: pup-bridge

pup-bridge-publish — shipping a new pup bridge version

The pup bridge is cloud-owned at wiki.adom.inc/adom/pup-bridge. This is the step-by-step ship recipe + the traps that cost real time. Companion: pup-bridge-dev (architecture / ownership), PUBLISHING.md (terse recipe), CLAUDE.md (ownership boundary).

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 (server.js, chrome.js, bridge.json, node deps). 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 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)

export ADOM_WIKI_TOKEN=$(cat /var/run/adom/api-key 2>/dev/null)   # container default; or session.json's session_token
adom-wiki whoami    # MUST show a real user (e.g. [email protected]). Strict-auth rejects the shared "Developer" token.

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

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-bridge/plugins/puppeteer 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. node_modules is NEVER included (AD npm installs it on spawn; the seed ships it; an 88 MB zip would blow wiki limits):

import zipfile, os, hashlib
root, out = "src", "adom-bridge-puppeteer-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"):
            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 chrome.js + your change are inside before shipping.

3. Publish the zip as a Release (releases PRESERVE your sha)

adom-wiki release create adom/pup-bridge 1.1.1 --title "..." --changelog "<≥2 words>"
adom-wiki release upload adom/pup-bridge 1.1.1 adom-bridge-puppeteer-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/pup-bridge/1.1.1/adom-bridge-puppeteer-v1.1.1.zip | sha256sum

4. Point the manifest at the release + push it (git repo)

Edit adom-bridge-puppeteer-manifest.jsonversion, url (the release download URL), sha256, size (from step 3 — must match the served asset or bridge_install rejects it). Then:

adom-wiki repo push adom/pup-bridge \
  --files adom-bridge-puppeteer-manifest.json src/bridge.json src/BRIDGE_VERSION src/server.js src/chrome.js \
  -m "v1.1.1: <what changed>"

5. Publish the container skills as a pkg — keep it LEAN, and MIND THE TRAPS

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/[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 Bridge.

Traps hit live (don't relearn these)

  • pkg pack includes EVERYTHING by default → a 16 MB 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.
  • files[] lists ONLY user filesSKILL.md, install.sh, uninstall.sh, README.md, package.json. The dev/publish skills are source-only and are NOT in files[]. (If you ever add an extra USER skill under skills/, list its skills/<name>/SKILL.md explicitly — a bare "skills" dir entry silently includes ZERO files.) Always tar tzf to confirm exactly what shipped.
  • 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) — THREE GATES, and none of them is "the manifest version matched"

Learned the hard way 2026-07-24: a ship can report success while deploying NOTHING. Three independent failures stacked in one session — a release upload that silently didn't replace the served zip, an edge cache serving a stale zip to AD's download path, and AD preserving stale node_modules so a dependency bump never applied. Each gate below catches one; skip them and you WILL pass a broken deploy.

GATE 1 — the SERVED ZIP hash, fetched the way AD fetches it (PLAIN url, NO cache-buster). The manifest version matching is NOT proof. AD downloads the manifest's url with no cache-buster, so an edge cache can hand it a STALE zip while your ?cb=-busted curl sees the fresh one. bridge_install's sha256 guard then correctly REFUSES it (sha256 mismatch: manifest=… got=…).

curl -sL -o /tmp/served.zip "https://wiki.adom.inc/download/adom/.../adom-bridge-puppeteer-v<V>.zip"   # NO ?cb=
test "$(sha256sum /tmp/served.zip | cut -d' ' -f1)" = "<local zip sha>" || echo "STALE — do not install"

If it stays stale after a few retries, the edge cache is poisoned at that URL: bump to a FRESH version to get a new URL the cache has never seen. Re-uploading to the same version often won't bust it.

GATE 2 — bridge_install actually succeeded. NEVER pipe it to /dev/null. A sha mismatch (Gate 1) surfaces ONLY here, as an "ok": false / "error": "sha256 mismatch…". Capture the output and grep for "status": "ok"; do not print "installed" unless it's there.

GATE 3 — the version ON DISK, not what install claimed.

adom-bridge-cli --target <name> shell_execute '{"command":"cmd /c type \"%LOCALAPPDATA%\\Bridge\\bridges-cache\\puppeteer\\BRIDGE_VERSION\"", "reason":"…"}'

(Use cmd /c type, not powershell -File — a damaged PS profile on some boxes drops -File to an interactive banner and burns the timeout.)

DEPENDENCY BUMPS need an extra step. If you changed package.json/package-lock.json (e.g. a puppeteer bump to move CfT), the code deploys but the DEPS do NOT: AD's preserve_client_deps carries the old node_modules across re-sync. Force a fresh install: bridge_killrmdir /s /q …\node_modulesrestart_bridge (AD npm-installs when node_modules is missing). Then confirm with node -e "require('puppeteer').executablePath()" on the box. (Fleet-wide this needs an AD fix — npm-install-on-lockfile-change — filed in AD_REQUEST_bridge_staleness.md.)

# manifest serves at the updateManifestUrl path (necessary, NOT sufficient — see Gate 1):
curl -s https://wiki.adom.inc/api/v1/pages/pup-bridge/files/adom-bridge-puppeteer-manifest.json | jq .sha256
adom-bridge-cli --target <name> bridge_install '{"manifestUrl":".../files/adom-bridge-puppeteer-manifest.json","force":true}'
adom-bridge-cli --target <name> pup_readiness '{}'
# render the page in pup + READ the screenshot (curl 200 is NOT verification). Confirm version + Skills tab.

What the pkg ships: USER skills only; dev/publish are source-only (Bridge SDK)

The Bridge SDK ("Where every file lives", corrected 2026-07-06) is authoritative:

  • The pkg carries ONLY your USER skill(s). For pup that's the root SKILL.md (name: pup). It's the single entry in files[]'s skill list; the container can't drive the verbs without it.
  • DEV + PUBLISH skills are SOURCE-ONLY — this skill and pup-bridge-dev. They live in the repo under publish-skills/<name>/SKILL.md and dev-skills/<name>/SKILL.md, are NOT in package.json files[], are NOT installed by install.sh, and do NOT need user-invocable:false (they never reach a container to trigger). Maintainers read them via repo clone / the Files tab.
  • Open-vs-closed source does NOT matter — the pkg ships only user skills either way. (Earlier SDK wording gated this on openness; that was wrong and is corrected.)

The pkg's package.json DECLARES its dependency on AD so pkg install pulls the AD CLI + core skills into the container:

"dependencies": { "adom/adom-bridge": "^1.9.x" }   // pkg dep — pulls the AD CLI + core skills

This is the ONE dependency the skillpack package.json carries. Your SERVER's npm deps (puppeteer, …) live ONLY in the Release zip's src/package.json, never here.

Verify after adom-wiki pkg pack: shipped 1 user skill(s); tar tzf *.tgz shows exactly SKILL.md, install.sh, uninstall.sh, README.md, package.jsonno skills/, no dev-skills/, no *.zip, no src/, no *.png; and package.json has dependencies: { "adom/adom-bridge": … }.

History (so nobody re-thrashes this — it flipped 3×): the pkg ships ONLY the pup user skill; dev + publish are source-only in dev-skills//publish-skills/; the skillpack package.json declares the adom/adom-bridge dep. Settled 1.8.23 (2026-07-06) on the maintainer's corrected guidance.

Keep the pkg tarball LEAN — NO zip, NO src/, NO heroes

The pkg tarball is docs only — the pup USER skill + scripts; never the bridge runtime zip, src/, hero PNGs, node_modules, or the dev/publish skills. Guards: the package.json files allowlist (SKILL.md, 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: no *.zip, no src/, no *.png, no skills/, no dev-skills/; shipped 1 user skill(s); and package.json has dependencies: { "adom/adom-bridge": … }. ~12 KB is right (one SKILL.md + scripts); MEGABYTES means a binary/src/ leaked in (it was 16 MB before the guards). rm -f *.tgz after.

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 stale retired-wiki URLs (the #1 fork/extract gotcha)

The only wiki host is wiki.adom.inc. wiki-ufypy5dpx93o.adom.cloud 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 us live — a dead static/apps zip link). Before every publish:

grep -rnI 'wiki-ufypy5dpx93o\|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):

curl -s https://wiki.adom.inc/api/pages/adom/pup-bridge | grep -c wiki-ufypy5dpx93o   # 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-bridge-sdk, /adom/kicad-bridge, /adom/fusion-bridge, /adom/adom-bridge

Discovery triggers — lead with everyday USER phrases

Lead discovery_triggers with everyday phrases, not dev jargon — the single most important is bare pup, plus open in pup, open my app in pup, screenshot the page, record a browser window, etc. 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 "open my app in pup" should rank this page #1.

⚠ Node-bridge gotcha: the cache copy needs node_modules (don't outrun the bundled seed)

pup is a Node bridge with heavy native deps (puppeteer, sharp, keytar). The release zip is SOURCE-ONLY (no node_modules). kicad/fusion (Python) stream fine, but a wiki-streamed pup cache copy crashes MODULE_NOT_FOUND because AD spawns node server.js without installing node_modules (the bespoke start_pup_bridge npm-installs only the BUNDLED copy; the cache copy gets nothing). So:

  • The working runtime delivery for pup is the BUNDLED NSIS seed (it keeps node_modules). Wiki-stream the SKILLS (pkg) freely; the RUNTIME effectively ships with the AD installer until AD is fixed.
  • Do NOT publish a wiki RUNTIME (src/) version NEWER than the AD bundled seed — a box with our updateManifestUrl persisted would pull the newer source-only zip into the cache and crash (it supersedes the good bundled copy). Keep the wiki runtime version == the bundled seed version.
  • The real fix is AD-side (give cache-installed node bridges their node_modules — npm install the cache or reuse the bundled node_modules). Tracked in HANDOFF-TO-ADOM-DESKTOP.md. Once it ships, normal wiki-streaming + updateManifestUrl auto-update works for pup like the other bridges.

Final step of EVERY publish — show it in pup (standing rule)

After any release/repo-push/pkg-publish, refresh the LIVE wiki page in a pup window the user can see (pup_navigate the existing session to https://wiki.adom.inc/adom/pup-bridge?v=<ver>, cache-busted), screenshot it, and hand over the link. Never report "published" without showing it.

Binary/hero pushes: ok:true, commit:null = the server honored the repo .gitignore

repo push (and the raw files API) return {ok:true, files_count:1} but commit:null and the file NEVER lands when the repo's committed .gitignore matches it (this repo ignores *.png). No error is raised — the only tell is commit:null + a 404 on the file. Fix: add a !hero-vN.png unignore line, push .gitignore FIRST (real commit hash returned), then push the PNG, then verify anonymously: HTTP 200 AND md5sum matches the local file. Use a NEW versioned filename per hero (CDN staleness).

Pushing an update LIVE on a desktop (AD >= 1.9.76)

refresh_bridges '{"name":"puppeteer"}' downloads the update AND auto-restarts a running bridge — no manual kill, no bridge_install force needed for the update path. Then PROVE it went live: bridge_check_updates must show liveVersion == current and staleProcess:false (liveVersion reads the running process's /status version — keep BRIDGE_VERSION correct). If a process serves stale code, restart_bridge '{"name":"puppeteer"}' kills (tracked PIDs + port orphans) and respawns from cache. Drop any taskkill/manual-PID habits.

Per-verb timeout budgets (AD >= 1.9.79 contract)

bridge.json declares timeouts with BARE verb keys (no pup_ prefix): resolution = exact verbs -> first matching prefixes -> default -> AD global 60s. pup declares: prewarm 240 (blocking CfT download), use 240 (pup_use can trigger installs), open_window 150 (spawn-verified fallback worst case = 3 candidates x 3 attempts x 30s CDP wait), record* 90. An AD timeout is NON-terminal: callers get {stillRunning:true, statusVerb, timeoutSeconds} and poll pup_readiness. When adding a verb that can block >60s, add its bare key here AND bump runtime version (bridge.json+BRIDGE_VERSION+manifest+zip).

⛔ EVERY SHIP IS THREE ARTIFACTS, IN THIS ORDER (John, 2026-07-25 — learned the hard way twice)

A release is not "shipped" when the zip installs. Skills → pkg tarball → release zip. Do all three, in that order, every time. Both failures below were invisible from the release side, which is exactly why they need a checklist rather than judgement.

1. UPDATE THE SKILLS FIRST (before you build anything). If this ship changed a verb's behavior, response fields, hints, or defaults, the user-facing SKILL.md (+ any skills/pup-*/SKILL.md) must say so. Consumers read the skill in their container; a hint your code returns but the skill never mentions is half-delivered. Measured failure: after shipping the native-first flip, renderCheck/_verifyRender, the AI-thread naming, and the bug-report invitation, the user-facing skill documented none of it — grep found 5 of 6 new concepts missing. John asked "are your skills up to date that users would get in their container?" and the honest answer was no.

2. THEN PUBLISH THE PKG TARBALL. adom-wiki pkg publish --org adom is what actually delivers those skills. A release does NOT ship skills.

  • Bump package.json version to the version you are shipping, so the pkg and the release agree.
  • THE TRAP: a release and its pkg share ONE version list, and the resolver picks the MAX row and fails if that row has no tarball. Shipping releases 1.9.94→1.9.104 while the last pkg sat at 1.9.93 made the max row a tarball-less release and broke every constraint install of the pageadom-wiki pkg update reported STALE_INSTALL: the registry no longer resolves it and skipped pup entirely. Nothing in the release pipeline noticed, because the release half was perfect.
  • Verify, do not assume: curl -s -o /dev/null -w "%{http_code}" https://wiki.adom.inc/api/v1/packages/adom/pup-bridge/<VER>/tarball must be 200. ship.sh GATE 2b now checks this and warns, but the warning only helps if you read it.

3. THEN THE RELEASE ZIP + MANIFEST (the existing recipe: bump BRIDGE_VERSION + bridge.json → build → release upload → repoint the manifest → repo pushbridge_install → verify on disk).

Order matters: skills are text inside the pkg, so publishing the pkg before editing the skills ships the old docs; and cutting the release last means the version you verify on disk is the one whose skills and pkg you already published.

Quick self-check before you call a ship done:

grep -ciE '<new concept>' SKILL.md            # 0 = the skill is stale, go back to step 1
python3 -c "import json;print(json.load(open('package.json'))['version'])"   # must equal the release
curl -s -o /dev/null -w '%{http_code}\n' https://wiki.adom.inc/api/v1/packages/adom/pup-bridge/$(cat src/BRIDGE_VERSION)/tarball