SKILL.md
Browser Anti-Stall Protocol (playwright-cli)
Degree of freedom: LOW. Almost every step is exact. Only "why it stalled" and the next hypothesis are [HIGH freedom].
Apply these rules to EVERY browser action. No exceptions.
This repo drives browsers with playwright-cli, not the Playwright MCP. The MCP exposes one browser per server and a persistent profile can only be locked by one process at a time, so parallel agents on the same repo fight over tabs and profile locks. The CLI gives every agent its own isolated browser via -s=<session>, costs far fewer tokens (no tool schemas or verbose trees loaded into context), and runs natively in parallel shells.
Read references/mcp-to-cli-map.md if you encounter old browser_* MCP tool calls — it maps every tool to its CLI command. Read references/playwright-session-coordination.md before your first command — session naming, persistent logins (incl. the Google/CDP block), and cleanup.
How to reason
- Observe — snapshot, screenshot, console, requests, and the session name
- Interpret — stale ref, SPA hydration, pending request, or a real blocker
- Classify — one new-hypothesis retry / 2s sleep-cycle / BLOCKER report
- Recover — one real user action, then look; never
evalto click
Worked example
Observe: checkout
click; URL still/cart; console hydration warning;POST /api/checkoutpending; session-s=qa-checkout.
Interpret: SPA not ready + possibly a stale ref — not "the button is missing".
Classify: attempt 2 —find/waitFora landmark, freshsnapshot, then click. Notrun-codeto submit.
Stop at 4: still stuck → BLOCKER with console + requests + screenshot.
Self-critique before reporting
- Headed +
-s=— every call; never a shared session name - Inspection-only eval —
eval/run-codenever click, type, or submit - Evidence before retry — max 4 attempts, then BLOCKER
- Right owner — product QA behavior stays with the calling test/audit skill
Invocation — always this form [LOW freedom — run exactly]
PW="npx --yes @playwright/cli@latest" # portable; survives fnm/nvm version switches
$PW -s=<session> <command> [args]
-s=<session>is mandatory on every call. Name it after your task or branch
(-s=qa-checkout, -s=audit-ux-home). Two agents must never share a session name.
- Do not rely on a global
npm i -ginstall. Underfnm/nvmthe global prefix is
per-shell and disappears; npx always resolves.
--json/--raware available when you need machine-readable output.
0. Manual & headed — never scripted (read first) [LOW freedom — run exactly]
You are driving a real, visible browser to feel what a user feels. A green script proves nothing about UX — see the screen and watch the logs.
- Headed, always. The CLI defaults to headless — you MUST pass
--headedonopen.
If you cannot see the window, say so rather than proceeding blind.
- One real action at a time.
click,type,fill,select,hover,press,drag
exactly as a user would. Never chain a whole flow into one code snippet.
eval/run-codeare inspection-only. Use them ONLY to read state (DOM, computed
styles, storage, perf) or to wait for an element — never to click, type, navigate, or submit. Driving the UI through code bypasses real events and hides the bug you are hunting.
- No test files, no runner. Do not write
*.spec.ts, runnpx playwright test, or use
codegen. You are here to experience the flow, not automate past it.
- Look after every action. Fresh
snapshot+screenshot+console+requests, plus the
dev-server terminal. Real pain surfaces on screen and in logs, not in an assertion.
1. Session lifecycle [LOW freedom — run exactly]
$PW -s=qa-checkout open --headed http://localhost:3000 # start (once)
$PW -s=qa-checkout goto http://localhost:3000/cart # navigate within the session
$PW -s=qa-checkout snapshot # get refs
$PW -s=qa-checkout close # end YOUR session when done
$PW list # see all sessions (status, profile, headed)
$PW close-all # only when you own every session
$PW kill-all # last resort: stale/zombie processes
openstarts a browser;gotonavigates an already-open one. Callingopentwice on the same
session is wasteful — use goto.
- Close only your own session. Never
close-allwhile another agent may be mid-test. - Add
--browser chrome|firefox|webkit|msedge,--device "iphone 15", or--mobileonopen
when the task calls for it.
2. Navigation guard [LOW freedom — run exactly]
After every open / goto / reload:
snapshot— confirm the URL changed and the page has content.- If blank or unchanged →
sleep 2→snapshotagain. - Max 3 cycles (~6s). Still not loaded → report a blocker (§8) and move on.
Never assume navigation succeeded without a snapshot to confirm it.
3. Waiting — there is no wait command [LOW freedom — run exactly]
Playwright auto-waits for actionability on click/fill/select, so most explicit waits are unnecessary. When you genuinely must wait:
| Need | Do this |
|---|---|
| Fixed short pause | sleep 2 in the shell — never more than 3s per pause |
| Wait for text/element | run-code "async (page) => { await page.getByText('Dashboard').first().waitFor({ timeout: 5000 }); return 'ready'; }" |
| Wait for something to disappear | ...waitFor({ state: 'hidden', timeout: 5000 }) |
| Poll for content | find "<text>" → if no match, sleep 2 → retry (max 3) |
Always set an explicit timeout (milliseconds) in waitFor — the default 30s is far too long. Use the incremental pattern instead of one long block:
sleep 2 → snapshot → check ↓ not ready
sleep 2 → snapshot → check ↓ not ready
sleep 2 → snapshot → check ↓ still not ready
STOP → report blocker with evidence
This handles cold starts, SPA hydration, and slow APIs without ever blocking blindly.
4. SPA-specific rules [LOW freedom — run exactly]
SPAs (React, Next.js, Vue) fire load before hydration completes — never trust load events.
- Wait for a specific UI landmark that proves the app rendered (
run-code+waitFor, orfind). - If a spinner is showing, wait for it to reach
state: 'hidden'rather than sleeping.
5. Anti-loop: max 4 attempts per goal [LOW freedom — run exactly]
| Attempt | Action |
|---|---|
| 1 | Try the action normally |
| 2 | Alternative approach — re-snapshot for a fresh ref, try a CSS selector instead, scroll into view, or find the element |
| 3 | Gather evidence: console + requests |
| 4 | STOP. Report what blocked progress, with evidence. |
Never repeat the exact same failing action without new evidence.
Fresh refs after every state change. Refs from a stale snapshot are invalid after any navigate/click/fill/hover/key press. Re-snapshot before the next interaction. click also accepts a unique CSS selector, which survives state changes better than a ref.
6. Evidence before retry [LOW freedom — run exactly]
When something is not working, gather evidence FIRST, then form a hypothesis:
console— JS errors, warnings (console errorto filter by level)requests— pending/failed calls;request <n>/response-body <n>for detailsnapshot— the actual DOM state, not what you assumescreenshot --filename .playwright-mcp/<name>.png— visual state
Only retry once you have a new hypothesis grounded in that evidence.
7. Timeout budget [LOW freedom — run exactly]
| Scope | Max time |
|---|---|
| Single interaction (click, fill, select) | 15 seconds |
| Navigation + verification | 30 seconds |
| Multi-page flow | 5 minutes |
| Full session | 15 minutes |
Exceeded? Skip it and log [TIMEOUT] skipped: <step>. One stuck step must not kill the session.
8. Blocker reporting format [LOW freedom — this shape]
BLOCKER:
- Session: [-s= name]
- Page: [current URL]
- Goal: [what I was trying to do]
- Blocked by: [what prevented it]
- Evidence: [console errors / failed requests / screenshot observation]
- Suggestion: [most likely next step or manual action needed]
Actionable information beats a silent freeze.
9. Artifacts [LOW freedom — run exactly]
- Screenshots, snapshots, and logs go under
.playwright-mcp/(gitignored):
screenshot --filename .playwright-mcp/home-390.png. Name by route + viewport/step.
- The CLI also auto-writes snapshot
.ymlfiles to.playwright-cli/in the working directory —
also gitignored, never committed.
- Sweep any stray root-level
.png/.loginto.playwright-mcp/before ending the session.
10. Parallel agents [LOW freedom — run exactly]
Session isolation replaces the old tab-sharing etiquette — each agent gets its own browser:
# agent A # agent B (simultaneously, no conflict)
$PW -s=audit-ux open --headed … $PW -s=qa-checkout open --headed …
- Never reuse another agent's session name; never
close/kill-allsessions you did not open. listshows every session with its status, profile, and headed flag — check it before assuming.- Within one session, multiple tabs are still available (
tab-list,tab-new,tab-select,
tab-close); the fresh-refs rule applies after every tab switch.
- Signed-in state is shared through persistent profiles, not shared tabs — see
references/playwright-session-coordination.md.