← Commit history

v1.3.0: prefer already-installed browser (Chrome->Edge, fresh profile) => no Chrome-for-Testing download in the common case; CfT only last resort; drop prewarm; readiness reports browserKind/source

John Lauer ·f784171b1b · ·parent 0808ae2
8 files changed +102−108
SKILL.md+15−39
@@ -1,7 +1,7 @@ --- name: pup user-invocable: true-description: "Drive Chrome browser windows on the user's desktop via the adom-desktop CLI (the Puppeteer bridge): open/close windows + tabs, navigate, screenshot, eval JS, record, drive the user's REAL signed-in Chrome/Edge/Brave. Cold-start: on a fresh PC Adom Desktop background-fetches Chrome for Testing (Tier-1 prewarm); for 'is it ready' poll AD's bridge_readiness (not browser_status). Trigger words: pup, puppeteer, browser window, open in pup, browser screenshot, browser reload, browser eval, visual debug, browser_open_window, browser_screenshot, browser_readiness, browser_prewarm, chrome for testing, open in browser."+description: "Drive Chrome browser windows on the user's desktop via the adom-desktop CLI (the Puppeteer bridge): open/close windows + tabs, navigate, screenshot, eval JS, record, drive the user's REAL signed-in Chrome/Edge/Brave. Cold-start: pup drives the browser already on the machine (installed Chrome, else Edge — every Windows PC has Edge) with a fresh isolated profile, so usually NO download; Chrome for Testing only as a last resort. For 'is it ready' use browser_readiness (not browser_status). Trigger words: pup, puppeteer, browser window, open in pup, browser screenshot, browser reload, browser eval, visual debug, browser_open_window, browser_screenshot, browser_readiness, browser_prewarm, chrome for testing, open in browser." ---  # Pup — Puppeteer Browser Control (with cold-start self-heal)@@ -13,54 +13,30 @@ Bash commands (NOT MCP tools): adom-desktop <command> '<json_args>' ``` -## ⭐ FIRST-TIME / COLD-START — read this BEFORE you panic that "open in pup did nothing"+## ⭐ FIRST-TIME on a fresh PC — usually a non-event now -On a brand-new machine (e.g. the user just installed Hydrogen Desktop, which bundles Adom Desktop),-**two things pup needs are NOT pre-installed by the installer**: Node.js and **Chrome for Testing**-(the actual Chrome build pup drives). The bridge now handles both gracefully — your job is to read the-structured response and follow it, not to give up.+**pup drives the browser that's ALREADY on the machine** — installed **Chrome**, else **Microsoft Edge**+(every Windows PC ships with Edge) — launched with a **fresh isolated profile**. So on a normal box there's+**no download and no wait**: `browser_open_window` just works, and it never touches the user's real+tabs/logins. Chrome for Testing is fetched **only as a last resort** if the machine somehow has no+Chromium browser at all. -When you run `browser_open_window` on a cold PC, one of these happens — handle each:+When you run `browser_open_window`, one of these happens — handle each:  | Response | What it means | What you do | |---|---|---|-| `errorCode: "node_not_found"` | Node.js isn't installed (the bridge can't even start). | Run `adom-desktop desktop_install_node '{}'`, wait for it, then retry the `browser_open_window`. |-| `errorCode: "chrome_for_testing_installing"` (`installing:true`) | **Chrome for Testing isn't cached yet; Adom Desktop is fetching it in the BACKGROUND (Tier-1 prewarm, ~150 MB, one time).** The bridge did NOT block. EXPECTED on a fresh PC — NOT a failure. | **Poll AD's `bridge_readiness` until Chrome reports ready** (usually 1–3 min), THEN re-issue the exact same `browser_open_window`. You do NOT install anything yourself. Do NOT report failure. (If it never becomes ready, `browser_prewarm` makes the bridge fetch it itself.) |-| `errorCode: "chrome_install_failed"` / `chrome_download_failed` | The Chrome download itself failed. | Read `readiness.lastError`. It's almost always no network / a proxy blocking `storage.googleapis.com`, or low disk — NOT a Node problem. Retry `browser_prewarm`; if it persists, surface `lastError` to the user. Fallback: drive their real browser via native mode (below). |-| `ok:true` / a real session | Everything was already warm. | Proceed normally. |+| `ok:true` / a real session | A browser (Chrome or Edge) was found and launched. | Proceed — the common case. |+| `errorCode: "node_not_found"` | Node.js isn't installed (the bridge can't even start). | Run `adom-desktop desktop_install_node '{}'`, wait, then retry. |+| `errorCode: "chrome_for_testing_installing"` (`installing:true`) | **RARE** — the machine has NO Chrome AND no Edge, so pup is fetching Chrome for Testing as a fallback (~150 MB, in the background; it did NOT block). | Poll **`browser_readiness`** until `{ready:true}`, then re-issue the same `browser_open_window`. **Faster fix:** ask the user to install Chrome or Edge (Edge ships with Windows) — pup then uses it instantly, no download. Don't report failure. |+| `errorCode: "chrome_install_failed"` | No Chrome/Edge AND the CfT fallback download failed. | Read `readiness.lastError` (usually no network/proxy or low disk). Ask the user to install Chrome/Edge, or drive their real browser via native mode (below). | -### "What do I have / is it ready?" → ask AD's `bridge_readiness` (NOT `browser_status`)+### "What do I have / is it ready?" -For "is pup ready / what's installed," call **`adom-desktop bridge_readiness`** — AD-core's readiness probe.-It reports server-up + Chrome-for-Testing readiness, including AD's **Tier-1 prewarm** progress.-`browser_status` only lists open sessions — it is NOT a readiness check.--### The clean cold-start recipe--```bash-# 1) Ask AD if pup is ready (poll until Chrome shows ready):-adom-desktop bridge_readiness '{}'--# 2) Once ready, open the window — it's instant:-adom-desktop browser_open_window '{"sessionId":"chip-fetcher","profile":"chip-fetcher","url":"<url>"}'-```--On a fresh PC, **Adom Desktop fetches Chrome for Testing in the background** (the bridge declares-`prewarm:["chrome-for-testing"]`); `browser_open_window` returns a non-blocking `{installing:true}` until-it's cached — it never blocks or crashes. So: poll `bridge_readiness` → then open. You never run an-installer yourself.--- **`bridge_readiness`** (AD-core) — THE readiness question: server-up + Chrome cached (+ prewarm progress).-- **`browser_readiness`** (this bridge) — pup's own **READ-ONLY** detail (Chrome build id, fetch phase). Never downloads.-- **`browser_prewarm`** — explicit fallback: have the bridge fetch Chrome itself if AD's prewarm didn't.+- **`browser_readiness`** (this bridge, **READ-ONLY**) → `{ready, browserKind, browserSource, browserExecutablePath, …}` — tells you which browser pup will drive (`chrome` / `edge` / `chrome-for-testing`) and whether it's ready. Never triggers a download.+- **`adom-desktop bridge_readiness`** (AD-core) is the platform-level "is this bridge ready?" probe — use it for the general readiness question. **`browser_status` is NOT a readiness check** (it only lists open sessions).  **Always surface a verb's `_hint` verbatim** when something isn't ready. -> Why this matters: before v1.1.0, a fresh PC with no Chrome for Testing made `browser_open_window`-> fail with a generic "Failed to launch Chrome after 3 attempts" and no actionable hint — the AI-> flailed. Now the bridge detects it, installs it, and tells you to poll readiness. Don't reintroduce-> the panic; just follow the table above.- ## How to Run Commands  ```bash
adom-bridge-puppeteer-manifest.json+4−9
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.2.3",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.2.3/adom-bridge-puppeteer-v1.2.3.zip",-  "sha256": "995ee3218a2827b45dbfc6918dac62b977732dc16c84501adb7c563c5c85eb59",-  "size": 128005,+  "version": "1.3.0",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.3.0/adom-bridge-puppeteer-v1.3.0.zip",+  "sha256": "8b80afbe95e0af54dfa2db88a12ff3b71f86711dfc3b479226b6db526cdc86f6",+  "size": 128877,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,10 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "prewarm": {-    "assets": [-      "chrome-for-testing"-    ]-  },   "released_at": "2026-06-30T00:00:00Z" }\ No newline at end of file
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-puppeteer-bridge",-  "version": "1.2.3",+  "version": "1.3.0",   "type": "app",   "description": "CONTAINER-SIDE install — the everyday `pup` user skill only. This pkg drops the `pup` skill (drive the browser bridge: open/close/tabs/screenshot/record, background-by-default) into ~/.claude/skills/ AND ~/.codex/skills/. It does NOT install the developer/publishing skills (pup-bridge-dev, pup-bridge-publish) — those are source-only; clone the repo (adom-wiki repo clone adom/adom-desktop-puppeteer-bridge) to get them from dev-skills/. It also does NOT contain the bridge runtime: the bridge (Node + Chrome for Testing) lives on the DESKTOP — bundled inside Adom Desktop and published here on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: `pkg install` = the pup user skill; dev skills = clone the source; the bridge = the Release zip + bundled in Adom Desktop.",   "keywords": [
page.json+4−34
@@ -4,15 +4,8 @@   "slug": "adom-desktop-puppeteer-bridge",   "title": "Adom Desktop - Puppeteer Bridge",   "brief": "The Adom Desktop Puppeteer (pup) bridge — drive Chrome for Testing (or the user's real Chrome/Edge/Brave) from the cloud: open/screenshot/eval/record, with cold-start self-heal that auto-installs Chrome for Testing on a fresh PC. Two artifacts: the Releases zip is the bridge runtime (also bundled in Adom Desktop); the pkg installs the container-side Claude skills.",-  "version": "1.2.3",-  "tags": [-    "skills",-    "pup",-    "puppeteer",-    "browser",-    "adom-desktop",-    "bridge"-  ],+  "version": "1.1.0",+  "tags": [],   "license": "MIT",   "discovery_triggers": [     "pup",@@ -53,7 +46,7 @@     "fork the pup bridge",     "puppeteer bridge source"   ],-  "discovery_pitch": "Use pup to drive Chrome from the cloud: open/close browser windows + tabs, navigate, screenshot, eval JS, and record windows — driving Chrome for Testing or the user's REAL Chrome/Edge/Brave, in the background so it doesn't disrupt the user. On a fresh PC, Adom Desktop background-fetches Chrome for Testing (Tier-1 prewarm) — poll bridge_readiness, then open. Call the adom-desktop browser_* verbs; the pkg installs the container-side skills.",+  "discovery_pitch": "Use pup to drive Chrome from the cloud: open/close browser windows + tabs, navigate, screenshot, eval JS, and record windows. It drives the browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC has Edge) — with a fresh isolated profile, so usually NO download and it doesn't disrupt the user; Chrome for Testing is fetched only as a last resort. For 'is it ready' use browser_readiness. Or pass nativeBrowser+nativeProfile to drive the user's REAL signed-in Chrome/Edge/Brave. Call the adom-desktop browser_* verbs.",   "sample_prompts": [     {       "label": "Open in pup",@@ -103,28 +96,5 @@   "created_at": "2026-05-28T05:28:43.991Z",   "updated_at": "2026-06-16T12:43:19.735Z",   "skills": [],-  "author_name": "John Lauer",-  "description": "CONTAINER-SIDE install — the everyday `pup` user skill only. This pkg drops the `pup` skill (drive the browser bridge: open/close/tabs/screenshot/record, background-by-default) into ~/.claude/skills/ AND ~/.codex/skills/. It does NOT install the developer/publishing skills (pup-bridge-dev, pup-bridge-publish) — those are source-only; clone the repo (adom-wiki repo clone adom/adom-desktop-puppeteer-bridge) to get them from dev-skills/. It also does NOT contain the bridge runtime: the bridge (Node + Chrome for Testing) lives on the DESKTOP — bundled inside Adom Desktop and published here on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: `pkg install` = the pup user skill; dev skills = clone the source; the bridge = the Release zip + bundled in Adom Desktop.",-  "keywords": [-    "pup",-    "puppeteer",-    "browser",-    "adom-desktop",-    "bridge",-    "skills"-  ],-  "files": [-    "SKILL.md",-    "skills/**",-    "install.sh",-    "uninstall.sh",-    "README.md",-    "package.json"-  ],-  "dependencies": {},-  "scripts": {-    "install": "./install.sh",-    "uninstall": "./uninstall.sh"-  },-  "org": "adom"+  "author_name": "John Lauer" }\ No newline at end of file
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.2.3\ No newline at end of file+1.3.0\ No newline at end of file
src/bridge.json+2−7
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.2.3",-  "description": "Launches with FULL browser permissions by default (all granted — notifications/geolocation/camera/mic/midi/sensors/clipboard/wake-lock/…) so no permission prompt ever blocks full debug; strictPermissions:true opts out. Tier-1 cold-start (for AD v1.9.47+ two-tier model): pup brings its OWN browser (Chrome for Testing), so it needs no detect-block. It declares prewarm:[chrome-for-testing] so Adom Desktop fetches Chrome in the BACKGROUND and reports readiness via bridge_readiness. The bridge server starts fine WITHOUT Chrome; browser_readiness is a READ-ONLY probe (ready = server-up AND Chrome cached; never triggers a download); browser_open returns a non-blocking {installing:true} (never blocks/crashes) until Chrome is cached; browser_prewarm is an explicit bridge-side fetch fallback. Headful browser automation. Default path drives Chrome for Testing (CfT) with managed pup profiles. v1.0.6+ optional native-drive: pass nativeBrowser+nativeProfile (from desktop_list_browsers) to browser_open_window to drive the user's REAL Chrome/Edge/Brave with the actual signed-in account + org policies + device enrollment intact. Smart flow: if the browser is CLOSED, AD launches it fresh immediately with NO prompt (nothing to disrupt) and reports back ready-to-drive (launchedFresh:true); if it's OPEN without the port, AD returns errorCode:'native_relaunch_required' so the caller asks the user, then re-call with nativeRelaunch:true. AD then closes only that browser's own PIDs cleanly (WM_CLOSE — Sessions saved; CfT bridge sessions + other Chromium left untouched) and relaunches with --restore-last-session so every tab/window comes back. If the debug port is policy-blocked (e.g. Google Workspace's RemoteDebuggingAllowed disabled — affects Chrome browser-wide, all profiles), native drive returns errorCode:'native_cdp_blocked' with a `diagnosis` plus an `alternatives` list of other installed browsers — Edge usually escapes Google management. The generic CfT path (browser_open_window with no nativeBrowser) always works as a fallback.",+  "version": "1.3.0",+  "description": "Drives a browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) — launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. browser_readiness (READ-ONLY) reports which browser will be used (browserKind/browserSource) + whether it's ready; browser_open never blocks on a download. Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. v1.0.6+ optional native-drive: pass nativeBrowser+nativeProfile (from desktop_list_browsers) to browser_open_window to drive the user's REAL signed-in Chrome/Edge/Brave (their actual profile, org policies, device enrollment) — with the smart relaunch/consent flow (native_relaunch_required / native_cdp_blocked). The generic path (no nativeBrowser) always works.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",   "license": "MIT",@@ -15,11 +15,6 @@     "stopMethod": "kill",     "killImageName": "node.exe"   },-  "prewarm": {-    "assets": [-      "chrome-for-testing"-    ]-  },   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/chrome.js+48−2
@@ -93,6 +93,48 @@ function detectChrome() {   return { installed: false, executablePath: null, buildId: expectedBuildId() }; } +// ── System browsers (the FAST path — no download) ─────────────────────────────+// Every Windows PC ships with Edge, and most have Chrome — both are Chromium and+// both are drivable by puppeteer. Preferring an ALREADY-INSTALLED browser (launched+// with a FRESH isolated profile) means the common case needs NO ~150 MB Chrome-for-+// Testing download. detectSystemBrowser probes the standard install locations.+function detectSystemBrowser() {+  const pf   = process.env['PROGRAMFILES']      || 'C:\\Program Files';+  const pf86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';+  const lad  = process.env['LOCALAPPDATA']      || '';+  const j = (base, ...rest) => (base ? path.join(base, ...rest) : null);+  const chromePaths = [+    j(pf,   'Google', 'Chrome', 'Application', 'chrome.exe'),+    j(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'),+    j(lad,  'Google', 'Chrome', 'Application', 'chrome.exe'),+  ].filter(Boolean);+  const edgePaths = [+    j(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),+    j(pf,   'Microsoft', 'Edge', 'Application', 'msedge.exe'),+  ].filter(Boolean);+  for (const p of chromePaths) if (fileExists(p)) return { found: true, kind: 'chrome', executablePath: p };+  for (const p of edgePaths)   if (fileExists(p)) return { found: true, kind: 'edge',   executablePath: p };+  // POSIX fallbacks (pup is Windows-first, but keep dev boxes working):+  for (const p of ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',+                   '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',+                   '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']) {+    if (fileExists(p)) return { found: true, kind: p.toLowerCase().includes('edge') ? 'edge' : 'chrome', executablePath: p };+  }+  return { found: false };+}++// The browser pup will actually launch, in preference order:+//   installed Chrome → installed Edge → cached Chrome-for-Testing → (none → fetch CfT).+// Launch uses a fresh --user-data-dir, so a system browser is driven CLEAN — it never+// touches the user's real tabs/logins (that's native-mode's job).+function resolveBrowser() {+  const sys = detectSystemBrowser();+  if (sys.found) return { found: true, kind: sys.kind, executablePath: sys.executablePath, source: 'system' };+  const cft = detectChrome();+  if (cft.installed) return { found: true, kind: 'chrome-for-testing', executablePath: cft.executablePath, source: 'cache', buildId: cft.buildId };+  return { found: false, kind: 'none', executablePath: null };+}+ // Programmatic install of the pinned CfT build. Resolves to { ok, executablePath, buildId } // or { ok:false, errorCode, error }. Deduped via _installInFlight. function installChrome({ onProgress } = {}) {@@ -212,11 +254,15 @@ async function ensureChromeReady({ background = true, onProgress } = {}) { // true (this module only runs inside the node bridge process), but we surface it // so the AI's readiness check is a single source of truth. function readiness() {+  const b = resolveBrowser();   const det = detectChrome();   const installing = !!_installInFlight || state.phase === 'installing';-  const ready = det.installed && !installing;+  const ready = b.found && !installing;   // a drivable browser is available (system OR cached CfT)   return {     ready,+    browserKind: b.kind,                  // 'chrome' | 'edge' | 'chrome-for-testing' | 'none'+    browserSource: b.source || null,      // 'system' | 'cache' | null+    browserExecutablePath: b.executablePath || null,     nodeInstalled: true,     nodeVersion: process.version,     chromeForTestingInstalled: det.installed,@@ -234,4 +280,4 @@ function readiness() {   }; } -module.exports = { detectChrome, installChrome, ensureChromeReady, readiness, expectedBuildId, cacheDir };+module.exports = { detectChrome, detectSystemBrowser, resolveBrowser, installChrome, ensureChromeReady, readiness, expectedBuildId, cacheDir };
src/server.js+27−15
@@ -1328,9 +1328,19 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer     browsers.delete(profileName);   } +  // Prefer an ALREADY-INSTALLED browser (Chrome → Edge; every Windows box has Edge)+  // so the common case needs NO ~150 MB Chrome-for-Testing download. Falls back to a+  // cached CfT. A fresh --user-data-dir (below) keeps it CLEAN — it never touches the+  // user's real tabs/logins (that's native-mode's job).+  const resolvedBrowser = chrome.resolveBrowser();+  if (resolvedBrowser.found) {+    console.log(`[launch] browser: ${resolvedBrowser.kind} (${resolvedBrowser.source}) — ${resolvedBrowser.executablePath}`);+  }+   const launchOpts = {     headless: false,     defaultViewport: null,+    ...(resolvedBrowser.executablePath ? { executablePath: resolvedBrowser.executablePath } : {}),     args: [       '--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security',       '--start-maximized', '--remote-debugging-port=0',@@ -1479,7 +1489,8 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer     // ─── Helper: one detached-launch attempt, returns browser or throws ───     const tryDetachedLaunch = async (attemptNum) => {       const { spawn } = require('child_process');-      const chromePath = puppeteer.executablePath();+      // Launch the resolved browser (installed Chrome/Edge preferred; cached CfT fallback).+      const chromePath = resolvedBrowser.executablePath || puppeteer.executablePath();       const debugPort = 9200 + Math.floor(Math.random() * 800);       const chromeArgs = [         ...launchOpts.args.filter(a => a !== '--remote-debugging-port=0'),@@ -2965,13 +2976,14 @@ const server = http.createServer(async (req, res) => {           // `profile` arg. Default (no native args) is byte-for-byte the same           // CfT path as v1.0.4 — strictly additive, no regressions.           const wantsNative = !!(args.nativeBrowser || args.nativeProfile);-          // ── Tier-1 cold-start gate (CfT path only) — READ-ONLY, never blocks ──-          // pup brings its own browser (Chrome for Testing). AD v1.9.47+ backgrounds-          // the Chrome-for-Testing fetch via this bridge's prewarm:["chrome-for-testing"]-          // declaration, so this gate does NOT trigger a download itself (no racing-          // AD-core) and NEVER blocks: if Chrome isn't cached yet it returns a pollable-          // {installing:true}. The explicit bridge-side fetch fallback is browser_prewarm.-          // Native drive uses the user's REAL browser (not CfT), so it skips this.+          // ── Cold-start gate (CfT path only) — READ-ONLY detect; never blocks ──+          // pup drives an ALREADY-INSTALLED browser (Chrome → Edge; EVERY Windows box+          // has Edge) with a fresh isolated profile — so readiness is almost always TRUE+          // instantly with NO download, and this gate just falls through. Only if the+          // machine has NO Chromium browser at all (no Chrome, no Edge, no cached CfT) —+          // rare — do we fall back to fetching Chrome for Testing, kicked off in the+          // BACKGROUND here (non-blocking, pollable {installing:true}). Native drive uses+          // the user's REAL browser, so it skips this.           if (!wantsNative) {             const rd = chrome.readiness();   // detect-only; never downloads             if (!rd.ready) {@@ -2979,22 +2991,22 @@ const server = http.createServer(async (req, res) => {                 sendJSON(res, {                   success: false,                   errorCode: rd.lastErrorCode || 'chrome_install_failed',-                  error: `Chrome for Testing (the browser pup drives) failed to install: ${rd.lastError}`,-                  chromeForTestingInstalled: false,+                  error: `No Chrome/Edge installed and the Chrome-for-Testing fallback failed to install: ${rd.lastError}`,                   readiness: rd,-                  _hint: 'A prior Chrome-for-Testing fetch failed (usually no network / a proxy blocking storage.googleapis.com, or low disk — NOT a Node problem). Retry `browser_prewarm`; if it keeps failing, surface readiness.lastError to the user. Fallback: drive the user\'s real browser via browser_open_window with nativeBrowser+nativeProfile.',+                  _hint: 'This machine has no Chrome or Edge, and the Chrome-for-Testing fallback download failed (usually no network / a proxy blocking storage.googleapis.com, or low disk). Fastest fix: have the user install Chrome or Edge (Edge ships with Windows) — pup will then use it instantly with no download. Or retry browser_prewarm. Fallback: drive their real browser via browser_open_window with nativeBrowser+nativeProfile.',                 });                 return;               }+              // No Chrome/Edge AND no cached CfT — fetch CfT as a last resort, in the background.+              if (!rd.installing) chrome.ensureChromeReady({ background: true });               sendJSON(res, {                 success: false,                 errorCode: 'chrome_for_testing_installing',                 installing: true,-                chromeForTestingInstalled: false,-                statusVerb: 'bridge_readiness',+                statusVerb: 'browser_readiness',                 readiness: rd,-                error: 'Chrome for Testing is not cached yet — Adom Desktop is fetching it in the background (Tier-1 prewarm).',-                _hint: 'FIRST-TIME on this PC: AD is downloading Chrome for Testing in the background (the bridge declares prewarm:["chrome-for-testing"]); the bridge did NOT block on it. Poll AD\'s `bridge_readiness` until Chrome reports ready, THEN re-issue this exact browser_open_window. Do NOT report failure — this is expected on a fresh machine. (If it never becomes ready, call `browser_prewarm` to have the bridge fetch Chrome itself.)',+                error: 'No Chrome/Edge installed on this machine — fetching Chrome for Testing as a fallback (~150 MB, one time).',+                _hint: 'RARE: this machine has neither Chrome nor Edge, so pup is downloading Chrome for Testing as a fallback (in the background — it did NOT block). Poll `browser_readiness` until {ready:true}, THEN re-issue this browser_open_window. Faster fix: have the user install Chrome or Edge (Edge ships with Windows) and pup uses it instantly with no download. Do NOT report a hard failure — this is the no-browser edge case.',               });               return;             }