pup-bridge-macos
Public Made by Adomby adom
The pup for macOS SKILL PACK: the AI skills that teach a cloud/container AI how to drive the macOS Puppeteer (pup) bridge (pup_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, credentials, lanes, settings, browsers+Chrome install, vendor logins and the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY: this is NOT the bridge runtime. The bridge itself is one signed macOS binary that Adom Bridge for macOS installs an
name: pup user-invocable: true description: "Drive a browser on the user's Mac from the cloud: the macOS Puppeteer 'pup' bridge (pup_* verbs via the adom-bridge-cli CLI). To the user these are PUP WINDOWS, full stop: never name the browser build or how pup drives it unless they ask. pup runs its own browser in a pup-managed DURABLE profile (logins/localStorage persist and are shared across pup windows), driven through pup's own extension channel with no debug port, which speaks the full DevTools protocol (CDP) through that extension, so every pup window has CDP-level access (pup_devtools for any raw command): never give up because 'CDP is off'. Windows live in a HIDDEN browser app so they never cover the user or take focus, yet stay fully drivable and capturable; NOT the user's real signed-in profile (that is the Adom extension's nbrowser_* verbs), NOT headless (a real rendered window, so screenshots/recording are true-to-life). Open/close windows + tabs, navigate, screenshot, eval JS, record, sign in from pup's own vault. This is the START-HERE skill: the mental model + when-to-use + the skill map; deeper topics route to the sub-skills. For 'is it ready' use pup_readiness (not pup_status). Trigger words: pup, puppeteer, browser window, open in pup, open my app in pup, browser screenshot, browser eval, browser reload, visual debug, headful chrome, pup_open_window, pup_screenshot, pup_readiness, pup_prewarm, chrome for testing, open in browser, open my web app, show my app in the browser, preview my app, open the app I just built, view my app, open localhost, show my dev server, pup_login, log into a site in pup, autofill password, saved login, durable profile, isolated window."
Pup: browser control on the user's Mac (start here)
What to call it when you talk to the user
They are pup windows. Say "I opened it in pup" or "your pup window". Lead with what the user sees (pup windows), not the machinery: the browser build, the "Google Chrome for Testing" window titles and how pup drives them are details to explain plainly whenever the user asks (pup-bridge#114). The build detail lives in the maintainer skills (pup-why-cft, pup-browsers-and-chrome) for when a user asks.
Drive a browser on the user's Mac from a cloud container via the adom-bridge-cli CLI. Every verb is a
Bash command (NOT an MCP tool):
adom-bridge-cli --ai-thread "<your thread>" pup_open_window '{"url":"https://example.com"}' # your thread's own window, no ids
adom-bridge-cli --ai-thread "<your thread>" pup_screenshot '{}' # zero-id: resolves to YOUR window
pup windows have CDP-level access: never give up because "CDP is off"
Every pup window is driven through pup's own browser extension, which speaks the full DevTools protocol (CDP) to the
page with no debug port. So you have everything CDP gives you on every pup window:
trusted clicks and keys (pup_click, pup_type, pup_input_dispatch), pup_eval, element and full-page
screenshots, downloads, fetches, HTTP auth, and pup_devtools {method, params} for any other DevTools command
(DOM, Runtime, Input, Page, Network, Emulation, iframes through Page.getFrameTree).
The separate "CDP lane" (lane:"cdp", a window with a debug port) is OFF by default, and you do not need it. "CDP is switched off, so pup
can't do this here" is never a true answer, and never ask for lane:"cdp" to get a feature: a
lane:"cdp" open with that lane off simply opens on the extension lane and says so. If something truly cannot be
done, report the exact verb and error on adom/pup-bridge-macos and keep going with what works.
Logins: pup manages your credentials (no Chrome picker)
pup runs its OWN password manager, so the AI signs into sites without Chrome's foreground autofill dropdown. Backed by the macOS Keychain (encrypted at rest); passwords are never logged, never echoed. pup's browser runs with an in-memory cookie key, so it never raises a keychain prompt of its own.
- Auto-capture: the user signs into any site once in a pup window and pup records the credential
to the vault as they submit. The next verb response carries
_credentialCaptured. Turn off withpup_configure {captureLogins:"off"}. This is THE way logins grow on a Mac: when a sign-in page has no saved login (_loginTip,no_credential), ask the user to sign in once in that pup window. - Auto-fill on return: a matching login page is filled in the background (no dropdown, no
foregrounding). The response carries
_autoLogin, which regurgitates the live domain and asks you to confirm it is the intended site (anti-phishing). - Submit is reason-gated: pup NEVER auto-submits a stored password. To sign in, call
pup_login {submit:true, submitReason:"<why this domain is the right one>"}. Baresubmit:trueis refused withsubmit_reason_requiredand the exact live host, so a lookalike domain is caught before the secret is sent. - Google is its own verb:
pup_google_signin {account, submitReason}(neverpup_login). Users usually have several Google accounts and pup never guesses: with noaccountit returns the list and you decide from the task or by asking. Full playbook: thepup-credentialsskill. - Importing the browser's own saved passwords is not available on macOS.
pup_import_scanfinds nothing andpup_import_browser_loginsanswersunsupported_platform: those passwords sit in the login keychain behind a per-app grant pup does not ask for. Do not offer an import to a Mac user. - Waiting on the human? TOAST, with the window attached.
notify_user {title, body, level:"warning", focus:{hwnd:<pup window>}}so clicking the toast opens the exact window that is waiting. pup toasts on every no-password answer itself; you still toast whenever YOUR thread is about to sit and wait. - Manage:
pup_credentials {}lists saved logins (host + username only);credential_delete {host}removes one;credential_set {host, username, password}stores one explicitly when the user gives it to you directly. Never ask the user to type a password into the chat. - Share the vault:
pup_vault_sync {peer, reason}merges the adom-you vault with another of the user's desktops in both directions (newest wins, pup-to-pup, no AI sees a password). - Reveal one to a LOCAL assistant:
pup_credential_reveal {host, reason}hands the saved username- password for ONE host to a caller running on the desktop, after the user clicks Reveal on a
sticky toast that names the caller, the host and your reason. No click, no secret. A relayed (cloud
container) call is refused with
relay_forbidden: from a cloud thread callpup_login {submit:true, submitReason}instead. Waiting on the click is never a timeout: past 45 s the verb answerserrorCode: blocked_on_user, stillRunning: true, consentIdwhile the toast stays up; call the SAME verb with the SAME args to collect the decision.pup_fill_cardbehaves the same.
- password for ONE host to a caller running on the desktop, after the user clicks Reveal on a
sticky toast that names the caller, the host and your reason. No click, no secret. A relayed (cloud
container) call is refused with
Payment cards: pup can finish a checkout
pup_card_set {nickname, number, expiry, cvv, name}stores a card in the Keychain; ask the USER for the details, never harvest them from a page.pup_cards {}lists nickname, brand, last4 and expiry only;pup_card_delete {nickname}removes one. No verb ever returns the number or CVV.pup_fill_card {nickname, merchant, amount}fills the card into the checkout WITHOUT the digits ever reaching you (it walks payment iframes like Stripe and returns booleans). Every fill is approved by the user in the moment on a desktop toast naming the merchant, amount and last4.
Addressing: your thread name IS the handle
You never need a window id or desktop_find_window for a pup window. pup keys every window to its
owning --ai-thread name and resolves ids for you:
- Open:
pup_open_window {"url":"..."}, omit sessionId. Your thread gets its OWN window (first open creates it, later opens become tabs in it), sized to the work area, in the background. - Come back to it, from any later turn: call verbs with NO ids:
pup_screenshot {},pup_reload {},pup_navigate {"url":"..."}all resolve to your thread's window. - See everything you own:
pup_my_windows {}: windows, tabs, urls, titles, state, plus the ready-to-run next steps. Start here whenever you are unsure what you have open. - Address a thread by name: any targeting verb accepts
"ai-thread"instead of an id. - Several windows on one thread? Label them with
window(e.g.{"window":"mytask-web"}), the ONLY time you need an explicit id.sessionIdis the legacy alias; responses carry both. - Raising onto the user's screen:
pup_raise_os_window {"foregroundReason":"..."}(gated). Every open/navigate response carries_nextrepeating these; trust the hints over hand-rolled desktop verbs.
One owned window, explicit tabs for overlapping work
Use one pup window per AI thread, with multiple tabs for different items. Start with pup_my_windows and
pup_list_tabs; reuse a matching tab and call pup_open_tab for a new item. Return with pup_switch_tab:
selecting a tab inside the browser keeps the window in the background. It does not authorize
pup_raise_os_window, a second window, or covering the user's editor.
For overlapping operations pass the exact window and tabId wherever supported. Independent reads and
downloads can run on separate tabs. Serialize navigation or mutations of the same tab. Tabs share the
profile's cookies and account state, so concurrent logins are not isolated; a separate profile is
isolated:true with its reason (its own extension-lane browser).
Never touch another thread's windows.
"Open my web app in the browser", the #1 use case, and its one trap
When the user asks to see an app they just built, pup IS the right surface: a real browser window
on their Mac that you can keep driving (reload after fixes, screenshot, eval). The ONE trap: an app
running in a CLOUD container is NOT reachable from the desktop as localhost:<port>. Use the
container's public proxy URL instead:
https://{container-slug}.adom.cloud/proxy/{port}/
Give the window your thread's name and re-use the SAME window on every "check it now" (navigate /
reload, don't spawn new windows). Under Hydrogen (local container) http://localhost:{port} IS
correct; the trap is cloud-only. pup lints obvious container-local URLs and warns you.
Vocabulary + routing: call this surface pup ("I opened it in pup"). When the user wants the page in THEIR OWN browser (their logins, their tabs, no AI driving), that is nb (native browser): say so and use nb. Displaying inside a Hydrogen panel next to the editor is wv. Driving/testing → pup, handing to the human → nb, docked preview → wv.
Platform qualifier: pup requires Adom Bridge. Under Hydrogen it is bundled. On Hydrogen
Cloud check adom-bridge-cli ping first; with no desktop, show the app in a wv panel.
THE CORE GOAL: the shared adom-you profile
pup exists to be an AI-driven browser that can do everything it needs to for you. The engine of that goal is the shared durable adom-you profile:
- Surf as adom-you by default. Every pup window on adom-you contributes every login it makes to the shared credential store, so over time pup signs in to every site it has seen.
- Isolation is the exception, and it is REASON-GATED.
isolated:truewithout anisolationReason(min 10 chars) is refused withisolation_reason_required. Legit reasons: testing logged-out views, acting as a different user, untrusted pages. An isolated window's logins are thrown away. (wikiView:"public"renders the anonymous wiki view in the same window.) - A non-adom-you window shows the ANONYMOUS identity tile in the toolbar, so a not-you window is visible at a glance.
ALWAYS say SAVE (John's rule)
When the browser offers to save something during your driving ("Save password?", "Save
address?", "Update password?"), the answer is always Save, unless the USER told you not to. Every
save enriches adom-you. If you filled a login via pup_login, pass save:true so the credential
persists. A save bubble you ignore is dismissed on the next navigation and the data is lost. The one
exception: an isolated:true window builds nothing, so skip it there.
The mental model: what pup actually is (read this once)
pup is one signed macOS program (a universal Rust binary) that Adom Bridge spawns on the user's Mac. When you open a window it does NOT attach to the user's running browser and does NOT run headless. It:
- Launches its own browser, Chrome for Testing by policy (the dated record is
docs/WHY-CFT.md). CfT is the build Google ships FOR automation, and it still honors--load-extension, which pup's toolbar needs. pup installs it into~/.cache/puppeteeron its own (pup_prewarmdoes it ahead of time). An installed Google Chrome (or Edge) in/Applicationsis the spawn-verified fallback while CfT is downloading.pup_readinesstells you which browser a Mac actually runs. pup uses a browser binary, never the user's profile. - …with a pup-managed profile. By DEFAULT there is no debug port: pup drives the window
through its own MV3 extension channel, because that port is the loudest automation signal a site can
read. The profile is one pup owns and persists, never the user's own Chrome profile. By default
every window shares ONE durable profile (
adom-you), so logins, cookies and localStorage accumulate and are shared across all pup windows. For a throwaway window pass{isolated:true}(with its reason), or{profile:"<name>"}for a separate durable jar. (The user's REAL signed-in profile is the Adom extensionnbrowser_*.) - Renders a REAL window, but keeps it in the BACKGROUND. Not headless: a real composited window,
so screenshots and recording are true-to-life. pup's browser app is hidden (the Cmd+H state), so
its windows never cover the user's work and never take focus, while you drive and screenshot them
fully.
pup_lower_os_windowre-hides;pup_raise_os_window(withforegroundReason) unhides and activates. One browser process serves a profile, so a shared-profile window group hides and shows together. Never hide the app while the user has it frontmost. - Spawn-verifies + falls through. pup launches each candidate in order and, if one won't start, falls to the next, so an open never hard-fails on a broken browser.
- Is addressed by your
--ai-threadname, and one window holds many tabs.
Power & rendering: throttling is DEFAULT and correct; highFps is the leased override
Background pup windows are THROTTLED on purpose, to protect the user's CPU and battery. Chrome
slows rendering of windows nobody is looking at (issue #14: three idle anti-throttled windows held a
laptop out of deep idle overnight and flattened its battery). A healthy background window can LOOK
frozen (an animation stops, a video pauses). That is throttling, not a hang: on-screen +
CDP-responsive + readyState:complete = throttled, not broken.
When a background window must keep rendering (recording it, a live view), upgrade that ONE
window: pup_open_window {highFps:true}. It is LEASED (20 min default, 120 max) and
auto-downgrades when idle; renew with pup_highfps_extend {sessionId, minutes}. Do NOT reach for
highFps to "fix" a window that merely looks frozen.
A native dialog FREEZES EVERY pup window, the #1 thing that looks like "pup is dead"
If a page raises alert(), confirm(), prompt(), or the "Reload site? Changes you made may not
be saved." prompt, that modal blocks the BROWSER, not just its own window. pup's windows share one
browser, so one dialog wedges every window you have open. Verbs on unrelated windows start timing
out with different wordings at once; none of them is the real problem.
adom-bridge-cli --ai-thread "<your thread>" pup_dialog '{"action":"dismiss"}' # keep the page, cancel the navigation
adom-bridge-cli --ai-thread "<your thread>" pup_dialog '{"action":"accept"}' # go ahead (a reload DISCARDS unsaved changes)
pup_dialog finds the one pending dialog wherever it is, answers it, and unblocks everything.
pup never answers it for you, on purpose: a beforeunload is a question about the user's UNSAVED
WORK. If the unsaved work might matter, read it out with pup_eval before you accept.
Under heavy load or a crash: pup self-heals, do NOT panic-kill or declare it dead
_degradedonpup_list_windows/pup_status: a window too busy to answer a live read comes back with its LAST-KNOWN url/title. It means busy, NOT crashed; retry shortly.- A window vanished and
_cleanupReportsays the user closed it: that close is FINAL. pup forgets the session and never brings it back. Your next verb answerssession_not_found; reopen withpup_open_windowif you still need the page. - A
_cleanupReportsays a window crashed: a browser that CRASHED (nonzero exit, the only proof pup accepts) has its windows relaunched by the health loop (15 s), signed in, in the background, at their last URLs. Do NOT kill Chrome, clean the profile, or reopen by hand. - One window times out but others work = a per-window jam, not a dead bridge.
⛔ HARD RULE: NEVER send NATIVE OS input without warning the user FIRST (John, 2026-07-29)
Native OS input (desktop_click, desktop_type, desktop_press_key, desktop_hover) does NOT go
to a window. It goes to WHATEVER WINDOW HAS FOCUS AT THAT INSTANT. If the user has switched away,
your keystrokes land in THEIR document, THEIR terminal, THEIR editor.
Prefer pup's own input (pup_click, pup_type, pup_press_key, pup_input_dispatch): it is
trusted, needs NO foreground and disturbs nobody. pup_assisted_click is not available on macOS
(unsupported_platform). If you genuinely must use native input, in this order:
notify_userTOAST: what you are about to do and that you need the window.desktop_caption: on-screen, so it is visible while they look at the window.- Say explicitly: "please don't type or click until I say done."
- WAIT until your window is frontmost and stays there; never dispatch while it is not.
- Re-verify the foreground IMMEDIATELY before EACH native event.
- Caption each step, and caption when you are DONE.
Native input returning ok:true means "dispatched to the OS", NOT "it reached your window".
Verify the effect in the page. If it seems not to land, the first hypothesis is "focus moved".
Standing rule: TOAST THE USER before foregrounding (John, repeatedly)
NEVER put a window on the user's screen silently. Any time you FOREGROUND a pup window, send a toast
FIRST (pup auto-fires notify_user on every granted foreground; still narrate WHY in the
foregroundReason). For a background nudge, use pup_alert_window: on macOS it posts a notification
the user can click and never takes focus ({stop:true} has nothing to clear).
Vendor sites: they load. The three conditions that make them load
- Drive the site through pup, not a hand-rolled Chrome. What makes vendor sites load is pup's
launch configuration as a whole, starting with the extension lane's missing debug port: over CDP,
st.com and mouser.com died with
ERR_HTTP2_PROTOCOL_ERRORwhile the extension lane loaded them. - Go at human pace: one tab at a time, ~7 s settle. Bursting tabs trips velocity rules and is the most common self-inflicted block.
- Clear that domain's cookies first (
pup_clear_cookies {domain}): a prior denial writes a reject verdict that keeps denying you long after the cause is fixed.
Mouser, Arrow, Octopart, Newark and analog.com still run bot walls that come and go. Check the page text for denied/captcha before trusting a load, and fall back to DigiKey, LCSC or TI. Per-site detail lives in the electronics-sites-skillpack. A body of 0 chars WITH a title is a bot wall, not a slow load; settle ~15 s after open/navigate before you read.
What the extension lane does. It is the normal pup window: pup_click, pup_hover, pup_type,
pup_press_key, pup_scroll, pup_fill, pup_select, pup_input_dispatch, pup_annotate,
pup_screenshot, pup_screenshot_element, pup_screenshot_full_res, pup_record_start/stop, and
pup_eval (retried through DevTools on CSP/Trusted-Types pages). Still CDP-only (answered with
lane_needs_cdp): selector-driven pup_download, realKeystrokes logins, iframe targeting,
webSecurity:false. The pup-lanes skill is the authority on what each lane does.
When a page comes back as botWall
Every page load is checked for a bot wall (Cloudflare, PerimeterX, Akamai, DataDome, generic). The
reply then carries botWall: {vendor, host, url} and a _hint:
- Stop loading pages from that host. Every retry raises the site's bot score.
- Parts data (stock, price, datasheet): use
adom-parts-search, not product pages. - Need the page itself: ask the user for ONE click on the check in that window
(
pup_alert_window, orpup_raise_os_windowwith a reason), or open it in their own browser withnbrowser_*. Never solve it yourself.
A _paceWarning means you loaded a guarded vendor again within 15 s. Slow down.
When pup vs the Adom extension (nbrowser_*)
| The task | Tool |
|---|---|
| Screenshot / scrape / test / demo a page; drive an app you're building; sites pup has a login for | pup (this) |
| Needs the user's REAL signed-in Chrome: their SSO, their existing sessions, a hand-off with no AI driving | Adom browser extension (nbrowser_*) |
Skill map: this is a pack
| Skill | Read it for |
|---|---|
pup (this file) |
The mental model, when-to-use, quick start, the essentials |
pup-windows-sessions-tabs |
Window names, ownership, background vs foreground rules, many tabs in one window, list/switch/close |
pup-screenshots-recording |
Screenshots (full-page, full-res), reading the shot, recording |
pup-browsers-and-chrome |
Browser detection, pup_use to pin a browser, installing Chrome / Chrome for Testing, readiness, the cold-start error table |
pup-credentials |
The vault, sign-in, Google accounts, vault sync |
pup-lanes |
How pup drives a window and the rules that keep closes final |
pup-settings |
Every setting, behavior by behavior |
pup-adom-wiki |
Driving + verifying wiki.adom.inc pages in pup |
pup-vendor-login |
Signing into vendor portals |
pup-why-cft |
WHY the driving browser is Chrome for Testing |
(Maintaining the bridge itself? The pup-bridge-dev + pup-bridge-publish skills are source-only in the
repo, not shipped here.)
Quick start: the 90% path
# Open in the BACKGROUND (automatic, never disturbs the user), then drive it invisibly.
adom-bridge-cli --ai-thread "<your thread>" pup_open_window '{"url":"http://localhost:3000"}'
adom-bridge-cli --ai-thread "<your thread>" pup_eval '{"expr":"document.title"}' # verify it really loaded
adom-bridge-cli --ai-thread "<your thread>" pup_screenshot '{}' # true-to-life PNG
- Your
--ai-threadname IS the owner. Apup_open_windowwith no ai-thread is refused withcaller_identity_required. ok:truemeans navigation started, not that content rendered: checkrenderCheck,pup_evalondocument.title, or a screenshot.- On a fresh Mac the first open may include the one-time Chrome for Testing fetch (
pup_prewarmdoes it ahead of time). The rare cold-start cases are inpup-browsers-and-chrome.
Things the Windows line has that this Mac app does not
macOS has no per-window taskbar button, so there is no taskbar identity, thread tile, favicon badge,
jump list, taskbar progress bar or flash. pup_set_identity and pup_audit_icons answer
mode:"plain", platform:"macos"; pup_progress answers painted:false; taskbarActivity reads
off; pup_configure reports taskbarSupported:false. Do not point a Mac user at an icon: tell
windows apart with pup_list_windows / pup_list_tabs and the window title (your thread name leads it).
Key rules (the essentials, details in the sub-skills)
- Always pass
sessionId/windowon session-scoped verbs when your thread runs more than one window. ONE DANGEROUS EXCEPTION:pup_close. There'{}'means ALL sessions, every window, across every AI thread on the machine, immediately. To close one window usepup_close_window {"sessionId":"…"}, neverpup_close '{}'. - Background is guaranteed; a gated raise is the only way to show a window: not sizing, not navigating, not screenshotting.
- Own your window: always pass
--ai-thread. Never navigate/close a window you don't own. - One window, many tabs: use
pup_open_tab, not a secondpup_open_window. pup_readiness(notpup_status) answers "is it ready / which browser?".pup_evalaccepts statement lists (location.assign('...'); 'done'works); a genuinely bad expression FAILS witheval_failed.- Complex page fighting you? It's usually shadow DOM or a nested scroll container. See
pup-screenshots-recording→ "Driving & verifying tricky pages." - Never broadly kill Chrome:
pup_close_windowfor a specific session. - Surface a verb's
_hintverbatim whenever it reports not-ready or failure. - If pup's verbs TIME OUT, the bridge PROCESS is down, usually right after a restart or a crash.
Fire one
pup_open_window(a status probe does NOT respawn a down bridge), then pollpup_list_windowswith backoff. Do NOT spam opens while Bridge saysbridge_starting. - Full machine-readable catalog:
adom-bridge-cli --ai-thread "<your thread>" pup_describe '{}'.
Verify what you rendered: pup checks it FOR you
ok:true means the navigation worked. It says NOTHING about whether the page rendered. Every
pup_open_window / pup_open_tab / pup_navigate / pup_reload response can carry:
renderCheck:{ok, httpStatus, signals[], excerpt, bodyChars, elements}. Signals includehttp_404,page_says_not_found,dev_server_cannot_get,server_error_text,stack_trace_visible,effectively_blank,no_stylesheets_loaded,broken_images_N._verifyRender: what to do. WhenrenderCheck.okis false it leads with "THIS PAGE DID NOT RENDER CORRECTLY, pup checked it, so do NOT report success".
If renderCheck.ok === false, it is not working yet: never report success. Screenshot it, READ the image, fix
the cause, pup_reload, and re-check. A clean renderCheck is not proof the page is right; when
you generated or edited the content, screenshot it anyway.
Every adom-bridge-cli call needs --ai-thread
Adom Bridge refuses any command that does not say which AI conversation is asking
(errorCode: caller_identity_required). It is a global flag that goes before the verb:
adom-bridge-cli --ai-thread "chip-fetcher tab 3" pup_open_window '{"sessionId":"cf-web","url":"..."}'
Pick a name the USER would recognise, never a UUID. pup puts it at the FRONT of the window title so
a user running many AI threads can tell windows apart. It is attribution, not permission. Do
NOT export ADOM_AI_THREAD: threads share one container, so it names the shell rather than you.
Found a pup bug? Please report it
Responses carry _reportIssues on failures. Check for a duplicate with
adom-wiki issue list adom/pup-bridge-macos, then
adom-wiki issue create adom/pup-bridge-macos --title "<symptom>" --category bug-report --body "<what you ran, expected, got, the VERBATIM response JSON, bridge version, macOS version>".
An Adom Bridge core problem (relay, bridge lifecycle, caller identity) goes to adom/adom-bridge.
AUDIT YOURSELF: when the user says "audit your windows"
Run all six sections, in order, and report them as a table:
- Sessions (bridge view),
pup_list_windows: count, sessionId, owner, tabCount, profile, ageMinutes,_degraded. Flag owner:null, duplicate sessions for one window, a wrong tabCount. - OS ground truth,
pup_window_infoper window (never resolve by title). A parked window's browser app is HIDDEN: that is the normal background state, not a lost window. - Health,
pup_evalper window:readyState,visibilityState, a1+1drivability check.visibilityState:hiddenis NORMAL for a parked window. Note any crashed tabs. - Log,
bridge_log_read: count[crash] [heal] [health] [unstick] [disconnect] [reclaim] [tabs] park-failed Handler error, and report park-failed and[reclaim] adoptedlines. - Browser footprint, processes + total MB, and pup's share vs the user's own Chrome.
- Cleanup state: leftover test windows, phantom sessions, stale session files.
Report rules: state what is WRONG first, name which findings are your own regressions, and never present an API-reported success as proof; prove it in the page or a screenshot.
⏱ Captions: NEVER longer than 2 seconds (John, standing rule)
A caption is a glance, not a dialog. Anything past ~2 s covers what the user is doing. The cap is
enforced in code (2000 ms), so longer values are clamped. If something needs longer attention it is
not a caption: use notify_user, or put it in the verb response.
FOREGROUNDING: always caption the REASON, for 2 seconds (John, standing rule)
Foregrounding takes over the user's screen while they work, which is why pup FORCES a
foregroundReason before it grants a raise. The reason must be SHOWN: every granted foreground
flashes an on-screen caption naming the window and quoting the reason, for 2 seconds at most, and it
is never skipped because a mode is quiet (it is deliberately NOT gated behind minimalTouch). If you
add a new path that foregrounds a window, it must go through the same captioned grant.
Recording the desktop: guide them to Adom Bridge, pup is the fallback
For the whole desktop, prefer Adom Bridge's native recorder (desktop_record_start /
desktop_record_stop) when this Mac's Bridge offers it. pup's pup_screen_record_start is the
fallback: a Meet-style screen share (getDisplayMedia into WebM). Recording ONE pup window's page
content stays pup's (pup_record_start); it needs ffmpeg (brew install ffmpeg; pup re-detects it
without a restart). Full detail in pup-screenshots-recording.
Know what you are spending: every open reports the machine's RAM
pup_open_window, pup_open_tab, pup_list_windows and pup_status return a memory block
(boxTotalMb, boxFreeMb, boxFreePct, browserMb, tabsOnThisBrowser, avgMbPerTab, _hint),
because pup runs on somebody's actual laptop and you are spending their RAM. browserMb is the whole
browser process tree for that profile; avgMbPerTab prices your next tab. Read the hint and act on
it. Under 25 % free, close what you are done with. Under 10 % free, open nothing more until you have
closed something: that is where macOS starts swapping and the user's whole desktop stutters. Close
tabs with pup_close_tab the moment a task is done.
---
name: pup
user-invocable: true
description: "Drive a browser on the user's Mac from the cloud: the macOS Puppeteer 'pup' bridge (pup_* verbs via the adom-bridge-cli CLI). To the user these are PUP WINDOWS, full stop: never name the browser build or how pup drives it unless they ask. pup runs its own browser in a pup-managed DURABLE profile (logins/localStorage persist and are shared across pup windows), driven through pup's own extension channel with no debug port, which speaks the full DevTools protocol (CDP) through that extension, so every pup window has CDP-level access (pup_devtools for any raw command): never give up because 'CDP is off'. Windows live in a HIDDEN browser app so they never cover the user or take focus, yet stay fully drivable and capturable; NOT the user's real signed-in profile (that is the Adom extension's nbrowser_* verbs), NOT headless (a real rendered window, so screenshots/recording are true-to-life). Open/close windows + tabs, navigate, screenshot, eval JS, record, sign in from pup's own vault. This is the START-HERE skill: the mental model + when-to-use + the skill map; deeper topics route to the sub-skills. For 'is it ready' use pup_readiness (not pup_status). Trigger words: pup, puppeteer, browser window, open in pup, open my app in pup, browser screenshot, browser eval, browser reload, visual debug, headful chrome, pup_open_window, pup_screenshot, pup_readiness, pup_prewarm, chrome for testing, open in browser, open my web app, show my app in the browser, preview my app, open the app I just built, view my app, open localhost, show my dev server, pup_login, log into a site in pup, autofill password, saved login, durable profile, isolated window."
---
# Pup: browser control on the user's Mac (start here)
## What to call it when you talk to the user
They are **pup windows**. Say "I opened it in pup" or "your pup window". Lead with what the user
sees (pup windows), not the machinery: the browser build, the "Google Chrome for Testing" window
titles and how pup drives them are details to explain plainly whenever the user asks (pup-bridge#114). The build
detail lives in the maintainer skills (pup-why-cft, pup-browsers-and-chrome) for when a user asks.
Drive a browser on the user's Mac from a cloud container via the `adom-bridge-cli` CLI. Every verb is a
**Bash command** (NOT an MCP tool):
```bash
adom-bridge-cli --ai-thread "<your thread>" pup_open_window '{"url":"https://example.com"}' # your thread's own window, no ids
adom-bridge-cli --ai-thread "<your thread>" pup_screenshot '{}' # zero-id: resolves to YOUR window
```
## pup windows have CDP-level access: never give up because "CDP is off"
Every pup window is driven through pup's own browser extension, which speaks the full DevTools protocol (CDP) to the
page with no debug port. So you have everything CDP gives you on every pup window:
trusted clicks and keys (`pup_click`, `pup_type`, `pup_input_dispatch`), `pup_eval`, element and full-page
screenshots, downloads, fetches, HTTP auth, and `pup_devtools {method, params}` for any other DevTools command
(DOM, Runtime, Input, Page, Network, Emulation, iframes through `Page.getFrameTree`).
The separate "CDP lane" (`lane:"cdp"`, a window with a debug port) is OFF by default, and you do not need it. "CDP is switched off, so pup
can't do this here" is never a true answer, and never ask for `lane:"cdp"` to get a feature: a
`lane:"cdp"` open with that lane off simply opens on the extension lane and says so. If something truly cannot be
done, report the exact verb and error on adom/pup-bridge-macos and keep going with what works.
## Logins: pup manages your credentials (no Chrome picker)
pup runs its OWN password manager, so the AI signs into sites without Chrome's foreground autofill
dropdown. Backed by the macOS Keychain (encrypted at rest); passwords are never logged, never echoed.
pup's browser runs with an in-memory cookie key, so it never raises a keychain prompt of its own.
- **Auto-capture**: the user signs into any site once in a pup window and pup records the credential
to the vault as they submit. The next verb response carries `_credentialCaptured`. Turn off with
`pup_configure {captureLogins:"off"}`. This is THE way logins grow on a Mac: when a sign-in page has
no saved login (`_loginTip`, `no_credential`), ask the user to sign in once in that pup window.
- **Auto-fill on return**: a matching login page is filled in the background (no dropdown, no
foregrounding). The response carries `_autoLogin`, which **regurgitates the live domain** and asks
you to confirm it is the intended site (anti-phishing).
- **Submit is reason-gated**: pup NEVER auto-submits a stored password. To sign in, call
`pup_login {submit:true, submitReason:"<why this domain is the right one>"}`. Bare `submit:true`
is refused with `submit_reason_required` and the exact live host, so a lookalike domain is caught
before the secret is sent.
- **Google is its own verb**: `pup_google_signin {account, submitReason}` (never `pup_login`). Users
usually have several Google accounts and pup never guesses: with no `account` it returns the list and
you decide from the task or by asking. Full playbook: the `pup-credentials` skill.
- **Importing the browser's own saved passwords is not available on macOS.** `pup_import_scan` finds
nothing and `pup_import_browser_logins` answers `unsupported_platform`: those passwords sit in the
login keychain behind a per-app grant pup does not ask for. Do not offer an import to a Mac user.
- **Waiting on the human? TOAST, with the window attached.** `notify_user {title, body, level:"warning",
focus:{hwnd:<pup window>}}` so clicking the toast opens the exact window that is waiting. pup toasts
on every no-password answer itself; you still toast whenever YOUR thread is about to sit and wait.
- **Manage**: `pup_credentials {}` lists saved logins (host + username only); `credential_delete
{host}` removes one; `credential_set {host, username, password}` stores one explicitly when the
user gives it to you directly. Never ask the user to type a password into the chat.
- **Share the vault**: `pup_vault_sync {peer, reason}` merges the adom-you vault with another of the
user's desktops in both directions (newest wins, pup-to-pup, no AI sees a password).
- **Reveal one to a LOCAL assistant**: `pup_credential_reveal {host, reason}` hands the saved username
+ password for ONE host to a caller running on the desktop, after the user clicks **Reveal** on a
sticky toast that names the caller, the host and your reason. No click, no secret. A relayed (cloud
container) call is refused with `relay_forbidden`: from a cloud thread call `pup_login {submit:true,
submitReason}` instead. **Waiting on the click is never a timeout**: past 45 s the verb answers
`errorCode: blocked_on_user, stillRunning: true, consentId` while the toast stays up; call the SAME
verb with the SAME args to collect the decision. `pup_fill_card` behaves the same.
## Payment cards: pup can finish a checkout
- `pup_card_set {nickname, number, expiry, cvv, name}` stores a card in the Keychain; ask the USER
for the details, never harvest them from a page. `pup_cards {}` lists nickname, brand, last4 and
expiry only; `pup_card_delete {nickname}` removes one. **No verb ever returns the number or CVV.**
- `pup_fill_card {nickname, merchant, amount}` fills the card into the checkout WITHOUT the digits
ever reaching you (it walks payment iframes like Stripe and returns booleans). **Every fill is
approved by the user in the moment** on a desktop toast naming the merchant, amount and last4.
## Addressing: your thread name IS the handle
You never need a window id or `desktop_find_window` for a pup window. pup keys every window to its
owning `--ai-thread` name and resolves ids for you:
- **Open**: `pup_open_window {"url":"..."}`, omit sessionId. Your thread gets its OWN window
(first open creates it, later opens become tabs in it), sized to the work area, in the background.
- **Come back to it, from any later turn**: call verbs with NO ids: `pup_screenshot {}`,
`pup_reload {}`, `pup_navigate {"url":"..."}` all resolve to your thread's window.
- **See everything you own**: `pup_my_windows {}`: windows, tabs, urls, titles, state, plus the
ready-to-run next steps. Start here whenever you are unsure what you have open.
- **Address a thread by name**: any targeting verb accepts `"ai-thread"` instead of an id.
- **Several windows on one thread?** Label them with `window` (e.g. `{"window":"mytask-web"}`), the
ONLY time you need an explicit id. `sessionId` is the legacy alias; responses carry both.
- Raising onto the user's screen: `pup_raise_os_window {"foregroundReason":"..."}` (gated). Every
open/navigate response carries `_next` repeating these; trust the hints over hand-rolled desktop verbs.
## One owned window, explicit tabs for overlapping work
Use one pup window per AI thread, with multiple tabs for different items. Start with `pup_my_windows` and
`pup_list_tabs`; reuse a matching tab and call `pup_open_tab` for a new item. Return with `pup_switch_tab`:
selecting a tab inside the browser keeps the window in the background. It does not authorize
`pup_raise_os_window`, a second window, or covering the user's editor.
For overlapping operations pass the exact `window` and `tabId` wherever supported. Independent reads and
downloads can run on separate tabs. Serialize navigation or mutations of the same tab. Tabs share the
profile's cookies and account state, so concurrent logins are not isolated; a separate profile is
`isolated:true` with its reason (its own extension-lane browser).
Never touch another thread's windows.
## "Open my web app in the browser", the #1 use case, and its one trap
When the user asks to see an app they just built, pup IS the right surface: a real browser window
on their Mac that you can keep driving (reload after fixes, screenshot, eval). The ONE trap: an app
running in a CLOUD container is NOT reachable from the desktop as `localhost:<port>`. Use the
container's public proxy URL instead:
```
https://{container-slug}.adom.cloud/proxy/{port}/
```
Give the window your thread's name and re-use the SAME window on every "check it now" (navigate /
reload, don't spawn new windows). Under Hydrogen (local container) `http://localhost:{port}` IS
correct; the trap is cloud-only. pup lints obvious container-local URLs and warns you.
**Vocabulary + routing:** call this surface **pup** ("I opened it in pup"). When the user wants the
page in THEIR OWN browser (their logins, their tabs, no AI driving), that is **nb** (native browser):
say so and use nb. Displaying inside a Hydrogen panel next to the editor is **wv**. Driving/testing
→ pup, handing to the human → nb, docked preview → wv.
**Platform qualifier:** pup requires Adom Bridge. Under **Hydrogen** it is bundled. On **Hydrogen
Cloud** check `adom-bridge-cli ping` first; with no desktop, show the app in a **wv** panel.
## THE CORE GOAL: the shared adom-you profile
pup exists to be **an AI-driven browser that can do everything it needs to for you**. The engine of
that goal is the shared durable **adom-you** profile:
- **Surf as adom-you by default.** Every pup window on adom-you contributes every login it makes to
the shared credential store, so over time pup signs in to every site it has seen.
- **Isolation is the exception, and it is REASON-GATED.** `isolated:true` without an
`isolationReason` (min 10 chars) is refused with `isolation_reason_required`. Legit reasons: testing
logged-out views, acting as a different user, untrusted pages. An isolated window's logins are
thrown away. (`wikiView:"public"` renders the anonymous wiki view in the same window.)
- A non-adom-you window shows the ANONYMOUS identity tile in the toolbar, so a not-you window is
visible at a glance.
### ALWAYS say SAVE (John's rule)
When the browser offers to **save** something during your driving ("Save password?", "Save
address?", "Update password?"), the answer is **always Save**, unless the USER told you not to. Every
save enriches adom-you. If you filled a login via `pup_login`, pass `save:true` so the credential
persists. A save bubble you ignore is dismissed on the next navigation and the data is lost. The one
exception: an `isolated:true` window builds nothing, so skip it there.
## The mental model: what pup actually is (read this once)
pup is one signed macOS program (a universal Rust binary) that Adom Bridge spawns on the user's Mac.
When you open a window it does NOT attach to the user's running browser and does NOT run headless. It:
1. **Launches its own browser, Chrome for Testing by policy** (the dated record is `docs/WHY-CFT.md`).
CfT is the build Google ships FOR automation, and it still honors `--load-extension`, which pup's
toolbar needs. pup installs it into `~/.cache/puppeteer` on its own (`pup_prewarm` does it ahead of
time). An installed Google Chrome (or Edge) in `/Applications` is the spawn-verified fallback while
CfT is downloading. `pup_readiness` tells you which browser a Mac actually runs. pup uses a browser
*binary*, never the user's *profile*.
2. **…with a pup-managed profile.** By DEFAULT there is **no** debug port: pup drives the window
through its own MV3 extension channel, because that port is the loudest automation signal a site can
read. The profile is one **pup owns and persists**, never the user's own Chrome profile. By default
every window shares ONE durable profile (`adom-you`), so logins, cookies and localStorage
**accumulate and are shared across all pup windows**. For a throwaway window pass `{isolated:true}`
(with its reason), or `{profile:"<name>"}` for a separate durable jar. (The user's REAL signed-in
profile is the **Adom extension** `nbrowser_*`.)
3. **Renders a REAL window, but keeps it in the BACKGROUND.** Not headless: a real composited window,
so screenshots and recording are true-to-life. pup's browser app is **hidden** (the Cmd+H state), so
its windows never cover the user's work and never take focus, while you drive and screenshot them
fully. `pup_lower_os_window` re-hides; `pup_raise_os_window` (with `foregroundReason`) unhides and
activates. One browser process serves a profile, so a shared-profile window group hides and shows
together. Never hide the app while the user has it frontmost.
4. **Spawn-verifies + falls through.** pup launches each candidate in order and, if one won't start,
falls to the next, so an open never hard-fails on a broken browser.
5. **Is addressed by your `--ai-thread` name**, and **one window holds many tabs**.
## Power & rendering: throttling is DEFAULT and correct; `highFps` is the leased override
**Background pup windows are THROTTLED on purpose, to protect the user's CPU and battery.** Chrome
slows rendering of windows nobody is looking at (issue #14: three idle anti-throttled windows held a
laptop out of deep idle overnight and flattened its battery). A healthy background window can LOOK
frozen (an animation stops, a video pauses). That is throttling, not a hang: on-screen +
CDP-responsive + `readyState:complete` = throttled, not broken.
**When a background window must keep rendering** (recording it, a live view), upgrade that ONE
window: `pup_open_window {highFps:true}`. It is **LEASED** (20 min default, 120 max) and
auto-downgrades when idle; renew with `pup_highfps_extend {sessionId, minutes}`. Do NOT reach for
`highFps` to "fix" a window that merely looks frozen.
## A native dialog FREEZES EVERY pup window, the #1 thing that looks like "pup is dead"
If a page raises `alert()`, `confirm()`, `prompt()`, or the **"Reload site? Changes you made may not
be saved."** prompt, that modal blocks the BROWSER, not just its own window. pup's windows share one
browser, so **one dialog wedges every window you have open**. Verbs on unrelated windows start timing
out with different wordings at once; none of them is the real problem.
```bash
adom-bridge-cli --ai-thread "<your thread>" pup_dialog '{"action":"dismiss"}' # keep the page, cancel the navigation
adom-bridge-cli --ai-thread "<your thread>" pup_dialog '{"action":"accept"}' # go ahead (a reload DISCARDS unsaved changes)
```
`pup_dialog` finds the one pending dialog wherever it is, answers it, and unblocks everything.
**pup never answers it for you, on purpose**: a `beforeunload` is a question about the user's UNSAVED
WORK. If the unsaved work might matter, read it out with `pup_eval` before you accept.
## Under heavy load or a crash: pup self-heals, do NOT panic-kill or declare it dead
- **`_degraded` on `pup_list_windows` / `pup_status`**: a window too busy to answer a live read comes
back with its LAST-KNOWN url/title. It means busy, NOT crashed; retry shortly.
- **A window vanished and `_cleanupReport` says the user closed it**: that close is FINAL. pup forgets
the session and never brings it back. Your next verb answers `session_not_found`; reopen with
`pup_open_window` if you still need the page.
- **A `_cleanupReport` says a window crashed**: a browser that CRASHED (nonzero exit, the only proof
pup accepts) has its windows relaunched by the health loop (15 s), signed in, in the background, at
their last URLs. Do NOT kill Chrome, clean the profile, or reopen by hand.
- **One window times out but others work** = a per-window jam, not a dead bridge.
## ⛔ HARD RULE: NEVER send NATIVE OS input without warning the user FIRST (John, 2026-07-29)
**Native OS input (`desktop_click`, `desktop_type`, `desktop_press_key`, `desktop_hover`) does NOT go
to a window. It goes to WHATEVER WINDOW HAS FOCUS AT THAT INSTANT.** If the user has switched away,
your keystrokes land in THEIR document, THEIR terminal, THEIR editor.
**Prefer pup's own input** (`pup_click`, `pup_type`, `pup_press_key`, `pup_input_dispatch`): it is
trusted, needs NO foreground and disturbs nobody. `pup_assisted_click` is not available on macOS
(`unsupported_platform`). If you genuinely must use native input, in this order:
1. **`notify_user` TOAST**: what you are about to do and that you need the window.
2. **`desktop_caption`**: on-screen, so it is visible while they look at the window.
3. **Say explicitly: "please don't type or click until I say done."**
4. **WAIT until your window is frontmost and stays there**; never dispatch while it is not.
5. **Re-verify the foreground IMMEDIATELY before EACH native event.**
6. **Caption each step**, and caption when you are DONE.
Native input returning `ok:true` means "dispatched to the OS", **NOT** "it reached your window".
Verify the effect in the page. If it seems not to land, the first hypothesis is "focus moved".
## Standing rule: TOAST THE USER before foregrounding (John, repeatedly)
NEVER put a window on the user's screen silently. Any time you FOREGROUND a pup window, send a toast
FIRST (pup auto-fires `notify_user` on every granted foreground; still narrate WHY in the
`foregroundReason`). For a background nudge, use `pup_alert_window`: on macOS it posts a notification
the user can click and never takes focus (`{stop:true}` has nothing to clear).
## Vendor sites: they load. The three conditions that make them load
1. **Drive the site through pup, not a hand-rolled Chrome.** What makes vendor sites load is pup's
launch configuration as a whole, starting with the extension lane's missing debug port: over CDP,
st.com and mouser.com died with `ERR_HTTP2_PROTOCOL_ERROR` while the extension lane loaded them.
2. **Go at human pace: one tab at a time, ~7 s settle.** Bursting tabs trips velocity rules and is
the most common self-inflicted block.
3. **Clear that domain's cookies first** (`pup_clear_cookies {domain}`): a prior denial writes a reject
verdict that keeps denying you long after the cause is fixed.
Mouser, Arrow, Octopart, Newark and analog.com still run bot walls that come and go. Check the page
text for denied/captcha before trusting a load, and fall back to DigiKey, LCSC or TI. Per-site detail
lives in the **electronics-sites-skillpack**. A body of 0 chars WITH a title is a bot wall, not a slow
load; settle ~15 s after open/navigate before you read.
**What the extension lane does.** It is the normal pup window: `pup_click`, `pup_hover`, `pup_type`,
`pup_press_key`, `pup_scroll`, `pup_fill`, `pup_select`, `pup_input_dispatch`, `pup_annotate`,
`pup_screenshot`, `pup_screenshot_element`, `pup_screenshot_full_res`, `pup_record_start/stop`, and
`pup_eval` (retried through DevTools on CSP/Trusted-Types pages). Still CDP-only (answered with
`lane_needs_cdp`): selector-driven `pup_download`, `realKeystrokes` logins, iframe targeting,
`webSecurity:false`. The `pup-lanes` skill is the authority on what each lane does.
### When a page comes back as `botWall`
Every page load is checked for a bot wall (Cloudflare, PerimeterX, Akamai, DataDome, generic). The
reply then carries `botWall: {vendor, host, url}` and a `_hint`:
- **Stop loading pages from that host.** Every retry raises the site's bot score.
- **Parts data (stock, price, datasheet): use `adom-parts-search`,** not product pages.
- **Need the page itself:** ask the user for ONE click on the check in that window
(`pup_alert_window`, or `pup_raise_os_window` with a reason), or open it in their own browser with
`nbrowser_*`. Never solve it yourself.
A `_paceWarning` means you loaded a guarded vendor again within 15 s. Slow down.
## When pup vs the Adom extension (`nbrowser_*`)
| The task | Tool |
|---|---|
| Screenshot / scrape / test / demo a page; drive an app you're building; sites pup has a login for | **pup (this)** |
| Needs the user's REAL signed-in Chrome: their SSO, their existing sessions, a hand-off with no AI driving | **Adom browser extension** (`nbrowser_*`) |
## Skill map: this is a pack
| Skill | Read it for |
|---|---|
| **`pup`** (this file) | The mental model, when-to-use, quick start, the essentials |
| **`pup-windows-sessions-tabs`** | Window names, **ownership**, **background vs foreground** rules, many tabs in one window, list/switch/close |
| **`pup-screenshots-recording`** | Screenshots (full-page, full-res), reading the shot, **recording** |
| **`pup-browsers-and-chrome`** | Browser detection, `pup_use` to pin a browser, **installing Chrome / Chrome for Testing**, **readiness**, the cold-start error table |
| **`pup-credentials`** | The vault, sign-in, Google accounts, vault sync |
| **`pup-lanes`** | How pup drives a window and the rules that keep closes final |
| **`pup-settings`** | Every setting, behavior by behavior |
| **`pup-adom-wiki`** | Driving + verifying **`wiki.adom.inc`** pages in pup |
| **`pup-vendor-login`** | Signing into vendor portals |
| **`pup-why-cft`** | WHY the driving browser is Chrome for Testing |
*(Maintaining the bridge itself? The `pup-bridge-dev` + `pup-bridge-publish` skills are source-only in the
repo, not shipped here.)*
## Quick start: the 90% path
```bash
# Open in the BACKGROUND (automatic, never disturbs the user), then drive it invisibly.
adom-bridge-cli --ai-thread "<your thread>" pup_open_window '{"url":"http://localhost:3000"}'
adom-bridge-cli --ai-thread "<your thread>" pup_eval '{"expr":"document.title"}' # verify it really loaded
adom-bridge-cli --ai-thread "<your thread>" pup_screenshot '{}' # true-to-life PNG
```
- **Your `--ai-thread` name IS the owner.** A `pup_open_window` with no ai-thread is **refused** with
`caller_identity_required`.
- **`ok:true` means navigation *started*, not that content rendered**: check `renderCheck`, `pup_eval`
on `document.title`, or a screenshot.
- **On a fresh Mac the first open may include the one-time Chrome for Testing fetch** (`pup_prewarm`
does it ahead of time). The rare cold-start cases are in `pup-browsers-and-chrome`.
## Things the Windows line has that this Mac app does not
macOS has no per-window taskbar button, so there is no taskbar identity, thread tile, favicon badge,
jump list, taskbar progress bar or flash. `pup_set_identity` and `pup_audit_icons` answer
`mode:"plain"`, `platform:"macos"`; `pup_progress` answers `painted:false`; `taskbarActivity` reads
`off`; `pup_configure` reports `taskbarSupported:false`. Do not point a Mac user at an icon: tell
windows apart with `pup_list_windows` / `pup_list_tabs` and the window title (your thread name leads it).
## Key rules (the essentials, details in the sub-skills)
- **Always pass `sessionId`/`window`** on session-scoped verbs when your thread runs more than one
window. **ONE DANGEROUS EXCEPTION: `pup_close`.** There `'{}'` means **ALL sessions, every window,
across every AI thread on the machine**, immediately. To close one window use
`pup_close_window {"sessionId":"…"}`, never `pup_close '{}'`.
- **Background is guaranteed; a gated raise is the only way to show a window**: not sizing, not
navigating, not screenshotting.
- **Own your window**: always pass `--ai-thread`. Never navigate/close a window you don't own.
- **One window, many tabs**: use `pup_open_tab`, not a second `pup_open_window`.
- **`pup_readiness`** (not `pup_status`) answers "is it ready / which browser?".
- **`pup_eval` accepts statement lists** (`location.assign('...'); 'done'` works); a genuinely bad
expression FAILS with `eval_failed`.
- **Complex page fighting you?** It's usually shadow DOM or a nested scroll container. See
`pup-screenshots-recording` → "Driving & verifying tricky pages."
- **Never broadly kill Chrome**: `pup_close_window` for a specific session.
- **Surface a verb's `_hint` verbatim** whenever it reports not-ready or failure.
- **If pup's verbs TIME OUT, the bridge PROCESS is down**, usually right after a restart or a crash.
Fire **one** `pup_open_window` (a status probe does NOT respawn a down bridge), then poll
`pup_list_windows` with backoff. Do NOT spam opens while Bridge says `bridge_starting`.
- Full machine-readable catalog: `adom-bridge-cli --ai-thread "<your thread>" pup_describe '{}'`.
## Verify what you rendered: pup checks it FOR you
**`ok:true` means the navigation worked. It says NOTHING about whether the page rendered.** Every
`pup_open_window` / `pup_open_tab` / `pup_navigate` / `pup_reload` response can carry:
- **`renderCheck`**: `{ok, httpStatus, signals[], excerpt, bodyChars, elements}`. Signals include
`http_404`, `page_says_not_found`, `dev_server_cannot_get`, `server_error_text`,
`stack_trace_visible`, `effectively_blank`, `no_stylesheets_loaded`, `broken_images_N`.
- **`_verifyRender`**: what to do. When `renderCheck.ok` is false it leads with *"THIS PAGE DID NOT
RENDER CORRECTLY, pup checked it, so do NOT report success"*.
**If `renderCheck.ok === false`, it is not working yet: never report success.** Screenshot it, READ the image, fix
the cause, `pup_reload`, and re-check. A clean `renderCheck` is not proof the page is *right*; when
you generated or edited the content, screenshot it anyway.
## Every adom-bridge-cli call needs `--ai-thread`
Adom Bridge refuses any command that does not say which AI conversation is asking
(`errorCode: caller_identity_required`). It is a **global flag** that goes before the verb:
```bash
adom-bridge-cli --ai-thread "chip-fetcher tab 3" pup_open_window '{"sessionId":"cf-web","url":"..."}'
```
Pick a name the USER would recognise, never a UUID. pup puts it at the FRONT of the window title so
a user running many AI threads can tell windows apart. It is **attribution, not permission**. **Do
NOT `export ADOM_AI_THREAD`**: threads share one container, so it names the shell rather than you.
## Found a pup bug? Please report it
Responses carry `_reportIssues` on failures. Check for a duplicate with
`adom-wiki issue list adom/pup-bridge-macos`, then
`adom-wiki issue create adom/pup-bridge-macos --title "<symptom>" --category bug-report
--body "<what you ran, expected, got, the VERBATIM response JSON, bridge version, macOS version>"`.
An Adom Bridge core problem (relay, bridge lifecycle, caller identity) goes to `adom/adom-bridge`.
## AUDIT YOURSELF: when the user says "audit your windows"
Run all six sections, in order, and report them as a table:
1. **Sessions (bridge view)**, `pup_list_windows`: count, sessionId, owner, tabCount, profile,
ageMinutes, `_degraded`. Flag owner:null, duplicate sessions for one window, a wrong tabCount.
2. **OS ground truth**, `pup_window_info` per window (never resolve by title). A parked window's
browser app is HIDDEN: that is the normal background state, not a lost window.
3. **Health**, `pup_eval` per window: `readyState`, `visibilityState`, a `1+1` drivability check.
`visibilityState:hidden` is NORMAL for a parked window. Note any crashed tabs.
4. **Log**, `bridge_log_read`: count `[crash] [heal] [health] [unstick] [disconnect] [reclaim]
[tabs] park-failed Handler error`, and report park-failed and `[reclaim] adopted` lines.
5. **Browser footprint**, processes + total MB, and **pup's share vs the user's own Chrome**.
6. **Cleanup state**: leftover test windows, phantom sessions, stale session files.
**Report rules:** state what is WRONG first, name which findings are your own regressions, and never
present an API-reported success as proof; prove it in the page or a screenshot.
## ⏱ Captions: NEVER longer than 2 seconds (John, standing rule)
A caption is a **glance**, not a dialog. Anything past ~2 s covers what the user is doing. The cap is
enforced in code (2000 ms), so longer values are clamped. If something needs longer attention it is
not a caption: use `notify_user`, or put it in the verb response.
## FOREGROUNDING: always caption the REASON, for 2 seconds (John, standing rule)
Foregrounding takes over the user's screen while they work, which is why pup FORCES a
`foregroundReason` before it grants a raise. **The reason must be SHOWN**: every granted foreground
flashes an on-screen caption naming the window and quoting the reason, for 2 seconds at most, and it
is never skipped because a mode is quiet (it is deliberately NOT gated behind `minimalTouch`). If you
add a new path that foregrounds a window, it must go through the same captioned grant.
## Recording the desktop: guide them to Adom Bridge, pup is the fallback
For the whole desktop, prefer Adom Bridge's native recorder (`desktop_record_start` /
`desktop_record_stop`) when this Mac's Bridge offers it. pup's `pup_screen_record_start` is the
fallback: a Meet-style screen share (getDisplayMedia into WebM). Recording ONE pup window's page
content stays pup's (`pup_record_start`); it needs ffmpeg (`brew install ffmpeg`; pup re-detects it
without a restart). Full detail in `pup-screenshots-recording`.
## Know what you are spending: every open reports the machine's RAM
`pup_open_window`, `pup_open_tab`, `pup_list_windows` and `pup_status` return a **`memory`** block
(`boxTotalMb`, `boxFreeMb`, `boxFreePct`, `browserMb`, `tabsOnThisBrowser`, `avgMbPerTab`, `_hint`),
because pup runs on somebody's actual laptop and you are spending their RAM. `browserMb` is the whole
browser process tree for that profile; `avgMbPerTab` prices your next tab. **Read the hint and act on
it.** Under 25 % free, close what you are done with. Under 10 % free, open nothing more until you have
closed something: that is where macOS starts swapping and the user's whole desktop stutters. Close
tabs with `pup_close_tab` the moment a task is done.