← Commit history
CHANGELOG.md+50−340
@@ -1,294 +1,4 @@-# Changelog — Adom Desktop Puppeteer Bridge--## UNRELEASED (rides the next version) — AD-base re-discovery + self-guarded powershell primitives--### A Key Rule taught the wrong mental model for `browser_close` (destructive)--Found while enumerating which pup verbs execute on `{}` — a safety audit prompted by KICAD-publishing kicad's dangerous list, not by a bug report.--`SKILL.md`'s Key Rules state that on session-scoped verbs `'{}'` targets the **active** session.-That is true of every such verb except one: **`browser_close '{}'` closes ALL sessions**, every-window, across every AI thread on the machine, immediately and without confirmation. So an AI that-correctly internalised the general rule and wanted to close one window had a documented reason to-expect `browser_close '{}'` to do exactly that — and would instead have destroyed every window,-including long-running dashboards belonging to other threads.--The verb's behaviour is intentional and its describe entry already said "(destructive)", so this is-not a code defect and the semantics are unchanged — a breaking change to a documented verb would be-the wrong fix. What was wrong was the *mental model* the skill taught. Both surfaces now carry the-exception explicitly: the Key Rule names it, and the describe entry (what an AI actually reads-before calling) states that `{}` here does not mean the active session and points at-`browser_close_window` for the single-window case.--Worth noting the general shape: a correct general rule with one unstated exception is more dangerous-than no rule, because it manufactures confidence in precisely the case it doesn't cover.--### Recorded WHY the desktop_taskbar gates stay (the obvious reading is wrong)--Raised with AD as an open question — pup gates every `desktop_taskbar` call on `win32`, and if mac-AD had gained the capability those gates would be silently suppressing a shipped feature. AD-answered from source: **it is not a no-op on macOS.** `requestUserAttention` really bounces the-Dock and `NSDockTile` really sets badge text.--The gates stay anyway, for a reason neither side predicted: **the capability is real but the-addressing model is incompatible.** macOS has no per-window taskbar button, so AD's mac path-ignores `titleContains`/`hwnd` (it reports `hwnd:0`) and paints **AD's own Dock tile**. pup's use is-inherently per-session-window — *which of these five pup windows is this thread driving* — so-un-gating would badge Adom Desktop instead of the pup window, with every session fighting over one-tile. AD also skips `overlay.icon` on mac outright, which is what these calls pass.--The guard now carries that reasoning in full, because "Windows-only" was an imprecise rationale-that invites a future un-gate the moment someone learns `desktop_taskbar` works on a Mac. It does;-it just cannot address what pup needs addressed. The per-window equivalent of this cue on macOS is-`browser_alert_window`'s toast-and-unhide path.--Method note worth keeping: both ABE and I initially read "returns success on macOS" as "works on-macOS". It does work — but our evidence established dispatch and arg validation, not effect, and-the *real* answer turned out to be a third thing neither reading covered.--### The RUNTIME describe catalog and hints still said "taskbar" after the docs were fixed--KICAD's finding, reproduced in pup: after fixing the shipped SKILL.md entries I had not touched-`browser_describe` or the runtime `_hint`s — **the surface the SDK's first principle says the AI-actually reads.** AD relays these verbatim, so they outrank the skill file they contradicted.--- `browser_alert_window`'s describe entry read *"Flash the taskbar icon"*. Now states both forms-  (Windows: taskbar flash; macOS: unhide at z-position + an AD toast whose click focuses), matching-  the implementation that has been correct since the port.-- The Chrome-install hint was entirely Windows-shaped — UAC, `awaiting_uac`, "click YES on the UAC".-  On macOS the install is `hdiutil` + `ditto` into `/Applications` with **no elevation step and no-  `awaiting_uac` phase at all**, so a Mac AI would poll for a phase that never arrives and tell the-  user to approve a prompt that never appears. Now branches on `process.platform`.-- `browser_wiki_set_view` (the jump-list callback, which cannot fire on macOS) and-  `browser_configure` are scoped, the latter pointing at the `platform` / `taskbarSupported` fields-  so a caller branches on data rather than parsing prose.--The generalisable bit is KICAD's: *"the other place"* is not a memorisable list — for pup it was-frontmatter and then the describe catalog, for abe a README feature promise, for kicad runtime verb-metadata. What is constant is the question: after fixing a claim, ask **what else asserts this**,-and enumerate rather than recall.--### BEHAVIOURAL claims: shipped skills described a taskbar the Mac doesn't have--The worst of the doc classes, and the one no tool the fleet built can detect — these sentences-contain **no path and no platform word**, so nothing greps them. pup's shipped skills documented-Windows taskbar behaviour as simply *what happens*: window-type taskbar icons, overlay badges,-jump lists, `browser_alert_window` "flashes the taskbar", the wiki view-toggle right-click menu.--Two of them changed what an AI would DO, which is why this outranks a cosmetic doc bug:--- `SKILL.md` told the AI **"do not open a second window to 'check' what a taskbar icon already-  answers."** On macOS there is no icon to read, so the AI would skip a legitimate check on the-  strength of information it can never have. Now: on macOS use `browser_list_tabs`, and expect the-  user to describe a window by title or URL rather than by an icon.-- `pup-adom-wiki` told the AI to **tell the user to right-click the pup taskbar button**. A Mac user-  sent looking for that control cannot distinguish "not there" from "I missed it". Now scoped-  Windows-only with the one-verb `wikiView` alternative — and the caveat was moved ABOVE the-  instruction it qualifies, since a note placed after the advice arrives after the AI has acted.--Also scoped: the `taskbarGrouping` preference sections in `SKILL.md` and-`pup-windows-sessions-tabs`, the README's icon/overlay/jump-list sections, and-`browser_alert_window` — which documents its real macOS behaviour (unhide at z-position + an AD-toast whose click focuses) rather than implying a flash that cannot occur.--Unscoped behavioural claims went 19 → 5; the remaining five are image alt-text and verb-table rows-inside already-scoped sections. Deliberately not driven to zero — tightening prose to satisfy a-matcher would be gaming the metric rather than helping the reader.--### Shipped skills asserted Windows-only facts the macOS port made false--**Second pass — my own first pass was incomplete, in the way this class always is.** After fixing-the Edge claim in `SKILL.md`'s BODY I left the identical claim in the same file's FRONTMATTER-`description`, 19 lines above. That is the worst place to leave it: the description is what the-skill loader surfaces and what an AI reads to decide whether pup is even relevant, so it outranks-the body it contradicted. Also scoped the README's AUMID/Alt-Tab/Win11-taskbar paragraph, which-presented Windows window-identity as "under the hood" universal; macOS has no AUMID and no-taskbar, so it now says what carries the signal there instead.--Found by redoing the sweep Hydrogen's way — enumerate every line mentioning the platform and READ them-— instead of my first method, a grep with an exclusion filter. The filter excluded lines-containing "window", and pup is a browser-WINDOW bridge, so it had been hiding most of the file-from me. A clean result in this class is easy to fake.--Three places in the **shipped** pkg told the consumer AI something untrue on a Mac. None of them-is a path, so no sweep this fleet built could see them — they are flat platform *assertions*,-the class KICAD found in their README.--- **"every Windows PC ships Edge, so there's usually no download"** (`SKILL.md`, `README.md`,-  `pup-browsers-and-chrome`). True on Windows; false on macOS, where Edge is not preinstalled-  and Safari is not CDP-drivable. A Mac with no Chrome takes the *download* path, so the AI was-  promising the user "no download" precisely where a download happens. Now states both-  platforms and points at `browser_readiness` instead of an assumption.-- **The Chrome-install escalation section** described a Windows UAC and the "click YES to finish-  installing" approval flow as if universal. macOS has no elevation step at all: pup mounts the-  `.dmg` with `hdiutil`, `ditto`s to `/Applications`, and falls back to `~/Applications`.-  Verified against `chrome.js` rather than assumed — there is no `sudo` and no-  `administrator privileges` call anywhere in the mac path. An AI reading the old text on a Mac-  would wait for a consent click that is never coming. The Windows-only scope is now explicit.--The z-order paragraph was already dual-form (`Windows: SetWindowPos … macOS: hidden, Cmd+H`),-which is the tell for this class: the parts of the port that required *behaviour* changes got-their prose reviewed, and the parts that "just worked" did not.--### `browser_configure` claimed a taskbar change that cannot happen off Windows--`taskbarGrouping` and `agentActivity` both drive Windows-only machinery — taskbar buttons,-badges and the progress bar are stamped via `desktop_set_window_identity` / `desktop_taskbar`,-and `stampPupIdentity`/`brandSweep` early-return on non-win32. The setting was still stored, but-the `_hint` said *"Applied to live windows immediately"* unconditionally. On macOS that handed-the calling AI a confident description of something that did not happen, and per the SDK's first-principle the AI reads the verb OUTPUT and never the on-disk skill — so it would report a-taskbar change to a user who has no taskbar.--The hint is now platform-accurate (Windows wording unchanged), and the response carries-`platform` + `taskbarSupported` so a caller can branch on a field instead of parsing prose.--Found via ABE's *description-site* rule, which is the half that every static sweep this fleet ran-today structurally cannot see: hint/error text is not a construction site — nothing joins it and-nothing stats it — but it instructs the CALLER, so the bug executes one process away and looks-like the model getting it wrong. Construction sites need AST/base-expression enumeration;-description sites need a content grep over user- and AI-facing strings. The two rules are-disjoint and both are required.--### The main skill installed under the REPO name instead of its own name (`pup`)--`install.sh` deployed the start-here skill to `~/.claude/skills/adom-desktop-puppeteer-bridge/`-while that same `SKILL.md` declares `name: pup`. The skill directory name is the slug the loader-surfaces, so a third-party `pkg install adom/adom-desktop-puppeteer-bridge` got the start-here-skill under a slug that contradicted its own frontmatter, its documented trigger words (`pup`,-`open in pup`, …), and every cross-reference in the four sub-skills — all of which route to-`pup`. All four sub-skills already installed under names matching their `name:` fields, as does-every other skill on a machine (`adom-desktop-fusion`, `adom-desktop-kicad`, …); the main skill-was the lone exception. The repo name and the skill name are simply different things and had-been conflated.--Now installs as `pup`, and `install.sh` removes any pre-1.9.86 mis-slugged copy first so an-upgrade can't leave two start-here skills shadowing each other. `uninstall.sh` removes both the-new and the legacy directory. Verified against a sandbox `HOME`: fresh install yields 5/5-directories matching their declared names, an upgrade over a pre-existing-`adom-desktop-puppeteer-bridge/` removes it, and uninstall leaves the skills tree empty.--Found while auditing whether pup's release artifacts were correct for a public release — the-file-set sweeps that preceded it had covered only `src/*.js`, so the skillpack scripts had never-been in scope. That is the fleet's "complete over the wrong set" failure mode, applied to my own-audit.--### Windows browser candidates are no longer built on macOS (cwd-relative path class)--`listSystemBrowsers()` built its Windows candidate paths on every platform and relied on-`fileExists()` to drop them. That was safe — but safe by the *consumer's* behaviour rather than-by the value's, and the values themselves were the trap the fleet audit named: on macOS,-`path.join('C:\\Program Files', 'Google', ...)` yields `C:\Program Files/Google/...`, which is-**not absolute**, so it was probed at `<cwd>/C:\Program Files/...` — a path that could in-principle exist and become a wrong-but-real answer. Same class as an `|| ''` default resolving-against cwd.--Windows candidates are now constructed only when `process.platform === 'win32'`. A Windows path-can never match on mac, so this is zero behaviour change (verified: same single Chrome candidate-resolved, no non-absolute candidates) while removing the class outright and dropping five-pointless syscalls per call. Windows behaviour is byte-identical.--### Stale AD base after an AD self-update (real bug, found by the fleet's fallback-chain rule)--pup cached its resolved direct-API base permanently (`_adApiBase` / `_adVerifiedBase`, both-`!== undefined` short-circuits that nothing ever reset), and `adCommand` resolved `null` on a-connection error with no re-discovery. **AD self-updates and restarts, which rotates its-direct-API port** — so after any AD update, pup's cached base pointed at a dead port and every-AD call (alert toasts, taskbar/identity, the Windows window verbs) failed silently for the life-of the bridge process. A sibling bridge hit the identical shape in the wild: a frozen port-hammered for hours with nothing surfacing.--Fix: on any connection-level failure (error or timeout) pup now invalidates BOTH caches and-retries ONCE with fresh discovery, bounded by a `_rediscovered` flag so a genuinely-absent AD-still fails fast. The lesson generalizes: **a fallback chain needs both a live leg and a-re-discovery trigger** — the port file is the live leg, the invalidate-and-retry is the trigger.-A chain whose first leg is a cached/frozen value is correct right up until the thing it points-at moves, and that failure is silent.--Verified on macOS by simulating a rotated port (`ADOM_DIRECT_API_URL` pointed at a dead port):-the call recovers via the port file and subsequent calls stay healthy.--### Self-guard the powershell primitives--Fleet mac-porting audit (five instances of the PATH trap across four bridges in an hour)-surfaced its MIRROR IMAGE, and pup had the latent shape: a Windows call left ungated on the-mac path fails SAFE (ENOENT → the error branch → the same answer), so it never surfaces as a-bug — you just burn a doomed process spawn on every call, forever.--Audited all 14 Windows-binary invocation sites in server.js: every one is platform-gated-today, including all 10 `runPsHidden*` callers, so pup has no such spawn on mac right now.-But the three helpers themselves (`runPsHidden` / `runPsHiddenAsync` / `runPsHiddenPromise`)-are the only powershell.exe spawn points and had NO internal guard — one future ungated-caller reintroduces the bug. They now self-guard, returning the shapes their callers already-expect (`''`, `undefined`, `Promise.resolve('')`). Zero behavior change on either platform;-this removes a latent trap. Same defense-in-depth reasoning the EDA bridges applied by making-modules import-safe under the wrong interpreter: you don't own who calls you next.--Verified on macOS against the hardened build: open (backgrounded), lower (applied), alert-(applied), raise, screenshot — all pass.--## 1.9.85 — 2026-07-24 — MERGE: mac-parity line + John's Windows 1.9.76–1.9.84 line--The two release lines had silently diverged (mac 1.9.74–75 vs Windows 1.9.76–84, neither-containing the other; caught when a 1.9.75 publish rolled the fleet manifest back over-1.9.84). 1.9.85 is the three-way merge (base 1.9.73): all of John's 1.9.76–84 server/chrome-changes + full mac parity + the items below. Also restores the pup-cat-*9.ico category icons-to BOTH the git repo and the zip (they previously lived only in release zips, so mac-line-zips built from the repo silently dropped them).--### carried from the unreleased 1.9.75---1. VERIFIED AD BASE. KICAD's env-injection finding (confirmed from hd-control source):-   an HD-spawned bridge gets ADOM_DIRECT_API_URL pointed at Hydrogen's control API (:47084),-   which serves no notify verbs — so pup's alert toast would silently post to the wrong-   API under Hydrogen hosting. adCommand now resolves a VERIFIED base: probe each candidate's-   GET /commands for the verb surface pup needs (notify_user), in trust order env →-   ~/.adom/direct-api-port → default :47200, and use the first that answers. Live-tested:-   with a bogus env pointing at a dead port, ping still reaches the real AD via the port-   file. Fleet-locked resolution order (window-verb-shapes draft) implemented exactly.-2. SEED-COMPLETE ZIP. recorder-icon.ico + recorder-icon-256.png now ship in the release-   zip (AD's verify_bundle requires them; they previously lived only in the NSIS-era-   seed), so AD-core re-seeds are a pure wholesale copy of the release.--## 1.9.74 — 2026-07-23 — macOS parity: the whole OS-presentation layer, mac-native--Until now pup on macOS ran "headless-in-spirit": every CDP verb worked, but the OS layer-(background-by-default, lower/raise, alert) silently no-op'd (`applied:false`) because it was all-Win32/PowerShell. Verified live on a Mac (AD 1.9.167, feat/macos-parity):--1. BACKGROUND-BY-DEFAULT, THE MAC WAY. AppKit clamps windows onto a visible screen, so the-   Windows off-screen-launch trick can't work. Instead the detached launch now runs on darwin-   too (`--no-startup-window` + spawn detached), and the bridge HIDES the browser process-   (the Cmd+H state, via System Events) BEFORE its first window is created — windows born-   into a hidden app are born invisible. The park then sizes the window full-screen via CDP-   (while hidden, so a later raise looks right) and re-asserts the hide. Cross-origin-   navigation cannot unhide it. Screenshots/eval/recording all work on the hidden window-   (--disable-backgrounding-occluded-windows). One session = one process, so hide is-   per-session; shared-profile window groups hide together (documented DEGRADED vs per-hwnd).-2. LOWER / RAISE. browser_lower_os_window = hide (applied:true, verified via System Events);-   browser_raise_os_window / focus (foregroundReason gate unchanged) = unhide + activate by-   pid. The wiki-toggle raise path (osRaiseSessionWindow) gained the same branch.-3. ALERT. macOS has no taskbar flash and no API to bounce another app's Dock tile. The mac-   nudge: un-hide the window at its z-position (visible, NOT focused) + an AD native toast-   whose click focuses the window (notify_user focus:{titleContains}); {stop:true} dismisses-   the toast (notify_dismiss).-4. CHROME INSTALL. browser_use {browser:'chrome',install:true} now works on mac: official-   universal dmg → hdiutil attach → ditto to /Applications (falls to ~/Applications when not-   writable). No UAC machinery needed. ~/Applications is also probed by detection now.-5. PROCESS SCANS. The POSIX ps-scan filters were case-sensitive on 'chrome' — mac system-   Chrome ("…/MacOS/Google Chrome") never matched, so rescan/recovery missed it. Now /chrome|-   msedge|Microsoft Edge/i.-6. RECORDING. ffmpeg detection probes Homebrew/MacPorts paths (/opt/homebrew/bin,-   /usr/local/bin, /opt/local/bin — AD spawns bridges outside a login shell, so PATH lacks-   them), and the not-found hint says `brew install ffmpeg` on mac instead of winget. Verified:-   tab-record of a HIDDEN window produces a playable webm.-7. CfT PREWARM on branded-browser fallback now kicks on darwin too.--Windows behavior is unchanged: every new branch is `process.platform === 'darwin'`-gated, and-the AUMID/jump-list/overlay/tooltip identity stack stays Windows-only by design (no Dock-equivalent; the window title's "(session: <id>)" suffix is the mac identity).+# Changelog — Puppeteer Bridge  ## 1.9.43 — 2026-07-20 — Why some icons had overlays and others did not, plus stacked session titles @@ -438,7 +148,7 @@ ids, adoption or the reason none happened) so a silent failure cannot hide again dragged-out Chrome window arrived with an EMPTY name and the one event that mattered was discarded (verified in the live ring: the needed event was seq 32, name ""). The filter is gone: any window-opened triggers the follow-up, which is cheap and idempotent anyway. Also drains on-browser_list_tabs. Confirmed the underlying detection is sound: after John's drag, the dragged tab+pup_list_tabs. Confirmed the underlying detection is sound: after John's drag, the dragged tab reported window origin 335,292 while its siblings shared 0,0.  ## 1.9.31 — 2026-07-20 — Drag a tab out and it becomes its own pup window (event-driven, no polling)@@ -466,7 +176,7 @@ Root cause of "the toggle closed my window and never brought it back". osRaiseSe the browser PID as `_detachedPid || process().pid`, so for a DETACHED launch (every Windows pup browser) it ran PowerShell SetForegroundWindow against the LAUNCHER pid's MainWindowHandle, which is a different window than the session's. PowerShell dutifully reported "focused" each time while the-real window stayed hidden and unfindable. The proven browser_raise_os_window handler resolves the pid+real window stayed hidden and unfindable. The proven pup_raise_os_window handler resolves the pid as `process()` ONLY, which is null when detached, so it SKIPS PowerShell and relies on the CDP place+maximize+bringToFront that actually works. The helper now matches it exactly. Verified live: the same window that ignored seven in-code raises came back instantly via the verb's path.@@ -608,7 +318,7 @@ now, so tinting the favicon plate was a redundant and off-law second colour sign  ## 1.9.18 — 2026-07-20 — Fix: the green plate never painted (overlay ran before auth was known) -The 1.9.16/1.9.17 plate colour was correct but never appeared. Ordering bug in browser_open_window:+The 1.9.16/1.9.17 plate colour was correct but never appeared. Ordering bug in pup_open_window: applyAppOverlay paints the overlay EARLY, and `session._wikiAuthed` is only assigned a few lines LATER from the auth probe - so the plate was always composited in the logged-out colour, even for a window opened with wikiView:"authed". (The log tell: the per-session AUMID re-registers with@@ -708,7 +418,7 @@ AD shipped all four asks; pup now consumes them. - **Jump-list header icon (AD 1.9.141).** Not a registry fix — there is no ApplicationIcon value on   AUMID keys and the registry cannot brand the header at all. The header obeys the window's Relaunch*   TRIO, and Windows ignores RelaunchIconResource unless RelaunchCommand is ALSO set. stampPupIdentity-  now passes relaunchCommand (a real `browser_focus_window` for that session, carrying a+  now passes relaunchCommand (a real `pup_focus_window` for that session, carrying a   foregroundReason since the raise gate demands one, with Windows-argv-escaped quotes). - **Jump-list cmd flash (AD 1.9.143)** needed no pup change (AD interposes a GUI-subsystem shim);   pup re-sets jump lists at every open/navigate, so pre-1.9.143 lists heal automatically.@@ -749,7 +459,7 @@ the ACTUAL entry point the user hits, not a convenient proxy for it.  Follow-up to 1.9.7's foreground raise, from ralph-testing it live. Three fixes: (1) the OS raise now uses async PowerShell (runPsHiddenPromise) instead of sync — the sync call blocked pup's whole event-loop for up to 8s, stalling every other request. (2) browser_wiki_set_view now RESPONDS the instant+loop for up to 8s, stalling every other request. (2) pup_wiki_set_view now RESPONDS the instant the window is raised and runs branding/overlay/jump-list/auth-recheck in the BACKGROUND, so a jump-list click doesn't wait ~30s on cosmetics (that stall was likely why the switch felt like it "never worked"). (3) The tab glyph is set optimistically on switch and then self-corrects via a@@ -761,8 +471,8 @@ because the first auth check raced the page load. The taskbar wiki view-toggle worked functionally (verified: it relaunches the window under the authed profile, /auth/me=200, tab title flips to "🔓 Logged in") but the user couldn't SEE it: the callback only did a CDP maximize, so the relaunched window came back behind whatever they were-looking at. Now browser_wiki_set_view does a real OS SetForegroundWindow raise (extracted into a-reusable osRaiseSessionWindow helper from the proven browser_raise_os_window path — CDP place ++looking at. Now pup_wiki_set_view does a real OS SetForegroundWindow raise (extracted into a+reusable osRaiseSessionWindow helper from the proven pup_raise_os_window path — CDP place + clear WS_EX_NOACTIVATE + SetForegroundWindow, with a WMI child-PID fallback) plus a taskbar flash in case Windows' foreground lock denies the raise. Clicking the jump-list task now unmistakably brings the switched window to the front. (The ~50ms cmd flash on click is AD's console-subsystem@@ -791,12 +501,12 @@ source/drafts/owner cards vs public = what the world sees. Self-describing at a ## 1.9.4 — 2026-07-19 — Fix the concurrent-launch wedge + liveness/recovery hints  Root-caused a live cold-start wedge: after a bridge restart, two near-simultaneous-browser_open_window calls for the SAME profile each spawned their own detached Chrome onto the+pup_open_window calls for the SAME profile each spawned their own detached Chrome onto the same --user-data-dir, collided on Chrome's SingletonLock, and mutually taskkilled each other's process — so NONE came up and the bridge looked hung for minutes (verbs timed out, AD LED dim). Fix: `_launchInFlight` dedup — the first launch per profile registers a Promise and concurrent callers JOIN it instead of racing a second Chrome. Also added AI-facing recovery hints so other-threads don't relearn this: browser_readiness now explains that a status probe does NOT respawn a+threads don't relearn this: pup_readiness now explains that a status probe does NOT respawn a down bridge (only a window/tab verb does) and warns against spamming opens during "bridge_starting"; the pup SKILL and dev-skill gained a liveness/restart/wedge troubleshooting section (the AD LED + browser.bridgeRunning are truth; bridge_list status:"running" is not liveness).@@ -807,7 +517,7 @@ AD 1.9.139+ shipped #208 (desktop_set_window_jumplist). Every WIKI pup window no taskbar RIGHT-CLICK task to flip between the logged-in and public view — "🔓 Switch to logged-in view" on a public window, "📖 Switch to public view" on an authed one. The jump list attaches to the per-session AUMID (registry-only, from 1.9.2), so it's a PER-WINDOW menu. Clicking it runs-AD's bundled CLI → the new browser_wiki_set_view verb, which relaunches THAT window under the+AD's bundled CLI → the new pup_wiki_set_view verb, which relaunches THAT window under the other cookie jar (profile = userDataDir is fixed at launch, so a view switch is a relaunch) and brings it on-screen (a user click, so foregrounding is warranted). The menu is added/cleared on open and on every navigation (drops off when a window leaves Adom); split mode only (grouped mode@@ -835,7 +545,7 @@ refreshes on every Adom navigation, so a window that logs in mid-session flips t The taskbar overlay now shows the ACTIVE tab's site favicon for ANY tab count — you always see what the window is showing (ti.com, mouser, …). The tab count moves to the hover tooltip ("Adom Pup — 3 tabs (icon = active tab)"). Overlay follows tab switches (updates on-browser_switch_tab). Fallback to a plain count badge only when the active page serves no favicon.+pup_switch_tab). Fallback to a plain count badge only when the active page serves no favicon. Grouped-mode dual counter unchanged.  ## 1.8.99 — 2026-07-19 — Auth gating is Adom-wide; overlay = site favicon (1 tab) or count (2+)@@ -897,7 +607,7 @@ The deliberate public-vs-private visibility check flips the SAME window between pup can now open a wiki.adom.inc page from EITHER viewpoint, and hints teach the AI to show BOTH so the user can verify publish visibility ("did private source leak to the public?"). - Default open = PUBLIC/anonymous (fresh profile, logged out — what the world sees).-- `browser_open_window {url, wikiView:"authed"}` routes to a reserved PERSISTENT profile whose+- `pup_open_window {url, wikiView:"authed"}` routes to a reserved PERSISTENT profile whose   on-disk cookie jar holds the 30-day SSO session. First use returns `wikiLoginNeeded:true` with   a hint to raise the window for a one-time hydrogen.adom.inc SSO; thereafter it stays logged in. - No token→cookie shortcut exists (wiki auth is cookie-based SSO; the container CLI bearer is a@@ -909,10 +619,10 @@ so the user can verify publish visibility ("did private source leak to the publi ## 1.8.92 — 2026-07-19 — App-favicon overlay: a pup window WEARS the app that drives it  John's standard: every Adom app already serves a favicon (<link rel="icon">, monochrome white-mark per adom-ui-design; the canonical webview identity per hydrogen-tab-icons) — so pup now+mark per adom-ui-design; the canonical webview identity per hd-tab-icons) — so pup now auto-fetches the loaded page's favicon and wears it in the taskbar overlay, composited onto a dark rounded plate (white marks need dark backing). "That pup window with the shotlog icon IS my-shotlog thread." Explicit overrides: browser_open_window {appIconB64, appName}. An app-looking+shotlog thread." Explicit overrides: pup_open_window {appIconB64, appName}. An app-looking URL (localhost / *.adom.cloud proxy) serving NO favicon gets a 🎨 reprimand hint teaching the standard (adom-ui-design + app-creator). When an app icon is worn it owns the overlay slot and the tab count moves to the hover tooltip; split-mode count badges remain for icon-less windows;@@ -963,10 +673,10 @@ Per John: the badge circle no longer fills the whole overlay slot — ~60% size upper-right, hairline translucent outline instead of the heavy white ring. (Overlay icons are passed as base64, not paths, so no icon-cache filename dance needed for these.) -## 1.8.82 — 2026-07-18 — browser_configure: taskbar grouping is now a user preference+## 1.8.82 — 2026-07-18 — pup_configure: taskbar grouping is now a user preference  John's insight from the AUMID grouping bug: BOTH behaviors are legitimate preferences. New-`browser_configure {taskbarGrouping: "split"|"grouped"}` (persisted in ~/.adom/pup-settings.json,+`pup_configure {taskbarGrouping: "split"|"grouped"}` (persisted in ~/.adom/pup-settings.json, survives bridge updates): - `split` (default): per-session AUMID — one taskbar button per window, each badged with its own   tab count.@@ -999,13 +709,13 @@ Identity is per-HWND and stamped at birth, so windows born across rebrands/updat taskbar showing MIXED icon generations (plus dead-session ghosts inflating the count). New `brandSweep` re-asserts each live session's title suffix (title churn can make a window un-resolvable) and re-stamps it to the CURRENT identity — at startup (post-reattach) and after-every browser_rescan. Any future icon change now propagates to already-open windows within+every pup_rescan. Any future icon change now propagates to already-open windows within seconds of the bridge update instead of never.  ## 1.8.78 — 2026-07-18 — AD-version awareness: warn when the raise gate is bypassed  Every AD capability gap self-gates silently (identity → badge, bottom/force → PS fallback) EXCEPT-one: AD <1.9.124 intercepts browser_raise_os_window natively, so pup's foreground gate silently+one: AD <1.9.124 intercepts pup_raise_os_window natively, so pup's foreground gate silently does not exist there — a policy hole, not cosmetics. pup now probes the AD version at startup and, on affected boxes: logs a warning, turns the status chip yellow with an update-AD note, exposes `adVersion` + `raiseGateBypassed` on /status, and appends an upgrade advisory to open-response@@ -1069,8 +779,8 @@ prototype): 1.8.71's "converge by the second session" still ran the FIRST session on branded Chrome — and the first session is exactly the Aditya session. Now an unpinned new-window open REQUIRES cached CfT: if absent, pup kicks the ~150 MB install in the background and returns the pollable-chrome_for_testing_installing (poll browser_readiness, retry) instead of silently driving the-user's browser. The installed Chrome/Edge is reachable ONLY by explicit browser_use pin (hints+chrome_for_testing_installing (poll pup_readiness, retry) instead of silently driving the+user's browser. The installed Chrome/Edge is reachable ONLY by explicit pup_use pin (hints carry the identity-bleed warning). Existing/live sessions exempt. Companion AD ask filed (issue #204): flip prewarm_cft auto→always so install-time prewarm erases the first-use wait. @@ -1081,7 +791,7 @@ carry the identity-bleed warning). Existing/live sessions exempt. Companion AD a   branded browsers' identity features bleed into pup's "anonymous" windows (the documented   sign-in → profile-creation → consolidation → lost-user cascade). No first-open stall anywhere:   a branded-fallback launch kicks a ONE-TIME background CfT install, so every box converges to-  deterministic CfT by its second session. `browser_use` still pins explicitly.+  deterministic CfT by its second session. `pup_use` still pins explicitly. - **Codec caveat retired with evidence:** CfT (real Chrome build) decodes H.264/AAC/MP3/VP9   ("probably" via canPlayType on a live box) — the gap was legacy Chromium's, not CfT's. Only   Widevine DRM playback is out of scope.@@ -1096,16 +806,16 @@ Real incident: a thread background-opened a window then raised it 2 minutes late required no reason, recorded nothing, and didn't even log — forensics needed the focus-latch timestamp. pup now gates foregrounding the way abe does (dual key, one call): -- `browser_open_window {foreground:true}` WITHOUT `foregroundReason` → the open succeeds but the+- `pup_open_window {foreground:true}` WITHOUT `foregroundReason` → the open succeeds but the   window stays BACKGROUND, with `foregroundDenied:'foreground_reason_required'` + a hint (abe   semantics: withhold the foregrounding, not the work). With a reason (min 10 chars): granted.   Also accepts abe's arg name `userRequestedForeground`.-- `browser_raise_os_window`/`browser_focus_window` WITHOUT `foregroundReason` → REFUSED with+- `pup_raise_os_window`/`pup_focus_window` WITHOUT `foregroundReason` → REFUSED with   `errorCode: foreground_reason_required` and the contract in the hint. - Every granted foreground is logged (`[foreground] ... GRANTED — reason: ...`), recorded in   `lastAgentUpdate` (`FOREGROUND: <reason>`), and surfaced as `lastForeground {ts, verb, reason}`   in session status — "who put this window on my screen and why" is now one query.-- browser_describe teaches the whole contract, including return-to-background after the show-me.+- pup_describe teaches the whole contract, including return-to-background after the show-me.  ## 1.8.69 — 2026-07-15 — Sized opens keep their x/y (multi-monitor placement works); window-management granted @@ -1213,7 +923,7 @@ park moved the window on-screen anyway — an un-bottomed window lands on TOP of  - The park now retries hwnd-resolve + z-bottom up to 8× (~2.5s); if 'bottomed' is never confirmed,   the window STAYS off-screen and the verdict is an honest `park-failed` — invisible beats popping.-- `park-failed` windows self-heal opportunistically: any browser_status / list query retries the+- `park-failed` windows self-heal opportunistically: any pup_status / list query retries the   heal, on top of the existing reattach / open_tab / switch_tab touchpoints. - 1.8.58: badge calls check AD's real result + log `[badge] applied/FAILED`; the tab-path debounce   arms only on success (one silent failure no longer means a bare icon for 10 minutes).@@ -1233,7 +943,7 @@ park moved the window on-screen anyway — an un-bottomed window lands on TOP of   off-screen, re-park + re-badge (never touches a healthy or user-foreground window). Badge also   re-applies (debounced 10 min) on the tab hot path so long-lived shared windows stay badged   across bridge restarts. Anonymous window CREATORS now get an owner-tip in the open response-  (an ownerless long-lived window has zero cross-thread protection). Enriched `browser_describe`+  (an ownerless long-lived window has zero cross-thread protection). Enriched `pup_describe`   entries for `open_tab`/`switch_tab` (return shape, updateNote, background/click contract).  ## 1.8.53 — 2026-07-14 — Direct-API park: CDP + AD verbs replace the PowerShell window-hunt@@ -1340,7 +1050,7 @@ primitive around **minimize**, which removes the whole conflict:   minimized window keeps compositing frames. - The only late passes are safety re-minimizes that RESPECT a user restore: the instant the window is   un-minimized by a real click (physical input), pup latches `_userForeground` and never touches it again.-- `browser_lower_os_window` / post-resize re-park also minimize now. `raise` restores via CDP as before.+- `pup_lower_os_window` / post-resize re-park also minimize now. `raise` restores via CDP as before.  ## 1.8.46 — 2026-07-14 — Resolve the REAL browser window (the background park was targeting a phantom) @@ -1366,7 +1076,7 @@ windows the human must be able to click, SEE, and use normally — both at once.   raise, but it *bailed without moving the window on-screen* — so clicking a still-off-screen window   activated an invisible window (user saw nothing). Now the user-raise path brings the window ON-SCREEN   (centered ~80%, `SWP_SHOWWINDOW`, HWND_TOP) and leaves it foreground. Background↔foreground handoff.-- **`browser_alert_window {stop}` truly clears the flash.** `FLASHW_STOP` only stops the *pulse*; the+- **`pup_alert_window {stop}` truly clears the flash.** `FLASHW_STOP` only stops the *pulse*; the   Win11 attention tint persists until the window is ACTIVATED. New `osClearFlashByActivate` clears it by   activating the window OFF-SCREEN (invisible → tint clears) then handing focus back — no pop, no focus   steal. Reports a real `cleared` / `already-foreground`, never a fire-and-forget guess.@@ -1393,11 +1103,11 @@ landing inside that 2.5s window got yanked back. ## 1.8.35 → 1.8.43 — 2026-07-13/14 — Flash routes through AD; taskbar badge; snappy flash; cleanup nudge  - **Taskbar flash + clear route through AD-core** (`desktop_flash_window`) instead of pup's own-  `FlashWindowEx` — one mechanism, native, reliable. `browser_alert_window {stop}` now returns+  `FlashWindowEx` — one mechanism, native, reliable. `pup_alert_window {stop}` now returns   AD's real `applied` result (no more fire-and-forget `cleared:true` lies). Snappy: flashes on   open before the page finishes loading. - **Adom favicon badge** overlaid on the pup taskbar button via AD's `desktop_taskbar` overlay.-- **`🧹 CLEANUP` hint** in the open response + `browser_status` when windows are ≥30m old or ≥5 open.+- **`🧹 CLEANUP` hint** in the open response + `pup_status` when windows are ≥30m old or ≥5 open.  ## 1.8.24 → 1.8.34 — 2026-07-13 — Background-by-default, actually correct (no pop / no cover / no focus steal / no cmd flash) @@ -1430,16 +1140,16 @@ All five items shipped (AD relays verb args verbatim → no AD-side change):   window's taskbar via real `FlashWindowEx` (`FLASHW_TRAY|FLASHW_TIMERNOFG`) so the user notices the   agent touched it — **never steals focus**, **self-suppresses while foreground**, **clears on click**.   Debounced: one flash per window per ~5s burst. Suppress per-call with `{silent:true}`, treat a read as-  read-only with `browser_eval {readOnly:true}`, or silence a whole session with-  `browser_open_window {autoFlash:false}`.+  read-only with `pup_eval {readOnly:true}`, or silence a whole session with+  `pup_open_window {autoFlash:false}`. - **`updateNote` + `lastAgentUpdate`.** Pass `updateNote` on any mutating verb; pup records-  `{tabId,verb,note,ts}` and surfaces it top-level **and per-tab** in `browser_list_tabs` /+  `{tabId,verb,note,ts}` and surfaces it top-level **and per-tab** in `pup_list_tabs` /   `list_windows` / status — answers "which tab did the agent just change?".-- **Raise no longer leaks keystrokes.** `browser_raise_os_window`/`focus_window` blur the page's active-  element before `bringToFront()`. Docs steer agents to `browser_alert_window` (or the auto-flash) for+- **Raise no longer leaks keystrokes.** `pup_raise_os_window`/`focus_window` blur the page's active+  element before `bringToFront()`. Docs steer agents to `pup_alert_window` (or the auto-flash) for   "I updated this", reserving raise for an explicit show-me.-- **`browser_open_tab` returns `tabId` + `tabCount`** explicitly (no more diffing `list_tabs`).-- **New `browser_render_html {sessionId,html,waitUntil?,updateNote?,silent?}`** — renders a local HTML+- **`pup_open_tab` returns `tabId` + `tabCount`** explicitly (no more diffing `list_tabs`).+- **New `pup_render_html {sessionId,html,waitUntil?,updateNote?,silent?}`** — renders a local HTML   string via `page.setContent` (UTF-8-correct; a `data:` URL mojibakes accented chars without a charset). - Note: `desktop_screenshot_window titleContains/sessionId` is an **AD-core** verb — routed to AD, not   pup. The `fullPage` nested-scroll `nestedScroll:true` warning shipped in 1.8.21.@@ -1457,13 +1167,13 @@ Consolidated summary of the arc since 1.1.0 (per-release detail lives in the wik   longer foregrounds either. `foreground:true` is the only way to show a window. - **Window ownership (1.8.16):** sessions carry an advisory `owner`; declared cross-thread navigate/close   is refused (`session_owned_by_another_thread`), anonymous grabs are reprimanded — stops AI threads-  stealing each other's windows. `browser_status`/`list_windows` expose `owner` + `ageMinutes`.+  stealing each other's windows. `pup_status`/`list_windows` expose `owner` + `ageMinutes`. - **Chrome install drives the user through the UAC (1.8.12–1.8.15):** on a locked-down box, escalates to   an elevated install and — via AD ≥1.9.84's direct-API — fires a native "click YES" toast from the   bridge itself, pinging the attended device (cross-AD `target`) and re-arming if it expires. Degrades   cleanly on older AD (raise UAC once + relay hint, never spam).-- **Missing verbs + catalog completeness (1.8.8–1.8.11):** added browser_click/type/press_key/back/-  forward/evaluate and documented 13 working-but-unadvertised verbs in `bridge.json` + `browser_describe`.+- **Missing verbs + catalog completeness (1.8.8–1.8.11):** added pup_click/type/press_key/back/+  forward/evaluate and documented 13 working-but-unadvertised verbs in `bridge.json` + `pup_describe`. - **Title-suffix resilience (1.8.17):** persistent `evaluateOnNewDocument` injector so AD's title-based   raise/lower window lookup stops flaking right after a navigation. - **Skillpack restructure:** public `pup` + `pup-bridge-dev` + `pup-bridge-publish` as@@ -1471,7 +1181,7 @@ Consolidated summary of the arc since 1.1.0 (per-release detail lives in the wik  ## 1.1.0 — 2026-06-29 — Cold-start self-heal + ownership extraction -**Why:** On a fresh PC (Adom Hydrogen bundles Adom Desktop; the installer pre-installs neither+**Why:** On a fresh PC (Hydrogen Desktop bundles Bridge; the installer pre-installs neither Node nor Chrome for Testing), the first "open … in pup" failed with a generic "Failed to launch Chrome after 3 attempts" + a developer-only string ("run npx puppeteer browsers install chrome") — no `errorCode`, no auto-install, no actionable hint. The AI couldn't recover.@@ -1480,15 +1190,15 @@ install chrome") — no `errorCode`, no auto-install, no actionable hint. The AI - **`src/chrome.js`** (new) — detect (validates the *actual* executable, not just "some dir exists"),   auto-install programmatically via the bundled `@puppeteer/browsers` (no npx), pinned to the exact   buildId this puppeteer expects, with download-progress + install-state dedupe + a readiness snapshot.-- **`browser_open_window`** — a cold-start gate (CfT path only; native-drive skips it) that detects a+- **`pup_open_window`** — a cold-start gate (CfT path only; native-drive skips it) that detects a   missing CfT, kicks off a NON-BLOCKING background install, and returns a structured-  `{ errorCode:"chrome_for_testing_installing", installing:true, statusVerb:"browser_readiness",-  _hint:"poll browser_readiness then retry" }` — instead of blindly spawning a missing binary 3× then+  `{ errorCode:"chrome_for_testing_installing", installing:true, statusVerb:"pup_readiness",+  _hint:"poll pup_readiness then retry" }` — instead of blindly spawning a missing binary 3× then   throwing. On install failure → `chrome_install_failed`/`chrome_download_failed` with a real hint   (no more silent continue-to-doomed-launch).-- **`browser_readiness`** (new verb) — `{ ready, chromeForTestingInstalled, installing,+- **`pup_readiness`** (new verb) — `{ ready, chromeForTestingInstalled, installing,   installProgressPct, ... }`. Poll until `ready:true`.-- **`browser_prewarm`** (new verb) — install CfT WITHOUT opening a window (warm before first use);+- **`pup_prewarm`** (new verb) — install CfT WITHOUT opening a window (warm before first use);   `{wait:true}` blocks. - **Status chip** — `/status` now emits `led`/`summary`/`tooltip` and tells the TRUTH: yellow   "Installing Chrome N%" / "Chrome not installed", red on install failure, green when ready (was always@@ -1498,10 +1208,10 @@ install chrome") — no `errorCode`, no auto-install, no actionable hint. The AI  **Hygiene:** - `BRIDGE_VERSION` ↔ `bridge.json` version lockstep fixed (was 1.0.14 vs 1.0.16).-- `bridge.json`: `statusVerb:"browser_readiness"` + `updateManifestUrl` so AD auto-updates from this-  page; new verbs registered in `verbs[]` + `browser_describe`.+- `bridge.json`: `statusVerb:"pup_readiness"` + `updateManifestUrl` so AD auto-updates from this+  page; new verbs registered in `verbs[]` + `pup_describe`.  **Ownership:** extracted from `adom-desktop/plugins/puppeteer` into this standalone wiki repo (`wiki.adom.inc/adom/adom-desktop-puppeteer-bridge`), mirroring the cloud-owned kicad/fusion model.-Adom Desktop keeps a bundled SEED for first-run/offline and auto-pulls newer versions from here via+Bridge keeps a bundled SEED for first-run/offline and auto-pulls newer versions from here via `updateManifestUrl`.
CLAUDE.md+8−8
@@ -1,8 +1,8 @@ # CLAUDE.md — adom-desktop-puppeteer-bridge (READ THIS, it is YOUR project) -This repo is **the Adom Desktop Puppeteer ("pup") bridge** — the canonical source of truth, published+This repo is **the Puppeteer ("pup") bridge** — the canonical source of truth, published to `wiki.adom.inc/adom/adom-desktop-puppeteer-bridge`. If you are an Adom AI touching anything pup /-`browser_*` / Chrome-for-Testing, **you own and maintain this code here.** Adom Desktop (AD) ships a+`pup_*` / Chrome-for-Testing, **you own and maintain this code here.** Bridge (AD) ships a bundled SEED of this bridge for first-run/offline, but the live code is THIS repo and AD auto-pulls it via the `updateManifestUrl` in `bridge.json`. (Extracted from `adom-desktop/plugins/puppeteer` on 2026-06-29, mirroring the kicad/fusion cloud-owned model.)@@ -14,19 +14,19 @@ via the `updateManifestUrl` in `bridge.json`. (Extracted from `adom-desktop/plug   bridge into `…\bridges-cache\puppeteer`, the stable port, supervisor), `desktop_install_node`, and the   `node_not_found` gate that fires BEFORE this bridge can spawn. AD also runs `npm install` in the bridge   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+- **YOU (this bridge) own:** every `pup_*` / `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 source-only maintainer skills in `dev-skills/`/`publish-skills/`), and the edit →+  fields on `/status`, the `pup_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 →+- So: a `pup_*` 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-  capability is missing (a new lifecycle verb, calling `browser_prewarm` on embedded first-run), file+  capability is missing (a new lifecycle verb, calling `pup_prewarm` on embedded first-run), file   that against AD — don't re-absorb the bridge.  ## Map of what you manage (`src/` = the runnable bridge, zipped to the cache) -- `src/server.js` — the bridge HTTP server / verb router. Cold-start gate in `browser_open_window`;-  `/status` + `/health` (led/summary/tooltip chip); `browser_readiness` + `browser_prewarm` verbs.+- `src/server.js` — the bridge HTTP server / verb router. Cold-start gate in `pup_open_window`;+  `/status` + `/health` (led/summary/tooltip chip); `pup_readiness` + `pup_prewarm` verbs. - `src/chrome.js` — Chrome-for-Testing self-heal. detect (validates the real exe), install   (programmatic via the bundled `@puppeteer/browsers`, pinned to puppeteer's expected buildId), the   install-state singleton, `ensureChromeReady` (the gate), `readiness` (powers the verb + chip).
HANDOFF-TO-ADOM-DESKTOP.md+13−13
@@ -15,11 +15,11 @@ exactly like the kicad and fusion bridges: - **Manifest (updateManifestUrl target):** `https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json` - **Release zip:** `https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.1.0/adom-bridge-puppeteer-v1.1.0.zip` (sha256 `525eabb0350ea0eaf52a949e9b3026f5a4a035868afe9546a7478298b9539b8c`, source-only — node_modules is reconstructed by AD's `npm install` on spawn, as today) -**What changed in 1.1.0 (the reason for all this):** cold-start self-heal. On a fresh PC (Hydrogen bundles+**What changed in 1.1.0 (the reason for all this):** cold-start self-heal. On a fresh PC (HD bundles AD; the installer pre-installs neither Node nor Chrome for Testing), the first "open in pup" used to fail with a generic "Failed to launch Chrome after 3 attempts" + a dev string (npx…). Now the bridge detects a missing Chrome for Testing, auto-installs it (programmatically via the bundled-@puppeteer/browsers), and exposes `browser_readiness` + `browser_prewarm`; `/status` reports+@puppeteer/browsers), and exposes `pup_readiness` + `pup_prewarm`; `/status` reports led/summary/tooltip truthfully (yellow while installing). The `node_not_found` path you already own is unchanged and still correct. @@ -56,19 +56,19 @@ unchanged and still correct.    so nothing pre-warms and the whole cold-start cost lands on the user's first "open in pup". Please    either:    - re-enable that idle-time prefetch task on the embedded path (it already calls-     `start_browser_bridge`, which now self-installs Chrome for Testing), **or**-   - after AD enters embedded mode, fire `browser_prewarm` once (fire-and-forget) to kick the Chrome+     `start_pup_bridge`, which now self-installs Chrome for Testing), **or**+   - after AD enters embedded mode, fire `pup_prewarm` once (fire-and-forget) to kick the Chrome      download in the background.    Either way, gate it behind the existing Node check (if Node is missing it's a no-op / the-   `node_not_found` path stays the recovery). Net effect: a fresh Hydrogen install quietly warms pup so the+   `node_not_found` path stays the recovery). Net effect: a fresh HD install quietly warms pup so the    user's first "open in pup" is instant instead of a 2–3 min download.  **Verify after your change:** on a fresh embedded boot with Node present, `bridge_list` shows pup at-1.1.0 (cache or seed), `browser_readiness` returns `{ready:true}` without the user ever asking, and+1.1.0 (cache or seed), `pup_readiness` returns `{ready:true}` without the user ever asking, and `bridge_check_updates` reaches the new page via `updateManifestUrl`. On a box without Node,-`browser_open_window` still returns the clean `node_not_found` → `desktop_install_node` path.+`pup_open_window` still returns the clean `node_not_found` → `desktop_install_node` path. -Questions / bugs in pup itself (a `browser_*` verb, a hint, the Chrome self-heal) → file them against+Questions / bugs in pup itself (a `pup_*` verb, a hint, the Chrome self-heal) → file them against the wiki repo, not adom-desktop. Generic AD capabilities (a new lifecycle verb, the prewarm trigger above) → adom-desktop, but don't re-absorb the bridge. @@ -109,7 +109,7 @@ These are AD-core-owned (we won't touch them, same as you don't touch pup), so p    `discovery_triggers` + `discovery_pitch` (no separate upload step).  6. **STALE URL — the retired wiki host.** Both the `adom-desktop-bridges` page AND the-   `adom-desktop-bridge-author` skill still cite **`wiki.adom.inc`** (retired). The only host+   `adom-desktop-bridge-author` skill still cite **`wiki-ufypy5dpx93o.adom.cloud`** (retired). The only host    is `wiki.adom.inc`. Grep + scrub (incl. the `page.json` `readme` body).  **Easiest path:** lift the content from the pup bridge's now-canonical publish playbook —@@ -132,7 +132,7 @@ analogue of why kicad/fusion (Python, no node_modules) stream fine but pup can't  - **(A) PREFERRED — give a cache-installed `spawn.kind:"node"` bridge its node_modules.** On spawn from   the cache, AD should either (i) `npm install`/`npm ci` in the cache dir first (the bespoke-  `start_browser_bridge` ALREADY does this for the bundled pup — extend it to / route the cache copy+  `start_pup_bridge` ALREADY does this for the bundled pup — extend it to / route the cache copy   through it), or (ii) reuse/symlink the bundled `node_modules` into the cache dir. Then wiki-streaming +   `updateManifestUrl` auto-update works for pup exactly like kicad/fusion. - **(B) OR keep pup runtime "bundled-only"** (the original model): the bundled-seed bump (handoff #2,@@ -156,17 +156,17 @@ spawn-verified fallback, self-warm, disk guards). The remaining cold-start block    Foundation, allow changes?"). A cloud AI cannot click it; the user must, and John explicitly does not    want a UAC here. 2. **Stale PATH after a *successful* install.** Even once Node installs, the already-running AD process-   keeps its OLD environment, so the very next `browser_*` STILL returns `node_not_found` /+   keeps its OLD environment, so the very next `pup_*` STILL returns `node_not_found` /    `could not run npm for cache bridge`. Reinstalling doesn't help (winget then says "No available upgrade"    = already installed). The bridge can never spawn until AD is relaunched.  **Both fixed by one change — install Node the portable, user-scoped way:** - Download the **Node ZIP** (`node-vXX-win-x64.zip`), extract `node.exe` into a user-writable dir (e.g.-  `%LOCALAPPDATA%\Adom Desktop\node\`). **No installer, no UAC.**+  `%LOCALAPPDATA%\Bridge\node\`). **No installer, no UAC.** - Spawn bridges with that `node.exe` **by absolute path** (AD already knows where it put it) — don't rely   on `PATH` at all. This also permanently kills the stale-PATH class of bug. - Even better: **bundle `node.exe` in the AD installer** (~50 MB) so a fresh AD already has Node — zero-  download, zero UAC, zero PATH dependency, instant first `browser_open_window`.+  download, zero UAC, zero PATH dependency, instant first `pup_open_window`. - If you keep a PATH-based flow, at minimum **probe common Node locations** (`%ProgramFiles%\nodejs`,   `%LOCALAPPDATA%\...`, nvm dirs) before declaring `node_not_found`, and refresh PATH from the registry   after install.
HANDOFF-notify-for-bridges.md+2−2
@@ -1,4 +1,4 @@-# Handoff to Adom Desktop: expose ALL AD verbs to bridges via the direct-API+# Handoff to Bridge: expose ALL AD verbs to bridges via the direct-API  **From:** pup (adom-desktop-puppeteer-bridge) **TL;DR:** A bridge should be able to call **any** AD verb the CLI can, through the@@ -9,7 +9,7 @@ user through a Chrome UAC — but the fix should be general, not a one-off for n ## The two directions (so we're precise)  - **Inbound (AD → bridge):** AD routes verbs to a bridge by its declared prefixes-  (`browser_*` for pup). Works fine; not the issue.+  (`pup_*` for pup). Works fine; not the issue. - **Outbound (bridge → AD):** a bridge's only callback channel is the direct-API. AD's own   `GET /commands` defines "reachable verbs" for a bridge as **`desktop_*` + `app:"dynamic"`   bridge verbs only.** Top-level/lifecycle verbs are absent. So a bridge has a *partial*
PROPOSAL-shared-runtimes.md+2−2
@@ -14,7 +14,7 @@ dead-ends, none of which are pup's to fix: 1. **UAC.** `desktop_install_node` uses Node's machine-wide **MSI**, manifested `requireAdministrator` →    a Windows UAC dialog. A cloud AI can't click it; if the user denies it, cold-start dead-ends. 2. **Stale PATH.** Even after a *successful* install, AD's already-running process keeps its old `PATH`,-   so the next `browser_*` STILL returns `node_not_found` / `could not run npm`. Reinstalling doesn't help;+   so the next `pup_*` STILL returns `node_not_found` / `could not run npm`. Reinstalling doesn't help;    AD must be relaunched. 3. **Firewall.** A hostless `listen()` binds all interfaces → "allow Node through the firewall?" (pup    fixed its own side in v1.6.0 by binding loopback, but the *runtime* provisioning is still AD's).@@ -43,7 +43,7 @@ needs a specific runtime version. ### 1. A shared, content-addressed runtime cache owned by AD  ```-%LOCALAPPDATA%\Adom Desktop\adom-runtimes\+%LOCALAPPDATA%\Bridge\adom-runtimes\     node-22.11.0\node.exe ...     node-18.20.4\node.exe ...        # only if some bridge pins it     python-3.12.7\python.exe ...
PUBLISHING.md+6−6
@@ -1,6 +1,6 @@ # Building + publishing this bridge -How to publish a new version of the Adom Desktop Puppeteer bridge. The wiki has TWO storage layers —+How to publish a new version of the Puppeteer bridge. The wiki has TWO storage layers — put each artifact in the right one (every pitfall here was hit live on the kicad/fusion bridges).  | Artifact | Where it goes | How |@@ -52,8 +52,8 @@ adom-wiki release upload  adom/adom-desktop-puppeteer-bridge 1.1.1 adom-bridge-p { "manifest_version": 1, "name": "puppeteer", "version": "1.1.1",   "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.1.1/adom-bridge-puppeteer-v1.1.1.zip",   "sha256": "<the release asset's sha256>", "size": <bytes>,-  "verbPrefixes": ["browser_","desktop_recorder_","desktop_record_"],-  "healthEndpoint": "/status", "statusVerb": "browser_readiness",+  "verbPrefixes": ["pup_","desktop_recorder_","desktop_record_"],+  "healthEndpoint": "/status", "statusVerb": "pup_readiness",   "released_at": "<UTC ISO8601>" } ``` ```bash@@ -65,12 +65,12 @@ adom-wiki repo push adom/adom-desktop-puppeteer-bridge \ > `sha256`/`size` MUST match the served asset or `bridge_install` rejects it. Re-download to confirm: > `curl -sL <download_url> | sha256sum`. -## 5 — install into Adom Desktop + test+## 5 — install into Bridge + test  ```bash adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json","force":true}'-# server-only change → AD reaps + respawns the bridge from the new cache on the next browser_* call.-# Then: adom-desktop browser_readiness '{}'   (confirms the new verbs/code are live)+# server-only change → AD reaps + respawns the bridge from the new cache on the next pup_* call.+# Then: adom-desktop pup_readiness '{}'   (confirms the new verbs/code are live) ```  (Multiple desktops on one relay → pass `--target <name>`; `adom-desktop targets` lists them.)
RECORDING.md+9−9
@@ -38,7 +38,7 @@ screenshots, and everything that is not video. **Opt in for recording: `highFps: true`.**  ```jsonc-browser_open_window { "sessionId":"demo", "url":"https://...", "highFps": true }+pup_open_window { "sessionId":"demo", "url":"https://...", "highFps": true } ```  That window renders at full rate even while occluded, so a background take is smooth. Because it@@ -66,22 +66,22 @@ This is the guarantee that makes the lease safe. **While a recording is actually cannot lapse.** pup keeps pushing it out for as long as frames are being pulled.  This is knowable rather than guessed, because recording runs through pup's own verbs-(`browser_record_start` / `browser_record_stop`), so pup has authoritative state, not a heuristic.+(`pup_record_start` / `pup_record_stop`), so pup has authoritative state, not a heuristic.  The activity signals, in priority order:  1. **An active recording** — authoritative, holds the lease open indefinitely.-2. **Verb activity** — any `browser_*` call against the session shows it is in use.-3. **An explicit extension** — `browser_highfps_extend`, for when you know up front you need+2. **Verb activity** — any `pup_*` call against the session shows it is in use.+3. **An explicit extension** — `pup_highfps_extend`, for when you know up front you need    longer but will not be recording the whole time.  ## Negotiating -pup tells you the lease terms in the `_hint` of every `browser_open_window` response, including+pup tells you the lease terms in the `_hint` of every `pup_open_window` response, including when it will downgrade and how to keep it. If you need longer:  ```jsonc-browser_highfps_extend { "sessionId":"demo", "minutes": 45 }+pup_highfps_extend { "sessionId":"demo", "minutes": 45 } ```  You do **not** need to keep extending during a take. Extend when you are staging a long session@@ -94,7 +94,7 @@ logs a warning when a recording starts on a normal-power window so a bad video i  ``` [highfps] "demo" recording started on a NORMAL-power window — frames will drop to ~1-2 fps-          while it is occluded. Reopen with browser_open_window {highFps:true} for a+          while it is occluded. Reopen with pup_open_window {highFps:true} for a           full-rate background take. ``` @@ -102,9 +102,9 @@ If the window is genuinely visible on screen for the whole take, a normal window  ## Checking state -`browser_open_window` returns a `highFps` field: `false` for a normal window, or+`pup_open_window` returns a `highFps` field: `false` for a normal window, or `{active, minutes, expiresAt, msRemaining, recordingActive}` for a leased one. The same shape comes-back from `browser_highfps_extend`.+back from `pup_highfps_extend`.  ## Rules of thumb 
RESTRUCTURE-PLAN.md+1−1
@@ -81,7 +81,7 @@ src/     navigate.js          # navigate/reload/back/forward/wait/fetch_url     input.js             # click/hover/type/press_key/scroll/input_dispatch/assisted_click     read.js              # eval/screenshot*/errors/render_html/set_viewport-    record.js            # browser_record_* + desktop_record_* + recorder window+    record.js            # pup_record_* + desktop_record_* + recorder window     admin.js             # readiness/prewarm/status/describe/configure/use/deps/login/rescan   chrome.js              # unchanged (Chrome-for-Testing self-heal)   credential_vault.js    # unchanged
dev-skills/pup-aumid-decisions/SKILL.mdadded+84
@@ -0,0 +1,84 @@+---+name: pup-aumid-decisions+description: The complete decision history on AUMID / taskbar identity in pup - what was tried, what it broke, the settled NO-AUMID decision, and the process rule that prevents silently reversing it again. READ BEFORE touching anything involving AppUserModelID, taskbar grouping, window identity, or "separate taskbar icons".+---++# AUMID in pup: the full decision history (and why it is SETTLED: NO)++This file exists because on 2026-07-31 the maintainer AI silently re-enabled AUMID stamping+(v1.9.245) two days after John spent those two days getting pup to STOP using AUMID, and John had+to catch it himself: "we just spent 2 days having you stop using aumid and you just started using+aumid again? i'm so confused by you." Never again. Read this whole file before touching window+identity.++## What AUMID is (Windows facts, all verified the hard way)++- AppUserModelID is a per-WINDOW property (settable via the window's property store). The Windows+  taskbar groups windows by it: same AUMID = one button, distinct AUMIDs = separate buttons.+  Unstamped windows group by their process.+- It is per-HWND, NOT per-process: separate taskbar buttons do NOT require separate Chrome+  profiles/processes. (True, but irrelevant - see the decision.)+- Changing a window's AUMID makes the shell DESTROY and RECREATE its taskbar button. The button's+  art is baked at creation and never re-read (WM_SETICON, SHChangeNotify, FRAMECHANGED etc. all+  do nothing). This destroy/recreate is the single most invasive thing pup ever did to a window.+- Taskbar PINS bind to one AUMID forever: stamp a new id and the user's pin now points at a ghost.+- A tab DRAGGED OUT of a stamped window creates a fresh window that inherits NOTHING - it appears+  as a plain Chrome button until re-stamped (John caught this live).+- The jump-list flyout on freshly stamped windows blips (shows ~200-300ms then vanishes; second+  right-click works). Four hypotheses eliminated, never fixed, tracked on AD.++## The timeline++1. **Enthusiasm.** John: "if AUMID can change a taskbar icon, why aren't we just doing it that+   way?" Per-session AUMIDs (v1.8.81), then category-carrying AUMIDs with re-stamp on category+   flip (v1.9.27), wiki logged-in/out icon flips, jump lists, the works.+2. **The machinery metastasized.** Re-stamps on SPA navigation, re-registration churn, adoption+   re-stamps for dragged-out tabs, flip cooldowns to stop buttons being destroyed mid-use.+3. **The reckoning (2026-07-29).** John: "you crash chrome with the way you control it... what are+   you doing that's so different trying to control chrome that you cause crashes and freezes in+   the first place? that's where you should be looking." Answer: all the cosmetic shell machinery+   is cross-process traffic synchronizing with Chrome's UI thread.+4. **MINIMAL-TOUCH (v1.9.185).** The honest experiment: stop doing the extra things. No AUMID+   stamps, no jump lists, no progress bars, no title-observer churn.+5. **THE MEASURED VERDICT.** John: "ok, so it seems not branding the icons is making pup perform+   better. i wonder though if doing overlays is gentler rather than having to do AUMID's so could+   you try adding back overlays?" -> **THE SETTLED DECISION: overlays YES (gentle, per-hwnd badge,+   no button destruction), AUMID NO.** v1.9.191 allowed overlays back under minimal-touch.+6. **Cleanup confirmed (2026-07-31 morning).** Zero Adom.Pup* registry keys, every session+   curAppId=null. John: "we stopped using aumid so what are you talking about?"+7. **The relapse (2026-07-31 afternoon, v1.9.245-247).** John asked a QUESTION: "when i say to pup+   'open a window' i generally want them as separate taskbar icons. is that not as feasible now+   that you do everything under the adom-you profile?" The maintainer AI treated the question as a+   mandate and re-enabled per-window AUMID stamping ("stamp-once"), shipping it within minutes,+   without ever saying "this reverses the no-AUMID decision from two days ago." John caught it.+8. **Reverted (v1.9.248).** Back to the settled decision: no AUMID, overlays only, windows group+   by process.++## The settled decision (do not reverse without the process below)++- **pup does NOT stamp AUMIDs. Ever. In any mode.** Windows keep Chrome's own identity; the+  taskbar groups pup windows by Chrome process (one shared-profile process = one button).+- **Branding = the composed overlay badge only** (page favicon + Adom mark, painted per-hwnd via+  ITaskbarList3 SetOverlayIcon). It never destroys a button, and the measured experiment showed+  pup performs better without the identity machinery.+- The dashboard mimic models exactly this: one button per Chrome process, badge = last-painted.++## The unresolved tension (be honest about it, don't "solve" it silently)++John ALSO genuinely wants separate taskbar icons per window. On Windows the ONLY mechanisms are:+(a) per-window AUMID - settled NO; (b) one process per window - loses the shared login profile;+(c) the user's own taskbar setting "Combine taskbar buttons: Never" - user-global, his call, no+pup code involved. These wants CONFLICT. Only John can re-weigh them, with the history in front+of him. If he explicitly re-decides toward (a) with full knowledge, the gentle shape is stamp-once+at creation (no re-stamps, no jump lists) - it existed briefly as v1.9.245-247 and did produce one+button per window with correct badges. But it still reintroduces per-window shell traffic of the+kind the minimal-touch experiment indicted, and it is OFF until he explicitly says otherwise.++## The process rule this file enforces++**Never reverse a settled architectural decision on the strength of an ambiguous message.**+A question ("is that not feasible?") is a question - answer it, with the tradeoff, and ASK before+shipping a reversal. Any change that re-enables something previously torn out after days of pain+MUST (1) name the decision being reversed and when it was made, (2) state what problem that+decision was solving and why it will not come back, and (3) get John's explicit go-ahead FIRST.+"He asked for the outcome this enables" does not count as consent to the mechanism he banned.
dev-skills/pup-bridge-debug/SKILL.md+29−29
@@ -1,6 +1,6 @@ --- name: pup-bridge-debug-description: "DEVELOPER debugging playbook for a MISBEHAVING Adom Desktop Puppeteer (pup) bridge — read this the moment pup feels broken, BEFORE re-diagnosing from scratch. Covers: pup verbs time out / hang, the AD bridge LED is dim, browser.bridgeRunning:false, the bridge keeps restarting / respawning every ~60s, opens succeed but the window vanishes or browser_list_windows is empty seconds later, a cold-start that never finishes, 'Chrome process exited immediately' storms, and how to tell a pup self-crash from AD reaping. Encodes the 2026-07-19 root causes (concurrent-launch wedge, crash-poison recovery loop, restart-reap-vs-respawn) so nobody re-derives them. Trigger words: pup timing out, pup verbs hang, pup not responding, pup bridge down, dim LED, bridgeRunning false, pup keeps restarting, pup respawn loop, bridge flapping, spawned with no reap, bridge_log_read, cold start stuck, Chrome exited immediately, window disappears, list_windows empty, pup wedged, recoverSessions crash, SingletonLock collision, pup diagnose."+description: "DEVELOPER debugging playbook for a MISBEHAVING Puppeteer (pup) bridge — read this the moment pup feels broken, BEFORE re-diagnosing from scratch. Covers: pup verbs time out / hang, the AD bridge LED is dim, browser.bridgeRunning:false, the bridge keeps restarting / respawning every ~60s, opens succeed but the window vanishes or pup_list_windows is empty seconds later, a cold-start that never finishes, 'Chrome process exited immediately' storms, and how to tell a pup self-crash from AD reaping. Encodes the 2026-07-19 root causes (concurrent-launch wedge, crash-poison recovery loop, restart-reap-vs-respawn) so nobody re-derives them. Trigger words: pup timing out, pup verbs hang, pup not responding, pup bridge down, dim LED, bridgeRunning false, pup keeps restarting, pup respawn loop, bridge flapping, spawned with no reap, bridge_log_read, cold start stuck, Chrome exited immediately, window disappears, list_windows empty, pup wedged, recoverSessions crash, SingletonLock collision, pup diagnose." ---  Parent skill: **pup-bridge-dev** (build/publish). Companion: **pup** (user skill).@@ -29,7 +29,7 @@ All are AD-**core** verbs (they do NOT route through pup, so they answer even wh Trust the **LED + `browser.bridgeRunning` + a real verb reply**, never `bridge_list status`.  **Hygiene:** do NOT spam a sick bridge. Every pup verb against a down/wedged bridge burns a full 30s-AD relay timeout and *adds* congestion (a retry loop firing `browser_open_window` every few seconds+AD relay timeout and *adds* congestion (a retry loop firing `pup_open_window` every few seconds was itself part of the wedge on 2026-07-19). Use the AD-core tools above (they're instant), read the log, THEN act once. @@ -39,7 +39,7 @@ log, THEN act once.   is down or wedged. Go to §Liveness/restart. - **verbs time out, then briefly work, then time out again; LED flickers** → **flapping** (respawn   loop). Read `bridge_log_read` — spawn-with-no-reap every ~N seconds confirms it. Go to §Crash loop.-- **`browser_open_window` returns `ok:true` (full response) but seconds later `browser_list_windows`+- **`pup_open_window` returns `ok:true` (full response) but seconds later `pup_list_windows`   is empty / the window is gone / even `desktop_list_windows` (AD-core) can't find it** → the process   is **crashing right after responding**, then respawning fresh with no sessions. Go to §Crash loop. - **cold start never finishes; several opens all say `bridge_starting`; readiness times out for@@ -50,14 +50,14 @@ log, THEN act once. ## §Liveness/restart — `restart_bridge` REAPS; only some verbs RESPAWN (do NOT re-derive)  `restart_bridge` (and a crash) leaves pup **down** until AD auto-spawns it on the next call. But AD-spawns it for **window/tab verbs** (`browser_open_window`, `browser_list_windows`), **NOT** for a-bare status probe (`browser_readiness`, `status`). So **polling `browser_readiness` to "wait for pup+spawns it for **window/tab verbs** (`pup_open_window`, `pup_list_windows`), **NOT** for a+bare status probe (`pup_readiness`, `status`). So **polling `pup_readiness` to "wait for pup to come back" waits forever** — the probe never triggers a spawn and never reaches the down process. This burned ~15 min on 2026-07-19.  **Recovery playbook (a down bridge):**-1. Fire **ONE** `browser_open_window` — it returns `bridge_starting` fast and triggers the spawn.-2. Then poll `browser_list_windows` (also auto-spawning) with **backoff** until it answers.+1. Fire **ONE** `pup_open_window` — it returns `bridge_starting` fast and triggers the spawn.+2. Then poll `pup_list_windows` (also auto-spawning) with **backoff** until it answers. 3. **Do NOT spam opens** while it says `bridge_starting` — one is enough; concurrent opens used to    collide (§Wedge). `refresh_bridges {name:"puppeteer"}` (AD ≥1.9.76) is the download-and-restart    path for UPDATES; `restart_bridge` is kill-and-respawn.@@ -94,7 +94,7 @@ new one every ~60s.  ## §Wedge — the concurrent-launch collision (fixed v1.9.4) -**The pattern:** a cold start that never finishes; multiple `browser_open_window`s all stuck on+**The pattern:** a cold start that never finishes; multiple `pup_open_window`s all stuck on `bridge_starting`; `Chrome process exited immediately` storms. **Cause:** a fresh profile is not in the `browsers` map until its Chrome launch FINISHES (~10-15s), so two near-simultaneous opens for the **same profile** (an AI retry/poll loop, OR two threads sharing a profile) each spawned their OWN@@ -104,7 +104,7 @@ launch. They mutually killed each other; none came up; the bridge looked hung.  **The fix (do not remove):** `_launchInFlight` (Map `profileName`→Promise) in `getOrLaunchBrowser` — the first launch per profile registers a Promise and concurrent callers **JOIN** it instead of racing-a second Chrome. **Corollary for callers/AIs:** never spam `browser_open_window` for the same session+a second Chrome. **Corollary for callers/AIs:** never spam `pup_open_window` for the same session while it says `bridge_starting`.  ## §Jump-list task does NOTHING when clicked (Windows argv quoting, fixed v1.9.9)@@ -112,7 +112,7 @@ while it says `bridge_starting`. **Symptom:** the user clicks a taskbar jump-list task, a cmd window flashes for ~50ms, and nothing happens. **Cause:** `desktop_set_window_jumplist` task `args` become a COMMAND LINE, and the CLI's argv parser **STRIPS unescaped double quotes**. Raw JSON args-(`browser_wiki_set_view {"sessionId":"x","view":"authed"}`) therefore arrive as+(`pup_wiki_set_view {"sessionId":"x","view":"authed"}`) therefore arrive as `{sessionId:x,view:authed}` and the CLI dies instantly:  ```@@ -123,8 +123,8 @@ argv parser **STRIPS unescaped double quotes**. Raw JSON args ONE intact argument. Any jump-list task passing JSON MUST do this:  ```js-const taskArgs = (v) => `browser_wiki_set_view "${JSON.stringify({ sessionId, view: v }).replace(/"/g, '\\"')}"`;-// → browser_wiki_set_view "{\"sessionId\":\"wiki-demo\",\"view\":\"authed\"}"+const taskArgs = (v) => `pup_wiki_set_view "${JSON.stringify({ sessionId, view: v }).replace(/"/g, '\\"')}"`;+// → pup_wiki_set_view "{\"sessionId\":\"wiki-demo\",\"view\":\"authed\"}" ```  **Diagnose it in one shot** — run the task's command line yourself via `shell_execute` and read the@@ -154,7 +154,7 @@ in-page `wikiToast` DOM pill replaced `desktop_caption` for wiki-toggle feedback ## ⚠ THE TESTING LESSON (this is how the bug above survived to the user)  **Test the ACTUAL entry point the user hits, not a convenient proxy for it.** The wiki toggle was-"verified" by calling `browser_wiki_set_view` directly over the CLI — which bypasses the jump-list+"verified" by calling `pup_wiki_set_view` directly over the CLI — which bypasses the jump-list command-line segment where the bug actually lived. The verb worked perfectly every time; the feature was 100% broken for the user. Directly-invoking the handler proves the DESTINATION works, never the PATH. For any user-triggered surface (jump-list task, pinned shortcut, protocol handler,@@ -235,7 +235,7 @@ pup misbehaving → **read `bridge_log_read` first.** Spawn-with-no-reap every ~ self-crashing (native crash on boot; suspect on-disk poison in `~/.adom/pup-sessions/*` → the `recoverAttempts` circuit breaker handles it, clearing the dir is the stop-gap). Verbs timing out with a dim LED but AD-core answers = process down; a **status probe won't respawn it** — fire ONE-`browser_open_window` then poll `browser_list_windows`. Cold start stuck across several opens =+`pup_open_window` then poll `pup_list_windows`. Cold start stuck across several opens = concurrent-launch wedge (already fixed by `_launchInFlight`; don't spam opens). `bridge_list status:"running"` is not liveness — the **LED + browser.bridgeRunning + a real verb reply** are. @@ -296,7 +296,7 @@ catching a class the others miss:    Chrome for Testing" is an unbranded pup window. The count check catches duplicate sessions. 2. **Deterministic category**: read each `Adom.Pup.<sid>.<cat>.<gen>` key's `IconResource` and    compare against the category derived from that session's own tab URLs. Get the URLs from-   `browser_list_tabs` PER SESSION - `browser_list_windows` returns a single `url` field, and using+   `pup_list_tabs` PER SESSION - `pup_list_windows` returns a single `url` field, and using    it graded a correct mixed window as a failure. 3. **Pixel** (crop each button's rect, measure it): assert the Adom teal tile is present. This is the    layer that catches a white or generic tile, which is invisible to layers 1 and 2 because the@@ -358,9 +358,9 @@ window. The bug is a live window being category-less at all; fix the session sta  ## Jump list: a "Close ALL Adom Pup windows" task on every window (v1.9.51) -`updateWikiJumplist` now always appends a close-all task (`browser_close "{}"`), so every pup window's+`updateWikiJumplist` now always appends a close-all task (`pup_close "{}"`), so every pup window's taskbar right-click menu can close the whole fleet; wiki windows still get their view-toggle task-above it. `browser_close` with no sessionId kills every pup Chrome, clears sessions, AND now+above it. `pup_close` with no sessionId kills every pup Chrome, clears sessions, AND now unregisters each per-session AUMID + runs `pruneStaleAumids`, so closing all leaves zero stale taskbar identities. @@ -395,13 +395,13 @@ Fix (v1.9.52):   "not yet", not "fall back to the legacy overlay badge"). - **`unregisterPupAumid` clears the AUMID's jump list (`tasks:[]`) before unregistering**, so a   category flip / window close no longer orphans its CustomDestinations file. Both close paths-  (`browser_close`, `browser_close_window`) already unregister, so both now clear.+  (`pup_close`, `pup_close_window`) already unregister, so both now clear. - Verify: after opening a window there must be exactly ONE `Adom.Pup.<sid>.*` key (no `.plain`) and   the pup jump-list file count must equal the window count. `[jumplist] "<sid>" set N task(s) on   <appId>` logs each commit for diagnosis.  To purge historical orphans on a machine, delete pup-task `*.customDestinations-ms` files (they carry-`browser_close` / `browser_wiki_set_view` in plaintext) older than a few minutes; live windows+`pup_close` / `pup_wiki_set_view` in plaintext) older than a few minutes; live windows re-commit theirs within seconds.  ## Jump-list task rows: short titles + the retired default icon (v1.9.53–55)@@ -456,7 +456,7 @@ mechanism is the window's **Relaunch* property trio**, NOT the registry IconReso - `RelaunchIconResource` = the icon the header row shows (AD now derives it from the `iconPath` passed   to `desktop_set_window_identity`). - `RelaunchCommand` MUST also be set or the shell IGNORES the relaunch icon for the header (keeps the-  exe's icon, e.g. Chrome). The bridge already passes `relaunchCommand` (browser_focus_window).+  exe's icon, e.g. Chrome). The bridge already passes `relaunchCommand` (pup_focus_window). - `RelaunchDisplayNameResource` = the header label ("Adom Pup").  The bridge needed NO change — it already passed `iconPath` (category icon) + `relaunchCommand` +@@ -507,7 +507,7 @@ green/yellow/red progress bar on the button (ITaskbarList3 progress). No AD work  ### Duplicate session tags bloat the caption (fixed v1.9.56) -Measured live: `"○ Public · Adom Hydrogen - Adom Wiki (session: hdr-wiki) (session: hdr-wiki)"`+Measured live: `"○ Public · Hydrogen Desktop - Adom Wiki (session: hdr-wiki) (session: hdr-wiki)"` (107 chars). TWO MutationObservers touch this title — the session-suffix injector and the wiki state-glyph injector — and a page that rewrites its own title can let both fire before either sees the other's write, so each appends. The old injector only asked "is my marker absent?", so a second copy@@ -523,7 +523,7 @@ taskbar PROGRESS bar answers it with ZERO AD work: `desktop_taskbar {progress:{s exists, it paints ON the button (glanceable across the whole taskbar), and it is a SEPARATE channel from the overlay icon, so the favicon overlay is untouched. -- `markSessionBusy(sessionId)` is called from the `/command` dispatch for any `browser_*` verb that is+- `markSessionBusy(sessionId)` is called from the `/command` dispatch for any `pup_*` verb that is   not in `PASSIVE_VERBS` (list/status/readiness/configure/prewarm/rescan/wait). A screenshot or an   eval counts as driving; polling does not, or every window would read permanently busy. - State is `indeterminate` (marquee), NOT a percentage. An agent's work has no known length and a@@ -532,7 +532,7 @@ from the overlay icon, so the favicon overlay is untouched.   A thread firing 200 verbs costs 2 AD calls, not 200. Verified in the log:   `[activity] "act-a" agent driving — taskbar progress ON` ... `idle — taskbar progress OFF`. - Cleared on both close paths (`clearSessionBusy`).-- Opt-out: `browser_configure {agentActivity:"off"}` (default on). It is the user's taskbar.+- Opt-out: `pup_configure {agentActivity:"off"}` (default on). It is the user's taskbar.  **Colour check (measured, matters):** both `indeterminate` and `normal` render BLUE (the Windows accent), not red, so the bar reads as "working" and NOT as "error". Verified by painting@@ -684,7 +684,7 @@ John: "do you even log all the actions against you?" — fair hit. The log recor (park / identity / overlay / jumplist) but never WHAT WAS ASKED of it, so "what foregrounded this window?" was unanswerable after the fact. There is now one compact line per verb: -    [verb] browser_open_window session=parkedge url=https://example.com owner=parktest+    [verb] pup_open_window session=parkedge url=https://example.com owner=parktest  It includes only the args that change behaviour (sessionId, url, foreground, foregroundReason, view, tabId, owner) and skips `bridge_log_read` so reading the log does not pollute it.@@ -732,16 +732,16 @@ renderer; the window went unresponsive and 5 crashed CfT processes lingered at 8    profile's `--user-data-dir` after the tree kill, catching orphaned/crashed renderers that    `taskkill /T` misses. Scoped to the profile dir, so it can never touch the user's real browser. -Recovery: `browser_reload` / `browser_navigate` accept a crashed tab (`allowCrashed`) and clear the+Recovery: `pup_reload` / `pup_navigate` accept a crashed tab (`allowCrashed`) and clear the flag once the renderer respawns.  Verified: crash a tab -> first screenshot returns `errorCode:tab_renderer_crashed` (not a timeout);-sibling tab still evaluates; `browser_navigate` to a real URL recovers it; close leaves 0 profile+sibling tab still evaluates; `pup_navigate` to a real URL recovers it; close leaves 0 profile procs (measured 10 -> 0 via `process_list`, NOT PowerShell — a `powershell -File` invocation kept dropping to an interactive banner here; `cmd /c` and AD's `process_list` verb are reliable). -**Test gotcha:** do NOT recover a crashed tab with `browser_reload` if you crashed it by navigating to-`chrome://crash` — reload re-loads `chrome://crash` and re-crashes. Use `browser_navigate` to a real+**Test gotcha:** do NOT recover a crashed tab with `pup_reload` if you crashed it by navigating to+`chrome://crash` — reload re-loads `chrome://crash` and re-crashes. Use `pup_navigate` to a real URL, or reload only when the tab's real URL is benign.  ## The bridge "crashed" toast after closing several windows (v1.9.81) — never run sync exec on a hot path@@ -762,7 +762,7 @@ Rules:   pings = watchdog respawn). If AD reports a "crash" but there's no stack in the log, hunt for a   BLOCKING call first. - Verification that catches this: after any change to close/launch paths, do a burst test — N rapid-  `browser_close_window` calls each immediately followed by `ping`, asserting round-trip stays+  `pup_close_window` calls each immediately followed by `ping`, asserting round-trip stays   sub-second (measured ~600ms healthy; the sync version stalls multi-second). - PowerShell reliability on user boxes is NOT a given. `powershell -File` can drop to an interactive   banner (profile damage) and burn its whole timeout. For process queries prefer AD's `process_list`
dev-skills/pup-bridge-dev/SKILL.md+71−71
@@ -1,6 +1,6 @@ --- 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."+description: "DEVELOPER skill for building, publishing, and maintaining the 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, pup_readiness internals, pup release, pup manifest, ship pup bridge." ---  Parent skill: **adom-desktop-puppeteer-bridge**@@ -52,8 +52,8 @@ 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) |+| **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 | **Bridge**, 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 Bridge; 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.@@ -68,11 +68,11 @@ The full chain when a user on a fresh container says *"open my app in pup"*: 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.+3. **The skill calls the bridge.** `pup`'s SKILL.md tells the AI to run `adom-desktop pup_*` verbs.+   AD routes `pup_*` 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`.+   cold-start self-heal (chrome.js) installs Chrome for Testing on first `pup_open_window`.  **Two install paths, keep BOTH healthy:** - **Discovery path (above)** — works once `discovery_triggers` include the user phrases (done) + the@@ -138,24 +138,24 @@ degraded path exists). Contract:  ## Cold-start self-heal — the architecture (what 1.1.0 added, the reason this repo exists) -On a fresh PC (Hydrogen 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+On a fresh PC (HD bundles AD; the installer pre-installs neither Node nor Chrome for Testing), the first+`pup_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",+- **`pup_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:"pup_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).+- **`pup_readiness`** (poll) + **`pup_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`.+  call). AD/HD must call `pup_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) @@ -174,10 +174,10 @@ What we learned and baked into the code:   `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+  the browser that actually launched (auto default); `pup_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, but the already-running AD process keeps its OLD PATH — so the very next `pup_*` 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@@ -189,7 +189,7 @@ What we learned and baked into the code: - **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+- **Install REAL Chrome on request.** `pup_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.@@ -197,10 +197,10 @@ What we learned and baked into the code:   `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+  `pup_*` 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;+  showed nothing). Two consequences baked into SKILL.md: (1) prefer core/`pup_*` 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 +@@ -232,17 +232,17 @@ What we learned and baked into the code:   `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`.+  `node.state:ready` (adom-managed), the bridge is `status:running` with a live pid, and `pup_readiness`+  (the declared **statusVerb**) works — but EVERY other `pup_*` 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/+  `pup_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+- **Wedged-bridge diagnosis recipe (learned on AD 1.9.77).** Symptoms: every `pup_*` 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@@ -261,7 +261,7 @@ DESTINATION right is its own discipline. The rule, verified with a live mistake:  **An issue lives on the issue list of the repo that OWNS the code that must change.** -- A pup bug (a `browser_*` verb, a hint, the CfT self-heal, the status chip) is YOURS: fix it here, or+- A pup bug (a `pup_*` verb, a hint, the CfT self-heal, the status chip) is YOURS: fix it here, or   track it on the pup page (`adom/adom-desktop-puppeteer-bridge`). - An AD-CORE gap (a lifecycle verb, spawn / provision / bind behavior, `preserve_client_deps`, the relay   dropping a response field, the stdout capture, `bridge_log_read`, a manifest field AD must honor) goes@@ -306,7 +306,7 @@ FOUR clauses, all absolute, and clause 2 BOUNDS every mechanism built for clause    clever, no matter what problem it claims to solve. If a placement bug needs fixing, fix it at    the SOURCE (creation/park targeting), never with a loop that patrols the foreground afterward. 3. **Every pup-initiated foreground captions its REASON for 2 seconds.** Enforced at the choke-   point: `browser_raise_os_window` captions inline, and `osRaiseSessionWindow(session, sid,+   point: `pup_raise_os_window` captions inline, and `osRaiseSessionWindow(session, sid,    reason)` captions for every other raise path (wiki view toggle, sign-in raise, auto-login,    heal-retry). A new raise path without a reason caption is a bug. (Callers that already narrate,    like assisted_click, pass reason=null to avoid a double caption.)@@ -329,7 +329,7 @@ session's own hwnd) - both source bugs, both fixed at the source. ## Background-by-default without stealing focus — THE SOLVED PLAYBOOK (do NOT re-derive this)  This is the single hardest problem in the bridge and it has been solved TWICE from scratch. If you are-touching `browser_open_window`, window placement, focus, or "the pup window popped up / stole my typing /+touching `pup_open_window`, window placement, focus, or "the pup window popped up / stole my typing / flashed a terminal", **read this whole section before changing anything.** The final design lives in `osBackgroundWindowByPid` + `tryDetachedLaunch` + the raise handler (search those). @@ -338,7 +338,7 @@ flashed a terminal", **read this whole section before changing anything.** The f (a) pops the window over the user's work, (b) steals keyboard focus for the ~1–3s of browser startup, so the user's keystrokes land in the invisible pup window. Requirements that must hold SIMULTANEOUSLY: R1 never visually pop / cover the user's work · R2 the window is reachable (taskbar-clickable) · R3 CDP-`browser_screenshot` still works · R4 never steal keyboard focus · R5 no console/terminal flash.+`pup_screenshot` still works · R4 never steal keyboard focus · R5 no console/terminal flash.  **The working design (v1.8.51+). The KEY insight, found after a week of warring with re-park loops: `--no-startup-window` eliminates the focus steal at the SOURCE, so there is NO war to fight.**@@ -415,7 +415,7 @@ algorithm wins):**    (`osBottomUnlessForeground`) exists ONLY because z-order has no direct API yet; it also reads    `GetForegroundWindow` — the ONE honest "did the user click it" signal. Never grow the PS surface.    (`AttachThreadInput` focus-handback machinery still exists in the legacy helpers used by-   `browser_lower_os_window`; with `--no-startup-window` nothing steals focus, so new code never needs it.)+   `pup_lower_os_window`; with `--no-startup-window` nothing steals focus, so new code never needs it.) 4. **What little PowerShell remains runs HIDDEN** → satisfies R5. `execFileSync('powershell.exe',    [...args], {windowsHide:true})` + `-WindowStyle Hidden` (see `runPsHidden`), NEVER    `execSync('powershell …')` — that goes through cmd.exe and flashes a console window. (A cmd blip@@ -446,7 +446,7 @@ bridge update. Normal users only hit this on AD auto-updates; apps recover by re If this starts mattering, file it against AD (process-tree kill) — do not "fix" it by re-parenting Chrome in pup. -**`browser_raise_os_window` is the inverse:** move the window on-screen + maximize via CDP+**`pup_raise_os_window` is the inverse:** move the window on-screen + maximize via CDP `Browser.setWindowBounds`, then foreground it. If you ever add a non-activatable style (see below), raise must clear it first. @@ -528,7 +528,7 @@ must clear it first. - **Minimize→re-show loop as the focus drop** → on an on-screen window the minimize/restore is a visible   flicker; a burst of retries strobes. The `AttachThreadInput` handback needs no minimize, so no flicker. - **A pup-side localhost reachability probe** → redundant; AD core already reprimands a localhost URL when-  it fails to load (and Hydrogen's working localhost passes clean). Don't re-add it.+  it fails to load (and HD's working localhost passes clean). Don't re-add it.  ## The user↔AI balance — background for the AI, but the HUMAN can take any window (John's rule) @@ -551,7 +551,7 @@ the user. Every detection signal was shipped and every one false-fired (`GetLast 1. **During the ~2s launch gap** a taskbar click may show nothing (window still off-screen) or be    parked once. The user clicks ONCE more and the window comes up — cheap, bounded, honest. 2. **After the park, the user owns every raise, forever.** Nothing in pup can background a window-   the user brought up. The only re-park paths left are the explicit `browser_lower_os_window`+   the user brought up. The only re-park paths left are the explicit `pup_lower_os_window`    verb and the off-screen SELF-HEAL (which checks bounds first and never touches an on-screen    window). @@ -602,7 +602,7 @@ build, no branded skin), but its safety comes from these flags, not from any inh    profile-creation offers after login, profile-consolidation prompts, sync nags. None of that    exists in CfT. This incident is the concrete case for wiki issue #202 (prefer CfT): the pick    order should converge to **cached CfT → installed Chrome → Edge → CfT-download-last-resort**,-   with a background `browser_prewarm` whenever a branded fallback was used, so every box reaches+   with a background `pup_prewarm` whenever a branded fallback was used, so every box reaches    the deterministic CfT state by its second session without ever stalling a first open. 4. **Visual demarcation matters to the human.** Part of the confusion was that pup's window WAS    branded Chrome — indistinguishable from his real browser (plus a surprise green profile window).@@ -638,7 +638,7 @@ no Chrome logo, no CfT "Test" icon. Prototyped live on AdomLapper; every mechani   register BEFORE any window exists (install time), and know that a just-created .lnk needs an   Explorer restart to be seen (prototype confirmed: even fresh windows + explorer bounce weren't   enough alone — see the handle lesson below, which contaminated that test).-- **Pinning trap SOLVED**: point the registered .lnk at `adom-desktop-cli.exe browser_open_window+- **Pinning trap SOLVED**: point the registered .lnk at `adom-desktop-cli.exe pup_open_window   {...}` — a pinned "Adom Pup" then launches a REAL managed pup window through the bridge instead   of a bare orphan CfT. @@ -654,11 +654,11 @@ mechanical requirement.  **The production design (AD feature request):** 1. AD installer registers the "Adom Pup" Start Menu .lnk (AUMID `Adom.Pup`, adom-pup.ico, target =-   adom-desktop-cli browser_open_window) — warm AppResolver cache before any window ever exists.+   adom-desktop-cli pup_open_window) — warm AppResolver cache before any window ever exists. 2. New AD verb `desktop_set_window_identity {hwnd|titleContains, appId, iconPath}`: AD loads the    .ico ONCE (handles live as long as AD runs), stamps the AUMID property + WM_SETICON per window. 3. pup calls it at park time, same place as the badge; overlay badge retired for pup windows-   (kept for explicit branded `browser_use` pins, where the base icon is genuinely Chrome/Edge).+   (kept for explicit branded `pup_use` pins, where the base icon is genuinely Chrome/Edge). Rejected: patching CfT's exe icon resource (breaks signature/sha validation, SmartScreen/AV risk).  ## Per-session AUMID registration + the wiki view-toggle jump list (#207/#208, v1.9.2–1.9.3)@@ -673,7 +673,7 @@ UNREGISTERED per-session AUMID (`Adom.Pup.<sessionId>`, which split mode uses so its own taskbar button) fell back to the chrome.exe icon. `shortcut:false` registers the AUMID **registry-only** — no Start Menu .lnk, so no launcher spam for these throwaway per-window ids. pup registers on window create (`registerPerSessionAumid`, fired from `stampPupIdentity` in split-mode), tracks live ids in `_registeredSessionAumids`, and unregisters in `browser_close_window`.+mode), tracks live ids in `_registeredSessionAumids`, and unregisters in `pup_close_window`. The base `Adom.Pup` stays the shortcut-registered pinnable launcher (grouped mode uses it).  **#208 — `desktop_set_window_jumplist {appId, tasks:[{title, target?, args?, iconPath?,@@ -684,22 +684,22 @@ AD/bridge verb + JSON** — the click routes back through the LOCAL AD to pup wi round-trip. `tasks:[]` clears. The AUMID must be registered (from #207) or the shell ignores the list. pup uses this for the **wiki view toggle**: `updateWikiJumplist(session, sessionId)` sets one task on any Adom-URL window — "🔓 Switch to logged-in view" on a public window, "📖 Switch to-public view" on an authed one — with `args: browser_wiki_set_view {"sessionId":..,"view":..}`.+public view" on an authed one — with `args: pup_wiki_set_view {"sessionId":..,"view":..}`. Called on open (900ms after brand, so the AUMID is registered) and on every navigate (adds on a wiki page, clears when the window leaves Adom); split mode only; idempotent per (session,view) via `session._wikiJumplistView`; self-gates via `_jumplistSupported`. -**The callback — `browser_wiki_set_view {sessionId, view}` (pup verb).** A view switch is a+**The callback — `pup_wiki_set_view {sessionId, view}` (pup verb).** A view switch is a cookie-jar (userDataDir) switch, and the profile is FIXED at browser launch, so there is NO in-place cookie swap: it's a **close-and-relaunch** of the same sessionId under the other profile (`WIKI_AUTH_PROFILE` for authed, `session._publicProfile || sessionId` for public) at the same URL. It's a USER action (they clicked the taskbar task), so the relaunched window is brought on-screen — via CDP place+maximize + `bringToFront` (same on-screen move as the raise handler, no-PowerShell), NOT the gated `browser_raise_os_window` path (which would demand a foregroundReason a+PowerShell), NOT the gated `pup_raise_os_window` path (which would demand a foregroundReason a click can't supply). It re-checks auth, refreshes the in-tab glyph/brand/overlay/jumplist, and if the authed jar hasn't done the one-time SSO yet returns `wikiLoginNeeded` so the user logs in once in that window. AI threads should NOT call this verb — they pass `wikiView` on-`browser_open_window`; the verb exists for the taskbar affordance.+`pup_open_window`; the verb exists for the taskbar affordance.  ## Liveness, restart, and the concurrent-launch wedge (2026-07-19 — do NOT re-derive) @@ -707,13 +707,13 @@ in that window. AI threads should NOT call this verb — they pass `wikiView` on > misbehaving (verbs timing out, dim LED, respawn loop, window vanishing). The notes below are the > condensed version kept here for continuity. -A live incident that ate an hour: after a `restart_bridge`, pup's verbs (`browser_readiness`,-`browser_describe`, `browser_open_window`) all timed out for MINUTES and the AD GUI LED was dim.+A live incident that ate an hour: after a `restart_bridge`, pup's verbs (`pup_readiness`,+`pup_describe`, `pup_open_window`) all timed out for MINUTES and the AD GUI LED was dim. Three separate lessons, all now fixed/documented:  **1. The concurrent-launch wedge (pup's OWN bug, fixed v1.9.4 — the real root cause).** A fresh profile is not in the `browsers` map until its Chrome launch FINISHES (~10-15s). So two-near-simultaneous `browser_open_window`s for the same profile (an AI retry/poll loop, OR two+near-simultaneous `pup_open_window`s for the same profile (an AI retry/poll loop, OR two threads sharing a profile) each entered `getOrLaunchBrowser`, saw no cached browser, and each spawned its OWN detached Chrome onto the SAME `--user-data-dir`. They then collided on Chrome's `SingletonLock`, and each launch's lock-recovery `taskkill`ed "the orphan holding the profile" —@@ -722,7 +722,7 @@ bridge looked hung. **Fix:** `_launchInFlight` (Map profileName→Promise) — t registers a promise; concurrent callers JOIN it (`return await`) instead of racing a second Chrome. If you ever see "Chrome process exited immediately" storms or a wedged cold-start under load, check this dedup first. NEVER remove it. (Corollary for callers: do NOT spam-`browser_open_window` while AD says `bridge_starting` — fire ONE and poll.)+`pup_open_window` while AD says `bridge_starting` — fire ONE and poll.)  **2. LED / liveness — which signal is TRUTH.** The AD GUI bridge LED is ACCURATE: dim = the pup PROCESS isn't actively serving. Corroborating truth signals: AD `status` → `browser.bridgeRunning`,@@ -733,10 +733,10 @@ it's desired/installed state and will say "running" even when the process is dow  **3. `restart_bridge` REAPS; only some verbs RESPAWN.** `restart_bridge` (and a crash) leaves pup down until AD auto-spawns it on the next call — but AD spawns it for WINDOW/TAB verbs-(`browser_open_window` et al.), NOT for a bare status probe like `browser_readiness`/`status`. So-polling `browser_readiness` to "wait for pup to come back" waits FOREVER (the probe never triggers-a spawn and never reaches the down process). **Recovery playbook:** fire ONE `browser_open_window`-(it returns `bridge_starting` fast and triggers the spawn), THEN poll `browser_list_windows` (also+(`pup_open_window` et al.), NOT for a bare status probe like `pup_readiness`/`status`. So+polling `pup_readiness` to "wait for pup to come back" waits FOREVER (the probe never triggers+a spawn and never reaches the down process). **Recovery playbook:** fire ONE `pup_open_window`+(it returns `bridge_starting` fast and triggers the spawn), THEN poll `pup_list_windows` (also auto-spawning) with backoff until it answers. `refresh_bridges {name:"puppeteer"}` (AD ≥1.9.76) downloads-and-restarts in one step for the UPDATE path; `restart_bridge` is the kill-and-respawn. @@ -763,7 +763,7 @@ this). If you see it, suspect a startup/recovery path crashing on poison on-disk  ## The foreground gate (v1.8.70) — abe-parity dual key, and the AD interception hole -Foregrounding (open `foreground:true`, `browser_raise_os_window`, `browser_focus_window`) requires a+Foregrounding (open `foreground:true`, `pup_raise_os_window`, `pup_focus_window`) requires a `foregroundReason` (min 10 chars — quote/paraphrase of what the user said). Open-time: without it the open SUCCEEDS but stays background (`foregroundDenied`) — abe semantics: withhold the foregrounding, not the work. Raise-time: refused (`foreground_reason_required`). Granted foregrounds are logged@@ -771,10 +771,10 @@ not the work. Raise-time: refused (`foreground_reason_required`). Granted foregr `lastForeground {ts,verb,reason}` in status. Mirrors abe's `userRequestedForeground`+`foregroundReason` (also accepted as an arg alias) so the contract is identical across Adom's browser surfaces. -**HOLE CLOSED (AD 1.9.124, issue #163):** AD-core used to INTERCEPT `browser_raise_os_window` /-`browser_focus_window` natively (a pre-extraction fossil, flagged ~July 10 and finally deleted+**HOLE CLOSED (AD 1.9.124, issue #163):** AD-core used to INTERCEPT `pup_raise_os_window` /+`pup_focus_window` natively (a pre-extraction fossil, flagged ~July 10 and finally deleted 2026-07-15) — which bypassed pup's raise gate entirely and let a thread foreground a window with no-reason and no audit trail. As of AD 1.9.124 those verbs relay to pup like every other `browser_*`+reason and no audit trail. As of AD 1.9.124 those verbs relay to pup like every other `pup_*` verb, so the gate + audit trail hold everywhere (verified: reasonless raise → pup's foreground_reason_required refusal; granted raise → `[foreground] GRANTED` log + lastForeground in status). If a reasonless raise EVER succeeds again, suspect a reintroduced AD interceptor first —@@ -792,7 +792,7 @@ The flash SET routes through **AD-core `desktop_flash_window`** (`flashViaAD`); - **The only thing that clears the tint is activating the window.** That's why a user clicking the button   clears it. To clear it *programmatically without a visible pop*, activate it **off-screen**:   move off-screen → `SetForegroundWindow` (invisible, clears the tint) → park back on-screen z-bottom →-  hand focus back (`AttachThreadInput`). This is `osClearFlashByActivate`, wired into `browser_alert_window+  hand focus back (`AttachThreadInput`). This is `osClearFlashByActivate`, wired into `pup_alert_window   {stop}`. It reports `cleared` / `already-foreground` (not a fire-and-forget guess). - **Programmatic flash STATE** (answer "which windows are still flashing?" / "which window hasn't the   user seen?" without a screenshot): `getSessionInfo.flash` = `none|pending|cleared`, from the@@ -829,13 +829,13 @@ Full recipe in `PUBLISHING.md`. The load-bearing rules: ```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+adom-desktop --target <name> pup_readiness '{}'   # proves the new code is live ``` -- **No Node on the box** → `browser_*` returns `node_not_found` before the bridge spawns (AD core; correct).+- **No Node on the box** → `pup_*` 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+  (`pup_open_window` → `chrome_for_testing_installing` → poll `pup_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.@@ -857,7 +857,7 @@ argue from the real, current rationale — not the original one, which has partl driven FIRST — Chrome, else Edge (ships on every Windows PC) — in a fresh isolated profile with the identity-suppression flags. Chrome-for-Testing is fetched (~150 MB, once) ONLY as the last resort when there is no installed Chromium at all (e.g. a Mac with no Chrome). Order lives in `launchCandidates()`;-`warmup()` no longer force-downloads CfT when a native browser exists. A local `browser_use {browser:"cft"}`+`warmup()` no longer force-downloads CfT when a native browser exists. A local `pup_use {browser:"cft"}` pin still selects CfT explicitly — for a managed/enterprise box whose policies interfere with automation.  **Frame the question correctly.** pup wants a GENERIC, anonymous, disposable window — a fresh empty@@ -914,7 +914,7 @@ sign-in dialog" is NO LONGER a valid argument for CfT-first — the flags handle  - The no-download fast path on a fresh box (system browser used immediately). Native is now the   DEFAULT, so this is simply the common path — CfT is no longer prewarmed to "converge" the box.-- A CfT build regression — pin native with `browser_use {browser:"chrome"|"edge"}` as an escape hatch.+- A CfT build regression — pin native with `pup_use {browser:"chrome"|"edge"}` as an escape hatch. - Diagnostics like ruling CfT out as the cause of a freeze.  ### Bottom line (the decision, FLIPPED 2026-07-24)@@ -932,7 +932,7 @@ branded). The freeze that triggered this review was NOT a CfT bug — it was the renderers (fixed v1.9.89), so it was never a reason to keep or drop CfT either way.  **So the fleet default is native-first.** CfT stays a first-class option for the one case it earns its-weight (a policy-bound managed browser), reachable by `browser_use {browser:"cft"}`.+weight (a policy-bound managed browser), reachable by `pup_use {browser:"cft"}`.  ### The rule @@ -940,19 +940,19 @@ Native-first is the fleet default (installed Chrome → Edge → CfT-last-resort `warmup()`). Do NOT flip back to CfT-first without NEW evidence that native is genuinely failing for the user base (e.g. a wave of policy-bound-Chrome breakage) — the identity-dialog argument is dead (flags fix it on any binary) and the freeze was our own reap bug. If a specific box has a policy-bound or broken-Chrome, that is a per-machine `browser_use {browser:"cft"}` pin, not a fleet reversal. Keep the bundled+Chrome, that is a per-machine `pup_use {browser:"cft"}` pin, not a fleet reversal. Keep the bundled puppeteer's CfT build current regardless, so the last-resort binary isn't ancient.  ## ⛔ TEARDOWN + SWEEP DOCTRINE (2026-07-24 — this froze a user's laptop; do NOT re-derive) -A `browser_close` left orphaned Chromium renderers spinning at ~a full core each. On John's 16-core+A `pup_close` left orphaned Chromium renderers spinning at ~a full core each. On John's 16-core laptop overall CPU hit 88-98%, `dwm` (the cursor compositor) was starved to 38-63%, **the mouse froze and he had to reboot.** A second user independently reported pup at 90% CPU. James's issue #14 (three sessions running two days, battery drained) was the same root cause. Four rules came out of it, and they generalize to ANY cleanup code in this bridge.  **1. NEVER fire-and-forget a cleanup whose success you report.** The old reap was-`execFile(...)` with no `await`, so `browser_close` returned `ok` *before* the sweep finished or+`execFile(...)` with no `await`, so `pup_close` returned `ok` *before* the sweep finished or failed. A close that left processes behind still said `ok`. If a caller can't distinguish "cleaned up" from "gave up", the verb is lying. **Await the cleanup, then report a COUNT** (`chromeProcsRemaining`, which must be 0) so the caller can verify rather than trust.@@ -983,7 +983,7 @@ keys: `pid`/`commandLine`/`parentPid`) to count real OS processes before and aft  ## Resolvers must be PURE — the session-map purge (issue #16, fixed v1.9.92) -Symptom that made no sense: `browser_list_windows` reported **0 windows** while those windows were open+Symptom that made no sense: `pup_list_windows` reported **0 windows** while those windows were open on screen and fully drivable by `sessionId`. It survived `bridge_kill` + respawn, so it looked like persistent poison. It was neither a crash nor a wedge — it was a **read accessor deleting its own data.** @@ -1006,7 +1006,7 @@ function isSessionAlive(s) {            // OLD  `handleBrowserDisconnect()` does `browsers.delete(profileName)` on ANY CDP blip (sleep/wake, renderer crash, puppeteer hiccup) while deliberately KEEPING the sessions (`_lostBrowser = true`) and logging-"run browser_rescan to recover". So one blip made every session on that profile read as dead, and the+"run pup_rescan to recover". So one blip made every session on that profile read as dead, and the next verb that omitted a `sessionId` walked the map and **deleted the very entries the disconnect handler had just preserved.** `bridge_kill` didn't help because the respawn recovered from disk and then the first resolver call wiped it again.@@ -1026,7 +1026,7 @@ then the first resolver call wiped it again. snippet and run old-vs-new side by side. Mine printed `OLD -> sessions remaining: 0` / `NEW -> 2`, which proved the mechanism in seconds without touching the live box. Then confirm on the real machine: kill a window's actual Chrome via `process_kill`, call a NO-sessionId verb (the trigger), and assert-`browser_list_windows` still lists everything.+`pup_list_windows` still lists everything.  ## Multi-maintainer repo safety — last-push-wins WILL destroy work (issue #19) @@ -1092,7 +1092,7 @@ So pup ASKS for reports, in the place the AI actually reads (the SDK's first pri your OUTPUT, never your on-disk skill). `_reportIssues` carries the repo slug, the exact `adom-wiki issue list` / `issue create` commands, and what evidence to include (verbatim response JSON + bridge version + OS). It also routes AD-core problems to `adom/adom-desktop` so reports land on the repo-that owns the code. Opt out: `browser_configure {reportIssues:"off"}`.+that owns the code. Opt out: `pup_configure {reportIssues:"off"}`.  **Throttling is the whole design.** A line on every response trains the caller to skip the field; a line on every FAILURE is a bug report waiting to be filed. So: always on a failure (evidence still in hand),@@ -1218,7 +1218,7 @@ observation exercises identity registration, the window stamp, the jump list and single shot, and it is the cheapest possible regression net for this bridge:  ```bash-adom-desktop --target <box> --ai-thread "<you>" browser_open_window '{"sessionId":"smoke","url":"https://example.com"}'+adom-desktop --target <box> --ai-thread "<you>" pup_open_window '{"sessionId":"smoke","url":"https://example.com"}' adom-desktop --target <box> --ai-thread "<you>" bridge_log_read '{"name":"puppeteer","lines":40}'   # expect "stamped appId=", NOT "stamp FAILED" adom-desktop --target <box> --ai-thread "<you>" desktop_find_window '{"titleContains":"session: smoke"}'  # rect.left must NOT be ~-32768 ```@@ -1276,7 +1276,7 @@ foreground the window" — blaming the OS for a human taking their focus back. H foregrounded window almost immediately, every time, because he runs many threads. Required: toast + caption + an explicit "don't type or click until I say done" + wait-for-foreground + **re-verify before EACH event** + caption when done. `ensureSafeForNativeInput()` enforces it in the bridge;-`browser_assisted_click` is the ready-made path. Prefer CDP input always — it needs no foreground.+`pup_assisted_click` is the ready-made path. Prefer CDP input always — it needs no foreground.  **Autonomous machinery needs three safety rails, or it becomes the bug.** Twice in one day something I built to "help" broke the user's windows: the tab reconciler judged live tabs dead and destroyed two@@ -1286,10 +1286,10 @@ evidence, never on inference; (2) require the fault on TWO consecutive observati anything user-visible, and refuse a change that would be wholesale (e.g. a reconcile that would empty a session is a probe failure, not reality). -**Never invent UI that Adom already has.** I hand-drew a pup cursor instead of using Hydrogen's. The+**Never invent UI that Adom already has.** I hand-drew a pup cursor instead of using HD's. The canonical AI cursor is `adom-browser-extension/extension/src/cdp.js` → `cursorScript` (itself a-byte-faithful copy of Hydrogen's `hd-control` AI_CURSOR_JS). Copy it; do not redesign it. Same for anything-with a brand surface — check Hydrogen and the extension FIRST.+byte-faithful copy of HD's `hd-control` AI_CURSOR_JS). Copy it; do not redesign it. Same for anything+with a brand surface — check HD and the extension FIRST.  **Prove taskbar/visual work ON GLASS, never from the API.** The badge API reported `favicon+adom` success while the badge was literally invisible (dark favicon on a dark plate). Three revisions were@@ -1453,7 +1453,7 @@ misbehaving rather than as an agent choosing to interrupt. Related rules that travel with this one: - Native OS input (`desktop_click` / `desktop_type`) needs the full warn-and-wait protocol, not just a   caption, because it goes to whatever window has focus.-- `browser_assisted_click` already implements toast + caption + wait-for-ready + re-verify. Use it+- `pup_assisted_click` already implements toast + caption + wait-for-ready + re-verify. Use it   instead of hand-rolling raise-then-click.  ## The FG-STEAL GUARD (John, 2026-07-31: "why did that pup window that just opened disrupt me in the foreground?")@@ -1470,7 +1470,7 @@ dead in the normal mode - that gap is exactly how the adom-theme window disrupti  ## The pup dashboard layout (John's spec, non-negotiable) -The dashboard (`/dashboard`, opened via `browser_dashboard` in its OWN window with its own icon) MUST+The dashboard (`/dashboard`, opened via `pup_dashboard` in its OWN window with its own icon) MUST present three levels, top to bottom:  1. **A mimic of the Windows taskbar at the very top** - one button per REAL taskbar icon, in the SAME
dev-skills/pup-bridge-test/SKILL.md+67−10
@@ -26,7 +26,7 @@ three were patched in 1.9.151 and docs-writer still mis-bound). Rules that apply to EVERY scenario (each one was paid for):  - **Up-gate first.** After any install/restart, wait until a REAL verb answers — poll-  `browser_list_windows` until `count` is non-null. `bridge_list` showing the version only proves+  `pup_list_windows` until `count` is non-null. `bridge_list` showing the version only proves   the file landed; the bridge may still be in its npm-reconstruct window and every test will   false-fail (it torched two full runs on 2026-07-26). - **Never `>/dev/null` an open.** Capture `ok` + `error` + `errorCode` per call. Blind opens cost@@ -53,7 +53,7 @@ Rules that apply to EVERY scenario (each one was paid for):   Follow it with the post-raise geometry table: revealing must NOT change the standard frame.  - **EVERY test table includes the OVERLAY column** (John's standing rule, 2026-07-27): per-  window, report the `overlay` field from `browser_list_windows` — `favicon:<host>` (the active+  window, report the `overlay` field from `pup_list_windows` — `favicon:<host>` (the active   tab's icon, the standard), `count:N` (no favicon; tab-count badge fallback), or `none` with   its reason. After ANY reveal phase, re-read it: raising redraws the taskbar button and v1.9.150   re-wears the overlay at +1.2s — a `none` after a raise on a favicon-serving page is the@@ -61,7 +61,7 @@ Rules that apply to EVERY scenario (each one was paid for):  - **The post-open table's full column set** (John, 2026-07-27): sessionId | position | WxH |   overlay | **flashCount** | **progressBarCount** | **progressBarMs** — all read straight from-  `browser_list_windows` (v1.9.152+). Expected for a plain background open: flashCount exactly 1,+  `pup_list_windows` (v1.9.152+). Expected for a plain background open: flashCount exactly 1,   progressBar count/ms reflecting only real agent driving (a giant progressBarMs on an idle window   = a stuck busy-state). After a reveal phase, re-read: overlay must survive, flashCount must NOT   have grown unless a new operation ran.@@ -76,7 +76,7 @@ Open (no sessionId, thread-identified) → eval `1+1` → screenshot → close.  ## 2. The 5-thread simulation ("the John test", 2026-07-27) Five `sim-*` threads open windows with NO sessionIds and differing tab counts (extra opens from-the same thread must return `openedAsTab:true`). Then: bare `browser_raise_os_window` on three of+the same thread must return `openedAsTab:true`). Then: bare `pup_raise_os_window` on three of them (MUST be denied), reasoned raise (MUST be granted), eval sweep on all five (no crashes), wait 10s, close two as their own threads, verify the survivors' exact tab counts and that the closed pair is gone.@@ -93,7 +93,7 @@ logged per window.  ## 4. The 3-phase test (open → reload-all → fg-sweep) 10 heavy PARALLEL opens (this is what found the sibling-kill + Chrome-exit chain), wait 10s,-reload ALL in parallel, wait 20s, `browser_raise_os_window` sweep with a quoted+reload ALL in parallel, wait 20s, `pup_raise_os_window` sweep with a quoted `foregroundReason`, 1s apart. **Expect:** 30/30 OKs. Any 'Navigating frame was detached' or 'Session not found' = the serialization regressed (`_profileOpenChain`).@@ -120,7 +120,7 @@ close window → kill the profile's Chrome → reopen → still signed in. 2026-07-26; use the-internet.herokuapp.com for repeatable runs.)  ## 9. Cross-thread pollution-ALPHA opens a window; BETA calls `browser_open_tab` with NO sessionId, then WITH ALPHA's id.+ALPHA opens a window; BETA calls `pup_open_tab` with NO sessionId, then WITH ALPHA's id. **Expect:** first refused `no_target_window`; second refused `session_owned_by_another_thread`; ALPHA's tab count unchanged both times. @@ -128,13 +128,13 @@ ALPHA's tab count unchanged both times. Root cause established live: John's "pup seems frozen" is USUALLY a **CDP jam, not a crash**. Under heavy load (many heavy SPAs on the one shared Chrome + memory pressure) Chrome stays ALIVE (check `Get-Process chrome` count — it's ~45, not 0) but every CDP verb times out. The pre-1.9.172-`browser_list_windows`/`browser_status` did uncapped CDP reads (`page.title()` + a focus `evaluate`)+`pup_list_windows`/`pup_status` did uncapped CDP reads (`page.title()` + a focus `evaluate`) SERIALLY per window, so ONE busy window hung the whole handler and the CLI returned `count: null` — the frozen symptom. Fixed 1.9.172: both verbs gather sessions in PARALLEL with a 2.5s per-session timeout and an in-memory `getSessionInfoFast` fallback; busy windows come back marked `degraded`. **Test:** open 5-6 heavy windows (maps, earth.google, youtube, figma, diagrams), force memory-pressure (`browser_eval` pushing `new Uint8Array(1MB)` x50 per window), and hammer-`browser_list_windows` x15 WHILE the allocations run.+pressure (`pup_eval` pushing `new Uint8Array(1MB)` x50 per window), and hammer+`pup_list_windows` x15 WHILE the allocations run. **Expect:** 15/15 return the full list (never `null`/timeout), each < ~2.5s; `_degraded` appears only if a window is genuinely mid-jam. Proven 1.9.173: 15/15 live, 37-226ms, 0 null. @@ -151,7 +151,7 @@ off a 50-line log read under load — the count-based check is the reliable proo  ## 12. TORTURE (the "never crash" push, 2026-07-28) The full brutal load, with an INDEPENDENT monitor thread proving pup never goes frozen mid-storm.-From thread `pup-monitor`, sample `browser_list_windows` every 3s for the whole run. From thread+From thread `pup-monitor`, sample `pup_list_windows` every 3s for the whole run. From thread `pup-torture`, open 6 heavy windows and run several rounds of {50MB alloc x6 + navigate churn to earth/youtube}. Then settle 40s and check recovery. **Expect:** the monitor's null/timeout count is **0** across every sample (pup is NEVER frozen to a@@ -167,3 +167,60 @@ failures" on 2026-07-26 were the harness (npm window, outer timeout), not the br distinguishing jam vs crash: `shell_execute` still answers when only the CDP path is jammed (the Node event loop is fine), so `Get-Process chrome | Measure` tells you alive-but-jammed (~45 procs) vs actually-crashed (0) in one call — do this BEFORE assuming a crash.++## ⛔ Testing rule: a TEST may never fire native OS input without warning the user++Every scenario here runs on John's real machine while he is working. `desktop_click` / `desktop_type`+/ `desktop_press_key` go to **whatever window has OS focus**, so a test that fires them blind types+into whatever he is doing. This happened. Before any native step in a test: toast + caption + an+explicit "don't type or click until I say done" + wait until the target window is genuinely+foreground + re-verify before each event + caption when done (`ensureSafeForNativeInput` /+`pup_assisted_click`). If you cannot warn him, use CDP input (`pup_click` / `pup_type` /+`pup_hover`) — it needs no foreground and cannot disturb him.++**Also: a green API result is not a passing test.** `ok:true` from a native verb means the event went+to the OS, not that it reached your window; the badge API reported success on an invisible badge; and+`pup_type` reports success while keystrokes never land in a window whose renderer is not+accepting input. Verify the EFFECT (read the field back, screenshot the taskbar, probe the DOM).++## AUDIT YOURSELF — the standard procedure when John says "audit your windows" (2026-07-30)++**"Audit your windows" ALWAYS includes the ICONS.** John had to tell me this twice: I reported+sessions, health, geometry and logs and left the taskbar out, and the icon state was where the real+regressions were hiding (a window that silently lost its badge after a restart; 8 stale AUMID keys for+3 windows). An audit without the icon section is not an audit — and it is not finished until you have+looked at the taskbar ON GLASS, because the API reports a badge as applied while it is invisible.++Run ALL SEVEN sections, in this order, and report them as a table:++1. **Sessions (bridge view)** — `pup_list_windows`: count, sessionId, owner, tabCount, profile,+   ageMinutes, `_degraded`. Flag: owner:null (ownership protection is off), duplicate sessions+   pointing at one window, a tabCount that does not match what was opened.+2. **ICONS / TASKBAR (never skip)**+   - per window: the `overlay` field — `favicon+adom:<host>` (correct), `count:N`, or `none` **plus+     its reason**. A `none` on a favicon-serving page is a regression; a `none` on local PDFs is+     legitimate (no favicon exists).+   - AUMID keys: `reg query HKCU\Software\Classes\AppUserModelId` filtered to `Adom.Pup*`.+     Expect **exactly 1** (`Adom.Pup`) under minimalTouch. More = stale keys from closed windows, or+     per-window identity churn that destroys/recreates taskbar buttons.+   - **ON GLASS:** `desktop_screenshot_screen`, crop the taskbar strip, and LOOK. Confirm each pup+     button wears the composite badge (page favicon + Adom mark inset flush in the lower-right).+     Report how many of N windows are actually badged.+3. **OS ground truth** — `desktop_find_window {titleContains:"(session:"}`: one real window per+   session (no ghosts, no untracked strays), and each rect at the standard inset frame+   (15,15 2531x1499 on this laptop). A maximized window reads ~-11,-11 2582x1550 — that is+   maximized, not stranded. `-32000,-32000` = minimized, confirm with IsIconic before calling it lost.+4. **Health** — `pup_eval` per window: `readyState`, `visibilityState`, a `1+1` drivability check.+   `visibilityState:hidden` is NORMAL for a parked/minimized window; only **frontmost-yet-hidden** is+   the broken state. Note any crashed tabs.+5. **Log** — `bridge_log_read`, count `[crash] [heal] [health] [unstick] [disconnect] [reclaim]+   [tabs] park-failed Handler error`. Explicitly report **park-failed** and **[reclaim] adopted**+   lines: a park-failed window is sitting wherever Chrome left it, and an adoption means pup LOST a+   window and re-bound it (which produces duplicate sessions for one window).+6. **Chrome footprint** — procs + total MB, and **pup's share vs the user's own Chrome**. Never+   report a big total as if it were all pup's.+7. **Cleanup state** — leftover test windows, phantom sessions, stale session files.++**Report rules:** state what is WRONG first, name which findings are your own regressions, and never+present an API-reported success (badge applied, verb ok:true) as proof — prove it on glass or in the+page.
dev-skills/pup-browser-import/SKILL.md+32−1
@@ -5,6 +5,37 @@ description: How to import a user's existing Chrome logins (passwords, cookies/s  # Importing the user's Chrome logins into pup (the adom-you seed) +## ✅ NOW LIVE (2026-08-04) — AD shipped the decrypt helper++The elevated decrypt (the whole blocker below) landed in **Bridge v1.9.220** as+**`desktop_decrypt_pup_credentials`** (this was adom/adom-desktop #51). The pup side is wired:+the dashboard Settings → "Import logins from Chrome / Edge" picker (from `pup_import_scan`) →+Import button → `import-run` action → `chrome.adCommand('desktop_decrypt_pup_credentials', …)`+→ `credentialVault.mergeImportedCredentials(rows)`.++**THE ONE RULE THAT MATTERS: call it LOCALLY, never over the relay.** It is a loopback/bridge-only+verb. `chrome.adCommand` already routes over AD's **direct API** (127.0.0.1:47200 from+`~/.adom/direct-api-port`), which is the correct local path — so calling it from the bridge is right.+A relayed/cloud call (e.g. `adom-desktop --target … desktop_decrypt_pup_credentials` from the+container) is REFUSED with `errorCode: relay_forbidden` and pops NO UAC. AD fires its OWN consent+dialog (names the caller) + a UAC prompt — that UAC **is** the user's permission for the decrypt.++Call shape: `{ browsers:['chrome','edge'], include:['passwords'] }` (add `'cookies'` for best-effort+cookies). Returns `{ ok, passwords[], cookies[], stats{profilesRead,passwordsOk,passwordsFailed,+cookiesOk,cookiesFailed} }`. Each password row: `{origin, username, password, sourceBrowser,+sourceProfile, sourceAccount}`. errorCodes to handle: `relay_forbidden` (you called it wrong — call+locally), `consent_declined` (user said no at AD dialog), `elevation_declined` (user cancelled UAC),+`helper_missing` (broken install). Per-item decrypt failures land in `stats.*Failed` and are NEVER+fatal — newest Chrome v20 blobs are best-effort if Google rotated the elevation-service key; legacy+v10/v11 always decrypt.++**Plaintext discipline (do this exactly):** the plaintext comes back ONCE over loopback and AD never+logs it. pup must not either: hand rows straight to `mergeImportedCredentials` (keytar), then ZERO the+`password` fields; NEVER log a password, NEVER echo it into another verb's args or the dashboard+response (the `import-run` handler returns COUNTS ONLY). The vault stores each password in the OS+keychain under a per-entry composite account and keeps only host+username+identity tags in the index.++ The `pup` core goal (see the root `SKILL.md`) is to build up an **adom-you** profile: the pup browser accretes the user's real logins so the agent can act as them. The fastest seed for that is to import the logins the user ALREADY has in their everyday Chrome. This file is the hard-won map of@@ -97,7 +128,7 @@ STRUCTURAL FACT (verified): the ABE master key lives in each browser's SHARED `U ONCE, decrypt EVERY profile with it.  1. **Discover everything.** Enumerate every profile of every Chromium browser (Chrome, Edge, Brave if-   present), reusing Hydrogen's existing browser-profile-scan logic. For each profile read `Preferences`+   present), reusing HD's existing browser-profile-scan logic. For each profile read `Preferences`    → `account_info[].email` to get the SOURCE ACCOUNT NAME ([email protected] vs [email protected] vs    [email protected]) — the identity tag is the whole point. 2. **Passwords via ONE admin-elevated SYSTEM helper, ALL profiles.** AD-side (the container can't
dev-skills/pup-cdp-discipline/SKILL.md+1−1
@@ -76,7 +76,7 @@ threadpool-backed (OK); genuinely synchronous CPU on the loop is not.  ## The acceptance test (prove it, every time) A CDP change is not done until it passes the **concurrent heavy-load stress test** from-`pup-differential-diagnosis`: fire 4+ heavy pages at once and poll `browser_readiness` throughout — it+`pup-differential-diagnosis`: fire 4+ heavy pages at once and poll `pup_readiness` throughout — it must stay responsive 10/10, zero `error sending request`. If a single open works but the batch drops the bridge, you have NOT fixed it. 
dev-skills/pup-differential-diagnosis/SKILL.md+2−2
@@ -57,9 +57,9 @@ overlap (a migration, a batch, an audit loop). So the real repro is **concurrent  ```bash # fire N heavy opens at once, then poll the bridge the whole time — it must stay responsive-for n in 1 2 3 4; do ( adom-desktop --target <host> --ai-thread stress browser_open_window \+for n in 1 2 3 4; do ( adom-desktop --target <host> --ai-thread stress pup_open_window \   "{\"sessionId\":\"stress-$n\",\"url\":\"<heavy url>?s=$n\"}" & ); done-for i in $(seq 1 10); do adom-desktop --target <host> browser_readiness '{"reason":"probe"}'; sleep 3; done+for i in $(seq 1 10); do adom-desktop --target <host> pup_readiness '{"reason":"probe"}'; sleep 3; done ```  If the probes stay `ready` through the load, the bridge is healthy under it. A `DROP` / `error sending
dev-skills/pup-icon-consistency/SKILL.md+65−2
@@ -25,7 +25,7 @@ mismatch John will catch. Treat the model as a claim that must be kept honest, n    teal (or all Chrome), that's the mismatch. **A window fails to stamp when its stamp can't resolve a    handle** - i.e. the title-lookup bug (see `pup-window-targeting`). Fix = make EVERY window stamp    (all teal) or none (all Chrome); never a mix. Verify with `_curAppId` per window in the snapshot.-2. **AD and HD are NOT pup.** Adom Desktop and Hydrogen Desktop are Tauri apps (`class=Tauri Window`)+2. **AD and HD are NOT pup.** Bridge and Hydrogen Desktop are Tauri apps (`class=Tauri Window`)    with their OWN teal Adom-branded taskbar tiles. They are not pup windows and not ghosts; pup    correctly excludes them. Do not mistake them for pup icons (I did, and restarted Explorer chasing    "ghosts" that were AD+HD). Enumerate `Chrome_WidgetWin_1` only to find pup windows.@@ -42,12 +42,75 @@ mismatch John will catch. Treat the model as a claim that must be kept honest, n    assert its own bookkeeping. Red is transient right after a respawn (before repaint) - John's rule:    the LED should never sit red, so treat a just-adopted/not-yet-painted window as pending, not red. +## The favicon-overlay compositor bug (v1.9.326 — John: "icons are STILL wrong")++Even with the model consistent, a pup tile can wear the WRONG or a BARE badge because the composite+dropped the favicon. Two root causes, both fixed in 1.9.326, both worth knowing:++1. **Format judged by URL, not bytes.** The compositor set `isIco` from `src.href.endsWith('.ico')`.+   But `resolveFaviconRich` → `analyzeIconBytes` has ALREADY re-encoded the favicon to PNG, so a site+   whose favicon URL ends in `.ico` (Google Docs' `docs-favicon-2026-v2.ico`, many others) arrived as+   PNG, went down the ICO branch, `icoLargestPng` returned null on PNG bytes, the favicon layer was+   dropped, and the badge rendered as the **bare Adom mark**. FIX: sniff the real magic bytes+   (`00 00 01 00` = ICO; else decode as a normal image). NEVER branch on the href extension.+2. **The generic-glyph fallback set `src` but not `iconPng`.** The composite draws ONLY `iconPng`;+   the "no usable favicon → generic globe" fallback assigned a new SVG to `src` and left `iconPng`+   null, so undecodable favicons ALSO went mark-only. FIX: render the glyph straight into `iconPng`,+   and set `src.href='generic:page'` so `_overlayIsGeneric` (hence the LED) is honest (amber, not a+   lying green).++Debug method that actually works: pull `%TEMP%\pup-badge-debug\<session>.png` — the EXACT bytes pup+handed Windows — and look. If the composed PNG is bare/wrong, it's the compositor (above); if the PNG+is right but the taskbar is wrong, it's delivery/targeting (`pup-window-targeting`). Do NOT diagnose+this from a downscaled taskbar screenshot; crop the native-res `Shell_TrayWnd` capture per-tile.++## The overlay GUARANTEE — why a window ends up BARE, and the three layers that fix it++John, repeatedly: "ensure you ALWAYS have an overlay icon working consistently... every verb that+loads tabs / opens windows / changes pup must trigger a correct overlay AND main-tile analysis." And+with the notch shipped, a bare window is now a VISIBLE HOLE, so this got sharper.++ROOT CAUSE of a bare window (found by tracing, not guessing): `applyAppOverlay` REFUSES to paint when+it can't confirm the window's hwnd belongs to pup — the "Edge guard" (never paint a badge onto a+non-pup taskbar button). So **a missing overlay is almost always an UNRESOLVED HWND**, not a compositor+bug. Anything that churns adoption — a bridge respawn (e.g. the user rebuilding AD in a loop), a+dragged-out tab, sleep/wake — forces hwnd re-resolution and leaves the window transiently bare.++The guarantee is THREE layers (all must stay intact):+1. **Per-event triggers** — `framenavigated` → `scheduleOverlayRefresh` (any nav from any source),+   `MUTATING_VERBS` → refresh, open/close tab+window → refresh. These fire the recompute immediately on+   a real change.+2. **The enforcer backstop** — a 4s sweep (NOT gated on minimalTouch) that repaints ANY bare-or-stuck+   window regardless of which verb did or didn't fire. Bare cooldown 4s. It re-resolves the hwnd each+   attempt (via `applyAppOverlay` → `resolveSessionHwndByBounds`). This is the real guarantee: even if a+   verb forgets to trigger, the window is healed within ~4s. Do NOT slow it back down, and do NOT let it+   `continue` past the overlay paint when a base-tile re-stamp is pending (they are independent AD calls).+3. **The baked notch fallback** — the static category tile has a small Adom mark baked INTO the notch, so+   a window that is momentarily bare shows a tidy branded corner instead of a raw transparent hole. The+   live favicon overlay covers it when present. This makes a transient bare state look intentional, not+   broken — critical because the notch made bareness visible.++If overlays are STILL unreliable after this: the fault is hwnd resolution (`pup-window-targeting`), not+the overlay code. Chase why `resolveSessionHwndByBounds` / the birth-hwnd / persisted-hwnd path fails to+re-confirm the window after adoption — that is the thing that starves the overlay.++## The Adom mark on the overlay is AUMID-CONDITIONAL (John, 2026-08-05)++The composed favicon overlay carries the little Adom mark in its lower-right ONLY when **aumidIcons is+OFF**. Rationale (John): the mark exists to say "this is Adom-controlled." When AUMID is ON, the base+taskbar tile IS the teal Adom category tile — that already says it — so a second mark on the favicon is+redundant. AUMID on → overlay is the CLEAN site favicon, nothing added. AUMID off → favicon + Adom mark+(the base is plain Chrome and needs the branding). The badge composite cache key includes the AUMID+state so it recomposes on toggle. (Related: the baked Adom-mark notch fallback only shows when there is+NO favicon at all, a transient placeholder the live favicon covers — keep it small/central so an opaque+favicon fully hides it and it can't read as a redundant second mark.)+ ## Rules that keep it honest  - **Never claim the dashboard "matches the taskbar" from a screenshot you took.** pup's screen grabs   downscale (~1568px) and blur the icons. To actually SEE the taskbar: native-res capture via a   desktop-side `.NET CopyFromScreen` (watch multi-monitor - `PrimaryScreen` may be the wrong display),-  or map an icon to a window by **flashing** it (`browser_alert_window`) and seeing which button+  or map an icon to a window by **flashing** it (`pup_alert_window`) and seeing which button   highlights. Enumerate `Chrome_WidgetWin_1` windows (hwnd+pid+title) as the authoritative window list. - **Keep stamping all-or-nothing.** The mismatch is almost always "1 of 3 stamped." Chase the window   that failed to stamp; it's a handle-resolution failure (`pup-window-targeting`), not a dashboard bug.
dev-skills/pup-jumplist/SKILL.md+16
@@ -51,6 +51,22 @@ can eat the menu. Until the over-committing is gone: right-click, if it blips away, right-click again ~1s later. Say this plainly; do not pretend the first click works. +## The "Switch to logged-in view" task did NOTHING (v1.9.332)++John clicked the "Switch to logged-in view" jump-list task and nothing happened. Cause: the callback+`pup_wiki_set_view` short-circuited with `{unchanged:true}` (just `bringToFront`, NO reload)+whenever pup BELIEVED the window was already in the requested view. And pup makes wiki windows default+to `authed`, so it almost always believed "already logged in" → the click was a silent no-op. Worse,+pup's view belief is unreliable (the default-authed may never have actually signed the page in; a cookie+expired), so "do nothing because I think you're already there" is exactly the wrong call.++FIX: a real USER click (caller `aiThread: 'user-taskbar-menu'`, which every jump-list task carries via+`jarg`) must ALWAYS re-assert the requested view — fall through to the mint/navigate path, which is a+genuine reload that GUARANTEES the view (for `authed` it re-mints a magic link / auto-signs from AD's+desktop token, actually signing the page in). Only a PROGRAMMATIC same-view call keeps the `unchanged`+short-circuit. The task's own description promises a "harmless reload" — honor it. Detect the user click+by `_callerThread === 'user-taskbar-menu'` (the header the CLI sets from the task's JSON `caller`).+ ## How jump lists work (the parts that ARE correct) - Tasks attach to the **AUMID**, so a per-session appId gives that window its own menu. Requires   `aumidIcons` ON (jump lists ride on the stamp) - see `pup-icon-consistency`.
dev-skills/pup-window-targeting/SKILL.md+1−1
@@ -1,6 +1,6 @@ --- name: pup-window-targeting-description: THE LAW for how pup finds a session's OS window for any Adom Desktop window verb - ALWAYS by its resolved HANDLE (hwnd), NEVER by title. Title lookup is unreliable (titleTag is off by default and pages rewrite their own <title>) and it has independently broken the overlay paint, the park, the taskbar flash, the AUMID stamp, AND the jump list - the same bug, over and over. READ THIS before adding or touching ANY chrome.adCommand('desktop_*') call that acts on a window, or before adding a desktop_find_window resolver.+description: THE LAW for how pup finds a session's OS window for any Bridge window verb - ALWAYS by its resolved HANDLE (hwnd), NEVER by title. Title lookup is unreliable (titleTag is off by default and pages rewrite their own <title>) and it has independently broken the overlay paint, the park, the taskbar flash, the AUMID stamp, AND the jump list - the same bug, over and over. READ THIS before adding or touching ANY chrome.adCommand('desktop_*') call that acts on a window, or before adding a desktop_find_window resolver. ---  # pup window targeting: hwnd, never title (the recurring bug, killed)
publish-skills/pup-bridge-publish/SKILL.md+9−9
@@ -1,6 +1,6 @@ --- 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."+description: "DEVELOPER skill — the exact, battle-tested recipe for publishing a new version of the Puppeteer (pup) bridge to wiki.adom.inc, plus every gotcha hit live. NOT for general users. Read before cutting a release: which artifact goes where (pkg=skills, release=bridge zip), version-lockstep, the `files` allowlist, keeping the tarball lean (NO zip / NO src / NO heroes / NO node deps), the pkg ships ONLY the pup USER skill (dev/publish skills are source-only in dev-skills/ + publish-skills/, never in the pkg — open-vs-closed source doesn't matter), the skillpack package.json declaring dependencies:{ adom/adom-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**@@ -89,7 +89,7 @@ adom-wiki pkg notes adom/[email protected] -m "Container skill ```  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.**+**container-side skills only; the bridge runtime is the Release zip + bundled in Bridge.**  ## Traps hit live (don't relearn these) @@ -137,7 +137,7 @@ the output and grep for `"status": "ok"`; do not print "installed" unless it's t  **GATE 3 — the version ON DISK, not what install claimed.** ```bash-adom-desktop --target <name> shell_execute '{"command":"cmd /c type \"%LOCALAPPDATA%\\Adom Desktop\\bridges-cache\\puppeteer\\BRIDGE_VERSION\"", "reason":"…"}'+adom-desktop --target <name> shell_execute '{"command":"cmd /c type \"%LOCALAPPDATA%\\Bridge\\bridges-cache\\puppeteer\\BRIDGE_VERSION\"", "reason":"…"}' ``` (Use `cmd /c type`, not `powershell -File` — a damaged PS profile on some boxes drops `-File` to an interactive banner and burns the timeout.)@@ -153,7 +153,7 @@ npm-install-on-lockfile-change — filed in AD_REQUEST_bridge_staleness.md.) # manifest serves at the updateManifestUrl path (necessary, NOT sufficient — see Gate 1): curl -s https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json | jq .sha256 adom-desktop --target <name> bridge_install '{"manifestUrl":".../files/adom-bridge-puppeteer-manifest.json","force":true}'-adom-desktop --target <name> browser_readiness '{}'+adom-desktop --target <name> pup_readiness '{}' # render the page in pup + READ the screenshot (curl 200 is NOT verification). Confirm version + Skills tab. ``` @@ -246,7 +246,7 @@ etc. The wiki **auto-generates** the discover snippet from `discovery_triggers` 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:+bespoke `start_pup_bridge` npm-installs only the BUNDLED copy; the cache copy gets nothing). So:  - **The working runtime delivery for pup is the BUNDLED NSIS seed** (it keeps node_modules). Wiki-stream   the SKILLS (pkg) freely; the RUNTIME effectively ships with the AD installer until AD is fixed.@@ -260,7 +260,7 @@ bespoke `start_browser_bridge` npm-installs only the BUNDLED copy; the cache cop ## 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>`,+see (`pup_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@@ -282,11 +282,11 @@ 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` ->+bridge.json declares `timeouts` with BARE verb keys (no pup_ prefix): resolution = exact `verbs` -> first matching `prefixes` -> `default` -> AD global 60s. pup declares: prewarm 240 (blocking CfT download),-use 240 (browser_use can trigger installs), open_window 150 (spawn-verified fallback worst case = 3+use 240 (pup_use can trigger installs), open_window 150 (spawn-verified fallback worst case = 3 candidates x 3 attempts x 30s CDP wait), record* 90. An AD timeout is NON-terminal: callers get-{stillRunning:true, statusVerb, timeoutSeconds} and poll browser_readiness. When adding a verb that can+{stillRunning:true, statusVerb, timeoutSeconds} and poll pup_readiness. When adding a verb that can block >60s, add its bare key here AND bump runtime version (bridge.json+BRIDGE_VERSION+manifest+zip).  ## ⛔ EVERY SHIP IS THREE ARTIFACTS, IN THIS ORDER (John, 2026-07-25 — learned the hard way twice)