← Commit history

v0.16.32 (insiders): nbe tally (nb-bridge#50): nbrowser_readiness now reports which browsers are on the machine, every profile, and where the extension is installed, switched off or missing, recomputed from disk on every call so it is never stale, with an offer line an AI can put to the user; new nbrowser_prewarm (declared as bridge.json prewarm.verb) runs the same tally when ab installs or updates nb

John Lauer ·80450cbd82 ·1mo ago ·parent 5afa28c
8 files changed +88−10
bin/nb-dashboard+1−1
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.31';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.32';           // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.16.31\ No newline at end of file+0.16.32\ No newline at end of file
bridge/bridge.json+8−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.31",+  "version": "0.16.32",   "expectedExtensionVersion": "0.2.15",   "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",   "homepage": "https://wiki.adom.inc/adom/nb-bridge",@@ -24,6 +24,7 @@     "nbrowser_status",     "nbrowser_readiness",     "nbrowser_install_chrome",+    "nbrowser_prewarm",     "nbrowser_chrome_status",     "nbrowser_ping",     "nbrowser_progress",@@ -158,5 +159,10 @@       }     ]   },-  "uninstall": "nbrowser_uninstall"+  "uninstall": "nbrowser_uninstall",+  "prewarm": {+    "verb": "nbrowser_prewarm",+    "requiresHostApp": false,+    "assets": []+  } }
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.31",+  "version": "0.16.32",   "private": true,   "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).",   "main": "server.js",
bridge/server.js+58−2
@@ -1711,6 +1711,7 @@ const VERB_META = {   nbrowser_status: { hint: 'Connectivity check: connected profiles + active browser. Call this first if any verb errors "not connected".', related: ['nbrowser_profiles', 'nbrowser_ping', 'nbrowser_open_window'], pitfalls: ['a profile\'s extension SLEEPS when that profile has no open windows - it then shows disconnected; open a window to wake it'] },   nbrowser_readiness: { hint: 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? NEVER spawns work or installs anything. state:"ready" = drive now; state:"needs-extension" = offer to install it (Level-2, see installing-the-extension) or it is asleep (open a window in that profile). Also reports chromeInstalled: false does NOT mean install Chrome: nb drives whatever native browser is there (Edge ships with Windows); read nativeBrowser.chromeNeeded, which is true only when NO supported browser exists, and offer nbrowser_install_chrome only then or when the user asks for Chrome by name.', related: ['nbrowser_status', 'nbrowser_open_window', 'nbrowser_install_chrome'], pitfalls: ['"needs-extension" can mean not-installed OR installed-but-asleep - nbrowser_status\'s tooltip distinguishes', 'this verb never installs; to install, run the installing-the-extension flow', 'for a machine-wide "what do I have" answer prefer ab\'s bridge_readiness (aggregates all bridges, spawns none)'] },   nbrowser_install_chrome: { hint: 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a QUIET user-scoped install (no admin on a normal PC). NON-BLOCKING - returns immediately; poll nbrowser_chrome_status. On a locked-down box it escalates to a notify-first UAC (fires a sticky "Approve now" toast, waits for the tap, then raises the prompt). This is step 1 of getting the AI onto the user\'s REAL browser - after Chrome is in, help them sign into Google + load the extension.', related: ['nbrowser_chrome_status', 'nbrowser_readiness', 'installing-the-extension'], pitfalls: ['on a locked-down box the quiet install parks on a UAC; a fresh Windows 11 keeps Do Not Disturb on until its one-time Allow-notifications prompt is answered, so the Approve-now toast is never seen: nbrowser_chrome_status.notifications says so, and {retryUac:true} shows the prompt again for someone looking at the screen without re-downloading', 'non-blocking: do NOT expect Chrome to exist on return - POLL nbrowser_chrome_status until phase:"ready"', 'awaiting_uac means a prompt is on the user\'s desktop - relay it; do not re-trigger install', 'Windows only for now', 'this only INSTALLS Chrome; the user still signs into Google themselves (their credential) - guide them via setting-up-native-chrome'] },+  nbrowser_prewarm: { hint: 'READ-ONLY tally, run by ab once per installed version (bridge.json prewarm.verb): which browsers are on the machine, every profile, and where nbe is installed / switched off / missing, plus an offer line an AI can put to the user. Saves a timestamped snapshot; nbrowser_readiness recomputes the same tally live on every call.', related: ['nbrowser_readiness', 'nbrowser_profiles', 'installing-the-extension'], pitfalls: ['the saved snapshot can age; for the current state read nbrowser_readiness (nbe block), which is never cached', 'offer is a suggestion for the USER, never an instruction to install: install only with their OK'] },   nbrowser_chrome_status: { hint: 'READ-ONLY: is native Google Chrome installed, and how is an in-flight install going? Returns {installed, executablePath, phase, installing, awaitingUac, edgeInstalled, nativeBrowserPresent, chromeNeeded, notifications}. chromeNeeded is true ONLY when no supported browser exists at all: on an Edge-only box Chrome is optional and the hint says so, so do not install it unless the user asks for Chrome by name. phase: idle|installing|awaiting_uac|ready|failed. Poll this after nbrowser_install_chrome. Never installs or spawns.', related: ['nbrowser_install_chrome', 'nbrowser_readiness'], pitfalls: ['awaiting_uac + notifyChannel:true = a toast is on the desktop, keep polling; notifyChannel:false = relay the UAC to the user yourself', 'installed:true means Chrome.exe is present - next help the user sign into Google + load the extension'] },   nbrowser_ping: { hint: 'Round-trip liveness through to the extension.', related: ['nbrowser_status'], pitfalls: ['a pong proves the extension is reachable, NOT that any profile has a drivable window — use nbrowser_status/readiness for that'] },   nbrowser_progress: { hint: 'FAST poll for MEASURED progress of every in-flight verb: active[] carries one block per running verb (phase, stepLabel, percent, elapsedSec, estimatedSec, etaSec, confidence measured|estimated, startKind, sessionId). Estimates come from this machine\'s own rolling per-verb history (also returned as verbDurations). Poll once a second while something slow runs; empty active[] = idle.', related: ['nbrowser_activity', 'nbrowser_status'], pitfalls: ['percent is capped at 95 until the verb actually completes - the reply of the slow verb itself carries the 100% terminal block', 'confidence is estimated (draw it dimmed) until three real samples exist for that verb on this machine', 'open-ended verbs (wait, wait_for, download_wait, keepalive, record_start) report no percent: render an indeterminate sweep, never a fake 0%'] },@@ -1925,8 +1926,12 @@ function dispatch(verb, args) {         ? ` ⛔ VERSION SKEW (likely the cause of "Native host has exited"): ${staleOnDisk.map((x) => `${x.profile} has extension ${x.installed}`).join(', ')}, but this bridge (${VERSION}) expects extension ${EXPECTED_EXT}. An extension/host that old cannot complete the native-messaging handshake, so the host exits with no message and the SW never connects. FIX: update the extension in that profile to ${EXPECTED_EXT} (re-sync the extension/ + host/ folders per installing-the-extension), then nbrowser_dev_reload {"profile":"<profile>"}. Note: the extension line (${EXPECTED_EXT}) and the skills-package line (a 0.5.x number on the wiki page) are DIFFERENT version streams - match the extension against expectedExtensionVersion, not the pkg version.`         : '';       const claudeExt = claudeExtAnywhere();+      let inv = null; try { inv = nbeInventory(); } catch {}+      const offerHint = inv && inv.offer ? ` NBE TALLY: ${inv.offer}` : '';       return resolve({ success: true, output: JSON.stringify({         ready,+        // Where nbe is installed / switched off / missing, recomputed from disk on THIS call (never cached).+        nbe: inv ? { asOf: inv.asOf, browsers: inv.browsers, tally: inv.tally, installedIn: inv.installedIn, disabledIn: inv.disabledIn, missingIn: inv.missingIn, offer: inv.offer } : null,         state: ready ? 'ready' : 'needs-extension',   // a pending socket that failed its ping probe is NOT "resolving" - nothing real is coming         pendingProfiles: pend.map(([k]) => k),   // DRIVABLE, but the profile email is not resolved yet         zombieHosts: zombies,                    // always [] - see the isZombie note; kept for shape stability@@ -1949,11 +1954,11 @@ function dispatch(verb, args) {         // nb drives whatever native browser is present (adom/nb-bridge#46): on an Edge-only box Chrome is         // optional, and an AI reading chromeInstalled:false must not conclude it should install Chrome.         nativeBrowser: nativeBrowserSummary(),  // {chromeInstalled, edgeInstalled, nativeBrowserPresent, chromeNeeded, edgeProfiles, chromeProfiles}-        _hint: extensionStale+        _hint: (extensionStale           ? `STALE EXTENSION: ${staleDetail}; this bridge expects ${EXPECTED_EXT}. New verbs (wait_for/keepalive/download_wait/set_date/totp/robust-click) will return "verb not implemented yet" on the stale profile(s). SELF-HEAL: (1) refresh the unpacked extension dir to the current build (installing-the-extension -> Updating, or re-sync the extension/ folder), then (2) nbrowser_dev_reload {"profile":"<staleProfile>"} - it re-reads disk in 2 passes and reconnects in ~10s. If the on-disk build is ALREADY current, step (2) alone heals it.` + skewHint           : ready           ? `nbe (the Adom Native Browser Extension) is connected (${live.length} profile(s)) and current (v${EXPECTED_EXT || '?'}); drive it with nbrowser_open_window. Read-only probe - it never spawns work or installs.`-          : `nbe (the Adom Native Browser Extension) is NOT connected. ${pend.length ? `(${pend.length} host socket(s) - ${pend.map(([k]) => k).join(', ')} - are open but FAILED a live ping probe: a native host is connect-looping with no working extension behind it. Do NOT wait on these; treat this box as needs-extension.) ` : ''}${NBE_PITCH}${chExe ? '' : 'This box has NO native Google Chrome (Edge may be present). If the user wants the AI to drive their REAL Chrome, offer nbrowser_install_chrome (installs Google Chrome, mostly no prompt). '}${claudeExt.installed ? `NOTE: the user HAS the Claude for Chrome extension (${claudeExt.profiles.join(', ')}) - you cannot drive it (closed control plane), but the honest interim is real: they can ask Claude in their browser to work a page interactively, while you offer nbe for full automation from any AI thread. ` : ''}It may be not-yet-installed (a Level-2 proactively-offered install: OFFER to install it, then run installing-the-extension - never make them do it) OR installed-but-asleep (the SW sleeps when a profile has no open window). Full onboarding: the setting-up-native-chrome skill. Read-only: never installs or spawns.` + skewHint + hostHint,+          : `nbe (the Adom Native Browser Extension) is NOT connected. ${pend.length ? `(${pend.length} host socket(s) - ${pend.map(([k]) => k).join(', ')} - are open but FAILED a live ping probe: a native host is connect-looping with no working extension behind it. Do NOT wait on these; treat this box as needs-extension.) ` : ''}${NBE_PITCH}${chExe ? '' : 'This box has NO native Google Chrome (Edge may be present). If the user wants the AI to drive their REAL Chrome, offer nbrowser_install_chrome (installs Google Chrome, mostly no prompt). '}${claudeExt.installed ? `NOTE: the user HAS the Claude for Chrome extension (${claudeExt.profiles.join(', ')}) - you cannot drive it (closed control plane), but the honest interim is real: they can ask Claude in their browser to work a page interactively, while you offer nbe for full automation from any AI thread. ` : ''}It may be not-yet-installed (a Level-2 proactively-offered install: OFFER to install it, then run installing-the-extension - never make them do it) OR installed-but-asleep (the SW sleeps when a profile has no open window). Full onboarding: the setting-up-native-chrome skill. Read-only: never installs or spawns.` + skewHint + hostHint) + offerHint,       }) });       });   // end probe.then     }@@ -1991,6 +1996,16 @@ function dispatch(verb, args) {       return resolve({ success: true, output: JSON.stringify({ ok: true, installing: true, phase: 'installing', edgeInstalled: nbSum.edgeInstalled, chromeNeeded: nbSum.chromeNeeded, _note: nbSum.edgeInstalled ? 'Edge is already on this box and nb drives it; Chrome is optional. Continuing because you asked.' : undefined, _hint: 'Installing Google Chrome in the BACKGROUND (downloading Google\'s official installer, then a quiet user-scoped install). On a normal PC this is silent, NO prompt. On a locked-down box (VM/managed) it needs a Windows approval - the bridge fires a sticky "Approve now" toast and waits for the user to tap it, then raises the UAC. Poll nbrowser_chrome_status: phase goes installing -> awaiting_uac -> ready. Then help the user sign into Google + load the extension (setting-up-native-chrome).', statusVerb: 'nbrowser_chrome_status' }) });     }     // nbrowser_chrome_status - READ-ONLY: is native Chrome installed / how's an in-flight install going?+    // nbrowser_prewarm - ab calls this once per installed version (~20s after the first boot after an install+    // or update; adom/nb-bridge#50). nb has no assets to fetch; what is worth doing early is the tally.+    if (verb === 'nbrowser_prewarm') {+      let inv; try { inv = nbeInventory(); } catch (e) { return resolve({ success: false, error: 'inventory failed: ' + String((e && e.message) || e) }); }+      const t = inv.tally;+      const summary = `${[inv.browsers.chrome && 'Chrome', inv.browsers.edge && 'Edge'].filter(Boolean).join(' + ') || 'no supported browser'}; ` ++        `${t.profiles} profile(s): nbe installed in ${t.installed}, switched off in ${t.disabled}, missing from ${t.missing}`;+      return resolve({ success: true, output: JSON.stringify({ ok: true, summary, ...inv,+        _hint: `Tally taken ${inv.asOf} and saved to ${INVENTORY_FILE}. It is a snapshot: nbrowser_readiness recomputes it live on every call, so read that (its nbe block) for the current state.${inv.offer ? ' ' + inv.offer : ''}` }) });+    }     if (verb === 'nbrowser_chrome_status') {       const st = chromeStatus();       const nb = nativeBrowserSummary();@@ -2030,6 +2045,7 @@ function dispatch(verb, args) {         d('nbrowser_activity', 'Bounded "who drove what, when" activity trail (max 600 entries / ~1h, in-memory, resets on bridge restart). Returns recent verb entries + a per-session summary. Debug a stray badge or see what is running.', { sessionId: 'optional str', thread: 'optional str', profile: 'optional str', verb: 'optional str', errorsOnly: 'optional bool', sinceMs: 'optional int', limit: 'optional int (<=600)' }, { ok: 'bool', entries: '[{agoMs,verb,sessionId,thread,profile,ok,errorCode}]', activeSessions: '[{sessionId,thread,purpose,count,lastAgoMs}]' }, 15, { example: {} }),         d('nbrowser_readiness', 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? Never spawns work or installs. Use for "is it ready / do I have the browser extension" instead of nbrowser_status. Also flags a STALE extension (loaded build older than this bridge expects) so you fix it up-front instead of hitting "verb not implemented yet" mid-task.', {}, { ready: 'bool', state: '"ready"|"needs-extension"', installing: 'bool', extensionConnected: 'bool', profiles: '[str]', extensionVersions: '{profile:version}', expectedExtensionVersion: 'str|null', extensionStale: 'bool', staleProfiles: '[str]', chromeInstalled: 'bool', chromeInstallPhase: 'str', claudeExtension: '{installed:bool, profiles:[str]} - Claude for Chrome presence (human fallback; never drivable)' }, 15, { example: {} }),         d('nbrowser_install_chrome', 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a quiet user-scoped install (no admin on a normal PC); escalates to a notify-first UAC on a locked-down box. NON-BLOCKING - poll nbrowser_chrome_status. Step 1 of putting the AI onto the user\'s real signed-in browser.', { elevate: 'optional "auto"(default) | true (skip quiet, straight to UAC) | false (quiet only, no prompt)', retryUac: 'optional bool - re-raise the Windows security prompt from the installer already on this PC (no re-download), for a caller with eyes on the screen when the toast could not be seen' }, { ok: 'bool', installing: 'bool', installed: 'bool?', alreadyInstalled: 'bool?', executablePath: 'str?', statusVerb: 'str' }, 30, { statusVerb: 'nbrowser_chrome_status', example: {} }),+        d('nbrowser_prewarm', 'READ-ONLY tally of browsers, profiles and where nbe is installed / off / missing (ab runs it after an install). readiness recomputes it live.', {}, { ok: 'bool', summary: 'str', asOf: 'str', browsers: 'obj', tally: 'obj', installedIn: 'arr', disabledIn: 'arr', missingIn: 'arr', profiles: 'arr', offer: 'str?' }, 60, { example: {} }),         d('nbrowser_chrome_status', 'READ-ONLY: is native Google Chrome installed / how is an install going? Poll after nbrowser_install_chrome.', {}, { notifications: '{checked,toastsEnabled,adomBridgeEnabled,toastMayBeMuted,_hint} - Windows toast switches, so a caller knows when the Approve-now toast could not have been seen', canRetryUac: 'bool', installed: 'bool', executablePath: 'str?', phase: '"idle"|"installing"|"awaiting_uac"|"ready"|"failed"', installing: 'bool', awaitingUac: 'bool', errorCode: 'str?' }, 15, { example: {} }),         d('nbrowser_ping', 'Round-trip liveness check through to the extension.', {}, { ok: 'bool', pong: 'bool' }, 15),         d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { left: 'optional int (logical px)', top: 'optional int', width: 'optional int (default: the newWindowSize setting; fill = the work area inset 10 px, pup R1)', height: 'optional int', state: 'optional "maximized"|"normal"', sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),@@ -3319,6 +3335,46 @@ startHttp(argPort());  // Enumerate all Chrome + Edge profiles from each browser's Local State (info_cache: dir -> {user_name}), // so we can map a live window (its OS title carries the signed-in email on Google pages) back to a KEY.+// THE NBE TALLY (John, 2026-09-22): which browsers are on this machine, every profile in them, and where+// the Adom extension (nbe) is installed, switched off, or missing - so an AI can offer the install ("want me+// to add the extension to your Edge work profile so I can do more?") instead of guessing. NEVER CACHED:+// it is re-read from the browsers' own files on every call (a few ms), so a profile added or an extension+// loaded a minute ago shows up at once. nbrowser_prewarm runs the same read early and saves a timestamped+// snapshot (inventory.json) for anything that reads the file; readiness always recomputes.+const INVENTORY_FILE = path.join(os.homedir(), '.adom', 'bridges', BRIDGE, 'inventory.json');+function nbeInventory() {+  const nb = nativeBrowserSummary();+  const liveKeys = new Set(userConns().map(([k]) => k));+  const profiles = [];+  for (const kp of knownProfiles()) {+    let st = {}; try { st = adomExtStateForProfile(kp.browser, kp.profileDir) || {}; } catch {}+    const nbe = !st.checked ? 'unknown' : !st.installed ? 'missing' : !st.enabled ? 'disabled' : 'installed';+    profiles.push({ profile: kp.key, browser: kp.browser, label: kp.displayName || null, profileDir: kp.profileDir, nbe,+      nbeVersion: st.version || null, connected: liveKeys.has(kp.key),+      stale: !!(st.version && EXPECTED_EXT && cmpVer(st.version, EXPECTED_EXT) < 0) });+  }+  const keys = (state) => profiles.filter((x) => x.nbe === state).map((x) => x.profile);+  const installedIn = keys('installed'), disabledIn = keys('disabled'), missingIn = keys('missing');+  const name = (k) => { const x = profiles.find((y) => y.profile === k); return x && x.label ? `${k} ("${x.label}")` : k; };+  let offer = null;+  if (profiles.length && !installedIn.length && !disabledIn.length) {+    offer = `The Adom extension (nbe) is not in any browser profile on this machine (${profiles.map((x) => name(x.profile)).join(', ')}). ` ++      'Worth offering the user, once, in plain words: "Want me to add the Adom extension to your browser? Then I can work in your real, signed-in ' ++      'browser directly: read pages, click, fill forms and take screenshots." Only with their OK; the installing-the-extension skill does the rest.';+  } else if (missingIn.length || disabledIn.length) {+    offer = `nbe is installed in ${installedIn.length ? installedIn.map(name).join(', ') : 'no profile'}` ++      `${missingIn.length ? `, missing from ${missingIn.map(name).join(', ')}` : ''}` ++      `${disabledIn.length ? `, and switched off by the user in ${disabledIn.map(name).join(', ')}` : ''}. ` ++      'If the user works in one of the missing profiles, you could offer: "Want me to add the Adom extension there too?" Only with their OK. ' ++      'Never switch a disabled one back on without asking: they may have turned it off on purpose.';+  }+  const inv = { asOf: new Date().toISOString(),+    browsers: { chrome: !!nb.chromeInstalled, edge: !!nb.edgeInstalled },+    tally: { profiles: profiles.length, installed: installedIn.length, disabled: disabledIn.length, missing: missingIn.length },+    installedIn, disabledIn, missingIn, profiles, offer };+  try { fs.mkdirSync(path.dirname(INVENTORY_FILE), { recursive: true }); fs.writeFileSync(INVENTORY_FILE, JSON.stringify(inv, null, 1)); } catch {}+  return inv;+} function knownProfiles() {   const out = [];   const local = process.env.LOCALAPPDATA;
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.31",+  "version": "0.16.32",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",
skills/adom-native-browser/SKILL.md+14
@@ -85,6 +85,20 @@ On yes, run **installing-the-extension**. Never demand the user click through `c themselves, and never ask them to type a long path - WE own that orchestration over Bridge's generic OS verbs, with a single `notify_user` consent button. +### Where the extension is, per profile: the nbe tally (bridge 0.16.32)++`nbrowser_readiness` carries an `nbe` block: which browsers are installed, a count of profiles, and the+profile keys where the extension is `installedIn`, `disabledIn` (switched off by the user) and `missingIn`.+It is re-read from the browsers' own files on every call, so it is never out of date. When some profile+lacks the extension, the block's `offer` line (also appended to the readiness hint) gives the wording+to put to the user. For example, their Chrome has it but their Edge work profile does not: "Want me to+add the Adom extension to your Edge work profile too?"++- Offer once, at a moment it helps, and only install on their yes.+- Never switch a disabled one back on without asking. They may have turned it off on purpose.+- `nbrowser_prewarm` runs the same tally when Adom Bridge installs or updates nb, and saves a timestamped+  snapshot. That snapshot can age, so for the current state always read readiness.+ ## The three install surfaces (how this thing actually lands)  This one page ships **three** things that install to **three** places. All three must line up for a
tools/verb-battery.mjs+4−2
@@ -73,6 +73,8 @@ const T = {   profiles: () => { const r = call('nbrowser_profiles'); const ps = r.profiles || []; ctx.profileCount = ps.length; const def = ps.find((p) => p.profile === r.active); const pups = ps.filter((p) => p.origin === 'pup').length; return A(ps.length > 0 && !(def && def.origin === 'pup'), `${ps.length} profiles, default ${r.active} (origin ${def ? def.origin : 'none'}), ${pups} pup connection(s) set aside`, `default is pup's browser or no profiles: ${JSON.stringify(r).slice(0, 160)}`); },   resolve_profiles: () => { const r = call('nbrowser_resolve_profiles'); return A(r.ok !== false, 'resolved', JSON.stringify(r).slice(0, 120)); },   // An Edge-only box is a PASS: nb drives whatever native browser is there (nb-bridge#46).+  // The nbe tally (nb-bridge#50): browsers, profiles, where nbe is. Must be fresh: compare against readiness's live copy.+  prewarm: () => { const r = call('nbrowser_prewarm'); const rd = call('nbrowser_readiness'); const t = r.tally || {}; const live = (rd.nbe && rd.nbe.tally) || {}; const fresh = Date.now() - Date.parse(r.asOf || 0) < 60000; const same = JSON.stringify(t) === JSON.stringify(live); return A(r.ok === true && t.profiles > 0 && fresh && same && Array.isArray(r.installedIn), `${r.summary}; readiness agrees`, `tally ${JSON.stringify(t)} vs readiness ${JSON.stringify(live)} fresh:${fresh}`); },   chrome_status: () => { const r = call('nbrowser_chrome_status'); const ok = r.installed === true || (r.nativeBrowserPresent === true && r.chromeNeeded === false && r.edgeInstalled === true); return A(ok, r.installed ? `chrome installed, phase:${r.phase}` : 'edge-only box: chromeNeeded:false, Chrome reported optional', JSON.stringify(r).slice(0, 140)); },   describe: () => { const r = call('nbrowser_describe'); const n = (r.verbs || []).length; const meta = metaCount(); return A(n === meta, `describe:${n} == VERB_META:${meta}`, `describe:${n} != VERB_META:${meta} — regenerate!`); },   scroll: () => { const r = call('nbrowser_scroll', { sessionId: SID, x: 200, y: 200, deltaY: 300 }); return A(r.ok === true && r.dispatched === 'wheel' && r.scrollAfter && typeof r.pageScrolled === 'boolean', `wheel dispatched; pageScrolled=${r.pageScrolled}`, JSON.stringify(r).slice(0, 160)); },@@ -269,7 +271,7 @@ const T = { // ─── coverage map: EVERY VERB_META verb decides its fate here ─────────────── export const COVERAGE = {   status: 'live', readiness: 'live', ping: 'live', progress: 'live', scroll: 'live', input_unknown_type: 'guard', hover_raw_xy: 'live', apply_settings: 'skip:internal bridge-to-extension push, exercised by the settings test through nbrowser_settings', whoami: 'live', profiles: 'live',-  resolve_profiles: 'live', chrome_status: 'live', describe: 'live', activity: 'live',+  resolve_profiles: 'live', prewarm: 'live', chrome_status: 'live', describe: 'live', activity: 'live',   os_windows: 'live', wake_profile: 'live', rescan: 'live', resync_sessions: 'live',   open_window: 'live', pin_session: 'live', use_profile: 'live', list_windows: 'live',   window_state: 'live', switch_window: 'live', navigate: 'live', reload: 'live',@@ -347,7 +349,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {   // are an ab lifecycle fact, not a verb regression.   call('nbrowser_wake_profile', { profile: PROFILE });   for (let i = 0; i < 10; i++) { const r = call('nbrowser_readiness'); if (r.ready === true) { console.log(`warm-up: ready after ${i} poll(s)\n`); break; } execFileSync('sleep', ['3']); }-  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',+  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','prewarm','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',     'open_window','pin_session','use_profile','list_windows','window_state','switch_window',     'navigate','reload','back','forward','wait','wait_for','keepalive',     'eval','evaluate','screenshot','screenshot_full_res','errors','fetch_url',