aboutsummaryrefslogtreecommitdiff
path: root/test
AgeCommit message (Collapse)Author
2026-08-02fix(domains): report a page-side throw instead of blaming the refamankumarsingh77
runOnElement checked only whether Runtime.callFunctionOn reached Chrome, not whether the function it ran threw. A page-side exception left result.value undefined, the result guard rejected it, and browser_fill, browser_focus and browser_select_option all reported "No element matched: <ref>" for an element that was there — sending the agent to re-snapshot a ref that was fine and discarding the real error. The throw is now surfaced as cdp_error carrying the exception text, which is distinguishable from both the no-match and stale-ref paths; those still fire for a genuinely missing or stale element. The selector path already reported throws correctly via evaluateJs. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): one fill engine, so browser_fill stops lying about checkboxesamankumarsingh77
browser_fill and browser_fill_form carried separate in-page implementations that disagreed on the same element. The worst case was silent: browser_fill on a checkbox fired the HTMLInputElement value setter, which writes the submit value and leaves `checked` alone, then reported status ok — a no-op the agent had no reason to doubt. Both tools now share one engine, with browser_fill's documented <select> rejection kept as an explicit pre-check and per-tool focus behaviour kept as a parameter. Deliberate, user-approved behaviour changes: - browser_fill on a DISABLED input now returns an error instead of success. The old success was the bug; browser_fill_form already refused it. - browser_fill on a checkbox/radio now toggles `checked` instead of the value. - browser_fill_form on a contenteditable now uses execCommand("insertText"), so it drives ProseMirror/Slack-style editors as browser_fill already did. browser_fill keeps appending interactiveDiff and republishing refs; fill_form still does not. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): keep element refs stable across a mutationamankumarsingh77
interactiveDiff re-assigned refs by tree position after every fill, click and select, so any element appearing or disappearing shifted the numbering and a ref the agent was still holding silently resolved to a different element — while the tools kept promising that refs survive re-renders. Refs are now carried over by backendNodeId and only genuinely new elements draw a new number. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(tool): stop the socket gate rejecting every tool call on Windowsamankumarsingh77
The tool gate ran access() on DAEMON_SOCKET_PATH, which on Windows is the named pipe \\.\pipe\pi-browser-daemon. Named pipes are not filesystem entries, so access() always fails there and every browser_* call returned "Browser harness not initialized" no matter how healthy the daemon was. spawn.ts already skipped the identical check for this reason; that knowledge is now one shared daemonSocketMayExist() used by both call sites, with platform and path as parameters so the branch is testable off-platform. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): route background-tab events to the tab they belong toamankumarsingh77
The event loop dropped every event whose sessionId was not the active tab's, so the per-tab console and network buffers could only ever hold data captured while their tab was in the foreground. The filter now drops only sessions this instance never attached, and the dialog, frameNavigated and loadEventFired arms resolve their tab from the event's sessionId instead of assuming the active tab — without that, a background tab's dialog would have been recorded against the foreground one. Tabs are also registered before the domain-enable round trips, so events arriving during attach are no longer dropped as unknown. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f4 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): re-enable domains when re-attaching to a known tabamankumarsingh77
Every Target.attachToTarget mints a new sessionId and CDP domain enablement is per-session, but switchTo only enabled domains on a tab's first visit. Switching away and back left the refreshed session with Page, Network, Runtime and Log disabled, so that tab silently stopped collecting console and network records and stopped reporting dialogs and load events for the rest of the session. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02test: fake browser client over a stub transportamankumarsingh77
Wires a real CdpSession to a canned-response transport so domain tools can be driven without Chrome. Unconfigured client methods return an error rather than a success, so a test cannot pass by accident. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f30 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02chore: fail the build on unguarded type assertionsamankumarsingh77
A clean tsc does not prove the absence of casts. This scans src/ for as any, ts-ignore, non-null assertions, inline object casts, and double casts, wired into a new npm run check that CI runs as a single step. The escape hatch is an external allowlist module rather than an inline comment marker: comments are not used in this codebase, and an entry carries a count, so adding a new cast to an already-exempted file still fails until someone raises a number that shows up in the diff. Exactly one exemption is needed today, for the dynamic sharp import. scripts/ was gitignored wholesale, so the two new files are un-ignored individually and everything else dropped there stays ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: close the remaining unguarded type assertionsamankumarsingh77
Types the event-queue iterator's TReturn as undefined so the terminal result needs no cast; declares the isError field the pi runtime reads by duck-typing; binds socket to a local const in the daemon transport; replaces the @ts-ignore on the optional sharp import with a non-literal specifier so src/ still typechecks where sharp was not installed. Adds src/util/guards.ts as the single home for narrowing over unknown — isRecord plus scalar/array readers — and converts the two private isObject copies in cdp/types.ts and daemon/protocol.ts along with every inline `as Record<string, unknown>` in domains/js.ts and domains/render.ts. Gives Accessibility.getFullAXTree a real result schema, closing the last unvalidated CDP boundary: only nodeId is required, every object carries additionalProperties, and each AX value stays Unknown, so the tree decodes as a whole or fails as a whole rather than being filtered. Guards the five tool-result `details` readers, the daemon attach response, the persisted session state, the browser-namespace flag, and the fs errno narrowing, each feeding an already-present failure path. The daemon-transport socket binding is not a bug fix: send() runs synchronously inside sendWithTimeout's promise executor, so socket cannot be reassigned between the check and the write. It removes the assertion and makes the write immune to a later refactor that adds an await. Two assertions survive by design and are documented in the task report: the never->unknown re-widening in util/tool.ts, which the type-erased tool registry makes unavoidable, and the AsyncFunction constructor lookup in domains/js.ts, which JavaScript exposes no typed binding for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: validate JSON at every parse boundaryamankumarsingh77
Ten sites cast or implicitly typed JSON.parse output; JSON.parse("null") would have satisfied all of them. parseJson returns a Result and validates against a typebox schema. js.ts keeps unknown — user script output has no fixed shape. daemon/bridge.ts assigned JSON.parse straight to CdpRawMessage with no guard at all; it now uses isCdpRawMessage like the transport does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): stop fill_form misreporting select-by-label, let it failamankumarsingh77
Three review fixes on the restored fill_form: - FILL_FN matches a <select> option by value, label or text but returns el.value, so filling by visible label ("Canada" -> value "ca") tripped the read-back mismatch check and reported a correctly-set field as "Filled 0/1 fields; issues: ref e4 (value mismatch)". The mismatch test now also accepts res.text, which FILL_FN sets only for selects, so mismatch detection is preserved for every other field kind. - fill_form always returned ok(), so total failure surfaced as a successful result reading "Filled 0/4 fields". It now returns invalid_state when okCount is 0, carrying the per-field issue list as the error message. Partial success stays ok(). - Removed the two comment blocks this task had added to test/manual/forms-test.ts. Neither behaviour fix has automated coverage; the manual suite that would exercise them still needs a live Chrome and was not run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): restore fill_form and set_checked, drop the stale forkamankumarsingh77
domains/forms.ts was a stale fork of form.ts imported only by a manual test. It held two working tools the registry never registered: fill_form and set_checked. Ported both onto the current pattern and deleted the fork; form.ts versions of the shared tools win. The fork's tools took ref as Type.Integer (a raw CDP backendNodeId). browser_snapshot has since moved to string refs ('e7') resolved through the session ref map, so as written the tools were not callable at all. Ported them onto resolveRefToObjectId and Type.String refs; the only user-facing strings that changed are the four that documented the old [ref=N] syntax. All page-side JS is carried byte-for-byte. form.ts exceeded 250 lines, so fillForm moved to form-batch.ts per the single-field/batch split. form.ts is still 425 lines — the pre-existing three tools account for that, so the split does not reach the target. test/manual/forms-test.ts is repointed and adapted to publish an 'eN' ref map, but NOT executed — it needs a live Chrome. Type-checked only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(domains): share target resolution and CDP error mappingamankumarsingh77
The ref-vs-x/y branch turned out to exist in one tool, not four; it is now one helper with tests covering the half-specified-coordinate and stale-ref cases. cdpCall folds the repeated cdp_error mapping at 24 active-session call sites; target-scoped callOnTarget/callBrowser sites are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(tools): declare concurrency in the tool definitionamankumarsingh77
serialized: true was applied in registry.ts, away from the tools it describes. Now a required concurrency field on each definition, with a test pinning the serialized set so the classes cannot drift. The lock check defaults to taking the mutex for any class that is not explicitly "parallel", so a definition that bypasses defineBrowserTool at runtime fails safe rather than racing on shared CDP state. The test script gains --conditions=import: pi-coding-agent publishes no "require" condition, so importing any tool definition from a CJS-loaded test could not resolve it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): validate event params against a typed tableamankumarsingh77
Malformed or unknown events are now dropped rather than hand-narrowed; consumers get typed params. Unknown methods are ignored, since the transport delivers every event Chrome emits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): restore fail-closed focus check in browser_typeamankumarsingh77
Adopting isFocusProbe inverted the safety default: the guard was ANDed into the condition gating the check, so a probe result the guard rejected skipped the check and fell through to Input.insertText — text typed into nothing, silently, which is the exact failure the block exists to prevent. The guard now feeds a value that defaults to { ok: false }, so a rejected shape produces the error instead of bypassing it. The other four guard adoptions (isFillResult, isFocusResult, isSelectResult, isPageResult) were already fail-closed: each feeds a value whose rejection lands on a pre-existing error branch. Also assert x/y in the box centre test — the fields snapshot's off-viewport filter reads — and wrap the zero-size message to the surrounding width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(domains): drop CDP casts, share the box-model unwrapamankumarsingh77
ref-resolve.ts and snapshot.ts had the same box-model unwrap, both using data.model! to work around an untyped response. Extracted to domains/box.ts; the typed schema makes the assertions unnecessary. Both derived the centre identically (quad corner + half the model width/height), so centreOf keeps that derivation; the shared zero-size rejection moved into boxOf, and snapshot keeps its own off-viewport (x/y < 0) filter. evalInIsolatedTab now goes through evaluateJson with the tab's own sessionId and a caller-supplied guard; both callers evaluate expressions ending in JSON.stringify, so they pass isJsonText rather than a check that checks nothing. Runtime.evaluate/callFunctionOn result casts in form.ts, forms.ts and keyboard.ts are replaced by named guards over the in-page result shapes. Runtime.callFunctionOn's params schema gained arguments/returnByValue/ awaitPromise/userGesture: four domain call sites legitimately pass them and typebox's additionalProperties:true does not widen the static type, so the params type was rejecting valid CDP fields. Params schemas are type-only (only results are compiled), so this is a compile-time widening with no runtime effect. tsc is green again for both tsconfigs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): let evaluateJson target a specific sessionamankumarsingh77
2026-08-02refactor(cdp): extract attachTo, getWindowId, evaluateJson helpersamankumarsingh77
Target.attachToTarget was decoded at 5 sites, Browser.getWindowForTarget at 5, and Runtime.evaluate unwrapped at another. Each is now one typed helper. Deletes the dead cdp/window-binding.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02test(cdp): cover call/callOnTarget/callBrowser through a stub transport; ↵amankumarsingh77
collapse decode into one path session-call-test.ts previously only exercised decodeResult (Task 3's surface, and mostly duplicating commands-test.ts) — call, callOnTarget, callBrowser, and the session's internal req helper had zero coverage. Add a stub CdpTransport (no socket, no browser) and test: a valid payload decodes to typed data, a garbage payload becomes invalid_response without throwing, a transport-level error passes through with its original kind (not relabeled), call/callOnTarget/callBrowser route the session id correctly, and opts.timeoutMs reaches the transport only when provided. Also collapse the four near-identical transport-then-decode bodies (req, call, callOnTarget, callBrowser) into one: the three public methods now delegate to req, which is the sole place unknown ends for every CDP call the session makes, internal or public. Public signatures are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): make session.call generic over the command tableamankumarsingh77
call/callOnTarget/callBrowser now take a known method and return its decoded result type. The session is where unknown ends; transport keeps returning unknown. Removes the casts in session.ts and target-factory.ts, including the session's own internal Target.*/Browser.* calls, which now decode through the same path as the public call methods. Also completes two params-schema gaps in commands.ts surfaced by this migration: Target.createTarget was missing openerId and Runtime.evaluate was missing userGesture, both real CDP fields src/cdp/ already sends. tsc is red project-wide (6 errors, all in src/domains/) — the sanctioned state between Tasks 4 and 7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02feat(cdp): add typed command table with validated decodersamankumarsingh77
One typebox schema per CDP command is the source of both the TS types and the runtime validator, so they cannot drift. Malformed responses become invalid_response errors instead of TypeErrors several frames later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02test: port remaining suites to node:testamankumarsingh77
Mechanical port of the hand-rolled check() harness. Import-after-env ordering preserved so suites still read their sandbox dirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(test): split P5/P6/P10 into one node:test per original check() labelamankumarsingh77
Task 1 review found three ported checks collapsed two original labels into one test(), losing the per-assertion granularity the brief's Step 7 requires. Splits P5, P6, and P10 back into two test() blocks each, using the original labels verbatim, with each block doing its own dynamic import after before() sets PI_CODING_AGENT_DIR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02chore(build): split tsconfig, typecheck tests, add node:test runneramankumarsingh77
test/ was never type-checked and had no runner. Adds tsconfig.base.json shared by a src project and a test project, an npm test script scoped to non-manual suites, and a CI step. Ports the profile store suite as proof. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26fix(profile): resolve replaced binaries and surface real launch failuresamankumarsingh77
Two failures made a profile launch report the wrong cause. Linux appends " (deleted)" to /proc/<pid>/exe once the running binary is unlinked, which is what a google-chrome package upgrade does while the browser stays open. detectRunningBrowser passed that literal string on as the executable path, so every launch tried to spawn "/opt/google/chrome/chrome (deleted)" and hit ENOENT. Executable paths are now validated against disk, with the marker stripped and argv[0] used as a fallback; a binary genuinely named "… (deleted)" still wins. spawn() reports a missing or non-executable binary asynchronously via an 'error' event rather than throwing, and that event was discarded — so openProfileWindow returned success, no window opened, and the caller blamed the browser 15s later with a sentinel timeout. The launch now awaits 'spawn'/'error', returns the real cause, and removes its handshake page on the failure path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25feat: pin the agent to a chosen browser profile (#16)Aman Kumar
* feat: pin the agent to a chosen browser profile (#15) Harness tabs were created with a bare Target.createTarget, which places them in Chrome's defaultBrowserContextId — and that value follows window focus. Which profile the agent acted as therefore depended on which browser window the user last clicked, so the same task could run as a work account on one run and a personal account on the next. /browser-profile lists the connected browser's profiles as "Name (email)" and pins one to ~/.pi/agent/browser-harness.json, so the choice outlives the session. The same picker appears on first browser_setup when nothing is pinned; with no interactive UI, or on cancel, setup continues with a note and the previous behavior. Chrome offers no direct route to another profile: Target.createTarget rejects a foreign browserContextId, and openerId does not inherit the opener's context. The window is opened through the browser's own command line (--profile-directory, handed to the running browser by Chromium's ProcessSingleton) and identified by a unique file:// sentinel page, since a bare about:blank#token argument is discarded. Further tabs come from window.open evaluated with userGesture: true — the only CDP-reachable way to place a tab in a non-default browser context — correlated by a unique token so concurrent isolated tabs stay distinct. A failed handshake is reported, never silently redirected to another profile. All four tab-creation sites now funnel through cdp/target-factory, so open_urls and the isolated tabs behind web_search / read_page cannot run with a different profile's cookies than the visible tabs. Also fixed along the way: snap Chromium is discovered, Windows uses %LOCALAPPDATA%, browsers launched with an explicit --user-data-dir are found, the running browser is chosen by ranking candidates against the connected user-data-dir rather than taking the first process found, and an installed-but-closed browser no longer reads as running. CI runs the profile suites and a real-browser end-to-end test on ubuntu, macOS, and Windows — profile discovery is the one part of the harness whose behavior genuinely differs per OS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: single-flight harness window creation Isolated tabs run concurrently (browser_read_page is registered unserialized), so several could each find "no harness window" and create one — two blank windows before this feature, two LAUNCHED profile windows after it. ensureHarnessWindow now shares one in-flight creation per client. Sharing introduces a second hazard: freshlyCreated is a claim on the seed tab, and if every waiter received it, they would all drive the same tab. Only the caller that started the creation keeps the claim; the rest open their own tab beside it. Covered by test/manual/profile-concurrency-test.ts (five concurrent isolated tabs, pinned and unpinned: distinct targets, one window, correct profile), wired into CI on Linux only — the coordination is pure JS and identical across platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: tilde expansion differs by purpose on Windows CI on windows-latest caught this: join("~", "x") yields "~\x" there, and expandTilde only handled "~/". The two uses are not the same, so they no longer share a helper. The agent dir stays byte-compatible with pi's own expandTildePath ("~/" only, every platform) — being more lenient would put the pin file somewhere pi never looks. $CHROME_USER_DATA_DIR is ours to interpret, so it accepts "~\" as well, which is what a Windows user would type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22feat: add forms domain and durable window binding (v0.9.0) (#14)Aman Kumar
- Add forms domain with a universal field setter that auto-detects element type and fires framework-compatible input events (React native-setter trick) - Add window-binding helper to capture and bind the real Chrome windowId as the session's durable window identity on every new-window code path - Wire supporting changes across CDP session/ownership, client, navigate, isolated-tab, prompt, and state - Add manual tests for forms and window binding - Bump version 0.8.3 -> 0.9.0 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08feat: add deep-research skill, /deep-research command, fix web-search-researcheramankumarsingh77
deep-research skill fans out to isolated web-search-researcher subagents (now using browser_web_search + browser_read_page instead of the non-existent web_search/web_fetch), coverage-driven loop with a hard ceiling, synthesizes a cited Markdown report. /deep-research command as explicit entry point. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08feat: add browser_read_page reader-mode toolamankumarsingh77
URL (or owned targetId) → clean main-article text with nav/ads/footer boilerplate stripped, via a dependency-free readability heuristic. DOM walk lives in an in-page capture expression; scoring/selection is a pure, fixture- tested function (link-density + boilerplate-ancestor filtering, body-text fallback for structure-less pages). Reuses the isolated-attach lifecycle; registered unserialized (safe concurrent read). 10 extractor unit scenarios. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08feat: add browser_web_search tool (real-Chrome SERP scrape, no API key)amankumarsingh77
Thin query→ranked-links primitive. Isolated-attach tab lifecycle mirroring browser_open_urls (never touches the user's current tab); pure Google SERP parser with semantic (a h3) extraction and redirect unwrapping; CAPTCHA and no-results surfaced as invalid_state + details.reason. Fixture-based parser tests (9 scenarios). Establishes the isolated-tab lifecycle and tsx+fixture test convention that browser_read_page and deep-research build on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init (#6)Aman Kumar
* feat: Unix socket daemon with auto-reconnect for persistent Chrome CDP Eliminates the 'Allow Remote Debugging' Chrome prompt that appeared on every pi session by multiplexing pi instances through a single persistent daemon process. Key additions: - Unix socket IPC (net.createServer + readline JSON framing) - CdpBridge with ID multiplexer + event router - DaemonTransport implementing CdpTransport interface - Auto-spawn with ensureDaemon() (lazy, checks socket) Fixes since initial implementation: - Bridge auto-reconnects to Chrome with exponential backoff (1s-60s). Previously the bridge tried once at startup and permanently failed if Chrome wasn't ready — causing 'Chrome not connected' on every browser tool call. - Daemon spawn uses project-local tsx binary instead of global --import flag, fixing silent spawn failure (ERR_MODULE_NOT_FOUND). - Stale ctx crash: persistState in onOwnershipChange wrapped in try/catch to prevent CDP event consumer crashes. 49 test assertions across 4 test files. Zero TypeScript errors. * feat: on-demand browser initialization Browser daemon no longer auto-starts on session_start. Instead: - session_start creates the client lazily but does NOT connect or spawn the daemon. Zero Chrome prompts for non-browser pi sessions. - Browser tool wrapper checks for daemon socket before each call. Socket missing → clear error: 'Run /browser-setup first'. Socket exists → proceeds silently (daemon already running). - /browser-setup is the explicit, one-time init point. It spawns the daemon and connects to Chrome only when the user opts in. - SKILL.md updated with Connection Setup instructions telling agents to direct users to /browser-setup when socket is missing. * fix: checkChromeRunning on macOS — match by substring not exact ps -A -o comm= returns full executable paths on modern macOS (e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome), not the basename. The old exact-match check (browserProcesses.includes(l)) never matched, causing false 'No browser instance running' errors. Changed to substring includes() with helper/renderer/crashpad filtering. * feat: browser_setup as agent-callable tool The agent can now self-recover from 'Browser harness not initialized' errors by calling browser_setup directly — no user mediation needed. - Extracted shared performSetup() from the slash command into setup.ts - Created src/domains/setup.ts with ensureAlive:false to bypass the socket guard (this tool IS the setup, can't require itself) - Registered browser_setup in the tool registry - Updated SKILL.md: agent calls browser_setup directly, not ask user - Removed stale test/manual/spawn-test.ts * release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init