← Commit history

Restructure to the wiki skillpack layout: dev/publish skills become real skills/<name>/SKILL.md sub-skills with Parent-skill lines, explicit install.sh/uninstall.sh/files[] lists (no globs), skill-map tables in SKILL.md+README; supersede the old dev-skills source-only hack

John Lauer ·162cdd2f58 ·4mo ago ·parent 09fa864
8 files changed +468−56
README.md+9−2
@@ -40,8 +40,15 @@ kicad/fusion bridges). Adom Desktop ships a bundled seed and auto-pulls newer ve  - Clone the source: `adom-wiki repo clone adom/adom-desktop-puppeteer-bridge` - Runtime lives in `src/` (`server.js`, `chrome.js`, `bridge.json`, …); the release zip is `src/` at zip-root.-- **Developer/publishing references** live in `dev-skills/` (`pup-bridge-dev.md`, `pup-bridge-publish.md`) —-  source-only, not in the pkg or on the Skills tab. See `dev-skills/README.md`.+- **Developer/publishing skills** are bundled sub-skills in the wiki skillpack layout —+  `skills/pup-bridge-dev/SKILL.md` + `skills/pup-bridge-publish/SKILL.md` (`user-invocable: false`,+  clearly marked DEVELOPER-only so they never trigger for everyday users). Skill map:++| Skill | What it's for |+|---|---|+| `pup` (root SKILL.md) | Everyday browser driving |+| `pup-bridge-dev` | Bridge internals + dev learnings |+| `pup-bridge-publish` | Publish/release recipe + gotchas | - Ownership boundary: `CLAUDE.md`. Publish recipe: `PUBLISHING.md`. History: `CHANGELOG.md`.  ## Related
SKILL.md+8
@@ -6,6 +6,14 @@ description: "Drive a generic browser window on the user's desktop via the adom-  # Pup — Puppeteer Browser Control (with cold-start self-heal) +## Skill map (this pack)++| Skill | What it's for |+|---|---|+| `adom-desktop-puppeteer-bridge` (pup, this file) | Everyday use: drive generic pup browser windows — open/navigate/screenshot/eval/record |+| `pup-bridge-dev` | DEVELOPERS: architecture, cold-start self-heal internals, field-tested learnings, repo layout |+| `pup-bridge-publish` | DEVELOPERS: the exact publish/release recipe + every gotcha (artifacts, manifests, versioning) |+ Drive Chrome windows on the user's desktop from the cloud via the `adom-desktop` CLI. All verbs are Bash commands (NOT MCP tools): 
install.sh+14−14
@@ -1,23 +1,23 @@-#!/bin/bash-# Container-side install: drop the pup Claude/Codex skills into the agent skills dir(s).+#!/usr/bin/env bash+# Install the pup skill pack: the main `pup` skill plus every bundled sub-skill.+# The publish linter requires each skill be listed explicitly (not a glob), so+# every skill below is named. Keep this list in sync with skills/ and uninstall.sh. # The bridge RUNTIME is NOT here — it ships in Adom Desktop + as the Releases zip. set -e PKG_DIR="$(cd "$(dirname "$0")" && pwd)"  deploy_into() {   local BASE="$1"-  mkdir -p "$BASE/adom-desktop-puppeteer-bridge"-  [ -f "$PKG_DIR/SKILL.md" ] && cp "$PKG_DIR/SKILL.md" "$BASE/adom-desktop-puppeteer-bridge/SKILL.md"-  for skill_dir in "$PKG_DIR/skills"/*/; do-    [ -d "$skill_dir" ] || continue-    name="$(basename "$skill_dir")"-    mkdir -p "$BASE/$name"-    cp -r "$skill_dir"* "$BASE/$name/"-  done-  echo "  -> $BASE"+  install_skill() { mkdir -p "$BASE/$1"; cp "$PKG_DIR/$2" "$BASE/$1/SKILL.md"; }+  # main pack skill (REQUIRED at repo root by the registry)+  install_skill adom-desktop-puppeteer-bridge SKILL.md+  # bundled sub-skills (one skill file each; user-invocable:false — developer refs)+  install_skill pup-bridge-dev     skills/pup-bridge-dev/SKILL.md+  install_skill pup-bridge-publish skills/pup-bridge-publish/SKILL.md+  echo "  -> $BASE (pup + 2 bundled sub-skills)" } -echo "Installing adom-desktop-puppeteer-bridge skills (pup + dev/publish sub-skills):"-deploy_into "$HOME/.claude/skills"                       # Claude Code-[ -d "$HOME/.codex" ] && deploy_into "$HOME/.codex/skills"   # Codex (if present)+echo "Installing adom-desktop-puppeteer-bridge skills:"+deploy_into "$HOME/.claude/skills"                          # Claude Code+[ -d "$HOME/.codex" ] && deploy_into "$HOME/.codex/skills"  # Codex (if present) echo "Done. The pup skill calls the adom-desktop CLI; if the bridge isn't installed on Adom Desktop it self-installs on first use."
package.json+4−3
@@ -1,8 +1,8 @@ {   "slug": "adom-desktop-puppeteer-bridge",-  "version": "1.8.4",+  "version": "1.8.5",   "type": "app",-  "description": "CONTAINER-SIDE install \u2014 the everyday `pup` user skill only. This pkg drops the `pup` skill (drive the browser bridge: open/close/tabs/screenshot/record, background-by-default) into ~/.claude/skills/ AND ~/.codex/skills/. It does NOT install the developer/publishing skills (pup-bridge-dev, pup-bridge-publish) \u2014 those are source-only; clone the repo (adom-wiki repo clone adom/adom-desktop-puppeteer-bridge) to get them from dev-skills/. It also does NOT contain the bridge runtime: the bridge (Node + Chrome for Testing) lives on the DESKTOP \u2014 bundled inside Adom Desktop and published here on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: `pkg install` = the pup user skill; dev skills = clone the source; the bridge = the Release zip + bundled in Adom Desktop.",+  "description": "CONTAINER-SIDE install \u2014 the pup skill pack in the wiki skillpack layout: the everyday `pup` user skill PLUS two bundled developer sub-skills (pup-bridge-dev, pup-bridge-publish; user-invocable:false, DEVELOPER-only descriptions so they never trigger for everyday users), all dropped into ~/.claude/skills/ AND ~/.codex/skills/ by an explicit install.sh list. It does NOT contain the bridge runtime: the bridge (Node + browser driving) lives on the DESKTOP \u2014 bundled inside Adom Desktop and published on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: pkg install = the skill pack; the bridge = the Release zip + bundled in Adom Desktop.",   "keywords": [     "pup",     "puppeteer",@@ -21,7 +21,8 @@   ],   "files": [     "SKILL.md",-    "skills/**",+    "skills/pup-bridge-dev/SKILL.md",+    "skills/pup-bridge-publish/SKILL.md",     "install.sh",     "uninstall.sh",     "README.md",
page.json+3−33
@@ -4,15 +4,8 @@   "slug": "adom-desktop-puppeteer-bridge",   "title": "Adom Desktop - Puppeteer Bridge",   "brief": "The Adom Desktop Puppeteer (pup) bridge — drive a real headful browser on the user's desktop from the cloud: open/screenshot/eval/record. Cold-start drives the browser ALREADY on the machine (installed Chrome, else Microsoft Edge — every Windows PC ships with Edge) with a fresh isolated profile, so the common case needs NO download; Chrome for Testing is fetched only as a last resort. Two artifacts: the Releases zip is the bridge runtime (also bundled in Adom Desktop); the pkg installs the container-side Claude skills.",-  "version": "1.8.4",-  "tags": [-    "skills",-    "pup",-    "puppeteer",-    "browser",-    "adom-desktop",-    "bridge"-  ],+  "version": "1.1.0",+  "tags": [],   "license": "MIT",   "discovery_triggers": [     "pup",@@ -103,28 +96,5 @@   "created_at": "2026-05-28T05:28:43.991Z",   "updated_at": "2026-06-16T12:43:19.735Z",   "skills": [],-  "author_name": "John Lauer",-  "description": "CONTAINER-SIDE install — the everyday `pup` user skill only. This pkg drops the `pup` skill (drive the browser bridge: open/close/tabs/screenshot/record, background-by-default) into ~/.claude/skills/ AND ~/.codex/skills/. It does NOT install the developer/publishing skills (pup-bridge-dev, pup-bridge-publish) — those are source-only; clone the repo (adom-wiki repo clone adom/adom-desktop-puppeteer-bridge) to get them from dev-skills/. It also does NOT contain the bridge runtime: the bridge (Node + Chrome for Testing) lives on the DESKTOP — bundled inside Adom Desktop and published here on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: `pkg install` = the pup user skill; dev skills = clone the source; the bridge = the Release zip + bundled in Adom Desktop.",-  "keywords": [-    "pup",-    "puppeteer",-    "browser",-    "adom-desktop",-    "bridge",-    "skills"-  ],-  "files": [-    "SKILL.md",-    "skills/**",-    "install.sh",-    "uninstall.sh",-    "README.md",-    "package.json"-  ],-  "dependencies": {},-  "scripts": {-    "install": "./install.sh",-    "uninstall": "./uninstall.sh"-  },-  "org": "adom"+  "author_name": "John Lauer" }\ No newline at end of file
skills/pup-bridge-dev/SKILL.mdadded+207
@@ -0,0 +1,207 @@+---+name: pup-bridge-dev+user-invocable: false+description: "DEVELOPER skill for building, publishing, and maintaining the Adom Desktop Puppeteer (pup) bridge. NOT needed by general users — they want the `pup` skill. Read this when editing the bridge code, cutting a new version, publishing to the wiki, wiring the cold-start Chrome-for-Testing self-heal, or understanding the pkg-vs-release-vs-bundled artifact model. Trigger words: pup bridge dev, publish pup bridge, build puppeteer bridge, bridge_install pup, adom-desktop-puppeteer-bridge, cold-start self-heal, browser_readiness internals, pup release, pup manifest, ship pup bridge."+---++Parent skill: **adom-desktop-puppeteer-bridge**++# pup-bridge-dev — building & shipping the Puppeteer bridge++The canonical source of truth for the pup bridge is **this wiki repo**+(`wiki.adom.inc/adom/adom-desktop-puppeteer-bridge`), extracted from `adom-desktop/plugins/puppeteer`+on 2026-06-29 to be cloud-owned like the kicad/fusion bridges. Read `CLAUDE.md` (ownership boundary)+and `PUBLISHING.md` (step-by-step recipe) in this repo alongside this skill.++## The THREE artifacts — know which is which (this is the #1 confusion)++A user installs TWO different things from this one page. They are not the same:++| Artifact | What it is | Who installs it | How |+|---|---|---|---|+| **pkg tarball** (Packages tab) | the **container-side Claude skills** (`SKILL.md` + `skills/*`) — docs that teach the cloud AI how to drive pup. **No bridge runtime.** | a cloud **container** | `adom-wiki pkg install adom/adom-desktop-puppeteer-bridge`, or auto via AD's `sync_skills` |+| **release zip** (Releases tab) | the **bridge RUNTIME** (`server.js`, `chrome.js`, `bridge.json`, node deps reconstructed on spawn) — the code that actually drives Chrome on the desktop | **Adom Desktop**, streamed into `bridges-cache\puppeteer` | `bridge_install {manifestUrl}` / `refresh_bridges` (via `updateManifestUrl`) |+| **bundled seed** (inside the AD installer) | a copy of the same runtime, shipped in the NSIS for first-run/offline | every AD install | ships with Adom Desktop; superseded by a newer cache copy (cache-over-bundled, numeric version) |++So: **the pkg is skills (container). The bridge is the zip (desktop) + the bundled seed.** Keep the+pkg description saying exactly that, or people think the tarball contains the bridge — it does not.++## How pup reaches a container — auto-discovery → auto-install → verbs++The full chain when a user on a fresh container says *"open my app in pup"*:++1. **Discovery.** The wiki's `discover` index (fed by this page's `discovery_triggers` — which now lead+   with `pup`, `open in pup`, `open my app in pup`, screenshot/record/tabs) matches the phrase. The+   container's regenerated `adom-wiki-discover` skill surfaces this page, so the AI proposes it.+2. **Auto-install the pkg.** The AI runs `adom-wiki skills install adom/adom-desktop-puppeteer-bridge`+   (a.k.a. `pkg install`). That extracts the tarball and runs `install.sh`, which drops `SKILL.md` ++   `skills/*` into **both `~/.claude/skills/` and `~/.codex/skills/`**. The `pup` skill is now loaded.+3. **The skill calls the bridge.** `pup`'s SKILL.md tells the AI to run `adom-desktop browser_*` verbs.+   AD routes `browser_*` to the puppeteer bridge.+4. **The bridge runtime.** It's already on the desktop: AD ships pup as a **bundled seed** (and supersedes+   it from this page's release zip via `updateManifestUrl` / `bridge_install`). On a truly fresh PC the+   cold-start self-heal (chrome.js) installs Chrome for Testing on first `browser_open_window`.++**Two install paths, keep BOTH healthy:**+- **Discovery path (above)** — works once `discovery_triggers` include the user phrases (done) + the+  discover index is regenerated. This is the "user said pup → AI installs the skill" path.+- **AD `sync_skills` path** — AD's daily `maybe_background_skill_sync` reads `bridge_list` → each bridge's+  `skillPkg` → `pkg install`s it into the container. For pup this requires AD to report+  `skillPkg = adom/adom-desktop-puppeteer-bridge` for the `puppeteer` bridge. **Today AD hardcodes it to+  `null`** (`AD_CORE_SKILL_BRIDGES = ["puppeteer"]` in `bridge_registry.rs`, from when pup's skill lived+  in the AD-core pkg). That must be removed now that the skill ships from this page — it's in the AD handoff.++## Repo layout++- `src/` — the runnable bridge (what the release zip contains, at zip-root). `server.js`, `chrome.js`,+  `credential_vault.js`, `bridge.json`, `BRIDGE_VERSION`, node `package.json`/`package-lock.json`, html/icons.+- `SKILL.md` (root) — the **user** skill (`name: pup`). Deployed to `~/.claude/skills/adom-desktop-puppeteer-bridge/`.+- `skills/pup-bridge-dev/SKILL.md` — THIS dev skill. `skills/pup-bridge-publish/SKILL.md` — the publish+  recipe. Both follow the wiki **skillpack** layout (folder + SKILL.md + `Parent skill:` line), are listed+  explicitly in `install.sh`/`package.json files[]`, and ship in the pkg with `user-invocable: false` ++  DEVELOPER-only descriptions so they never trigger for everyday users.+- `adom-bridge-puppeteer-manifest.json` (root) — the streaming manifest AD's `updateManifestUrl` points at.+- `package.json` (root) — the **adompkg descriptor** (slug/version/type/description) for `pkg publish`. NOT the node one.+- `install.sh` / `uninstall.sh` — deploy `SKILL.md` + `skills/*` into the container's `~/.claude/skills/`.+- `page.json`, `README.md`, `*.png` heroes — the wiki page presentation (the heroes are gitignored so the+  container pkg stays lean — they're already uploaded as page assets).++## Cold-start self-heal — the architecture (what 1.1.0 added, the reason this repo exists)++On a fresh PC (HD bundles AD; the installer pre-installs neither Node nor Chrome for Testing), the first+`browser_open_window` used to fail with a generic "Failed to launch Chrome after 3 attempts" + a dev+string. Now:++- **`src/chrome.js`** — `detectChrome()` (validates the ACTUAL exe, not "a dir exists"), `installChrome()`+  (programmatic via the bundled `@puppeteer/browsers`, pinned to the buildId puppeteer expects — never+  'stable', or launch() won't find it), an install-state singleton (dedup + progress), `ensureChromeReady()`+  (the gate — background install, returns immediately), `readiness()` (powers the verb + chip).+- **`browser_open_window` gate** (CfT path only; native-drive skips it): missing CfT → kick off a+  non-blocking install + return `{errorCode:"chrome_for_testing_installing", statusVerb:"browser_readiness",+  _hint:"poll then retry"}`. Install fail → `chrome_install_failed`/`chrome_download_failed` (no silent+  continue-to-doomed-launch).+- **`browser_readiness`** (poll) + **`browser_prewarm`** (install without opening a window).+- **`/status`** emits `led`/`summary`/`tooltip` truthfully (yellow installing, red failed, green ready).+- **Node** is handled by AD core BEFORE the bridge spawns (`node_not_found` → `desktop_install_node`) —+  not this bridge's job (the bridge is a node process; it can't run without node).+- **The one thing this bridge can't do:** pre-warm before first use (it isn't running until the first+  call). AD/HD must call `browser_prewarm` on embedded first-run — tracked in `HANDOFF-TO-ADOM-DESKTOP.md`.++## Field-tested cold-start learnings (v1.3–1.5, from 3 real fresh Windows boxes)++Tested on AdomLapper (all browsers), AdomTestWin11 (fresh vbox, Edge-only), winvm (fresh Azure, Edge-only).+What we learned and baked into the code:++- **Prefer the installed browser, don't download CfT.** Every Windows PC ships with Edge; most have+  Chrome. `resolveBrowser()`/`launchCandidates()` return [pinned default → Chrome → Edge → cached CfT];+  the launch loop **spawn-verifies** each and falls through on failure. A fresh isolated `--user-data-dir`+  keeps the system browser CLEAN (never touches the user's real tabs). CfT is now a genuine last resort.+- **Edge "exits immediately" was a false negative.** Edge (and some Chrome channels) spawn a LAUNCHER that+  relaunches the real browser onto a DIFFERENT PID, then the launcher exits. The old poll treated+  launcher-death as failure → killed good Edge launches. Fix: probe CDP FIRST each tick, and on+  launcher-death do a final CDP grace probe before giving up (`tryDetachedLaunch`).+- **A failed candidate poisons the shared profile dir.** An aborted Edge leaves `SingletonLock` /+  `DevToolsActivePort` in the userDataDir → the next candidate (Chrome/CfT) then also "exits immediately."+  Fix: `killChromeHoldingProfile()` (lock cleanup) BETWEEN candidates, not just between attempts.+- **Persist the chosen browser** (`pup-browser-default.json` in the CfT cache dir). `markVerified()` caches+  the browser that actually launched (auto default); `browser_use` pins one (forced default). Avoids+  re-probing every open. Forced beats auto; launch still falls back so a broken pin never hard-fails.+- **Node install has a PATH-staleness trap (AD-core, but guide the AI).** `desktop_install_node` installs+  Node, but the already-running AD process keeps its OLD PATH — so the very next `browser_*` STILL returns+  `node_not_found` / `could not run npm`. Reinstalling doesn't help. AD must be relaunched+  (`desktop_relaunch_self`) to pick up the new PATH. This is the exact "installed node, still broken"+  dead-end. Documented in SKILL.md + HANDOFF (AD should probe common node install dirs / refresh PATH, not+  rely on the stale env). winget also returns non-zero when Node is ALREADY present ("No available upgrade")+  — that's success, not failure.+- **Self-warm on spawn** (`chrome.warmup()` from `server.listen`): detect browsers + disk the moment AD+  installs/updates the bridge, so the first open is instant. Only background-fetches CfT if the box has NO+  Chrome/Edge AND no cached CfT (never on an Edge-only box — Edge is right there).+- **Disk + specific error codes.** `diskFreeMb()` gates the CfT install and the cold-start; failures map to+  `chrome_install_no_disk` / `chrome_download_network` / `chrome_install_permission` (not a generic string)+  so the AI's hint is actionable. `readiness()` surfaces `diskFreeMb`/`lowDisk`.+- **Install REAL Chrome on request.** `browser_use {browser:"chrome", install:true}` downloads Google's+  official offline installer and runs it silently; run as the (non-admin) user it installs USER-SCOPED into+  `%LOCALAPPDATA%\Google\Chrome` (no UAC), which `listSystemBrowsers()` already finds. Elevated context may+  install machine-wide + raise UAC — surfaced via `chrome_stable_not_detected` hint.+- **Verbose hints everywhere.** Every open returns `browser` (what we drove) + `availableBrowsers` ++  `defaultBrowser` + an `_hint` explaining how to switch/install. Goal: the AI resolves any browser+  situation in as few turns as possible without a human.+- **Desktop permission prompts gate open-ended verbs (learned live).** Core AD verbs + this bridge's+  `browser_*` verbs run with NO prompt. But `shell_execute` / `run_script` / arbitrary scripts pop an AD+  "Shell command needs approval" dialog (Approve 1h/24h/Bypass) — and the call **hangs until approved**+  (we saw `shell_execute` time out on the vbox with the popup waiting; winvm had it auto-approved and+  showed nothing). Two consequences baked into SKILL.md: (1) prefer core/`browser_*` verbs over shell;+  (2) before any `shell_execute`, `notify_user` the person to approve the popup. This is a separate gate+  from the Windows UAC (admin elevation) — don't conflate them.+- **Windows Firewall prompt = bind loopback (fixed v1.6.0).** A hostless `server.listen(PORT)` binds ALL+  interfaces (0.0.0.0), so Windows pops "Allow Node.js through the firewall (public/private)?" on first+  listen — a THIRD scary prompt after UAC + shell-approval, and worse on a fresh portable-node path the+  firewall has never seen. Fix: `server.listen(PORT, '127.0.0.1', ...)`. AD only ever talks to the bridge+  over loopback, and loopback sockets are firewall-exempt → the prompt never appears.+- **Zero-prompt Node bootstrap IS possible with typed verbs (PROVEN on winvm).** pup is a Node process, so+  it can't self-install Node — but the AI can, using AD's UNGATED typed verbs (only `shell_execute` /+  `run_script` / `wsl_exec*` prompt; everything typed does not). Proven flow, zero popups: `send_files` the+  portable Node zip → `launch_process` `C:\Windows\System32\tar.exe -xf` to extract → `registry_get` ++  `registry_set` HKCU\Environment\Path (append, REG_SZ) → user reopens AD (app restart, no prompt) so it+  re-reads PATH. NO UAC, NO shell-approval, NO firewall. The recipe lives in SKILL.md. The only human step+  is the AD reopen, which the shared-runtime proposal (absolute-path spawn) removes. `desktop_install_node`+  (UAC MSI) + `shell_execute` (approval popup) are fallbacks only.++## Cold-start bugs found on fresh boxes (AD-owned — report, don't try to fix in pup)++- **AD npm-install can't find `node` for lifecycle scripts (fixed ~AD 1.9.69).** On AD 1.9.63–1.9.65 the+  managed runtime ran `npm install` by absolute path but didn't put node on the child-process PATH, so any+  dep with a native/postinstall script that shells to `node` (keytar, sharp, **puppeteer**'s `install.mjs`)+  died: `'node' is not recognized`. Pup-side mitigation shipped v1.7.1: `sharp`+`keytar` are+  `optionalDependencies` + `sharp` loads gracefully (screenshots fall back to capture-size via a PNG-header+  dimension read). AD fixed the PATH on ~1.9.69 (npm install now succeeds on a fresh Edge-only box).+- **AD node-gate false-negative (open as of AD 1.9.69).** On a fresh box, `runtimes` shows+  `node.state:ready` (adom-managed), the bridge is `status:running` with a live pid, and `browser_readiness`+  (the declared **statusVerb**) works — but EVERY other `browser_*` verb returns AD-core `node_not_found`.+  The node pre-gate only exempts the statusVerb and does a stale/PATH-based node check that misses the+  portable managed runtime. pup CANNOT fix this (node_not_found fires in AD before the bridge). Reported on+  discussion #59; SKILL tells the AI to recognize it (readiness OK + runtimes ready + other verbs+  node_not_found = AD bug, don't loop desktop_install_node).+- **Takeaway:** when a fresh-box open fails, first read `runtimes` + `bridge_list` (pid/status) + try+  `browser_readiness`. If the runtime is ready and the bridge is running but verbs fail, it's an AD gate/+  provisioning bug — report it with that evidence; it is not a pup code bug.++## Publishing (the wiki has TWO storage layers — don't mix them)++Full recipe in `PUBLISHING.md`. The load-bearing rules:++1. **The bridge `.zip` → a RELEASE asset** (`adom-wiki release create` + `release upload`). NEVER+   `repo push` a `.zip` — it's gitignored and silently skipped; the manifest URL would 404.+2. **The manifest JSON + source → the git repo** (`adom-wiki repo push`). The manifest's `url` is the+   RELEASE download URL; its `sha256`/`size` MUST match the served asset (releases preserve your sha;+   re-download to confirm or `bridge_install` rejects it).+3. **The skills → a PKG** (`adom-wiki pkg publish`). The pkg description (root `package.json`) must say+   "container-side skills." `pkg pack` honors `.gitignore` for exclusion (no separate npmignore) — that's+   why the heroes are gitignored, to keep the tarball lean. Set per-version notes with `adom-wiki pkg notes`.+4. **Version display gotcha:** a RELEASE shows up in `adom-wiki pkg info` as a row with `type:null` (a+   wiki UI conflation). The real pkg rows have `type:"app"`. Use `release list` for true releases.+5. **`bridge.json` + `BRIDGE_VERSION` lockstep**, and keep the version **>** the AD bundled seed so+   cache-over-bundled wins.++## Testing a new version on a real desktop++```bash+adom-desktop targets    # multiple desktops → --target <name> is required+adom-desktop --target <name> bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json","force":true}'+adom-desktop --target <name> browser_readiness '{}'   # proves the new code is live+```++- **No Node on the box** → `browser_*` returns `node_not_found` before the bridge spawns (AD core; correct).+  That's a genuine cold-start demo of the FIRST link; it can't exercise `chrome.js` (no node to run it).+- **A box with Node but no Chrome for Testing** is the only place to see the full Chrome self-install live+  (`browser_open_window` → `chrome_for_testing_installing` → poll `browser_readiness` → opens). Most dev+  boxes already have CfT, so `readiness()` just returns `ready:true`.+- The streamed zip is **source-only**; AD runs `npm install` in the cache on first spawn (sharp/keytar+  ship prebuilds). The bundled seed already has node_modules. Don't put node_modules in the zip.++## Don't++- Don't edit `adom-desktop/plugins/puppeteer` as if it's live, and don't `release-bridge.sh puppeteer`+  from the adom-desktop repo — that clobbers this cloud-owned page.+- Don't ship the bridge runtime in the pkg tarball or the skills in the release zip. Two artifacts, two homes.+- Don't install 'stable' Chrome — pin to puppeteer's expected buildId (`chrome.js` does this).
skills/pup-bridge-publish/SKILL.mdadded+213
@@ -0,0 +1,213 @@+---+name: pup-bridge-publish+user-invocable: false+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 + `skills/**` glob trap, keeping the tarball lean (NO zip / NO src / NO heroes), bundling dev/publish skills the wiki skillpack way (skills/<name>/SKILL.md + explicit install.sh list), 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)++```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-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 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/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.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/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++```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 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` directory entries need a GLOB.** Listing `"skills"` (bare dir) silently includes ZERO skill+  files. Use **`"skills/**"`** (and/or the explicit `"skills/<name>/SKILL.md"`). Always `tar tzf` to+  confirm your skills actually made it in — a missing skill = an empty Skills tab.+- **`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)++```bash+# 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.+```++## Bundle dev/publish skills the wiki SKILLPACK way (superseded the old source-only hack)++Everyday users should get ONLY the `pup` user skill by *triggering*, but the wiki's blessed structure+(see the `wiki-skillpack` skill / the `adom-wiki-skillpack` reference repo) is: **every skill is a real+`skills/<name>/SKILL.md`**, listed EXPLICITLY (no globs) in `install.sh`, `uninstall.sh`, and+`package.json files[]`, with a `Parent skill: **adom-desktop-puppeteer-bridge**` first body line and a+skill-map table in the root SKILL.md + README.++History: we originally kept dev skills as loose `dev-skills/*.md` files to dodge the Skills-tab indexer.+That was based on a wrong assumption — the reference skillpack carries 14 SKILL.md files with+`skill_count: 0`, so the indexer does NOT blindly count SKILL.md files. The loose-file hack broke the+convention (no folder, no Parent line, no explicit install list) and made the dev skills invisible to+tooling. Superseded 2026-07-03.++Keeping everyday users out of dev skills is done the skillpack way instead:+- `user-invocable: false` in the sub-skill frontmatter;+- descriptions that lead with "DEVELOPER skill … NOT needed by general users" so they never trigger;+- the `pup` skill map tells the AI which skill is for whom.++## Keep the pkg tarball LEAN — NO zip, NO src/, NO heroes++The pkg tarball must be the `pup` user skill ONLY — never the bridge runtime zip, `src/`, or hero PNGs.+Guards: the `package.json` `files` allowlist (`SKILL.md`, `skills/**`, `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 there is **no+`*.zip`, no `src/`, no `*.png`**. A lean tarball is ~10 KB; if it's megabytes, something heavy leaked in+(it was 16 MB before the guards). `rm -f *.tgz` after inspecting.++## 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/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.
uninstall.sh+10−4
@@ -1,6 +1,12 @@-#!/bin/bash+#!/usr/bin/env bash+# Remove the pup skill pack (main skill + every bundled sub-skill) — keep in sync with install.sh. set -e-for BASE in "$HOME/.claude/skills" "$HOME/.codex/skills"; do+remove_from() {+  local BASE="$1"   rm -rf "$BASE/adom-desktop-puppeteer-bridge" "$BASE/pup-bridge-dev" "$BASE/pup-bridge-publish"-done-echo "Uninstalled adom-desktop-puppeteer-bridge skills"+  echo "  <- $BASE"+}+echo "Removing adom-desktop-puppeteer-bridge skills:"+remove_from "$HOME/.claude/skills"+[ -d "$HOME/.codex" ] && remove_from "$HOME/.codex/skills"+echo "Done."