Pup - Puppeteer Bridge
Public Made by Adomby adom
pup is the AI's own browser: a real, full Chrome on the user's desktop that the AI fully controls (a sandbox, not the user's signed-in browser). Rides Bridge; pup_* verbs open windows and tabs, navigate, screenshot, and eval JS.
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
.zipis a RELEASE, never a git file:*.zipis gitignored andrepo push --files X.zipreportsokbut 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.json → version, 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 packincludes 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, ANDpackage.jsonhas afilesallowlist. With both, the tarball drops to ~12 KB.files[]lists ONLY user files —SKILL.md,install.sh,uninstall.sh,README.md,package.json. The dev/publish skills are source-only and are NOT infiles[]. (If you ever add an extra USER skill underskills/, list itsskills/<name>/SKILL.mdexplicitly — a bare"skills"dir entry silently includes ZERO files.) Alwaystar tzfto confirm exactly what shipped.pkg packhonors.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 thefilesallowlist for that.- A RELEASE shows up in
adom-wiki pkg infoas a row withtype:null(wiki UI conflation). The real pkg rows aretype:"app". Useadom-wiki release listfor true releases; don't think your pkg shipped just because a version appears inpkg info. - sha/size mismatch →
bridge_installrejects the manifest. Releases preserve the uploaded sha, so use therelease uploadresponse values verbatim and re-download to confirm. - The Skills tab +
skill_count+adom-wiki skills installcome from the GIT REPO, not the pkg. The wiki indexes everySKILL.mdin the page repo (root = main skill;<dir>/SKILL.md= sub-skills) —page.skillsis null; it's a repo scan. So a file namedSKILL.mdANYWHERE in the repo becomes an indexed, tab-listed,skills install-able skill. (This is separate frompkg install, which installs the pkg tarball governed by thefilesallowlist.) 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_kill → rmdir /s /q …\node_modules
→ restart_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 infiles[]'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 underpublish-skills/<name>/SKILL.mdanddev-skills/<name>/SKILL.md, are NOT inpackage.json files[], are NOT installed byinstall.sh, and do NOT needuser-invocable:false(they never reach a container to trigger). Maintainers read them viarepo 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.json — no 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 ourupdateManifestUrlpersisted 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 installthe cache or reuse the bundled node_modules). Tracked inHANDOFF-TO-ADOM-DESKTOP.md. Once it ships, normal wiki-streaming +updateManifestUrlauto-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.jsonversionto 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 page —
adom-wiki pkg updatereportedSTALE_INSTALL: the registry no longer resolves itand 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>/tarballmust be 200.ship.shGATE 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 push → bridge_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
---
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)
```bash
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
```bash
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 install`s it on spawn; the seed
ships it; an 88 MB zip would blow wiki limits):
```python
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)
```bash
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.json` → `version`, `url` (the release download URL), `sha256`,
`size` (from step 3 — **must match the served asset or `bridge_install` rejects it**). Then:
```bash
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
```bash
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 files** — `SKILL.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=…`).
```bash
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.**
```bash
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_kill` → `rmdir /s /q …\node_modules`
→ `restart_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.)
```bash
# 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:
```jsonc
"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.json` — **no `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:
```bash
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):
```bash
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 page** —
`adom-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 push` → `bridge_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:**
```bash
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
```