name: pup-bridge-publish description: "DEVELOPER skill — the exact, battle-tested recipe for publishing a new version of the Adom Desktop 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-desktop } 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, adom-desktop-puppeteer-bridge publish."

Parent skill: adom-desktop-puppeteer-bridge

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

The pup bridge is cloud-owned at wiki.adom.inc/adom/adom-desktop-puppeteer-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-desktop/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/adom-desktop-puppeteer-bridge 1.1.1 --title "..." --changelog "<≥2 words>"
adom-wiki release upload adom/adom-desktop-puppeteer-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/adom-desktop-puppeteer-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/adom-desktop-puppeteer-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 Adom Desktop.

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)

# manifest serves at the updateManifestUrl path:
curl -s https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json | jq .sha256
# 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-puppeteer-manifest.json","force":true}'
adom-desktop --target <name> browser_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-desktop": "^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-desktop": … }.

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-desktop 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-desktop": … }. ~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/adom-desktop-puppeteer-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-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 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_browser_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 (browser_navigate the existing session to https://wiki.adom.inc/adom/adom-desktop-puppeteer-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 browser_ prefix): resolution = exact verbs -> first matching prefixes -> default -> AD global 60s. pup declares: prewarm 240 (blocking CfT download), use 240 (browser_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 browser_readiness. When adding a verb that can block >60s, add its bare key here AND bump runtime version (bridge.json+BRIDGE_VERSION+manifest+zip).