app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled with its AI thread's name in your taskbar or dock.
← Commit history
Publish 1.8.22
5 files changed
+576−17
install.sh+12−6@@ -1,9 +1,12 @@ #!/usr/bin/env bash-# Install the user-facing `pup` skill onto the container. That is the ONLY skill a user-# needs. The maintainer skills (pup-bridge-dev, pup-bridge-publish) deliberately do NOT-# ship in the pkg tarball — they live in the repo under dev-skills/ + publish-skills/-# (auto-excluded by the wiki packer), so a normal user's container stays lean.-# The bridge RUNTIME is NOT here either — it ships in Adom Desktop + as the Releases zip.+# Install the pup skill pack: the `pup` USER skill plus the dev/publish maintainer skills.+# Per the Bridge SDK "skills by openness" rule: pup is OPEN SOURCE, so its dev + publish+# skills ship in the pkg scoped `user-invocable:false` (they never trigger for everyday+# users, but any AI/maintainer cloning the container has the recipe). A CLOSED-source+# bridge would keep dev/publish in its private repo and ship only the user skill.+# Each skill is listed explicitly (the publish linter rejects globs); keep in sync with+# package.json files[] + 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)" @@ -12,7 +15,10 @@ deploy_into() { install_skill() { mkdir -p "$BASE/$1"; cp "$PKG_DIR/$2" "$BASE/$1/SKILL.md"; } # the user skill (REQUIRED at repo root by the registry) install_skill adom-desktop-puppeteer-bridge SKILL.md- echo " -> $BASE (pup)"+ # dev/publish skills (user-invocable:false — shipped because pup is open source)+ 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 maintainer sub-skills)" } echo "Installing adom-desktop-puppeteer-bridge skills:"
package.json+5−5@@ -1,6 +1,6 @@ { "slug": "adom-desktop-puppeteer-bridge",- "version": "1.8.21",+ "version": "1.8.22", "type": "app", "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": [@@ -21,16 +21,16 @@ ], "files": [ "SKILL.md",+ "skills/pup-bridge-dev/SKILL.md",+ "skills/pup-bridge-publish/SKILL.md", "install.sh", "uninstall.sh", "README.md", "package.json" ],- "dependencies": {- "adom/adom-desktop": ">=1.9.63"- }, "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh"- }+ },+ "dependencies": {} }
page.json+37−6@@ -3,9 +3,16 @@ "type": "app", "slug": "adom-desktop-puppeteer-bridge", "title": "Adom Desktop - Puppeteer Bridge",- "brief": "The Adom Desktop Puppeteer (pup) bridge \u2014 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 \u2014 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.1.0",- "tags": [],+ "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.22",+ "tags": [+ "skills",+ "pup",+ "puppeteer",+ "browser",+ "adom-desktop",+ "bridge"+ ], "license": "MIT", "discovery_triggers": [ "pup",@@ -46,7 +53,7 @@ "fork the pup bridge", "puppeteer bridge source" ],- "discovery_pitch": "Use pup to drive Chrome from the cloud: open/close browser windows + tabs, navigate, screenshot, eval JS, and record windows. It drives the browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC has Edge) \u2014 with a fresh isolated profile, so usually NO download and it doesn't disrupt the user; Chrome for Testing is fetched only as a last resort. For 'is it ready' use browser_readiness. Or pass nativeBrowser+nativeProfile to drive the user's REAL signed-in Chrome/Edge/Brave. Call the adom-desktop browser_* verbs.",+ "discovery_pitch": "Use pup to drive Chrome from the cloud: open/close browser windows + tabs, navigate, screenshot, eval JS, and record windows. It drives the browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC has Edge) — with a fresh isolated profile, so usually NO download and it doesn't disrupt the user; Chrome for Testing is fetched only as a last resort. For 'is it ready' use browser_readiness. Or pass nativeBrowser+nativeProfile to drive the user's REAL signed-in Chrome/Edge/Brave. Call the adom-desktop browser_* verbs.", "sample_prompts": [ { "label": "Open in pup",@@ -79,7 +86,7 @@ "install_hint": "", "version_cmd": "" },- "readme": "# Adom Desktop \u2014 Puppeteer (pup) Bridge\n\nDrive a real headful browser from the cloud: open/close browser windows + tabs, navigate, screenshot,\neval JS, and record windows. Cold-start drives the browser **already on the machine** \u2014 installed\n**Chrome**, else **Microsoft Edge** (every Windows PC ships with Edge) \u2014 launched with a **fresh isolated\nprofile**, so the common case has **no download** and never touches the user's real tabs/logins. **Chrome\nfor Testing** is fetched only as a **last resort** if the machine has no Chromium browser at all. You can\nalso drive the user's **real Chrome/Edge/Brave** (signed-in profile) via `nativeBrowser`+`nativeProfile`.\n\nCanonical page: **https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge**\n\n## Three artifacts \u2014 know which is which\n\n| Artifact | Where | What it is |\n|---|---|---|\n| **pkg** (Packages tab) | `adom-wiki pkg install adom/adom-desktop-puppeteer-bridge` | the **container-side `pup` skill** (drops into `~/.claude/skills` + `~/.codex/skills`). No runtime. |\n| **release zip** (Releases tab) | `https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/<ver>/adom-bridge-puppeteer-v<ver>.zip` | the **bridge runtime** (Node + Chrome-for-Testing self-heal) that Adom Desktop streams into its bridges-cache |\n| **bundled seed** | inside the Adom Desktop installer | the same runtime, shipped for first-run/offline; superseded by a newer cache copy via `updateManifestUrl` |\n\n**TL;DR:** `pkg install` = the pup skill for the container; the bridge itself = the Release zip + bundled in Adom Desktop.\n\n## Use it\n\n```bash\n# everyday: open + screenshot (works while lowered so it doesn't disrupt the user)\nadom-desktop browser_open_window '{\"sessionId\":\"app\",\"profile\":\"app\",\"url\":\"http://localhost:3000\"}'\nadom-desktop browser_lower_os_window '{\"sessionId\":\"app\"}'\nadom-desktop browser_screenshot '{\"sessionId\":\"app\"}'\n\n# cold-start (fresh PC, no Chrome for Testing): the bridge installs it; poll readiness then open\nadom-desktop browser_prewarm '{}'\nadom-desktop browser_readiness '{}' # \u2192 {ready:true} when Chrome for Testing is installed\n```\n\nFull everyday usage (tabs, recording, native-Chrome mode, cold-start playbook) is in the `pup` skill\n(`SKILL.md`).\n\n## Source & development\n\nThis bridge is cloud-owned here (extracted from `adom-desktop/plugins/puppeteer`, mirroring the\nkicad/fusion bridges). Adom Desktop ships a bundled seed and auto-pulls newer versions from this page.\n\n- Clone the source: `adom-wiki repo clone adom/adom-desktop-puppeteer-bridge`\n- Runtime lives in `src/` (`server.js`, `chrome.js`, `bridge.json`, \u2026); the release zip is `src/` at zip-root.\n- **Developer/publishing references** live in `dev-skills/` (`pup-bridge-dev.md`, `pup-bridge-publish.md`) \u2014\n source-only, not in the pkg or on the Skills tab. See `dev-skills/README.md`.\n- Ownership boundary: `CLAUDE.md`. Publish recipe: `PUBLISHING.md`. History: `CHANGELOG.md`.\n\n## Related\n\n- [Bridge SDK guide](https://wiki.adom.inc/adom/adom-desktop-bridges) \u2014 `bridge.json` schema, packaging, lifecycle\n- [KiCad bridge](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge) \u00b7 [Fusion 360 bridge](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)\n- [Adom Desktop (parent app)](https://wiki.adom.inc/adom/adom-desktop)\n\nMIT licensed.\n",+ "readme": "# Adom Desktop — Puppeteer (pup) Bridge\n\nDrive a real headful browser from the cloud: open/close browser windows + tabs, navigate, screenshot,\neval JS, and record windows. Cold-start drives the browser **already on the machine** — installed\n**Chrome**, else **Microsoft Edge** (every Windows PC ships with Edge) — launched with a **fresh isolated\nprofile**, so the common case has **no download** and never touches the user's real tabs/logins. **Chrome\nfor Testing** is fetched only as a **last resort** if the machine has no Chromium browser at all. You can\nalso drive the user's **real Chrome/Edge/Brave** (signed-in profile) via `nativeBrowser`+`nativeProfile`.\n\nCanonical page: **https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge**\n\n## Three artifacts — know which is which\n\n| Artifact | Where | What it is |\n|---|---|---|\n| **pkg** (Packages tab) | `adom-wiki pkg install adom/adom-desktop-puppeteer-bridge` | the **container-side `pup` skill** (drops into `~/.claude/skills` + `~/.codex/skills`). No runtime. |\n| **release zip** (Releases tab) | `https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/<ver>/adom-bridge-puppeteer-v<ver>.zip` | the **bridge runtime** (Node + Chrome-for-Testing self-heal) that Adom Desktop streams into its bridges-cache |\n| **bundled seed** | inside the Adom Desktop installer | the same runtime, shipped for first-run/offline; superseded by a newer cache copy via `updateManifestUrl` |\n\n**TL;DR:** `pkg install` = the pup skill for the container; the bridge itself = the Release zip + bundled in Adom Desktop.\n\n## Use it\n\n```bash\n# everyday: open + screenshot (works while lowered so it doesn't disrupt the user)\nadom-desktop browser_open_window '{\"sessionId\":\"app\",\"profile\":\"app\",\"url\":\"http://localhost:3000\"}'\nadom-desktop browser_lower_os_window '{\"sessionId\":\"app\"}'\nadom-desktop browser_screenshot '{\"sessionId\":\"app\"}'\n\n# cold-start (fresh PC, no Chrome for Testing): the bridge installs it; poll readiness then open\nadom-desktop browser_prewarm '{}'\nadom-desktop browser_readiness '{}' # → {ready:true} when Chrome for Testing is installed\n```\n\nFull everyday usage (tabs, recording, native-Chrome mode, cold-start playbook) is in the `pup` skill\n(`SKILL.md`).\n\n## Source & development\n\nThis bridge is cloud-owned here (extracted from `adom-desktop/plugins/puppeteer`, mirroring the\nkicad/fusion bridges). Adom Desktop ships a bundled seed and auto-pulls newer versions from this page.\n\n- Clone the source: `adom-wiki repo clone adom/adom-desktop-puppeteer-bridge`\n- Runtime lives in `src/` (`server.js`, `chrome.js`, `bridge.json`, …); the release zip is `src/` at zip-root.\n- **Developer/publishing references** live in `dev-skills/` (`pup-bridge-dev.md`, `pup-bridge-publish.md`) —\n source-only, not in the pkg or on the Skills tab. See `dev-skills/README.md`.\n- Ownership boundary: `CLAUDE.md`. Publish recipe: `PUBLISHING.md`. History: `CHANGELOG.md`.\n\n## Related\n\n- [Bridge SDK guide](https://wiki.adom.inc/adom/adom-desktop-bridges) — `bridge.json` schema, packaging, lifecycle\n- [KiCad bridge](https://wiki.adom.inc/adom/adom-desktop-kicad-bridge) · [Fusion 360 bridge](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge)\n- [Adom Desktop (parent app)](https://wiki.adom.inc/adom/adom-desktop)\n\nMIT licensed.\n", "author": { "id": "695820315b5f1e4db2fcf602", "name": "Kyle Bergstedt",@@ -96,5 +103,29 @@ "created_at": "2026-05-28T05:28:43.991Z", "updated_at": "2026-06-16T12:43:19.735Z", "skills": [],- "author_name": "John Lauer"+ "author_name": "John Lauer",+ "description": "CONTAINER-SIDE install — 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 — 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",+ "browser",+ "adom-desktop",+ "bridge",+ "skills"+ ],+ "files": [+ "SKILL.md",+ "skills/pup-bridge-dev/SKILL.md",+ "skills/pup-bridge-publish/SKILL.md",+ "install.sh",+ "uninstall.sh",+ "README.md",+ "package.json"+ ],+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "dependencies": {},+ "org": "adom" }
skills/pup-bridge-dev/SKILL.mdadded+263@@ -0,0 +1,263 @@+---+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.++## Calling AD back (outbound) — notify the user from inside the bridge (AD ≥ 1.9.84)++Everything above is AD → pup. The reverse — **pup → AD** — matters when the bridge must+reach the *user* on its own, mid-verb, with no AI turn. The canonical case: pup installs+real Chrome on a locked-down box that needs a **UAC**, and has to say "click YES" (and+re-nudge if it expires). See `chrome.js` `_elevatedInstallWithNotify` for the live impl.++**The channel.** A bridge's only callback is AD's loopback direct-API. Resolve it:+`ADOM_DIRECT_API_URL` env → else read `%USERPROFILE%\.adom\direct-api-port` (`host:port`) →+else `127.0.0.1:47200`. Then `POST /command`.++**AD ≥ 1.9.84 exposes the FULL verb surface to bridges** (before that, only `desktop_*` ++other bridges' verbs — `notify_user` returned *"Unknown desktop command"*, which is why the+degraded path exists). Contract:+- **`app` is inferred** — send `{ "command": "<verb>", "args": {…} }`, no `app`.+- **Token (optional, attribution only):** send the spawn-time `ADOM_BRIDGE_TOKEN` env as+ the `X-Adom-Bridge-Token` header → AD badges the call `bridge` in its Activity Log. A+ **stale token → 403**; on 403 retry once WITHOUT the header. Omitting it entirely is fine+ (a bridge is trusted-by-install; calls run ungated).+- **Capability-probe, don't hardcode:** `GET /commands` → `notify_user` appears in+ `.topLevel` on ≥1.9.84. Gate the notify flow on that; fall back to raise-UAC-once ++ a relay hint on older AD. pup caches this as `readiness.chromeStableNotifyChannel`.+- **Cross-AD `target`:** add `"target": "<clientName>" | "all"` to ANY `/command` to route+ it to a PEER AD via the relay — e.g. pup runs on a VM, the user is on their laptop, so+ ping the laptop while the UAC stays on the VM. `"attended"` (input-active box) is **not+ resolvable yet** → enumerate peers and fan out.+- **`targets` gotcha:** returns `{ clients: [...] }` and **includes self** (NOT+ `{ targets: [...] }`). Read `clients`, drop your own `os.hostname()`, dedupe.++**The two UX rules the impl bakes in (both were real bugs first):**+1. **Notify-FIRST, then raise the UAC.** Raising the UAC puts up the secure desktop, which+ *hides* any toast. So fire the sticky `scenario:"reminder"` toast with an `Approve now`+ button, poll `notify_response {id}` for the tap, and only THEN elevate.+2. **Never spam prompts you can't explain.** If the notify channel is unreachable, raise the+ UAC **once** and surface `awaiting_uac` + a hint for the AI to relay — do NOT loop+ re-raising a UAC the user is never told about.++## 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 are MAINTAINER tier (`user-invocable: false` + DEVELOPER-only descriptions so they never+ trigger for everyday users), each with a `Parent skill:` first body line. Per the Bridge SDK+ skills-by-openness rule they DO ship in the pkg (listed in `package.json files[]` + install.sh),+ because pup is OPEN SOURCE — a closed-source bridge keeps them in a private repo instead. (The+ `dev-skills/*.md` hiding convention is dead — don't use it.)+- `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 ONLY the `pup` user skill (`SKILL.md`) into `~/.claude/skills/` + `~/.codex/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.+ **AD ≥ 1.9.86 addendum:** every gate-hit now auto-surfaces the in-window approval dialog + toast ++ taskbar flash on the target box (1.9.84 briefly regressed to a silent `human_only` refusal with NO+ prompt — bricked shell on headless/RDP boxes). On `needs_approval`/`human_only`: run+ `adom-desktop --target <box> request_shell_approval` (re-surfaces the dialog + foregrounds AD;+ surfaces only, never grants), have the human click Approve/Bypass, then RETRY the original verb.+ Never call `shell_auto_approve` — it's human-only and refuses by design.+- **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.++- **Wedged-bridge diagnosis recipe (learned on AD 1.9.77).** Symptoms: every `browser_*` verb times out+ but `ping`/core verbs work. Check `bridge_list` → `status:"running"` with `livePids:[]` = a bridge that+ crashed at spawn while AD still forwards to its dead port. Read `~/.adom/bridge-logs/puppeteer.log` —+ the 1.9.77 regression showed Node dying at boot: `EISDIR: lstat 'C:'` (AD mangled the entrypoint path at+ the drive-letter colon). Cross-check another box on a different AD version to isolate AD-vs-pup. pup+ can't fix AD's spawner — report with the log + version comparison (that report got it fixed same-day).+- **Docs can have THREE copies (SDK lesson, 2026-07-04).** The bridge-SDK page carried the same content in+ README.md, SKILL.md, AND bridges-SDK.md — an edit landed on two and left the third stale, so audits+ reading the third saw pre-update text. When auditing against a doc, verify the EXACT file you read; when+ publishing multi-copy docs, push and verify every copy.++## 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+259@@ -0,0 +1,259 @@+---+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, keeping the tarball lean (NO zip / NO src / NO heroes / NO node deps), skills-by-openness (pup is OPEN SOURCE so its dev/publish skills ship in skills/<name>/ scoped user-invocable:false — the dead-and-buried dev-skills/*.md hiding convention is NOT used), the skillpack package.json carrying NO dependencies block, 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` entries: list each skill EXPLICITLY** (`SKILL.md`, `skills/pup-bridge-dev/SKILL.md`,+ `skills/pup-bridge-publish/SKILL.md`, …). A bare `"skills"` dir entry silently includes ZERO files; a+ `"skills/**"` glob works but the publish linter prefers explicit paths. Since pup is open source, the+ dev/publish skills ARE in `files[]` (they ship). 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)++```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.+```++## Skills by openness — what ships in the pkg (Bridge SDK rule)++The Bridge SDK ("Where every file lives" + the Self-audit checklist) is authoritative here:++- **USER skills ALWAYS ship** in the pkg — the container can't drive the verbs without them. For pup+ that's the root `SKILL.md` (`name: pup`).+- **DEV + PUBLISH skills ship in the pkg IFF the bridge is OPEN SOURCE**, scoped `user-invocable:false`+ + a DEVELOPER-only description so they never trigger for everyday users. pup is open source (MIT,+ public wiki repo), so `pup-bridge-dev` and `pup-bridge-publish` **DO ship** — as real+ `skills/<name>/SKILL.md`, listed EXPLICITLY (no globs) in `package.json files[]` + install.sh ++ uninstall.sh, each with a `Parent skill: **adom-desktop-puppeteer-bridge**` first body line.+- **If pup were CLOSED source**, the dev/publish skills would stay in a PRIVATE repo and ONLY the user+ skill would ship — the adom-desktop-core model (its RELEASE / WIKI_PUBLISH / SIGNING skills are never+ in its public pkg).++**⚠ `dev-skills/*.md` / `publish-skills/*.md` is a DEAD convention — do NOT use it.** The SDK checklist+says so explicitly. Hiding maintainer skills in those auto-excluded dirs is wrong for an OPEN-source+bridge (they should ship, scoped) — it's only ever appropriate to keep them out of the pkg by keeping+them in a *private* repo, which pup doesn't have. So: dev/publish skills go in `skills/<name>/`, they+ship, and the packer counting them as "user skills" in `shipped N user skill(s)` is EXPECTED.++**Verify after `adom-wiki pkg pack`:** `shipped 3 user skill(s)` (pup + dev + publish); the skillpack+`package.json` has **NO `dependencies`** block; and `tar tzf *.tgz` shows the 3 SKILL.md + install/+uninstall + README + package.json, with **no `*.zip`, no `src/`, no `*.png`**.++History (don't re-thrash this): 1.8.20 wrongly *hid* dev/publish in `dev-skills/`/`publish-skills/`+(dead convention) to trim the tarball; corrected 1.8.22 back to the SDK-compliant `skills/<name>/` ++`files[]` because pup is open source. The tarball is a bit larger (~28 KB) BY DESIGN — that's the cost+of an open-source bridge shipping its scoped maintainer skills, and it's correct.++## Keep the pkg tarball LEAN — NO zip, NO src/, NO heroes++The pkg tarball must be **docs only** — the `pup` user skill + the two open-source maintainer skills ++scripts; never the bridge runtime zip, `src/`, hero PNGs, `node_modules`, or a `dependencies` block.+Guards: the `package.json` `files` allowlist (`SKILL.md`, the two `skills/pup-bridge-*/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 `node_modules`; `shipped 3 user skill(s)`; and the+skillpack `package.json` has NO `dependencies`**. ~28 KB is right for pup (3 SKILL.md docs); 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/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).