Stabilize the Playwright E2E suite for a single pull request using evidence, not assumptions. Spans, traces, and the live app are the source of truth — not the CI dashboard. This skill never proposes a fix without a measurement to point at, and never commits a fix until three consecutive local runs prove it works.
This
SKILL.mdis a thin index. Detailed procedures live inrules/*.mdandtemplates/*.md. Each phase loads only what it needs.
What this skill combines
| Source | Role |
|---|---|
| Playwright Healer agent — external; one of the Playwright Test Agents on the Playwright MCP server | Test-debugging methodology — how to fix a Playwright test correctly. |
/playwright-trace-analyzer |
Per-run trace.zip extraction, hotspot ranking. |
/ci-auto-fix |
Reused only for Phase 7's single push + watch — the iteration loop no longer lives here. |
Dash0 MCP server (dash0-dev or dash0-prod) |
Historical evidence — failure recurrence, retry counts, span-level evidence across CI runs. |
| Local Playwright runner | Primary evidence source — trace.zip per run, OTel spans to Dash0 (ci.is_ci=false), and the live app for selector verification. |
| GitHub Actions (one call) | Final CI ratification at Phase 7. |
This skill is the orchestrator over those.
External dependency — the Playwright Healer agent. The healer methodology this skill drives fixes through is not an agent in this repo; it is Playwright's own healer (Playwright Test Agents: planner / generator / healer) running on the Playwright MCP server. Set it up with
npx playwright init-agents --loop=claude(Playwright ≥ 1.56). At runtime, Phase 5 uses the healer when the Playwright MCP is connected (mcp__playwright__*tools present) and falls back to the inline root-cause methodology when it isn't — so the skill works with or without it, but is strongest with it. It does not duplicate their content — each phase delegates.
Modes
| Mode | Default | Entry rule (what enters the fix queue) | Phase 5 (edits) | Phase 6 (local 3-pass gate) | Phase 7 (CI ratification) | Phase 8 output |
|---|---|---|---|---|---|---|
stabilize |
yes | failure_rate ≥ 0.10 over ≥ 5 attempts, or flake_count ≥ 2. |
Drafted, then double-gated before commit. | Required — 3 consecutive local passes per fixed test. | One push, one CI watch. | Stabilization report with before / after numbers, local-pass log, CI verdict. |
optimize |
Top-N slowest tests by total time, or actions with dur > 5×median. |
Skipped. | Skipped. | Skipped. | Recommendations-only report — humans apply the wins. |
stabilize is the default because optimization edits (tightening timeouts, removing waits) carry flake risk that warrants human judgment.
optimize runs Phases 1–4 only and emits a ranked recommendations report.
Input
$ARGUMENTS is parsed as [mode] [pr-ref] in any order:
optimize(literal token) selects optimize mode; anything else is treated aspr-ref.pr-refis a PR URL (https://github.com/<org>/<repo>/pull/13319) or PR number (13319).- If
pr-refis missing, auto-detect the open PR for the current branch (same path as/ci-auto-fixStep 0). - If
modeis missing, default tostabilize.
Resolve mode + PR before doing anything else.
See rules/input-resolution.md.
Workflow
Eight phases.
Do not skip a gate.
Phases 5, 6, and 7 are skipped in optimize mode (the Modes column says so explicitly).
| Phase | Name | Modes | Rule file | Gate |
|---|---|---|---|---|
| 0 | Resolve target | both | rules/input-resolution.md |
Mode + PR URL + branch + head SHA + owner / repo printed. |
| 1 | Pull historical telemetry | both | rules/telemetry-driven-analysis.md |
Dash0 spans for this PR fetched and grouped by test name; failure recurrence + retry counts measured (stabilize) or action dur distribution measured (optimize). |
| 2 | Local reproduction + trace capture | both | rules/local-iteration.md |
Each queued test run locally with --trace=on; trace.zip + (where available) fresh Dash0 spans tagged ci.is_ci=false captured. |
| 3 | Correlate spans ↔ traces | both | rules/root-cause-and-fix.md |
Each queued test has a span-side signature and a trace-side hotspot. |
| 4 | Root-cause synthesis | both | rules/root-cause-and-fix.md |
A single, evidence-anchored hypothesis per candidate, citing the span signature and the trace hotspot. Speculative hypotheses become recommendation-only entries, not fixes. |
| 5 | Draft fix + selector-existence check | stabilize only | rules/root-cause-and-fix.md, rules/fix-validation.md, rules/guard-rails.md |
Diff drafted; every new locator proven to resolve against source (static grep) or the live app (locator.count() ≥ 1). A locator that fails both checks is hallucinated — discard the diff and re-enter Phase 4 with that evidence. |
| 6 | Local verification — 3 consecutive passes | stabilize only | rules/local-iteration.md |
Fixed test runs locally ≥ 3 times with --trace=on and passes 3 times in a row. A single failure or flake within the streak resets the counter. Maximum 10 attempts per test before escalating. |
| 7 | CI ratification — one push, one watch | stabilize only | rules/verification-loop.md |
All passing fixes committed and pushed in a single push event; the resulting CI run is watched to conclusion and its telemetry compared against the Phase 1 baseline. |
| 8 | Report | both | templates/stabilization-report.md |
Stabilize: report with before / after numbers + local-pass log + CI verdict + residual risk. Optimize: recommendations-only report ranked by measured wall-clock impact. |
Inner iteration in stabilize mode is local and bounded — see rules/local-iteration.md.
The CI step in Phase 7 runs once.
If CI disagrees with the local result, that is a signal to escalate, not to re-enter the loop blindly.
Required reading by phase
Load on demand. Do not preload.
| Phase | Files |
|---|---|
| 0 | rules/input-resolution.md |
| 1 | rules/telemetry-driven-analysis.md, references/dash0-mcp-filters.md |
| 2 | rules/local-iteration.md |
| 3–4 | rules/root-cause-and-fix.md, rules/self-improvement-loop.md (read lessons — stabilize only) |
| 5 | rules/root-cause-and-fix.md, rules/fix-validation.md, rules/guard-rails.md |
| 6 | rules/local-iteration.md |
| 7 | rules/verification-loop.md, rules/self-improvement-loop.md (write lessons on ratification — stabilize only) |
| 8 | templates/stabilization-report.md |
For trace mechanics (zip → JSONL → action timeline), defer to /playwright-trace-analyzer.
Do not re-implement.
Core principles
- Iterate locally, ratify on CI. The inner loop is local because it is seconds-per-run, has the same trace artifacts, and emits the same OTel spans. CI runs once at the end as ground truth.
- Data first, hypothesis second.
Every fix is anchored to (a) a span with a measured failure rate, or (b) a trace action with a measured
dur. "I think this is flaky" is not a finding. - Two evidence layers, not one. Spans tell you which tests fail and how often across runs (historical and local). Traces tell you why one specific run failed. A fix is only credible when both layers agree.
- Validate empirically, not predictively. Confidence in a fix comes from running it, not from scoring it on paper. The two gates the skill enforces are both deterministic: selectors must exist (verifiable against source or live app), and the fixed test must pass three consecutive local runs (verifiable from exit codes).
- Selectors must exist before they are used. Every new locator in a draft fix is verified against the component source (static) and, when ambiguous, the running app (live). The skill never commits a fix that names an element that does not exist.
- Three consecutive local passes or no commit.
A single passing run can be a coin flip on a flake.
Three in a row — hardened with
--repeat-each=3when the measured flake rate is low (seerules/local-iteration.md) — filters out most unfixed flakes before we spend a CI cycle. The streak is necessary but not sufficient evidence; Phase 7's CI ratification plus the telemetry comparison is the real confirmation. - Never weaken the suite.
No
.skip,.fixme,waitForTimeout,continue-on-error,--no-verify, or removed assertions. The full list lives inrules/guard-rails.md. - Test-side fix unless the trace proves otherwise. Most flakes are selector, timing, or state-management bugs in tests. If the trace evidence points to product code, surface it to the user as a separate recommendation — do not silently mutate app code.
- One PR at a time. Cross-PR refactors belong in a different skill.
Self-Improvement
/e2e-pr-stabilizer gets better across runs through a two-tier lessons loop
(fast episodic tier + gated promotion), like autonomous-workflow and
fix-bug. In stabilize mode it reads e2e-pr-stabilizer-lessons at
Phase 4 (biasing the P1–P6 pattern classification and the Phase 5 locator
strategy) and writes at Phase 7 — gated on the telemetry ratification
signal, not the local 3-pass streak, so only fixes that CI actually confirmed
fixed accrue a working lesson. The two tiers split naturally: the global
scope holds universal race-shape → fix-shape mappings, the repo::{owner}/{repo}
scope holds app-specific locator robustness. Lessons are advisory — they
never relax a guard-rail, an empirical gate, or the 3-consecutive-pass
requirement. A recurring lesson (seen_count >= 3) is promotion-eligible via
/create-skill diagnose e2e-pr-stabilizer.
On the Phase 7 ratified verdict it also contributes a flaky hotspot to the
shared codebase-knowledge bucket (hotspot::<test.file> counter) — a fact no
other host produces, so every later code-changer plans around a known-unstable file.
Write only, merge-never-clobber, on the ratified SHA (contract:
../../../agents/shared/rules/codebase-knowledge.md; mechanics in the loop file's cross-bucket write section).
optimize mode skips the loop (no fix, no ratification signal). Lessons run
through LoreKit's memory.* tools (the lorekit-memory skill); if LoreKit is
not connected the loop is a silent no-op. Full contract:
rules/self-improvement-loop.md.
Anti-patterns
One-liners; the full list lives in rules/guard-rails.md.
- Pushing a fix because "the diff looks right" without three consecutive local passes.
- Trusting a single local pass as proof — flakes pass once routinely.
- Drafting a fix that uses
getByTestId('foo')when nothing in the component source emitsdata-testid="foo". - Patching
waitForTimeout(1500)to mask a race instead of fixing the wait condition. - Marking a test
.fixme()because it is "flaky" without a measured cause. - Treating a single failed CI run as evidence — flakes are statistical, so fetch the span history.
- Re-running CI hoping for a green without applying a code change.
- Editing product code based on speculation when the trace points at a selector or test-state issue.
Quickstart
/e2e-pr-stabilizer # stabilize, auto-detect PR
/e2e-pr-stabilizer 13319 # stabilize PR 13319
/e2e-pr-stabilizer https://github.com/<org>/<repo>/pull/13319 # stabilize via URL
/e2e-pr-stabilizer optimize # optimize, auto-detect PR
/e2e-pr-stabilizer optimize 13319 # optimize PR 13319
Once invoked, the skill drives end-to-end:
- Resolves the mode and the PR.
- Queries the Dash0 MCP for E2E spans filtered to this PR (
git.pull_request_link) — historical baseline. - Reproduces locally with
--trace=on, capturing trace.zip and (where the local OTel reporter is wired) fresh spans. - Correlates and produces an evidence-anchored finding set.
- stabilize: drafts each fix, verifies every new locator resolves against source or the live app, then commits locally.
- stabilize: runs the fixed test locally until it passes 3 times in a row (per fix).
- stabilize: pushes once; watches the CI run; compares fresh telemetry to baseline.
- Emits the report — stabilization (before / after + local-pass log + CI verdict) or optimization (recommendations).
Definition of Done
Both modes
- Mode (
stabilize|optimize) and PR target resolved and printed. - Historical telemetry pulled from the Dash0 MCP using the documented filter set, grouped by test name.
- Each queued test reproduced locally with
--trace=on; trace artifacts captured per run. - Each candidate has a span-side signature and a trace-side hotspot.
- Report written using the template, with the mode stated and findings ranked by measured impact.
stabilize only
- Every new locator in every applied fix was verified against the component source or via a live
locator.count() ≥ 1probe. - Every applied fix passed 3 consecutive local runs with
--trace=onand no failures or flakes within the streak. - Fixes committed locally, pushed in one push, CI run watched to conclusion.
- Fresh telemetry pulled and compared to baseline — failures eliminated, retry counts reduced.
- No
.skip,.fixme,waitForTimeout, orcontinue-on-errorintroduced (guard-rails check passed).
optimize only
- No commits, no pushes, no edits to test files.
- Each recommendation cites an estimated wall-clock saving (ms) based on the trace evidence.