stop path depends on setsid/ss, silently ineffective under Git Bash; see
local-workstation-quirks.md §5);
frontend is at `http://localhost:4444` and backend at `http://localhost:4096`
(see AGENTS.md Dev services section).
3.**Chromium is installed.** First run of `@playwright/mcp` downloads it
automatically; if the tool reports a missing browser, run
`bunx playwright install chromium` once in any shell.
## Tool selection recipe
The Playwright MCP exposes many tools. Pick by *what you need to observe*,
not by familiarity:
| Need | Tool | Why |
| --- | --- | --- |
| Open a URL | `browser_navigate` | Always the first call. |
| See the page structure (roles, names) | `browser_snapshot` | Returns the accessibility tree — stable across CSS refactors. Prefer this over `browser_take_screenshot` when asserting "does element X exist / is it labelled Y?". |
| See the page *visually* | `browser_take_screenshot` | For layout, spacing, color, overflow. Attach the PNG to your report. |
| Read console errors / warnings | `browser_console_messages` | Filter by level (`error`, `warn`). The first stop for any "page is blank" bug. |
| Click, type, hover, select | `browser_click`, `browser_type`, `browser_hover`, `browser_select_option` | Drive the flow. Always `browser_snapshot` first to get the `ref` for the target element. |
| Assert computed style / DOM state | `browser_evaluate` | Last resort. Snapshots cover 90%; reach for `evaluate` only when you need `getComputedStyle`, scroll positions, or arbitrary JS state. |
| Record a video / trace | `browser_start_tracing`, `browser_stop_tracing` | Heavy; use only when a bug is timing-sensitive and console+snapshot can't catch it. |
Default to the **user-visible browser pane** unless the task explicitly needs
an isolated headless Chromium (`surface: "headless"`). Pane agents now match
or exceed Playwright for the hard cases:
| Need | Pane tool / pattern | Notes |
| --- | --- | --- |
| Open / bind pane | `browser_open` (default surface) | Never touch the user's pane from a headless-only intent — pass `surface: "headless"` or `browser_pane_release` first. |
| Structure + refs | `browser_snapshot` | Open shadow is pierced; cross-origin iframes expose `f{seq}/e{n}` synthetic refs — prefer those over attr ids when acting into child frames. |
| Act (click/type/…) | `browser_act` / `browser_act_batch` | Locators (`role=` / `text=` / …) walk open shadow. Cross-origin click/type route through CDP trusted events. |
| Native dialogs | `browser_handle_dialog`**before** the act | Prefer `mode: "arm"` (or `act_batch.dialog_prearm`) so the pane parks the answer before the click that opens confirm/prompt — avoids the 1.5s auto-dismiss / widget-cancel race. Bind with `messageIncludes` when the page can raise multiple dialogs. Call `mode: "disarm"` if the trigger was cancelled. |
| Highlight / locator | `browser_highlight`, `browser_generate_locator` | Dual-leg: both pane and headless. Use highlight to confirm the ref before a write. |
| Failure recovery | re-snapshot + path candidates | Act/locator failures may return `pathCandidates` (frame/shadow path hints). Heal scores framePath so sibling iframes do not auto-heal across frames. |
Headless remains the right tool for parallel CI-style isolation and when the
user must not see the session. For interactive debug of `packages/app`, stay
the single mandatory sanitize boundary; the `.playwright-mcp/` layer
keeps its current semantics, additive not replaced) and cite the pack
directory. Evidence ladder: raw capture → `.playwright-mcp/` working
layer (gitignored) → sanitized Tier-1 pack → `BROWSER:` reference the
verifier validates mechanically.
## Rationalizations to refuse
| Excuse | Reality |
| --- | --- |
| "The unit test already covers it." | Unit tests assert component logic, not what Chromium actually renders. Visual and focus bugs live in the gap. |
| "I can tell from the JSX that it's aligned." | Computed layout depends on parent containers, fonts, and viewport. Screenshot the real page. |
| "Browser automation is overkill for a small change." | A `browser_navigate` + `browser_take_screenshot` pair is ~2 seconds. The cost is in *not* looking. |
| "I'll just run the whole e2e suite." | The suite answers "did anything break?" — it does not answer "does this specific change look right?". |
| "Snapshot is enough, skip the screenshot." | Accessibility trees hide overflow, clipping, and z-index bugs. Take both for visual changes. |
## Boundaries
- **Read-only by default.** This skill never writes files under
`packages/app/src/` — it observes. Edits flow through `implement`
(bugfix/refactor/port mode).
- **One page at a time.** Close tabs (`browser_close`) between scenarios so
state doesn't leak.
- **Evidence lives in the worktree's `.playwright-mcp/` dir.** Any screenshot
or trace you reference in a PR comment, verification report, or DoD entry
must be copied into the workflow worktree's `.playwright-mcp/` directory
(gitignored) — never left in `/tmp/`, `playwright-report/`, or any
machine-local path the reviewer cannot reach through octopus. Cite file
names + the worktree project path; the reviewer opens the octopus
screenshots UI (session panel tab or screenshots page) to view them.
## Session cleanup (mandatory)
A Playwright MCP browser is a real Chrome with a `/tmp` user-data-dir — its
mode follows `mcp.playwright.headless` (`headless-field-v1`, [org-internal #4393]): the repo
default `auto` spawns headed on desktop (win32/darwin; linux only with
DISPLAY/WAYLAND_DISPLAY set) and headless otherwise; an explicit `true`/`false`
or `OCTOPUS_MCP_HEADLESS=1|0` pins it. The mode is fixed per new session —
switching the config does not hot-switch a running browser. It
stays alive as long as its MCP server runs, and the MCP server stays alive as
long as its parent octopus backend runs. When a browser-debug session ends
without an explicit `browser_close`, OR the octopus session times out mid-task,
the Chrome + its MCP server are orphaned and accumulate across sessions —
burning CPU and leaving dead profile dirs (issue [org-internal #1315]); in headed mode the
orphan is a visible stray window.
**Iron rule: every browser-debug session MUST close its browser before ending.**
1.**Always close when done.** The last action of any browser-debug task is
`browser_close`. Treat it like a `return` — if you opened a browser, you
close it. Do not rely on session teardown to do it for you; it doesn't.
2.**Close on error too.** If the task aborts (bug not reproduced, tool error,
user redirect), still call `browser_close` before moving on. An abandoned
debugging session is the most common orphan source.
3.**If a browser was already orphaned** (you spot a stale Chrome in `ps`, or
a `/tmp/playwright_chromiumdev_profile-*` dir with no live session), do not
leave it. `bash script/dev-fresh.sh --full` (Windows workstations:
`dev-fresh-windows.sh`, same stop-path caveat as Preconditions §2) reaps
orphan dev sessions and
init-adopted browsers before restarting; run it to clean up. For a targeted
clean without restarting the dev server, kill the Chrome by its profile:
`pkill -f 'playwright_chromiumdev_profile'` (and any `agent-browser` harness
reparented to init).
**Why this matters:** a single forgotten `browser_close` leaves a Chrome
subtree (8+ processes) running indefinitely. Over days this is hundreds of
orphaned processes and wasted CPU. The MCP server does not auto-close its
browser on disconnect — only an explicit `browser_close` (or process reaping)
releases it.
## Screenshot output paths
The Playwright MCP server is launched WITHOUT `--output-dir` (see
`octopus.jsonc` → `mcp.playwright.command`): its default output location is
`.playwright-mcp/` in the process cwd (the worktree root), gitignored and
auto-evicted at 100 MB via `--output-max-size`. Auto-named output (no
`filename`) lands there — but an explicit `filename` can resolve against the
cwd and drop the PNG into the repo root, where the `/*.png` gitignore
backstop hides it from `git status` and nothing ever cleans it up.
**Iron rule: 截图文件名必须写进 `.playwright-mcp/`。** Every `filename`
passed to `browser_take_screenshot` (or any tool that writes a file) MUST
resolve into `.playwright-mcp/` — write it as `.playwright-mcp/<name>.png`,
or use an absolute path under `/tmp/octopus/`. NEVER a bare relative name
like `2202-after-models.png`: it leaks a root-level PNG that gitignore masks
but nobody deletes (the 2026-08 repo-root buildup — 40+ orphan PNGs — came
from exactly this).
Two valid destinations for screenshots, by purpose:
| Purpose | Destination | Why |
| --- | --- | --- |
| **Ad-hoc / throwaway** (debug a layout, confirm a fix landed, never cited in any report) | `.playwright-mcp/<name>.png`, or omit `filename` to auto-save into the same dir | Survives across the session, auto-evicted at 100 MB, never pollutes the repo. |
| **Evidence cited in a PR / report / DoD entry** | `.playwright-mcp/<name>.png` — the cwd is the worktree root, so no copy step is needed | Zero repo bloat (gitignored, never committed); viewable by the reviewer through octopus's own screenshots UI (session panel "Screenshots" tab / standalone screenshots page, served by `GET /screenshots?directory={worktree}`); lifecycle bounded to the worktree = the review window. |
**Evidence publishing ladder** (verified 2026-08-14 on this Gitea version):
1.**Copy into the workflow worktree's `.playwright-mcp/`** — primary path.
Cite file names + worktree project path; reviewer views via the octopus
screenshots UI. Verify reachability with
`GET /screenshots?directory={worktree}` before citing. Never commit
evidence binaries to git — screenshots accumulate per PR and git history
is unreclaimable (measured ~80KB/PR today, unbounded growth). Do not put
routine debug screenshots on the wiki either — base64-in-page bloats the
wiki git repo equivalently; the wiki evidence namespace is reserved for
the durable verify-stage evidence channel (rung 3).