agentleFS
napnap11napnap11/claude-skills/qa-fix/SKILL.md

qa-fix

Skill1 starsChanged 5 months agoMITView on GitHub

Source-level browser QA. Drives a real browser through Playwright to exercise a web app the way a person would — loads pages, clicks controls, submits forms, collects console errors, grabs screenshots, and logs each defect with hard evidence. Then it repairs the bugs in source with one atomic commit apiece and re-checks them. Three depth tiers: Quick (critical/high), Standard (+medium), Exhaustive (+cosmetic). Emits health scores and before/after proof. Use when the user says "qa fix", "/qa-fix", "QA this branch", "test this in the browser", "verify my changes", "drive the browser and find bugs", "run QA and fix", or wants source-level QA that tests, fixes, and re-verifies.

  • Reads credentials

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

50 rules. About 0 are common, 38 are distinctive, and 12 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈38

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 12

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If output is non-empty, STOP and use AskUserQuestion:

    line 98
  2. Distinctive:

    If neither exists, install Playwright Chromium:

    line 128
  3. Very specific:

    If the project already lists Playwright as a dependency, use that.

    line 136
  4. Very specific:

    Write .qa-reports/browse.mjs with the helper API.

    line 140
  5. Distinctive:

    When the user hands you a localhost URL, sanity-check that it's actually serving before you run the helper:

    line 364
  6. Very specific:

    Run full mode, then load baseline.json from a prior run.

    line 407
  7. Very specific:

    Run the bootstrap (install Playwright + write browse.mjs if needed).

    line 462
  8. Distinctive:

    Create the output dirs:

    line 463
  9. Very specific:

    Start a timer (date +%s at the start.

    line 473
  10. Distinctive:

    If the user hands you cookies in a non-Playwright format, convert them.

    line 485
  11. Very specific:

    Write a flow JSON, then run it with QA_PERSIST_STORAGE=1:

    line 493
  12. Distinctive:

    2FA / OTP: Ask the user for the code, then feed it into a follow-up flow.

    line 515
  13. Very specific:

    Read the screenshot inline so the user sees it.

    line 526
  14. Distinctive:

    Use the link list to plan which pages to visit.

    line 532
  15. Distinctive:

    Read the screenshot inline.

    line 557
  16. Distinctive:

    Write up each issue the moment you find it — don't batch.

    line 577
  17. Distinctive:

    Read both screenshots inline so the user sees the broken behavior.

    line 591
  18. Distinctive:

    ONLY touch files directly tied to the issue.

    line 698
  19. Distinctive:

    Read the source, understand the context.

    line 701
  20. Distinctive:

    No refactoring, no "improvements," no scope creep.

    line 703
  21. Distinctive:

    Read the after-screenshot inline.

    line 717
  22. Distinctive:

    If the project already runs Vitest / Jest / Playwright tests:

    line 728
  23. Distinctive:

    Run only the new file: If it passes:

    line 742
  24. Distinctive:

    If it fails, fix the test once.

    line 746
  25. Distinctive:

    Every 5 fixes (or right after any revert), compute:

    line 753
  26. Distinctive:

    If WTF > 20%: STOP. Show the user what you've done so far.

    line 765
  27. Distinctive:

    Hard cap: 30 fixes: After 30, stop regardless of what's left.

    line 767
  28. Very specific:

    Write the report to .qa-reports/qa-report-{domain}-{YYYY-MM-DD}.md.

    line 783
  29. Distinctive:

    Commit SHA (if fixed)

    line 809
  30. Distinctive:

    Before/after screenshots (if fixed)

    line 811
  31. Very specific:

    Test client-side navigation: use the flow action click on a <Link>, not just goto — catches App Router/SPA routing bugs.

    line 838
  32. Very specific:

    After dispatching an action through the UI, verify the selectors return the expected shape (use $B eval to inspect …

    line 843
  33. Distinctive:

    Look for N+1 query warnings in the console (development mode).

    line 847
  34. Distinctive:

    Test Turbo / Stimulus transitions — page swaps shouldn't flash.

    line 849
  35. Very specific:

    How to flag it: If a modal closes mid-edit, or a form resets when nothing user-driven happened, check the memo signature.

    line 889
  36. Distinctive:

    Why it's broken: If the page does re-mount, the wipe effect runs again.

    line 893
  37. Distinctive:

    How to flag it: When testing redirects to the current page, console.log the mount effect once at boot.

    line 894
  38. Distinctive:

    How to flag it: After completing a happy-path flow, navigate home.

    line 904
  39. Distinctive:

    Drive a real browser: Never swap static analysis in for actual interaction.

    line 961
  40. Distinctive:

    Visit every affected page: When a feature touches multiple pages (mount effect on page A + global hook on the app shell + per-page hook on …

    line 964
  41. Distinctive:

    Run the UX-logic-pattern checklist: Before writing the report, walk the patterns in "Common UX-Logic Patterns to Flag" against the diff.

    line 965
  42. Distinctive:

    Repro is everything: Every issue gets at least one screenshot.

    line 966
  43. Distinctive:

    Show screenshots inline: Read every screenshot/snapshot output file immediately after capture, so the user sees it.

    line 968
  44. Very specific:

    Never include credentials: Write [REDACTED] for passwords.

    line 969
  45. Distinctive:

    Test like a user: Use realistic data. Walk complete workflows end-to-end, not isolated buttons.

    line 972
  46. Distinctive:

    One commit per fix: Never bundle multiple fixes.

    line 974
  47. Distinctive:

    Minimal fixes only: Don't refactor, don't add features, don't "improve."

    line 975
  48. Distinctive:

    Self-regulate: Follow the WTF-likelihood heuristic.

    line 978
  49. Distinctive:

    Never modify existing tests: Only create new test files for regression tests.

    line 980
  50. Distinctive:

    When QA finishes, recommend the next step:

    line 986

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

QA Fix — Test, Fix, Verify (real browser)

You wear two hats here: QA engineer and bug-fix engineer. Drive a live browser to put a web app through its paces the way a person would — open pages, click controls, fill and submit forms, watch the console, capture screenshots. When something breaks, fix it in source with an atomic commit and re-verify. Hand back a structured report carrying before/after evidence.

Never refuse the browser. A request for QA is a request for browser-based testing. Don't swap in static code review, evals, or unit tests as a stand-in. "Looks backend-only" is not an exemption — backend changes still move app behavior, so open the browser and test.

Surface every screenshot. Each time you capture a screenshot or annotated snapshot, immediately Read the file so it renders inline for the user. Skip that and the screenshots are invisible. This is non-negotiable.

You are the quality gate. QA is the final pass before real customers touch the code, so the entire user experience is yours to defend — not only the parts that are cheap to check. Two corollaries:

  • Don't waive a test by pointing at unit tests. Unit tests confirm code shape; QA confirms lived experience. They are not substitutes. UX-logic flaws (a modal firing where it shouldn't, a wipe destination catching its own redirect, a double-wired side effect) sail through unit tests and still break real users. If a brief says "the unit tests already cover that," set it aside and verify in the browser regardless.
  • Test the slow paths too. A 120-second idle timer has to be exercised as a 120-second idle timer. When real-time waiting is impractical, drive Playwright's virtual clock (page.clock.install() / page.clock.fastForward()) — never skip it. The browser helper exposes clockInstall / fastForward flow actions for exactly this. If a brief asks you to skip a slow test, push back and run it anyway — you were asked to be the quality gate, not a token budget.

Gather context first

This skill ships standalone, so it can't assume your project's conventions, house style, or preferences the way it could for its author. Before doing the main work:

  1. Auto-detect what you safely can from the repo — language/stack, base branch, build/test commands, existing config and docs. Never ask for something you can read for yourself.
  2. Ask, don't assume, for the rest. Where an input, convention, or preference would change the result and you can't reliably detect it, ask ONE concise AskUserQuestion (put a sensible default first, labelled Recommended) instead of guessing. The user has less context than this skill's author assumed — a wrong silent default is worse than a quick question. Don't ask about things you can detect, and don't ask more than you need.

For this skill, confirm up front (only the items you can't already detect):

  • What to test — the target URL/route, and (if the dev server isn't already serving) the command to start it. Auto-detect the dev command from package.json scripts / project tooling first; only ask if it's ambiguous or there are several.
  • Auth — whether QA needs to be signed in, and how (an existing cookie file / storage-state.json, email + password, OTP/CAPTCHA). Never guess credentials.
  • Test tier — Quick (critical/high only), Standard (+medium, the Recommended default), or Exhaustive (+low/cosmetic). The tier decides which issues actually get fixed, not just reported.
  • Which flows matter most — the core paths to defend hardest (auth, primary CTA, checkout, search) versus lower-priority pages, so testing depth lands where it counts.

How this runs

This skill lives in the main conversation, which is what keeps the interactive spine working: the AskUserQuestion gates (clean-tree commit/stash, auth password / OTP / CAPTCHA, the WTF-likelihood STOP-and-ask, continue-or-stop prompts), the browser driving (every browse.mjs call is a Bash invocation whose screenshots you Read inline so the user can see them), the per-issue write-ups, the git commits, and the report writes all need the user, the live filesystem, git state, or inline screenshot display.

Two parts of the job are independent, non-interactive analysis bursts that can be parallelized when a run is large:

PartWhat it doesWhy it could fan out
Diff → affected-page mapping (diff-aware mode)one pass per changed file (or file group) traces which pages/routes it affectsmany changed files; tracing importers/consumers per file is independent code analysis, so doing them sequentially is pure latency
UX-logic-pattern scanone pass per pattern checks the diff for that bug class7 independent analytical questions over the same diff, each a self-contained "does this pattern apply here?"

By default, walk both of these sequentially in the main loop — it costs no extra tokens and keeps everything in one context. Before spawning any subagent or Workflow to fan them out, stop and ask the user, for example:

This is large ([N changed files / big diff]). I can do the diff-mapping and UX-pattern analysis inline here (cheaper, slower) or fan out [M] parallel subagents (faster, more tokens). Which do you want?

Spawn subagents/workflows only after an explicit yes. If the user declines or doesn't answer, do the whole job inline.

If you do fan out: Workflow agent() calls run in the background and cannot call AskUserQuestion, drive the browser, Read screenshots inline, or write files. So every gate/approval/question, every browse.mjs invocation, every inline screenshot Read, every git commit, and every file write (report, baseline, repro JSON, test files) stays in the main loop. A workflow only does headless code analysis (map a diff, scan for a bug pattern) and returns structured results; you act on them — driving the browser to confirm, documenting, fixing, committing — back in the main loop. Set model: 'opus' on every agent() call so no lightweight tier leaks through, and pass data in via args (Workflow scripts have no Date.now/Math.random/filesystem access — timestamps, screenshots, and writes belong in the main loop).

Arguments

Inspect $ARGUMENTS:

  • A target URL (e.g. http://localhost:3000) → the QA target; selects full mode.
  • A tier flag — --quick, --exhaustive → sets the tier (default Standard).
  • A mode flag — --regression <baseline.json> → regression mode against the named baseline.
  • A scope hint — "Focus on the billing page" → diff/scope narrowing.
  • An auth hint — "Sign in to user@example.com", "Import cookies from cookies.json" → auth setup.
  • Empty, on a feature branch → diff-aware mode against the detected base branch.

Setup

Parse the user's request

ParameterDefaultOverride example
Target URL(auto-detect or ask)http://localhost:3000
TierStandard--quick, --exhaustive
Modefull (URL given) or diff-aware (on a feature branch with no URL)--regression .qa-reports/baseline.json
Output dir.qa-reports/Output to /tmp/qa
ScopeFull app, or diff-scoped on a feature branchFocus on the billing page
AuthNoneSign in to user@example.com, Import cookies from cookies.json

Tier decides which issues get fixed:

  • Quick: Fix critical + high severity only
  • Standard: + medium severity (default)
  • Exhaustive: + low/cosmetic severity

Clean working tree required

git status --porcelain

If output is non-empty, STOP and use AskUserQuestion:

"Your working tree has uncommitted changes. QA needs a clean tree so each bug fix lands as its own atomic commit."

  • A) Commit my changes — make a single descriptive commit, then start QA (recommended)
  • B) Stash my changes — stash, run QA, pop the stash after
  • C) Abort — I'll clean up manually

Once the user picks, carry it out, then continue.

Detect base branch

git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'

If that fails, fall back to main, then master. If both exist (or neither is an obvious base) and the diff scope would change depending on which you pick, confirm the base branch with the user before diffing rather than guessing. Use the resolved name wherever later git diff/git log commands say "the base branch".


Browser automation bootstrap

This skill drives a real browser through Playwright. On the first run in a project, install it and lay down a reusable browser helper.

First-run install check

test -d node_modules/playwright || test -f .qa-reports/.playwright-installed

If neither exists, install Playwright Chromium:

mkdir -p .qa-reports
npx --yes playwright@latest install chromium 2>&1 | tail -5
touch .qa-reports/.playwright-installed

If the project already lists Playwright as a dependency, use that. Otherwise the install above lands the browser in ~/Library/Caches/ms-playwright/ and pulls the package on demand through npx.

Write the browser helper (one-time per project)

Write .qa-reports/browse.mjs with the helper API. Re-write only when it's missing.

test -f .qa-reports/browse.mjs || cat > .qa-reports/browse.mjs <<'BROWSE_EOF'
#!/usr/bin/env node
// Browser helper for qa-fix — wraps Playwright as a one-shot CLI.
// Usage:
//   node .qa-reports/browse.mjs goto <url> [--storage <file>]
//   node .qa-reports/browse.mjs screenshot <url> <out.png> [--full]
//   node .qa-reports/browse.mjs console <url>
//   node .qa-reports/browse.mjs links <url>
//   node .qa-reports/browse.mjs snapshot <url> <out.png>          # annotated
//   node .qa-reports/browse.mjs eval <url> '<expr-as-string>'
//   node .qa-reports/browse.mjs flow <flow.json> <out-dir>        # multi-step
//   node .qa-reports/browse.mjs viewport <url> <WIDTHxHEIGHT> <out.png>
import { chromium } from "playwright";
import fs from "node:fs/promises";

const [cmd, ...args] = process.argv.slice(2);
const STORAGE = process.env.QA_STORAGE_STATE || ".qa-reports/storage-state.json";

const launch = async () => {
  const browser = await chromium.launch({ headless: true });
  const ctxOpts = {};
  try {
    await fs.access(STORAGE);
    ctxOpts.storageState = STORAGE;
  } catch {}
  const context = await browser.newContext(ctxOpts);
  const page = await context.newPage();
  const errors = [];
  page.on("pageerror", (e) => errors.push({ type: "pageerror", message: e.message }));
  page.on("console", (msg) => {
    if (msg.type() === "error" || msg.type() === "warning") {
      errors.push({ type: msg.type(), text: msg.text() });
    }
  });
  page.on("requestfailed", (req) => errors.push({
    type: "requestfailed",
    url: req.url(),
    failure: req.failure()?.errorText,
  }));
  return { browser, context, page, errors };
};

const safeGoto = async (page, url) => {
  try {
    await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
    await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {});
  } catch (e) {
    return { ok: false, error: e.message };
  }
  return { ok: true };
};

const annotate = async (page) => {
  return page.evaluate(() => {
    const targets = Array.from(document.querySelectorAll(
      "a, button, input, select, textarea, [role=button], [role=link], [onclick]"
    ));
    return targets.slice(0, 60).map((el, i) => {
      const r = el.getBoundingClientRect();
      const tag = el.tagName.toLowerCase();
      const text = (el.innerText || el.value || el.getAttribute("aria-label") || "").trim().slice(0, 40);
      return { i, tag, text, x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
    });
  });
};

(async () => {
  if (cmd === "goto") {
    const url = args[0];
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    const title = await page.title().catch(() => "");
    const status = nav.ok ? 200 : 0;
    console.log(JSON.stringify({ url, ok: nav.ok, title, status, errors }, null, 2));
    if (process.env.QA_PERSIST_STORAGE === "1") {
      await page.context().storageState({ path: STORAGE });
    }
    await browser.close();
  } else if (cmd === "screenshot") {
    const [url, out, ...rest] = args;
    const full = rest.includes("--full");
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    await page.screenshot({ path: out, fullPage: full });
    console.log(JSON.stringify({ url, ok: nav.ok, out, errors }, null, 2));
    await browser.close();
  } else if (cmd === "snapshot") {
    const [url, out] = args;
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    const elements = await annotate(page);
    await page.screenshot({ path: out, fullPage: false });
    console.log(JSON.stringify({ url, ok: nav.ok, out, errors, elements }, null, 2));
    await browser.close();
  } else if (cmd === "console") {
    const [url] = args;
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    console.log(JSON.stringify({ url, ok: nav.ok, errors }, null, 2));
    await browser.close();
  } else if (cmd === "links") {
    const [url] = args;
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    const links = await page.$$eval("a[href]", (as) =>
      as.map((a) => ({ href: a.href, text: a.innerText.trim().slice(0, 80) }))
        .filter((l) => l.href && !l.href.startsWith("javascript:")),
    );
    const unique = Array.from(new Map(links.map((l) => [l.href, l])).values());
    console.log(JSON.stringify({ url, ok: nav.ok, errors, links: unique }, null, 2));
    await browser.close();
  } else if (cmd === "eval") {
    const [url, expr] = args;
    const { browser, page, errors } = await launch();
    const nav = await safeGoto(page, url);
    const result = await page.evaluate(expr).catch((e) => ({ __evalError: e.message }));
    console.log(JSON.stringify({ url, ok: nav.ok, errors, result }, null, 2));
    await browser.close();
  } else if (cmd === "viewport") {
    const [url, size, out] = args;
    const [w, h] = size.split("x").map(Number);
    const { browser, page, errors } = await launch();
    await page.setViewportSize({ width: w, height: h });
    const nav = await safeGoto(page, url);
    await page.screenshot({ path: out, fullPage: false });
    console.log(JSON.stringify({ url, ok: nav.ok, viewport: { w, h }, out, errors }, null, 2));
    await browser.close();
  } else if (cmd === "flow") {
    // Multi-step flow. Actions:
    //   goto, click, fill, press, wait (real time), waitFor (selector)
    //   clockInstall (virtual clock — must run BEFORE goto)
    //   fastForward <ms> (advance virtual time, fires queued timers)
    //   runFor <ms> (advance virtual time + drive Date)
    //   evalAssert <expr> (eval a JS expression, fail if falsy)
    //   screenshot, full (apply to any step)
    const [flowFile, outDir] = args;
    const flow = JSON.parse(await fs.readFile(flowFile, "utf8"));
    const { browser, page, errors } = await launch();
    const log = [];
    for (const step of flow) {
      try {
        if (step.action === "goto") await safeGoto(page, step.url);
        else if (step.action === "click") await page.click(step.selector, { timeout: 10000 });
        else if (step.action === "fill") await page.fill(step.selector, step.value, { timeout: 10000 });
        else if (step.action === "press") await page.press(step.selector || "body", step.value, { timeout: 10000 });
        else if (step.action === "wait") await page.waitForTimeout(step.value || 500);
        else if (step.action === "waitFor") await page.waitForSelector(step.selector, { timeout: 10000 });
        else if (step.action === "clockInstall") await page.clock.install({ time: step.value ? new Date(step.value) : undefined });
        else if (step.action === "fastForward") await page.clock.fastForward(step.value || 1000);
        else if (step.action === "runFor") await page.clock.runFor(step.value || 1000);
        else if (step.action === "evalAssert") {
          const r = await page.evaluate(step.value);
          if (!r) throw new Error(`evalAssert failed: ${step.value}`);
        }
        if (step.screenshot) {
          const out = `${outDir}/${step.screenshot}`;
          await page.screenshot({ path: out, fullPage: !!step.full });
          log.push({ ...step, ok: true, screenshot: out });
        } else {
          log.push({ ...step, ok: true });
        }
      } catch (e) {
        log.push({ ...step, ok: false, error: e.message });
        break;
      }
    }
    if (process.env.QA_PERSIST_STORAGE === "1") {
      await page.context().storageState({ path: STORAGE });
    }
    console.log(JSON.stringify({ flow: log, errors }, null, 2));
    await browser.close();
  } else {
    console.error("Unknown command:", cmd);
    process.exit(1);
  }
})().catch((e) => {
  console.error(JSON.stringify({ fatal: e.message }));
  process.exit(2);
});
BROWSE_EOF

Browser helper API summary

Set B='node .qa-reports/browse.mjs' for the session.

CommandPurpose
$B goto <url>Navigate; report title, status, console errors, page errors, request failures
$B screenshot <url> <out.png> [--full]Save a viewport (or full-page) screenshot
$B snapshot <url> <out.png>Screenshot + indexed list of clickable elements (text, position, size)
$B console <url>Just navigate and report console errors / pageerrors / failed requests
$B links <url>Extract all <a href> links on the page
$B eval <url> '<expr>'Evaluate a JS expression in page context; return result
$B viewport <url> <WxH> <out.png>Screenshot at a specific viewport (responsive checks)
$B flow <flow.json> <out-dir>Multi-step. Actions: goto, click, fill, press, wait (real ms), waitFor (selector), clockInstall, fastForward (virtual ms), runFor (virtual ms + Date), evalAssert (JS expr). Each step may also set screenshot and full.

Virtual time — for testing slow features

Features whose timers run in seconds-to-minutes (idle timeouts, debouncers, polling, animations) must NOT be skipped. Reach for the virtual clock:

[
  {"action": "clockInstall"},
  {"action": "goto", "url": "http://localhost:3000/kiosk/scan-id-card", "screenshot": "before.png"},
  {"action": "fastForward", "value": 105000},
  {"action": "screenshot", "screenshot": "warning-shown.png"},
  {"action": "evalAssert", "value": "document.body.innerText.includes('session ending')"},
  {"action": "fastForward", "value": 15000},
  {"action": "screenshot", "screenshot": "after-wipe.png"}
]

clockInstall has to run BEFORE goto. fastForward advances the virtual clock and fires every queued setTimeout/setInterval; runFor does the same and also drives Date.now(). The page never actually sleeps — a full 120s flow completes in seconds. This is mandatory for time-based features. "Skip — covered by unit tests" is not an acceptable answer.

The helper writes storage-state.json (cookies + localStorage) whenever QA_PERSIST_STORAGE=1 is set on a goto or flow call. Use it to carry auth across calls within the session.

Read every screenshot/snapshot output file with the Read tool the moment the command finishes, so the user sees it inline. Snapshots return JSON with an elements array — use those indices when you reference clickable targets in the test plan.

CDP / dev-server smoke

When the user hands you a localhost URL, sanity-check that it's actually serving before you run the helper:

curl -s -o /dev/null -w "%{http_code}\n" "$URL" || true

If it returns 0 / non-2xx, the dev server probably isn't running. Detect the likely start command first — read package.json scripts (e.g. dev, dev:local, start) or the project's tooling — then ask the user whether to start it with that command. Don't hardcode npm run dev:local, and don't start the server yourself unless explicitly asked.


Modes

Diff-aware (automatic on a feature branch with no URL)

The primary mode for a developer verifying their own work. Map the diff to the pages it touches, then test those pages.

  1. Pull the diff:
    git diff <base>...HEAD --name-only
    git log <base>..HEAD --oneline
  2. Work out which pages/routes the changed files affect:
    • Page/route files → the URL paths they serve
    • Component files → the pages that render them (grep importers)
    • API/service files → the pages that consume them (check consumers)
    • Style files → the pages that include them
    • Hook/utility files → trace through to the consuming components
  3. Build the test plan: for each affected page/route, list what to verify. Read the commit messages to understand intent.
  4. Test the affected pages in the browser (Phases 3-5).
  5. Check for regressions in adjacent code: read the importers of changed files, confirm the interfaces still line up.

When the diff is large, step 2 is the part that can fan out — but by default do it sequentially in this context. Only parallelize it as a Workflow after the user opts in; see Optional fan-out: diff → affected-page mapping below. Either way, the browser testing it feeds (steps 3-5) stays in the main loop.

Full (default when URL is provided)

Systematic exploration. Visit every reachable page off the navigation map. Document 5-10 well-evidenced issues. Produce a health score. Runs 5-15 minutes depending on app size.

Quick (--quick)

A 30-second smoke test. Hit the homepage plus the top 5 navigation targets. Check: does the page load? Console errors? Broken links? Produce a health score. No detailed issue write-ups.

Regression (--regression <baseline.json>)

Run full mode, then load baseline.json from a prior run. Diff them: which issues are fixed, which are new, what's the score delta? Append a regression section to the report.


Optional fan-out: diff → affected-page mapping (diff-aware mode)

When the diff touches a lot of files, mapping each changed file to the pages/routes it affects is independent, non-interactive code analysis — the kind of work that parallelizes cleanly. Default: do this inline, one file at a time, in the main loop. If the user opted into subagents (see How this runs), fan it out instead: one agent per changed file (or coherent file group), each reading the file, grepping importers, and tracing consumers, then returning the union of affected routes to the main loop for browser testing.

export const meta = {
  name: 'qa-fix-diff-impact',
  description: 'Parallel diff-to-affected-page mapping — one agent per changed file',
  phases: [{ title: 'Map' }],
}
const IMPACT_SCHEMA = {
  type: 'object',
  properties: {
    file: { type: 'string' },
    kind: { type: 'string' },                                     // page | component | api | style | hook | util | other
    routes: { type: 'array', items: { type: 'string' } },         // URL paths a user can reach this from
    importers: { type: 'array', items: { type: 'string' } },      // files that import/consume this
    intent: { type: 'string' },                                   // what the change appears to do
    regressionRisk: { type: 'string' },                           // adjacent interfaces to re-check
  },
  required: ['file', 'kind', 'routes'],
}
const impacts = await parallel(args.files.map(f => () =>
  agent(
    `You are mapping ONE changed file to the user-facing pages/routes it affects. Do NOT ask the ` +
    `user anything and do NOT drive a browser — return analysis only; the main loop tests in the browser.\n\n` +
    `CHANGED FILE: ${f.path}\n` +
    `DIFF FOR THIS FILE:\n${f.diff}\n\n` +
    `COMMIT MESSAGES (for intent):\n${args.commits}\n\n` +
    `Classify the file (page/route, component, api/service, style, hook, util, other). Then:\n` +
    `- page/route → which URL path(s) it serves\n` +
    `- component → which pages render it (grep importers)\n` +
    `- api/service → which pages consume it\n` +
    `- style → which pages include it\n` +
    `- hook/util → trace through to consuming components and their routes\n` +
    `Return the affected routes, the importer/consumer files, the apparent intent of the change, ` +
    `and any adjacent interface a regression check should re-verify.`,
    { label: `map:${f.key}`, phase: 'Map', model: 'opus', schema: IMPACT_SCHEMA }
  )
))
return impacts.filter(Boolean)

Pass args: { files: [{ key, path, diff }, ...], commits }. When the workflow returns, take the union of routes as the page test plan and the union of importers + regressionRisk as the regression-check set, then move into browser testing (Phases 3-5) and the regression check in the main loop. If the workflow fails or times out, fall back to mapping the diff sequentially in the main loop — which is also the default when the user hasn't opted into fan-out.


Workflow

Phase 1: Initialize

  1. Run the bootstrap (install Playwright + write browse.mjs if needed).
  2. Create the output dirs:
    mkdir -p .qa-reports/screenshots
  3. Set B='node .qa-reports/browse.mjs' for the session.
  4. Compute the report path:
    DOMAIN=$(echo "$URL" | sed -E 's|https?://||; s|[:/].*||')
    REPORT=".qa-reports/qa-report-${DOMAIN}-$(date +%Y-%m-%d).md"
  5. Start a timer (date +%s at the start; subtract at the end).

Phase 2: Authenticate (if needed)

Cookie file given:

# Convert browser-exported cookies into Playwright storage-state shape if needed
# (Many extensions export the Playwright format directly.)
QA_PERSIST_STORAGE=1 $B goto "$URL"

If the user hands you cookies in a non-Playwright format, convert them. Playwright expects:

{ "cookies": [{ "name": "...", "value": "...", "domain": "...", "path": "/", "expires": -1, "httpOnly": false, "secure": true, "sameSite": "Lax" }], "origins": [] }

Save it to .qa-reports/storage-state.json.

Login flow with email + password:

Write a flow JSON, then run it with QA_PERSIST_STORAGE=1:

cat > .qa-reports/login.json <<'EOF'
[
  {"action":"goto","url":"https://app.example.com/login","screenshot":"login-1.png"},
  {"action":"fill","selector":"input[type=email]","value":"user@example.com"},
  {"action":"fill","selector":"input[type=password]","value":"REDACTED_PLACEHOLDER"},
  {"action":"click","selector":"button[type=submit]","screenshot":"login-2.png"},
  {"action":"wait","value":2000}
]
EOF
QA_PERSIST_STORAGE=1 $B flow .qa-reports/login.json .qa-reports/screenshots

Swap REDACTED_PLACEHOLDER for the real password the user supplies via AskUserQuestion. Never commit the flow JSON with the real password. Delete it after running:

rm .qa-reports/login.json

In the report, write [REDACTED] for the password — never the real value.

2FA / OTP: Ask the user for the code, then feed it into a follow-up flow. CAPTCHA: Tell the user "Please complete the CAPTCHA in your browser. When you're done, hand me the new cookie file or storage-state.json and I'll continue."

Phase 3: Orient

Build a map of the app:

$B snapshot "$URL" .qa-reports/screenshots/initial.png

Read the screenshot inline so the user sees it. The JSON output gives you the indexed clickable elements (the elements array).

$B links "$URL"

Use the link list to plan which pages to visit. Keep same-origin links, dedupe.

$B console "$URL"

Note any errors on the landing page — they probably hit every page.

Detect the framework (record it in report metadata):

  • Next.js: __next in HTML, _next/data in failed requests
  • Rails: csrf-token meta tag
  • WordPress: wp-content paths
  • SPA: client-side routing, no full page reload between routes

For SPAs, links misses client-side routes — use snapshot and click into the nav menus instead.

Phase 4: Explore

Visit pages methodically. At each page:

$B snapshot "$URL/some-page" ".qa-reports/screenshots/page-name.png"
$B console "$URL/some-page"

Read the screenshot inline. Then run the per-page checklist:

  1. Visual scan — layout broken? elements overlapping? text cut off? images missing?
  2. Interactive elements — drive each one with a flow script. Click buttons, links, controls. Do they land where they should? Capture before/after screenshots.
  3. Forms — fill and submit (use a flow JSON). Try empty submit, invalid input, edge cases. Capture the validation messages.
  4. Navigation — every nav link, breadcrumb, and back button.
  5. States — empty, loading, error, overflow. Where realistic, force them via URL params or by mutating state with $B eval.
  6. Console — re-check after every interaction.
  7. Responsive — pick one mobile size and one tablet/desktop size:
    $B viewport "$URL/page" 375x812 .qa-reports/screenshots/page-mobile.png
    $B viewport "$URL/page" 1280x720 .qa-reports/screenshots/page-desktop.png

Depth judgment. Spend the time on core flows (auth, primary CTA, checkout, search). Spend less on terms/privacy/marketing pages.

Quick mode. Homepage + top 5 nav targets only. Skip the per-page checklist — just check: loads? console errors? broken links?

Phase 5: Document

Write up each issue the moment you find it — don't batch.

Interactive bugs (broken flows, dead buttons, form failures): use a flow with before/after screenshots.

cat > .qa-reports/repro-001.json <<'EOF'
[
  {"action":"goto","url":"https://app.example.com/cart","screenshot":"issue-001-before.png"},
  {"action":"click","selector":"button:has-text('Checkout')","screenshot":"issue-001-after.png"}
]
EOF
$B flow .qa-reports/repro-001.json .qa-reports/screenshots

Read both screenshots inline so the user sees the broken behavior.

Static bugs (typos, layout, missing images): one annotated snapshot is enough.

$B snapshot "$URL/page" .qa-reports/screenshots/issue-002.png

Issue format — append to the report file as you find each one:

### ISSUE-NNN: [Title]

**Severity:** Critical / High / Medium / Low
**Category:** Functional / UX / Visual / Console / Content / Performance / Accessibility
**Page:** `/some/route`
**Repro steps:**
1. Navigate to /some/route
2. Click the "Checkout" button
3. Observe: page navigates to /404

**Expected:** /checkout
**Actual:** /404
**Evidence:** ![before](screenshots/issue-001-before.png) ![after](screenshots/issue-001-after.png)
**Console:** (any errors that appeared during the repro)

Phase 6: Wrap Up

  1. Compute the health score (rubric below).
  2. Write the "Top 3 Things to Fix" — the 3 highest-severity issues, one line each.
  3. Aggregate console health — total errors seen across pages, by category.
  4. Severity counts in the summary table.
  5. Metadata — date, duration, pages visited, screenshot count, framework.
  6. Save a baseline:
    cat > .qa-reports/baseline.json <<EOF
    { "date": "$(date +%F)", "url": "$URL", "healthScore": $SCORE,
      "issues": [...], "categoryScores": {...} }
    EOF

Regression mode: load the previous baseline and compare:

  • Health score delta
  • Issues fixed (in the baseline, gone in the current run)
  • New issues (in the current run, absent from the baseline)

Append the regression section.


Health Score Rubric

Score each category 0-100. Take the weighted average.

Console (weight: 15%)

  • 0 errors → 100
  • 1-3 → 70
  • 4-10 → 40
  • 10+ → 10

Links (weight: 10%)

  • 0 broken → 100
  • Each broken link → -15 (min 0)

Per-category (Visual / Functional / UX / Content / Performance / Accessibility)

Each starts at 100. Deduct per finding:

  • Critical → -25
  • High → -15
  • Medium → -8
  • Low → -3 Min 0 per category.

Weights

CategoryWeight
Console15%
Links10%
Visual10%
Functional20%
UX15%
Performance10%
Content5%
Accessibility15%

final_score = Σ (category_score × weight)


Phase 7: Triage

Sort issues by severity. Decide what to fix by tier:

  • Quick: Critical + High only
  • Standard: + Medium
  • Exhaustive: + Low / cosmetic

Mark anything that can't be fixed from source (third-party widget bugs, infrastructure) as deferred regardless of tier.


Phase 8: Fix Loop

For each fixable issue, in severity order:

8a. Locate source

  • Grep error messages, component names, route paths.
  • Glob for files matching the affected page.
  • ONLY touch files directly tied to the issue.

8b. Fix

  • Read the source, understand the context.
  • Minimal fix. The smallest change that resolves the issue.
  • No refactoring, no "improvements," no scope creep.

8c. Commit

git add <only-changed-files>
git commit -m "fix(qa): ISSUE-NNN — short description"

One commit per fix. Never bundle. The fix(qa): prefix is a Conventional Commits example — match the repo's actual commit style instead (skim recent git log to detect it; if the history is mixed or there's no clear convention, keep this default).

8d. Re-test (browser)

$B screenshot "$URL/affected-route" .qa-reports/screenshots/issue-NNN-after.png
$B console "$URL/affected-route"

Read the after-screenshot inline. If the issue had a multi-step repro, re-run the same flow with the post-fix screenshot path.

8e. Classify

  • verified — re-test confirms the fix, no new errors.
  • best-effort — fix applied, but full verification needs auth state / an external service / a manual check.
  • reverted — regression detected → git revert HEAD → mark the issue deferred.

8e.5. Regression test (when a test framework exists)

Skip when: the classification isn't "verified", OR the fix is purely visual/CSS with no JS behavior, OR no test framework was detected and the user declined to bootstrap one.

If the project already runs Vitest / Jest / Playwright tests:

  1. Read 2-3 nearby tests to pick up naming, imports, assertion style.
  2. Trace the bug's codepath: what input triggered it? Which branches did it take? What's the precondition?
  3. Write the regression test. It MUST:
    • Set up the exact precondition that broke.
    • Perform the action that exposed the bug.
    • Assert the correct behavior (not just "doesn't throw").
    • Cover the adjacent edge cases you spotted during the trace.
    • Include a comment:
      // Regression: ISSUE-NNN — {what broke}
      // Found by qa-fix on {YYYY-MM-DD}
  4. Run only the new file. If it passes:
    git add <test-file> && git commit -m "test(qa): regression test for ISSUE-NNN — {desc}"
    If it fails, fix the test once. Still failing → delete it and defer. Taking >2 min to write → skip and defer.
  5. Test commits don't count toward the WTF heuristic below.

8f. Self-regulation (STOP AND EVALUATE)

Every 5 fixes (or right after any revert), compute:

WTF-LIKELIHOOD:
  Start at 0%
  Each revert:                +15%
  Each fix touching >3 files: +5%
  After fix 15:               +1% per additional fix
  All remaining are Low:      +10%
  Touching unrelated files:   +20%

If WTF > 20%: STOP. Show the user what you've done so far. Ask whether to continue.

Hard cap: 30 fixes. After 30, stop regardless of what's left.


Phase 9: Final QA

Once every fix is in:

  1. Re-visit every affected page in the browser.
  2. Compute the final health score.
  3. If final < baseline: WARN PROMINENTLY — something regressed.

Phase 10: Report

Write the report to .qa-reports/qa-report-{domain}-{YYYY-MM-DD}.md.

Summary block

QA REPORT — {branch or URL}
==========================
Date: YYYY-MM-DD
Branch: {branch}
Mode: full / diff-aware / quick / regression
URL: {target}
Pages visited: N
Screenshots: N
Issues found: N (X critical, Y high, Z medium, W low)
Issues fixed: N (verified: X, best-effort: Y, reverted: Z)
Issues deferred: N
Health score: X/100 → Y/100
Duration: M min

Per-issue detail (one block per issue)

  • ID, title, severity, category, page
  • Repro steps (numbered)
  • Expected vs Actual
  • Console output (if any)
  • Evidence: linked screenshots
  • Fix status: verified / best-effort / reverted / deferred
  • Commit SHA (if fixed)
  • Files changed (if fixed)
  • Before/after screenshots (if fixed)

Top 3 Things to Fix

The 3 highest-severity deferred issues, one line each.

Ship Readiness

SHIP READINESS:
  Critical issues remaining: N
  High issues remaining: N
  Health score: X/100
  Verdict: SHIP / SHIP WITH CAUTION / DO NOT SHIP
  • 0 critical + 0 high → SHIP
  • 0 critical + some high → SHIP WITH CAUTION
  • Any critical → DO NOT SHIP

PR summary line

"QA found N issues, fixed M, health score X → Y."


Framework-Specific Guidance

Next.js

  • Console: watch for Hydration failed / Text content did not match — hydration mismatches.
  • Network: _next/data/* 404s break data fetching.
  • Test client-side navigation: use the flow action click on a <Link>, not just goto — catches App Router/SPA routing bugs.
  • Watch for CLS on dynamic-content pages.

React + Redux

  • Click into pages whose initial state hasn't loaded yet — does the page survive undefined selectors?
  • After dispatching an action through the UI, verify the selectors return the expected shape (use $B eval to inspect window.__REDUX_DEVTOOLS_EXTENSION__ when available).
  • useEffect cleanup: navigate away mid-request, then back — does state reset cleanly?

Rails

  • Look for N+1 query warnings in the console (development mode).
  • Confirm the CSRF token is present in forms (<meta name="csrf-token">).
  • Test Turbo / Stimulus transitions — page swaps shouldn't flash.
  • Flash messages should appear and dismiss correctly.

WordPress

  • Plugin conflicts show up as console JS errors from different vendors.
  • Admin bar visibility for logged-in users.
  • REST API health: $B goto $URL/wp-json/ should return JSON.
  • Mixed-content warnings (http resources on https pages) are common — note them.

General SPA

  • $B links misses client-side routes — drive the nav via snapshot + click.
  • Stale state: navigate away and back — does the data refresh?
  • Browser history: simulate back/forward via $B eval "window.history.back()".
  • Watch for memory leaks: rerun the same flow 5x and check for piling-up console warnings.

Common UX-Logic Patterns to Flag

These flaws clear unit tests because each function/component is correct in isolation. They only surface under real-browser, end-to-end testing. Watch for them on every QA pass.

These seven patterns are independent analytical passes over the same diff. By default, walk them yourself, in this context, before driving the browser — each is a quick "does this bug class appear here, and where?" question. They can also be parallelized: if the user opted into subagents (see How this runs), fan them out as a Workflow so all seven are scanned concurrently — see Optional fan-out: UX-logic-pattern scan below. Either way, the scan only returns "does this pattern apply, and where" candidates; you confirm each one in the browser (counting console markers / Redux dispatches, watching the network tab, walking the redirect) in the main loop. The scan never drives the browser and never asks the user anything.

Idle timeout / wipe destination conflict

  • Pattern: A global idle timer fires after N seconds of inactivity, runs a wipe, then redirects to the "home" / "welcome" / "login" page. That home page is itself gated by the same timer.
  • Why it's broken: Idle on the home page → modal pops "session ending" → "wipes" → redirects to home (where you already are). The user gets interrupted with no security or UX gain. And the home page's own mount-time wipe already protects the next user.
  • How to flag it: When you see an idle-timeout hook, ask "what page does the wipe redirect to? Is that page itself gated by the same timer?" If yes, flag it as a UX bug — the redirect destination should be excluded from the timer's gate.

Multi-callback wiring duplication

  • Pattern: A reusable modal/component fires multiple callbacks on a single user action (e.g. onClick runs both onClickPrimary AND onClickPrimaryFlag). A consumer wires the same side effect (wipe, dispatch, navigate) to BOTH callbacks.
  • Why it's broken: One user action = the side effect runs N times. Idempotent ops just waste cycles; non-idempotent ops (analytics fires twice, double increments) corrupt state.
  • How to flag it: When inspecting modal usage, check the source: is the same function passed to two callback props? Read the modal's source — does it fire both? If yes, mark it as a wiring bug. Verify in the browser by counting console.log markers or Redux dispatches.

Timer reset by background updates

  • Pattern: A timer is supposed to reset only on user activity (touchstart, mousemove, keydown), but a background update (poll response, websocket push, focus event) accidentally trips one of those listeners.
  • Why it's broken: The "idle" patient is actually being kept alive by the system. Someone who walked away never sees the timeout fire.
  • How to flag it: Open the page, don't touch anything. Watch the network tab. If polls or websocket messages keep arriving and the timer keeps resetting, that's the bug. Check useEffect dependency arrays for spurious resets.

Snapshot signature triggering destructive effects

  • Pattern: A memoized signature is computed from an object that mixes identity fields with cosmetic ones. A side effect (close modal, broadcast, reset state) fires when the signature changes. Background polls flip the cosmetic fields → the side effect fires unexpectedly.
  • How to flag it: If a modal closes mid-edit, or a form resets when nothing user-driven happened, check the memo signature. Cosmetic fields like is_locked, last_updated_at, status_label should not be in destructive-effect signatures.

Mount-effect runs on same-route push

  • Pattern: A page's useEffect(() => { ... }, []) runs on first mount. The hook calls router.push("/same-route") on a timeout. Depending on Next.js routing semantics, the page may or may not re-mount.
  • Why it's broken: If the page does re-mount, the wipe effect runs again. If it doesn't, only the explicit wipe call runs. Subtle inconsistency between dev and prod, and between Pages Router and App Router.
  • How to flag it: When testing redirects to the current page, console.log the mount effect once at boot. Trigger the redirect. If the log fires again, the page re-mounted. Record the actual behavior in the report.

Activity events that don't model the kiosk hardware

  • Pattern: A kiosk uses an ID card reader, NFC, barcode scanner, or face-recognition camera. The idle timer listens on touchstart/mousemove/keydown. The user is interacting with hardware (a slow card scan), not touching the screen.
  • Why it's broken: The user is mid-scan; the modal pops, says "session ending"; the user is confused.
  • How to flag it: Read the hook's activity-listener list. Check whether the kiosk hardware actually fires those events (often it doesn't — card-reader events arrive via a JS bridge, not real DOM events). If hardware events don't reset the timer, flag it as a potential UX issue.

Stale state on "back to home"

  • Pattern: A user finishes a flow and taps "Back to Home." The home page still shows the previous user's name, history, or selected option.
  • Why it's broken: The wipe-on-mount didn't run, OR it ran but missed a slice, OR a memo cached the old state.
  • How to flag it: After completing a happy-path flow, navigate home. Inspect localStorage, sessionStorage, and the visible text for any prior-user identifiers. Read the home-page mount effect's wipe list and compare it to the slices touched during the flow.

Optional fan-out: UX-logic-pattern scan

The seven UX-logic patterns above are each a self-contained "does this bug class appear in this diff?" question. By default, walk them sequentially in the main loop. If the user opted into subagents, fan them out so all seven scan concurrently against the same diff and source tree; then confirm every candidate in the browser in the main loop.

export const meta = {
  name: 'qa-fix-ux-logic-scan',
  description: 'Parallel scan of the diff for the 7 UX-logic bug-class patterns',
  phases: [{ title: 'Scan' }],
}
const PATTERN_SCHEMA = {
  type: 'object',
  properties: {
    pattern: { type: 'string' },
    applies: { type: 'boolean' },                                 // does this bug class appear in the diff?
    candidates: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          location: { type: 'string' },                          // file:line or hook/component name
          finding: { type: 'string' },
          browserConfirmation: { type: 'string' },                // exactly how to confirm it in the browser
        },
        required: ['location', 'finding', 'browserConfirmation'],
      },
    },
  },
  required: ['pattern', 'applies', 'candidates'],
}
const scans = await parallel(args.patterns.map(p => () =>
  agent(
    `You are scanning a code diff for ONE class of UX-logic bug. Do NOT ask the user anything and do ` +
    `NOT drive a browser — return source-grounded candidates only; the main loop confirms each in the browser.\n\n` +
    `BUG-CLASS PATTERN (${p.key}):\n${p.spec}\n\n` +
    `DIFF UNDER REVIEW:\n${args.diff}\n\n` +
    `AFFECTED SOURCE (read these as needed):\n${args.affectedFiles}\n\n` +
    `Decide whether this bug class appears in the changed code. For each candidate, give the exact ` +
    `location (file:line or hook/component name), the finding, and precisely how to confirm it in the ` +
    `browser (which markers to count, which network traffic to watch, which redirect to walk).`,
    { label: `scan:${p.key}`, phase: 'Scan', model: 'opus', schema: PATTERN_SCHEMA }
  )
))
return scans.filter(Boolean)

Pass args: { patterns: [{ key, spec }, ...], diff, affectedFiles } where each spec is the matching pattern definition above (verbatim) and affectedFiles is the list/paths of source the diff touches. When the workflow returns, for every candidate with applies: true, drive the browser in the main loop to confirm it using the returned browserConfirmation recipe, then document confirmed bugs per Phase 5. If the workflow fails or times out — or the user never opted into fan-out — walk the patterns sequentially in the main loop instead.


Important Rules

  1. You are the quality gate. QA is the last check before customers see the code. The whole user experience is your responsibility — not just the parts that are cheap to test. Treat a skip as risk handed to the customer.
  2. Drive a real browser. Never swap static analysis in for actual interaction. Even if the diff "looks backend-only," open the browser and test.
  3. Don't waive a test by pointing at unit tests. Unit tests verify code shape; QA verifies user experience. They are not interchangeable. UX-logic flaws (modals firing where they shouldn't, wipe destinations catching their own redirect, double-fire wiring) clear unit tests and break real users. If a brief says "the unit tests cover that," set it aside and verify in the browser anyway.
  4. Test the slow parts. A 120-second idle timer gets exercised end-to-end. Use the virtual-clock pattern (clockInstall + fastForward/runFor flow actions) — never skip. If a brief asks you to skip, push back and run it anyway.
  5. Visit every affected page. When a feature touches multiple pages (mount effect on page A + global hook on the app shell + per-page hook on page B), test ALL of them in the browser. Wiring bugs hide in the cross-page interaction; a single-page check misses them.
  6. Run the UX-logic-pattern checklist. Before writing the report, walk the patterns in "Common UX-Logic Patterns to Flag" against the diff. Each is a bug class that clears unit tests and only surfaces here.
  7. Repro is everything. Every issue gets at least one screenshot. No exceptions.
  8. Verify before documenting. Retry the issue once to confirm it reproduces.
  9. Show screenshots inline. Read every screenshot/snapshot output file immediately after capture, so the user sees it. Skip this and the screenshots are invisible.
  10. Never include credentials. Write [REDACTED] for passwords. Delete any flow JSON containing a real secret right after running it.
  11. Write incrementally. Append each issue to the report as you find it. Don't batch.
  12. Check the console after every interaction. JS errors that don't show visually are still bugs.
  13. Test like a user. Use realistic data. Walk complete workflows end-to-end, not isolated buttons.
  14. Depth over breadth. 5-10 well-documented issues with evidence beat 20 vague descriptions.
  15. One commit per fix. Never bundle multiple fixes.
  16. Minimal fixes only. Don't refactor, don't add features, don't "improve."
  17. Ship a regression test with each fix. Phase 8e.5 is mandatory when a test framework is detected — every verified fix gets a test that locks the bug out. A comment doesn't prevent a regression; a test does.
  18. Revert on regression. Fix made things worse → git revert HEAD immediately, mark it deferred.
  19. Self-regulate. Follow the WTF-likelihood heuristic. When in doubt, stop and ask.
  20. Clean working tree required. Refuse to start with uncommitted changes (or offer commit/stash via AskUserQuestion).
  21. Never modify existing tests. Only create new test files for regression tests.
  22. Never delete output files. .qa-reports/ accumulates on purpose. The user reviews the history.
  23. Default to inline; only fan out on opt-in. The whole job runs in one context by default. Parallel Workflows are available for big diffs but require an explicit user yes first (see How this runs). And whenever you do fan out, the interactive + browser + write work stays in the main loop — never put an AskUserQuestion gate, a browse.mjs invocation, an inline screenshot Read, a git commit, or a file write inside a Workflow agent(), because background agents can't talk to the user, drive the browser, or touch the filesystem.

Next Steps — Review Chaining

When QA finishes, recommend the next step:

  • /pre-merge-review — pre-landing structural gate before shipping; run it as its own step after QA passes (it's a sibling skill — invoke /pre-merge-review directly, not as a subagent spawn).
  • /ship — when QA comes back clean and the user wants to merge, test, push, and open a PR.
  • /qa — if the user wants an end-to-end, page-level QA pass to complement this source-level run.
  • /code-review — if the fixes touched non-trivial logic and the user wants a line-level review of the diff.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.