main
Kyle Bergstedt Publish 3.0.0 259e308 13d ago

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 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.

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

# 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:

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.