← Commit history

SDK correction (maintainer-provided): pkg ships ONLY the pup user skill; dev/publish skills are source-only in dev-skills/ + publish-skills/ (not in files[], no user-invocable scoping); skillpack package.json declares dependencies:{adom/adom-desktop:^1.9.63} to pull the AD CLI + core skills; docs corrected

John Lauer ·6e95e06ed2 · ·parent 3a706d7
3 files changed +525−5
CLAUDE.md+7−5
@@ -16,7 +16,7 @@ via the `updateManifestUrl` in `bridge.json`. (Extracted from `adom-desktop/plug   dir on spawn (this bridge ships source-only; node_modules is reconstructed there + present in the seed). - **YOU (this bridge) own:** every `browser_*` / `desktop_record_*` verb + its response `_hint`s, the   **Chrome-for-Testing self-heal** (`src/chrome.js` — detect / auto-install / readiness), the status-chip-  fields on `/status`, the `browser_describe` catalog, the `pup` `SKILL.md` (+ the shipped maintainer skills `skills/pup-bridge-{dev,publish}`), and the edit →+  fields on `/status`, the `browser_describe` catalog, the `pup` `SKILL.md` (+ the source-only maintainer skills in `dev-skills/`/`publish-skills/`), and the edit →   publish → reinstall loop. - So: a `browser_*` verb misbehaves, a hint is missing, Chrome-for-Testing won't install, the chip lies →   it's **your code**, fix it here and republish. It is NOT an adom-desktop request. If a GENERIC AD@@ -37,10 +37,12 @@ via the `updateManifestUrl` in `bridge.json`. (Extracted from `adom-desktop/plug - `src/recorder.html` / `welcome.html` / icons — recorder UI + assets. - `adom-bridge-puppeteer-manifest.json` (repo root) — the streaming manifest AD's `updateManifestUrl`   points at; its `url` is the RELEASE download URL of the zip, with matching `sha256`/`size`.-- `SKILL.md` (root) — the user skill (`pup`), installed into `~/.claude/skills/` (via `install.sh` /-  `adom-wiki pkg`). The cold-start playbook lives here. The maintainer skills (`skills/pup-bridge-dev`,-  `skills/pup-bridge-publish`) DO ship in the pkg scoped `user-invocable:false` — per the Bridge SDK,-  an OPEN-SOURCE bridge ships its dev/publish skills; a closed one keeps them private. (`dev-skills/*.md` is dead.)+- `SKILL.md` (root) — the ONLY skill the pkg ships (`pup`), installed into `~/.claude/skills/` (via+  `install.sh` / `adom-wiki pkg`). The cold-start playbook lives here. The maintainer skills+  (`dev-skills/pup-bridge-dev`, `publish-skills/pup-bridge-publish`) are SOURCE-ONLY — per the Bridge SDK+  a pkg ships only user skills, so they stay in the repo, out of `files[]` (open-vs-closed doesn't matter).+- `package.json` (root, skillpack) — declares `dependencies:{ "adom/adom-desktop": "^1.9.x" }` so+  `pkg install` pulls the AD CLI + core skills. SERVER npm deps live ONLY in the Release zip's `src/package.json`.  ## The publish/reinstall loop 
dev-skills/pup-bridge-dev/SKILL.mdadded+261
@@ -0,0 +1,261 @@+---+name: pup-bridge-dev+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/`.+- `dev-skills/pup-bridge-dev/SKILL.md` — THIS dev skill. `publish-skills/pup-bridge-publish/SKILL.md` —+  the publish recipe. Per the Bridge SDK, a pkg ships ONLY user skills, so these are **source-only**: in+  the repo (read via `repo clone` / Files tab), NOT in `package.json files[]`, NOT installed by+  `install.sh`, and they need no `user-invocable` scoping (they never reach a container). Open-vs-closed+  source doesn't change this.+- `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).
publish-skills/pup-bridge-publish/SKILL.mdadded+257
@@ -0,0 +1,257 @@+---+name: pup-bridge-publish+description: "DEVELOPER skill — the exact, battle-tested recipe for publishing a new version of the Adom Desktop Puppeteer (pup) bridge to wiki.adom.inc, plus every gotcha hit live. NOT for general users. Read before cutting a release: which artifact goes where (pkg=skills, release=bridge zip), version-lockstep, the `files` allowlist, keeping the tarball lean (NO zip / NO src / NO heroes / NO node deps), the pkg ships ONLY the pup USER skill (dev/publish skills are source-only in dev-skills/ + publish-skills/, never in the pkg — open-vs-closed source doesn't matter), the skillpack package.json declaring dependencies:{ adom/adom-desktop } to pull the AD CLI + core skills, deploying skills to .claude AND .codex, scrubbing stale retired-wiki (wiki-ufypy5dpx93o.adom.cloud) URLs, user-first discovery triggers, sha matching, and verify steps. Trigger words: publish pup bridge, ship pup bridge, release pup, pkg publish, repo push, adom-wiki release, bridge manifest, pup version bump, no zip in tarball, dev skill source only, scrub old wiki url, codex skills, discovery triggers, adom-desktop-puppeteer-bridge publish."+---++Parent skill: **adom-desktop-puppeteer-bridge**++# pup-bridge-publish — shipping a new pup bridge version++The pup bridge is cloud-owned at `wiki.adom.inc/adom/adom-desktop-puppeteer-bridge`. This is the+step-by-step ship recipe + the traps that cost real time. Companion: `pup-bridge-dev` (architecture /+ownership), `PUBLISHING.md` (terse recipe), `CLAUDE.md` (ownership boundary).++## What goes WHERE — three artifacts, three homes (memorize this)++| Artifact | Home | Command | Carries |+|---|---|---|---|+| **bridge runtime `.zip`** | a **Release** asset | `adom-wiki release upload` | `src/` at zip-root (server.js, chrome.js, bridge.json, node deps). The thing AD runs. |+| **streaming manifest JSON** | the **git repo** | `adom-wiki repo push` | points at the release zip URL + its sha256/size |+| **container skills tarball** | a **Package** | `adom-wiki pkg publish` | `SKILL.md` + `skills/*` (the Claude skills). NO runtime. |++> The `.zip` is a RELEASE, never a git file: `*.zip` is gitignored and `repo push --files X.zip`+> reports `ok` but silently skips it (the URL then 404s). Binaries → `release upload`.++## Auth (once per container)++```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[]` lists ONLY user files** — `SKILL.md`, `install.sh`, `uninstall.sh`, `README.md`,+  `package.json`. The dev/publish skills are source-only and are NOT in `files[]`. (If you ever add an+  extra USER skill under `skills/`, list its `skills/<name>/SKILL.md` explicitly — a bare `"skills"` dir+  entry silently includes ZERO files.) Always `tar tzf` to confirm exactly what shipped.+- **`pkg pack` honors `.gitignore`, not a separate npmignore.** So you can't gitignore something you+  need in git (e.g. `src/`) to drop it from the pkg — use the `files` allowlist for that.+- **A RELEASE shows up in `adom-wiki pkg info` as a row with `type:null`** (wiki UI conflation). The real+  pkg rows are `type:"app"`. Use `adom-wiki release list` for true releases; don't think your pkg shipped+  just because a version appears in `pkg info`.+- **sha/size mismatch → `bridge_install` rejects the manifest.** Releases preserve the uploaded sha, so+  use the `release upload` response values verbatim and re-download to confirm.+- **The Skills tab + `skill_count` + `adom-wiki skills install` come from the GIT REPO, not the pkg.**+  The wiki indexes **every `SKILL.md`** in the page repo (root = main skill; `<dir>/SKILL.md` = sub-skills)+  — `page.skills` is null; it's a repo scan. So a file named `SKILL.md` ANYWHERE in the repo becomes an+  indexed, tab-listed, `skills install`-able skill. (This is *separate* from `pkg install`, which installs+  the pkg tarball governed by the `files` allowlist.) Corollary → see "Keep dev/publish skills source-only".++## Verify (every ship)++```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.+```++## What the pkg ships: USER skills only; dev/publish are source-only (Bridge SDK)++The Bridge SDK ("Where every file lives", corrected 2026-07-06) is authoritative:++- **The pkg carries ONLY your USER skill(s).** For pup that's the root `SKILL.md` (`name: pup`). It's the+  single entry in `files[]`'s skill list; the container can't drive the verbs without it.+- **DEV + PUBLISH skills are SOURCE-ONLY** — this skill and `pup-bridge-dev`. They live in the repo under+  `publish-skills/<name>/SKILL.md` and `dev-skills/<name>/SKILL.md`, are **NOT** in `package.json files[]`,+  are **NOT** installed by `install.sh`, and do **NOT** need `user-invocable:false` (they never reach a+  container to trigger). Maintainers read them via `repo clone` / the Files tab.+- **Open-vs-closed source does NOT matter** — the pkg ships only user skills either way. (Earlier SDK+  wording gated this on openness; that was wrong and is corrected.)++**The pkg's `package.json` DECLARES its dependency on AD** so `pkg install` pulls the AD CLI + core+skills into the container:+```jsonc+"dependencies": { "adom/adom-desktop": "^1.9.x" }   // pkg dep — pulls the AD CLI + core skills+```+This is the ONE dependency the skillpack package.json carries. Your SERVER's npm deps (`puppeteer`, …)+live ONLY in the Release zip's `src/package.json`, never here.++**Verify after `adom-wiki pkg pack`:** `shipped 1 user skill(s)`; `tar tzf *.tgz` shows exactly+`SKILL.md`, `install.sh`, `uninstall.sh`, `README.md`, `package.json` — **no `skills/`, no `dev-skills/`,+no `*.zip`, no `src/`, no `*.png`**; and `package.json` has `dependencies: { "adom/adom-desktop": … }`.++History (so nobody re-thrashes this — it flipped 3×): the pkg ships ONLY the `pup` user skill; dev ++publish are source-only in `dev-skills/`/`publish-skills/`; the skillpack `package.json` declares the+`adom/adom-desktop` dep. Settled 1.8.23 (2026-07-06) on the maintainer's corrected guidance.++## Keep the pkg tarball LEAN — NO zip, NO src/, NO heroes++The pkg tarball is **docs only** — the `pup` USER skill + scripts; never the bridge runtime zip, `src/`,+hero PNGs, `node_modules`, or the dev/publish skills. Guards: the `package.json` `files` allowlist+(`SKILL.md`, `install.sh`, `uninstall.sh`, `README.md`, `package.json`) **plus** `.gitignore` (`*.zip`,+`*.png`, `*.bundle`, `*.tgz`, the old `*-source.tar.gz`). **ALWAYS** `adom-wiki pkg pack` then+`tar tzf *.tgz` and confirm: **no `*.zip`, no `src/`, no `*.png`, no `skills/`, no `dev-skills/`;+`shipped 1 user skill(s)`; and `package.json` has `dependencies: { "adom/adom-desktop": … }`**. ~12 KB is+right (one SKILL.md + scripts); MEGABYTES means a binary/`src/` leaked in (it was 16 MB before the+guards). `rm -f *.tgz` after.++## Skills install to BOTH .claude AND .codex++`install.sh` deploys the user skill into **both `~/.claude/skills/` and `~/.codex/skills/`** (Codex only+if `~/.codex` exists); `uninstall.sh` removes from both. Codex is a real second agent skills home now —+don't ship an install.sh that only does `.claude`.++## Scrub stale retired-wiki URLs (the #1 fork/extract gotcha)++**The only wiki host is `wiki.adom.inc`. `wiki-ufypy5dpx93o.adom.cloud` is RETIRED** — as are the+`/static/apps/...` and `/wiki/apps/...` path formats. When you fork/extract a page, the inherited+`README.md` and the `page.json` `readme` body carry OLD URLs (this bit us live — a dead `static/apps`+zip link). Before every publish:++```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).