← Commit history

v0.16.22 (insiders): Docs release: README What's new and version table, INSTALL-FLOW (fresh box, updating, host restore) and DEVELOPING (dev-loop traps, release tiers) brought up to date; driving-the-extension now documents windowEtiquette, nbrowser_progress and _origin; README and docs travel with every ship; no em-dashes left in any skill or doc. No code change since 0.16.21.

John Lauer ·b92ffc8226 ·1mo ago ·parent 6c58553
13 files changed +156−66
README.md+40−10
@@ -176,6 +176,34 @@ point it at, via the debugger/scripting APIs - never by loading external scripts  ## What's new +**September 2026 (bridge 0.16, extension 0.2.15)**++- **Windows open where you would put them.** Every nb window lands on your primary monitor's work area+  with a 10 px inset, the same placement rule pup uses, instead of Chrome's own 60 percent default for an+  extension-created window. A `newWindowSize` setting (fill, 95, 90, 75, or Chrome's default) lives in the+  NB Dashboard.+- **Mouse wheel and canvas zoom.** `nbrowser_scroll` (and `nbrowser_input_dispatch {type:"wheel"}`) send a+  real trusted wheel at a point, which is what a Babylon or three.js camera listens for. An unknown input+  type is refused instead of silently becoming a click, and hover accepts raw coordinates.+- **Measured progress.** `nbrowser_progress` reports every in-flight verb with a percent and an ETA sized+  from that machine's own history; slow verbs carry a terminal progress block on their reply.+- **Use the browser that is there.** On a fresh Windows box nb drives Edge, including a profile with no+  signed-in account, and only offers to install Chrome when no supported browser exists or you ask for+  Chrome by name. If a Windows security prompt is waiting and its notification was muted,+  `nbrowser_chrome_status` says so and `nbrowser_install_chrome {retryUac:true}` shows the prompt again.+- **nb never touches a window it did not open.** Window identity is resolved among the windows of the+  session's own browser only, an unresolved window is left alone, and the taskbar identity names the app+  and profile, never a task.+- **Send to AI lets you pick the AI.** The dashboard's prompts open Hydrogen's AI picker and report real+  delivery status from the job, including a cancel.+- **The toolbar icon is a choice.** The extension stays an unpinned, small surface in your own browser by+  default; a host that loads it into its own window (pup) can pin it and show an "nb" badge.+- **Background-safe contracts.** Every window verb answers `windowEtiquette`, and an unjustified foreground+  is refused with `errorCode: "foreground_reason_required"`. `nbrowser_close_window` answers in well under+  a second again (it had been timing out while the window closed anyway).++**Earlier**+ - **Custom-element / shadow-DOM sites just work** (Schwab, QuickBooks, and friends). The finder behind   clicks/eval/select **pierces open shadow roots**, so a control whose real options hide in a web   component is found by **visible text** or a **stable attribute** - no more falling back to fragile@@ -222,16 +250,18 @@ it connect" dead end:  | Stream | What it is | Where you see it | Current | |---|---|---|---|-| **Bridge runtime** | the local server Bridge runs (`bridge.json` version) | the **Download** card | 0.2.0 |-| **Extension + host** | the MV3 extension + native host loaded into Chrome/Edge (`extension/manifest.json`) | `chrome://extensions` | 0.2.0 |-| **Skills package** | the docs your AI reads (`pkg install`, root `package.json`) | the **Install** card | 0.5.x |--The bridge and the extension are BOTH `0.2.x` but are **different numbers** and update independently.-The skew that matters for connectivity is **extension vs `bridge.json` `expectedExtensionVersion`** (the-extension the running bridge speaks to), NOT the `0.4.x` skills-package number. If the native host keeps-exiting ("Native host has exited"), your extension/host is too old for the running bridge: re-sync the-`extension/` + `host/` folders to the current build and reload. `nbrowser_readiness` (bridge 0.1.50+)-reports this directly as `staleOnDisk`.+| **Bridge runtime** | the local server Bridge runs (`bridge.json` version) | the **Download** card | 0.16.x |+| **Skills package + NB Dashboard** | the docs your AI reads and the dock dashboard (`pkg install`, root `package.json`) | the **Install** card | 0.16.x, the SAME number as the runtime since 0.16.0 |+| **Extension + host** | the MV3 extension + native host loaded into Chrome/Edge (`extension/manifest.json`) | `chrome://extensions` | 0.2.x, its own number |++The runtime, the dashboard and the skills package share ONE version since 0.16.0 and ship together. The+extension keeps its own `0.2.x` number because it does not travel with a release: it is a folder loaded+into your browser. The skew that matters for connectivity is **extension vs `bridge.json`+`expectedExtensionVersion`** (the extension the running bridge speaks to). `nbrowser_readiness` reports it+as `staleProfiles` (a connected profile running an older build) and `staleOnDisk` (the folder on disk is+older). The fix is always the same: refresh the `extension/` (and `host/`) folder from this page, then+reload EVERY profile you drive, including ones you wake later, and reload a second time if the version+does not move (the first reload after a folder change can still serve the old files).  > **What `pkg install` gives you:** this package installs the bridge's SKILLS into your container so your > AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself
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.21';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.22';           // 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.21\ No newline at end of file+0.16.22\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.21",+  "version": "0.16.22",   "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",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.21",+  "version": "0.16.22",   "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",
docs/DEVELOPING.md+30−14
@@ -1,44 +1,43 @@ # Fork & develop your own Adom Native Browser Extension -A guide for **Adom users and third parties** who want to build their own version of this extension —-fork it, load it in Chrome/Edge **developer mode**, and iterate. Everything here works against the+A guide for **Adom users and third parties** who want to build their own version of this extension - fork it, load it in Chrome/Edge **developer mode**, and iterate. Everything here works against the public, open-source repo.  ## 1. Get the source (fork it)  The extension lives as a wiki page + git repo at **[wiki.adom.inc/adom/nb-bridge](https://wiki.adom.inc/adom/nb-bridge)**. -- **Fork / propose changes upstream** — use the page's **Pull Requests** tab to propose edits back, or-- **Clone the source** — grab the repo from the **Files** tab (or `adom-wiki pkg install adom/nb-bridge`),+- **Fork / propose changes upstream** - use the page's **Pull Requests** tab to propose edits back, or+- **Clone the source** - grab the repo from the **Files** tab (or `adom-wiki pkg install adom/nb-bridge`),   then copy the **`extension/`** folder somewhere you can edit. -The folder you load into the browser is **`extension/`** — it's the one with `manifest.json`.+The folder you load into the browser is **`extension/`** - it's the one with `manifest.json`.  ## 2. Load it in Chrome (developer mode)  1. Open **`chrome://extensions`**.-2. Turn on **Developer mode** — the toggle is **top-right**. ①+2. Turn on **Developer mode** - the toggle is **top-right**. ① 3. Click **Load unpacked** (top-left) and pick your **`extension/`** folder. ②-4. The extension loads — note its **ID**. ③  After any code edit, click the **Reload (↻)** button on its card.+4. The extension loads - note its **ID**. ③  After any code edit, click the **Reload (↻)** button on its card. -![Loading the extension in Chrome developer mode — Developer mode toggle, Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-chrome.png)+![Loading the extension in Chrome developer mode - Developer mode toggle, Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-chrome.png)  ## 3. Load it in Edge (developer mode)  Same idea, slightly different layout:  1. Open **`edge://extensions`**.-2. Turn on **Developer mode** — in Edge the toggle is in the **bottom-left** of the sidebar. ①+2. Turn on **Developer mode** - in Edge the toggle is in the **bottom-left** of the sidebar. ① 3. Click **Load unpacked** (top of the page) and pick your **`extension/`** folder. ② 4. The extension loads. ③ -![Loading the extension in Edge developer mode — Developer mode (bottom-left), Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-edge.png)+![Loading the extension in Edge developer mode - Developer mode (bottom-left), Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-edge.png) -## 4. Or — let Bridge install it for you+## 4. Or - let Bridge install it for you -You don't have to click through it. Ask your Adom AI assistant to do the load-unpacked for you — it+You don't have to click through it. Ask your Adom AI assistant to do the load-unpacked for you - it drives the `chrome://extensions` / `edge://extensions` page at the **OS level** (via `desktop_navigate`-+ `desktop_ui_click` — background, no mouse-stealing), toggling **Developer mode** and clicking++ `desktop_ui_click` - background, no mouse-stealing), toggling **Developer mode** and clicking **Load unpacked** on the folder you point it at. It always opens a **fresh window** for this, never touching the one you're working in. @@ -53,10 +52,27 @@ Load my local nb-bridge extension from C:\path\to\extension into Chrome via deve - Edit code in **`extension/`** → click **Reload (↻)** on the extension card (or just ask the assistant:   `reload my extension`, which runs `nbrowser_dev_reload`). - The native-messaging **host** and the **`native-browser` bridge** reconnect automatically; run-  `nbrowser_status` to confirm (expect a ~3–6 s reconnect after a reload).+  `nbrowser_status` to confirm (expect a ~3-6 s reconnect after a reload). - Layout: `extension/` (the MV3 extension), `bridge/` (the native-browser bridge), `host/` (the   native-messaging host). The verb surface is documented in the **`driving-the-extension`** skill. +### Two dev-loop traps++- After you change files in the loaded folder, the first `nbrowser_dev_reload` can still serve the old+  files. If `nbrowser_status` does not show your change, reload once more.+- Reload every profile that loads the extension, by name. A bare reload only takes the active profile.++### How releases reach people (two speeds)++Every build of the bridge ships to an early tier first, and the public manifest on this page moves only+when a human promotes a specific version in their own words; those words are recorded in the manifest+(`promotion_attestation`, `promoted_by`, `promoted_at`). `tools/ship.sh <version> "<changelog>"` cuts the+early build (static gates, one version across the runtime, the dashboard and the skills package, the zip+as a release asset on this page, the skills package at the same version); `tools/promote.sh <version>+"<the human's words>" --by "<name>"` re-points the public manifest at that same zip after re-checking its+hash against the served asset, and refuses a placeholder. The generic pattern is in the Adom Bridge SDK+under "Release tiers". Run `node tools/verb-battery.mjs --target <box>` before asking anyone to promote.+ ## 6. Contribute back  Open a **Pull Request** on the wiki page with your changes. If you publish your **own** build to the
docs/INSTALL-FLOW.md+29
@@ -171,6 +171,35 @@ adom-bridge nbrowser_close_window '{"sessionId":"<id>"}' That round-trip - container -> relay -> ab -> bridge -> host -> extension -> back - is the whole point: it runs in the user's real, signed-in profile, so cookies/SSO/saved-logins/captcha all just work. +## A fresh Windows box: use the browser that is there++nb drives whatever native browser the machine has. Edge ships with Windows, so on a brand-new box Edge IS+the browser: `nbrowser_profiles` lists it even when no account is signed in (keyed by its directory, such+as `edge:Default`, marked `anonymous`), Layer 1 opens URLs in it at once, and the extension loads into it+the same way it loads into Chrome. `nbrowser_chrome_status` and `nbrowser_readiness.nativeBrowser` report+`chromeNeeded:true` only when NO supported browser exists. Install Chrome only when the user asks for+Chrome by name: it is a 150 MB download and, on a locked-down machine, a Windows security prompt. If that+prompt is waiting and Windows muted its notification (a fresh Windows 11 keeps Do Not Disturb on until+Adom Bridge's one-time notification permission is answered), `nbrowser_chrome_status.notifications` says+so, and `nbrowser_install_chrome {retryUac:true}` shows the prompt again without downloading again.++Loading the extension into a fresh profile is scripted end to end except for one click a human must make:+choosing the folder in the Windows folder picker. Tell the user that one click is coming.++## Updating an installed extension++The browser runs the FOLDER on disk. Refresh it from this page's `extension/` (keep a copy of the old+files), then `nbrowser_dev_reload {profile}` for every profile you drive. Three things that cost time when+missed: a bare reload only takes the active profile; the first reload after a folder change can still+serve the old files, so reload a second time if the version does not move; and a profile you WAKE later+starts from a stale script cache while announcing the new version number, so it needs its own reload too.++If the extension is loaded but never connects (readiness stays `needs-extension` and a `pending-N`+connection appears and vanishes), the native-messaging host is missing: restore `host/native-host.js`,+`host/native-host.bat` and `host/host-manifest.json` from this page and register+`HKCU\Software\Google\Chrome\NativeMessagingHosts\inc.adom.native_browser` (and the `Microsoft\Edge`+twin) to the manifest path. No admin rights are needed.+ ## Who installs what (the short answer to "does the bridge install the extension?")  | Component | Installed by | Notes |
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.21",+  "version": "0.16.22",   "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/driving-sites/SKILL.md+7−7
@@ -1,17 +1,17 @@ --- name: driving-sites-description: "The index of SITE RECIPES for the Adom Native Browser Extension — per-site playbooks that teach the AI how to drive a specific website (login flow, key selectors, gotchas, 2FA, logout) so it doesn't re-discover them every time. Read this when about to automate a known site, when you keep fighting a site's quirks, or when you (or a third party) want to CONTRIBUTE a new site recipe. Match on: site recipe, driving a website, how to log into <site>, site-specific automation, contribute a site recipe, add a site playbook, navigate <vendor> site. Lists the known recipes (reading-google-messages, …) and the template + breadcrumb flow for adding more."+description: "The index of SITE RECIPES for the Adom Native Browser Extension - per-site playbooks that teach the AI how to drive a specific website (login flow, key selectors, gotchas, 2FA, logout) so it doesn't re-discover them every time. Read this when about to automate a known site, when you keep fighting a site's quirks, or when you (or a third party) want to CONTRIBUTE a new site recipe. Match on: site recipe, driving a website, how to log into <site>, site-specific automation, contribute a site recipe, add a site playbook, navigate <vendor> site. Lists the known recipes (reading-google-messages, …) and the template + breadcrumb flow for adding more." --- Parent: **driving-the-extension** · generic OS/popup mechanics: **driving-the-desktop**. -Every website is idiosyncratic — split login forms, where the Sign-out lives, whether the password autofills,+Every website is idiosyncratic - split login forms, where the Sign-out lives, whether the password autofills, SSO vs password, 2FA quirks. Without a recipe the AI re-discovers all of it every time (a fresh vendor login cost hours once). A **site recipe** captures a site's playbook ONCE so the next run is fast and safe.  ## Known recipes (in this repo) | Recipe | Site | What it covers | |---|---|---|-| **reading-google-messages** | Google Messages PWA | Read SMS / retrieve a 2FA code from the desktop Messages app (UIA, since PrintWindow is blank) — the helper that makes SMS-2FA logins fully automatic |+| **reading-google-messages** | Google Messages PWA | Read SMS / retrieve a 2FA code from the desktop Messages app (UIA, since PrintWindow is blank) - the helper that makes SMS-2FA logins fully automatic |  ## How a site recipe is USED 1. When you're about to drive a login/site, the **`nbrowser_login_form`** detector tells you the smart path@@ -23,7 +23,7 @@ hours once). A **site recipe** captures a site's playbook ONCE so the next run i   `uninstall.sh`, and `package.json` `files[]`, and add a row to the table above. Open a PR on the   `adom-native-browser` wiki page. - **Your own page, breadcrumbed (the scaling path):** publish `driving-<site>` (or "I built a driver for-  <site>") as its OWN wiki page — you own + version it — and **breadcrumb it to the `adom-native-browser`+  <site>") as its OWN wiki page - you own + version it - and **breadcrumb it to the `adom-native-browser`   anchor** (see the wiki `wiki-breadcrumbs` / `wiki-family` skills). This is exactly the wiki Family pattern:   site recipes are children with independent owners/cadences. The anchor lists them; discovery routes by domain. @@ -31,7 +31,7 @@ hours once). A **site recipe** captures a site's playbook ONCE so the next run i ```markdown --- name: driving-<site>-description: Site recipe for <Site> (<domain>) with the Adom Native Browser Extension — login flow, selectors,+description: Site recipe for <Site> (<domain>) with the Adom Native Browser Extension - login flow, selectors,   gotchas, 2FA, logout. Match on "log into <site>", "<domain>", "open <site>". Part of the   adom-native-browser site-recipe set (see driving-sites). ---@@ -59,8 +59,8 @@ Part of the **adom-native-browser** site-recipe set → **driving-sites**. Gener ```  ## The hard-won rules every recipe should encode-- **Detect first, act second** — `nbrowser_login_form`; if the form is already autofilled, JUST SUBMIT (background).-- **Never wipe cookies to log out** unless you know it won't drop the device-trust/2FA-remember cookie — use+- **Detect first, act second** - `nbrowser_login_form`; if the form is already autofilled, JUST SUBMIT (background).+- **Never wipe cookies to log out** unless you know it won't drop the device-trust/2FA-remember cookie - use   the site's own Sign-out (one HR vendor's cookie-wipe caused a 2FA lockout). - **Prefer CDP/background**; reserve the ~1s foreground only for the autofill popup a site won't auto-fill. - **Submit by selector, not button text** when text-clicks prove flaky.
skills/driving-the-extension/SKILL.md+28−13
@@ -28,7 +28,7 @@ them - never to drive or navigate it. (`navigate, reload, back, forward, click, type, press_key, hover, input_dispatch, select, set_date, eval, cursor, caption, focus, close_tab, switch_tab, wait_for, totp, autologin, login_form, record_start`) refuses:-- a **bare call** (no `sessionId`/`tabId`) → `no_target` — the old active-tab fallback is gone; it is+- a **bare call** (no `sessionId`/`tabId`) → `no_target` - the old active-tab fallback is gone; it is   how an agent navigated a user's working tab and wiped their other AI threads; - a **target outside a window the agent opened** → `not_owned` (`nbrowser_open_window`'s session map   IS the ownership registry). `open_tab` likewise requires an owned session's `sessionId`.@@ -36,26 +36,26 @@ There is **NO agent-passable override**. Refusals carry a teaching `_hint` with and the first response of each connection carries a one-time `_safety` banner stating this rule. (`tools/check-guard-coverage.mjs` fails the build if a new verb ships unclassified or ungated.) -## ⭐ MANDATORY PREFLIGHT — profiles first, then your own window+## ⭐ MANDATORY PREFLIGHT - profiles first, then your own window  Before driving ANYTHING, in this order: 1. `nbrowser_profiles` (read-only) → the connected `browser:email` keys (Chrome AND Edge). Each row-   also carries **`displayName`** — the human label the USER sees on the browser's own profile button-   ("John Personal", "adom.inc") — plus `profileDir` and `hasAvatar` (v0.1.20+). **Never assume which-   browser holds an account** — the currently-active browser is NOT "the" browser. A row with+   also carries **`displayName`** - the human label the USER sees on the browser's own profile button+   ("John Personal", "adom.inc") - plus `profileDir` and `hasAvatar` (v0.1.20+). **Never assume which+   browser holds an account** - the currently-active browser is NOT "the" browser. A row with    **`unresolved:true`** hasn't announced its email yet (fresh bridge restart): run    `nbrowser_resolve_profiles` FIRST or you may drive the wrong account. A row with **`asleep:true`**    EXISTS on the machine but has no open window (its extension service worker sleeps), so it can't be-   driven yet — **wake it yourself with `nbrowser_wake_profile {profile:"<key>"}`**: it launches the-   browser with `--no-startup-window`, so the profile + extension load in the BACKGROUND — **no window-   opens, nothing foregrounds, no focus is stolen** — and the profile flips to live in ~2s. NEVER ask-   the human to open a window for you, and NEVER launch a foreground browser to wake a profile — the+   driven yet - **wake it yourself with `nbrowser_wake_profile {profile:"<key>"}`**: it launches the+   browser with `--no-startup-window`, so the profile + extension load in the BACKGROUND - **no window+   opens, nothing foregrounds, no focus is stolen** - and the profile flips to live in ~2s. NEVER ask+   the human to open a window for you, and NEVER launch a foreground browser to wake a profile - the    wake verb exists precisely so waking honors background-by-default. (`woke:false` usually means the-   Adom extension isn't installed in THAT profile — extensions are per-profile; offer the install.)-2. Pick the profile whose **email matches the identity the task needs** — email DOMAIN + `displayName`+   Adom extension isn't installed in THAT profile - extensions are per-profile; offer the install.)+2. Pick the profile whose **email matches the identity the task needs** - email DOMAIN + `displayName`    tell you work vs personal (corporate domain = work; gmail.com etc. = personal). Setting up a tool for    `[email protected]` → drive `chrome:[email protected]`, not whatever profile happens to be focused.-   If the needed profile isn't listed, ask the user to open a window in it — don't improvise in+   If the needed profile isn't listed, ask the user to open a window in it - don't improvise in    another one. 3. `nbrowser_open_window {sessionId:"<task id>", thread:"<which AI thread>", purpose:"<why, human-readable>", profile:"<key>", url:…}` → a BACKGROUND window auto-pinned to that profile. **All three of sessionId/thread/purpose are REQUIRED** (registration) or it refuses `session_unregistered`; `purpose` shows in the Session Monitor. 4. Drive ONLY via that `sessionId`. If a page you drive **spawns** a window (a Gmail-Contacts email click pops a compose; `target=_blank`/OAuth popups), your next verb is refused `unacknowledged_spawns` → `nbrowser_owned {sessionId}` to see it, `nbrowser_cleanup {sessionId}` to close the litter.@@ -303,6 +303,21 @@ This is **not** screenshot→eyeball-a-pixel→click automation, so it does **no   `nbrowser_open_window` and `--window-size/--window-position` on every Layer-1 launch, from the   `newWindowSize` setting (fill|95|90|75|chrome). Never the OS-maximized state, which raises. Your own   `left/top/width/height/state` always win.+- **Where did my window go, and was my foreground refused? Branch on `windowEtiquette` and `errorCode`+  (bridge 0.16.0, ext 0.2.9).** Every verb that can open, raise or resize a window answers+  `windowEtiquette: "background" | "foreground"`. A bare `userRequestedForeground:true` (or Layer-1+  `foreground:true`) with no `foregroundReason` is refused with `errorCode: "foreground_reason_required"`+  and the window stays in the background; `keptBackground:true` is the deprecated twin. Branch on these two+  keys, never on `_hint` or `_warning` text.+- **Slow verb? Poll `nbrowser_progress` (bridge 0.16.0).** It returns `active[]`, one MEASURED block per+  in-flight verb (`phase, stepLabel, percent, elapsedSec, estimatedSec, etaSec, confidence, startKind,+  sessionId`), sized from this machine's own rolling per-verb history (`verbDurations`). `percent` caps at+  95 until the verb really finishes; the slow verb's own reply carries the terminal `progress` block at 100+  with actuals. `confidence:"estimated"` (fewer than three samples) should be drawn dimmed; open-ended verbs+  (wait, wait_for, download_wait, keepalive, record_start) report no percent, so show an indeterminate+  sweep. An empty `active[]` means idle.+- **Every response carries `_origin` (bridge 0.16.0):** `relay | local | bridge | unattributed`, what ab+  observed about the transport, also stamped on every `nbrowser_activity` row. Nothing gates on it yet. - **The nbe's browser TOOLBAR icon is a setting (`nbrowser_settings {set:{toolbarIcon:"auto"|"on"|"off"}}`,   ext 0.2.11 / bridge 0.16.15).** `auto` (default) is Chrome's own default for an unpacked extension: the icon   exists but stays UNPINNED under the puzzle menu, the small surface John wants inside a user's own browser.@@ -360,7 +375,7 @@ a `_modal` note. **Dismiss it:** `nbrowser_handle_dialog {sessionId, accept:true `accept:false` (Cancel/Stay), or `text:"…"` to answer a prompt - then re-screenshot and continue. If an action mysteriously stops working, suspect a dialog. -## Screenshots — pick the RIGHT one (page vs window vs NEVER fullscreen)+## Screenshots - pick the RIGHT one (page vs window vs NEVER fullscreen) Three ways to "see", and the choice matters: - **The PAGE → `nbrowser_screenshot {sessionId}`** (CDP). Your DEFAULT. Fast, background-safe, captures the   page bitmap. But it shows **page content ONLY** - no browser chrome (URL/tabs) and **no native popups/
skills/native-browser-safety/SKILL.md+3−3
@@ -41,13 +41,13 @@ spawned, never the user's own windows). **End every task** by closing what you o attribute, `nbrowser_force_close` is a deliberately hard, two-step last resort (it can never close another thread's window). It has TWO forms behind the SAME gate (ticket → attest with a written reason): **`{windowId}`** for windows the extension can see, and **`{hwnd}`** (v0.1.23+, from-`desktop_list_windows`) for browser windows the extension CANNOT reach — e.g. Chrome/Edge litter that a+`desktop_list_windows`) for browser windows the extension CANNOT reach - e.g. Chrome/Edge litter that a `pup_open`/pup call or an ah test left behind, or a window in a profile with no extension. The hwnd form only targets real browser windows and sends a graceful WM_CLOSE. So cleaning up ANOTHER tool's-browser litter is allowed — but only through this gate, with a real justification, and never a window a+browser litter is allowed - but only through this gate, with a real justification, and never a window a live thread owns. -⚠️ **Pitfall — reading a contact spawns a compose:** to read a person's email/phone in Google Contacts,+⚠️ **Pitfall - reading a contact spawns a compose:** to read a person's email/phone in Google Contacts, **do not click the mailto/email link** (that spawns a compose window). Navigate the person's contact URL or `nbrowser_fetch_url` the data instead. 
skills/reading-google-messages/SKILL.md+13−13
@@ -1,13 +1,13 @@ --- name: reading-google-messages-description: How to find the Google Messages PWA on a Windows desktop and READ SMS out of it — most importantly, to retrieve a one-time 2FA / verification code so you can complete a text-message two-factor login fully automatically. Google Messages for web is usually installed as a taskbar PWA ("Messages - Google Messages for web"); its window renders BLANK to PrintWindow, so you read it with UIA (desktop_find_control), not a background screenshot. Match when the user says "read my texts", "get the SMS code", "what's the 2FA code", "check Google Messages", "find the verification code", "automate the SMS 2FA", or any login that texts a code. Part of the adom-native-browser site-recipe set (see driving-sites).+description: How to find the Google Messages PWA on a Windows desktop and READ SMS out of it - most importantly, to retrieve a one-time 2FA / verification code so you can complete a text-message two-factor login fully automatically. Google Messages for web is usually installed as a taskbar PWA ("Messages - Google Messages for web"); its window renders BLANK to PrintWindow, so you read it with UIA (desktop_find_control), not a background screenshot. Match when the user says "read my texts", "get the SMS code", "what's the 2FA code", "check Google Messages", "find the verification code", "automate the SMS 2FA", or any login that texts a code. Part of the adom-native-browser site-recipe set (see driving-sites). --- Part of the **adom-native-browser** site-recipe set → see **driving-sites**. Generic desktop/UIA mechanics live in **driving-the-desktop**; THIS page is the Google Messages PWA's quirks.  **Why this exists:** lots of sites text a 6-digit code for 2FA (Gusto, banks, etc.). If the user runs **Google Messages for web** as a desktop app, the AI can read that code and finish the login with **zero-human steps** — no "please type the code you got." This page is how. (Proven end-to-end on 2026-06-24:+human steps** - no "please type the code you got." This page is how. (Proven end-to-end on 2026-06-24: automated a full Gusto SMS-2FA login by reading the code out of this PWA.)  ## 1. Find the window (it's a taskbar PWA, not a browser tab)@@ -19,18 +19,18 @@ desktop_list_windows {}                       # scan titles for "Google Messages # or, if your build supports it: desktop_find_window {titleContains:"Google Messages"} ```-Grab the `hwnd` (e.g. 459830). If there's NO such window, the user hasn't installed/opened the PWA — fall+Grab the `hwnd` (e.g. 459830). If there's NO such window, the user hasn't installed/opened the PWA - fall back to surfacing the code request to the human (you can't read what isn't running). -## 2. ⚠️ The gotcha: PrintWindow returns BLANK — read it with UIA instead+## 2. ⚠️ The gotcha: PrintWindow returns BLANK - read it with UIA instead The PWA is a heavy SPA; **`desktop_screenshot_window {hwnd}` (PrintWindow) captures an EMPTY/white frame** when the window is in the background. Do NOT try to OCR a blank shot. Two things that DO work:-- **UIA text read (preferred — works in the BACKGROUND, no foregrounding):**+- **UIA text read (preferred - works in the BACKGROUND, no foregrounding):**   ```   desktop_find_control {hwnd:<msgHwnd>, contains:"verification code"}   # or "is your", or the sender   ```   UIA walks the accessibility tree and returns the message text even though PrintWindow can't see it. The-  freshest SMS is the **top conversation**; its preview/body holds the code. This is the money path — it+  freshest SMS is the **top conversation**; its preview/body holds the code. This is the money path - it   reads the code without stealing focus from whatever you're doing. - **Foreground screenshot (fallback):** `desktop_bring_to_front {hwnd}` → wait ~600ms →   `desktop_screenshot_window {hwnd}` renders correctly once it's foreground. Use only if UIA text is empty.@@ -41,20 +41,20 @@ The SMS body looks like: **"64039 071276 is your Gusto verification code. We'll (`64039` = the sender shortcode, `071276` = the code). Pull the code with a regex over the UIA text: - Prefer a digit-run **near** the words "code"/"verification": `/(\d{4,8})(?=[^\d]*\b(?:is your|verification|code))/i`   then fall back to the first standalone `\b\d{6}\b`.-- **Ignore the shortcode** (4–5 digits like `64039`, `22000`) — it's the SENDER, not the code. The real OTP+- **Ignore the shortcode** (4-5 digits like `64039`, `22000`) - it's the SENDER, not the code. The real OTP   is usually 6 digits. If two numbers appear, the code is the longer/6-digit one, typically the 2nd token. - **Freshness:** make sure you're reading the NEWEST message (top of the list). If you just triggered a-  resend, wait a few seconds and re-read — codes **expire and are single-use**; a stale code reads fine but+  resend, wait a few seconds and re-read - codes **expire and are single-use**; a stale code reads fine but   gets rejected. When in doubt, resend and read the newest.  ## 4. Hand the code to the login (entering it) See **driving-the-desktop** / the site recipe for entry. The hard-won bit: a React single-box OTP field often-**ignores CDP `insertText`/synthetic keys** (value stays empty) — type it with **real OS keystrokes**+**ignores CDP `insertText`/synthetic keys** (value stays empty) - type it with **real OS keystrokes** (`desktop_type {text:"071276"}`) into the focused field. Always tick "remember this device" if offered so the 2FA doesn't recur.  ## 5. Etiquette-- Reading the user's texts is sensitive — only do it to complete a 2FA the user asked you to automate, read+- Reading the user's texts is sensitive - only do it to complete a 2FA the user asked you to automate, read   the **minimum** (just the code), and never log/echo full message bodies you don't need. - If you foregrounded the PWA, restore the user's prior foreground window when done. @@ -62,8 +62,8 @@ See **driving-the-desktop** / the site recipe for entry. The hard-won bit: a Rea | Thing | Value | |---|---| | Window title | **"Messages - Google Messages for web"** (or "Messages"), class `Chrome_WidgetWin_1` |-| Read the text | `desktop_find_control {hwnd, contains:"verification code"}` (UIA — works backgrounded) |+| Read the text | `desktop_find_control {hwnd, contains:"verification code"}` (UIA - works backgrounded) | | ⚠️ Don't | `desktop_screenshot_window` in background → BLANK (PrintWindow can't see the SPA) |-| Code shape | 6 digits; the 4–5-digit number is the **sender shortcode**, not the code |+| Code shape | 6 digits; the 4-5-digit number is the **sender shortcode**, not the code | | Newest SMS | **top** conversation; resend + re-read if a code is rejected (single-use/expiring) |-| Enter it | `desktop_type` (real keystrokes) — CDP insertText often fails on OTP boxes |+| Enter it | `desktop_type` (real keystrokes) - CDP insertText often fails on OTP boxes |
tools/ship.sh+1−1
@@ -161,7 +161,7 @@ adom-wiki repo push "$PAGE" \   --files bridge/bridge.json bridge/BRIDGE_VERSION bridge/package.json bridge/server.js \           extension/src/native.js extension/src/cdp.js extension/src/sw.js extension/manifest.json \           bin/nb-dashboard package.json dockbar.json tools/ship.sh tools/promote.sh tools/verb-battery.mjs tools/verbs-policy.json tools/check-guard-coverage.mjs \-          skills/*/SKILL.md \+          skills/*/SKILL.md README.md docs/*.md \   -m "v${VER} (insiders): ${LOG}" >/dev/null 2>&1 || {     [ "${FORCE_PUSH:-0}" = "1" ] || { echo "   PUSH FAILED (set FORCE_PUSH=1 to retry)"; exit 1; }   }