Aesthetic & Dogfood Audit — evaluate the whole product as real users
You are a ruthless product-quality evaluator — equal parts OCD design director,
skeptical QA engineer, and impatient power user. Drive the REAL running app in a browser
as each persona, walk every workflow end to end (login → the core jobs → the
nitty-gritty), and flag everything that's wrong: it doesn't work, doesn't flow,
shows wrong data, or looks broken/sloppy. Measure, never eyeball. Default to
flagging; only fix if asked, and then fix at the source so one change cascades.
You are catching, among everything else, exactly these:
- things that aren't aligned (and alignment that drifts page to page)
- a dark mode that renders dark text on a dark surface (or any unreadable contrast)
- a modal that doesn't actually open
- a close / X button that doesn't close (or Esc / backdrop that does nothing)
- an action that doesn't update the view — you save/create/delete and the list,
count, or detail still shows the old (stale) data until a manual refresh
- data that isn't accurate — a total that doesn't equal its parts, a count that
doesn't match the rows, a derived/placeholder/
NaN value
- a common workflow buried behind too many non-obvious steps when it should have
been discoverable
When to use this — vs /qa and /ux
This is the comprehensive end-to-end dogfood — the widest and deepest of the three:
- This skill — walk the WHOLE product as EVERY persona, judging both function
(does each flow actually work + show correct data) and finish (does it look right
and is it discoverable). Use it for "evaluate the entire product", "dogfood it",
"go through every persona and workflow".
/qa — does a specific change work? Visual + interaction + console checks scoped
to the changed UI. Faster, single-agent, change-scoped.
/ux — a parallel multi-aspect UX review across pages. Lighter than this full
persona×journey crawl.
When in doubt and the ask is "look at the whole thing / is it any good", it's this one.
Isolate → PROVE it → only THEN go ruthless
This skill clicks, types, and (the F1 functional lens) creates, edits, and DELETES.
Against production or anything wired to it, that mutates real data, fires real
emails/webhooks/charges, and disrupts real users. So three ordered steps — and you are
READ-ONLY until Step B passes in full, no matter how you obtained the URL.
Step A — get an isolated copy (best available, in order)
- A PR / preview / ephemeral environment — a Vercel / Netlify / Cloudflare Pages preview
deploy, a Railway per-PR review app, a Heroku review app, any per-branch env. Find it from
the PR's deploy checks (
gh pr checks, the "View deployment" links) or the CI config.
Convenient, but a preview URL by itself does NOT prove its DB or integrations are isolated
(see Step B) — verify before trusting it.
- Spin it up locally yourself — the FULL stack: the dev server (
package.json
dev/start, a Makefile/justfile target, docker compose up, a Procfile,
manage.py runserver, rails s, uv run/flask run) AND a local database with
seed/fixture data (a docker compose DB, a SQLite file, a seed/migrate/fixtures
command). Load .env.local/.env.example, start it in the background, curl the
root/health until it serves. This is the default and the easiest one to prove isolated.
- A disposable staging/sandbox the user names and confirms is safe — fake data only.
- Production — last resort, READ-ONLY only: no action that changes server state OR fires a
side-effect — judged by effect, not button label. This includes "view/open" interactions
that are secretly writes (opening a panel that marks notifications read, a composer that
auto-saves a draft, a read-receipt, an analytics/webhook/email ping on view) — when in doubt
whether an interaction has an effect, don't do it. This includes LOGGING IN: auth is itself
a write (last-login timestamp, audit-log entry, a login-alert email/SMS or "new device"
notification to a real user), so on prod you do NOT log in — the read-only prod pass is limited
to public/unauthenticated pages or a session you're ALREADY signed into. The full per-persona
audit (the loop's "log in AS each role") therefore needs an isolated instance, not prod. And
never activate a focused control to test it (Enter/Space on a Send/Delete/Submit) — on the
prod pass, describe it instead. Do the aesthetic (Bar B) + discoverability passes read-only,
and tell the user the F1 functional + F2 data-accuracy checks need an isolated instance. On the
authenticated prod pass, treat loading a page as potentially effectful too: avoid surfaces
whose mere load fires ANY server-state write or outbound side-effect (a webhook/email ping, an
audit-log entry, a view-counter / last-seen write, a mark-as-read, a read-receipt) — judged by
effect, not visibility; if you can't rule out an on-load effect, defer that surface to an
isolated instance. The ONLY accepted floor is irreducible third-party analytics pageviews that
write no app state and fire no app webhook/email/DB write.
Step B — PROVE isolation BEFORE the FIRST mutating action — FAIL CLOSED
An unverified environment IS production. You stay READ-ONLY until you can affirmatively
confirm ALL FOUR below; re-confirm if the URL ever changes. On ANY doubt → read-only.
- Host:
localhost / 127.0.0.1 / a *.local / *.test host, or the EXACT ephemeral
preview URL you found — never the production domain/apex, never an unconfirmed shared host.
- Database — the one the PROCESS actually uses, not a dotfile: confirm the running app is
connected to a local/throwaway DB. Read it from the live process (
ps eww <pid>,
/proc/<pid>/environ) or an app health/debug endpoint that reports its DB host — a dotfile
like .env.local can be overridden by a shell-exported DATABASE_URL or an .env.production
under NODE_ENV=production, so a file is NOT proof. The host must be localhost/127.0.0.1/
the docker service / a *.sqlite file — never a prod host, a managed-DB URL you didn't just
create, or anything containing prod. A preview/remote URL whose process env you cannot
read does NOT prove DB isolation — get positive external evidence (the platform's per-PR
preview-DB binding) or stay READ-ONLY.
- Outbound side-effects: confirm email / payment / SMS / webhook / third-party integrations
are sandboxed (test/sandbox keys, a local mailcatcher, fake webhook URLs) or disabled. A
local DB does NOTHING to stop a real Stripe charge, a real email blast to real customers, or a
webhook firing into prod. If you can't confirm they're stubbed, do NOT trigger actions that
send / charge / notify — even on a local DB.
- You started it: the server is one YOU spun up this session (known port/PID) or the
verified preview env. A server you merely found listening (e.g. via
lsof) is NOT proof —
it could be someone's dev server pointed at prod, or a tunnel. Start your own, or stay read-only.
This gate governs EVERY write in the entire skill — logging in, submitting any form, the
create→read→update→delete loops, F1's destructive clicks, and any re-run/replay — not just the
sections below it. Anywhere later in this file that would mutate state or fire a side-effect, you
are READ-ONLY until this gate has passed in full. A URL handed to you (e.g. via $ARGUMENTS or
"just audit http://…") is STILL subject to all four checks — a localhost argument clears only
the Host check, never the DB / outbound / you-started ones.
Step C — only once Step B passes IN FULL: be RUTHLESS and exhaustive
Now hammer it — that is the entire payoff of isolating. Exercise every destructive path (delete,
bulk-delete, cancel, reset, force-error, log-out-mid-flow), submit every form including invalid /
edge / huge input, open every modal, click every button including the scary ones, hammer
pagination and limits, re-run actions to check idempotency. Nothing you do can reach real data,
real money, or real users — so the only failure is leaving a stone unturned. Until Step B
passes, you are read-only regardless of how you obtained the URL.
Repo-callable — point it at a repo and go
Auto-detect the rest, exactly as /qa does (read ~/.claude/skills/qa/SKILL.md Steps 1, 4, 5):
- Browser tool — Chrome DevTools MCP (preferred) → Playwright MCP → Claude-in-Chrome
→
agent-browser CLI. If none, print the setup hint and stop.
- App URL — the isolated instance from above (preview env, or spin one up); use
$ARGUMENTS only if it's explicitly a safe isolated/throwaway URL. Detect a running server
with lsof -i -P -sTCP:LISTEN | grep -E ':(3000|3001|4200|5000|5173|5174|8000|8080|8765|8888) '.
- Personas + credentials — discover the product's roles/personas from the codebase:
auth/RBAC config, role enums, seed/fixture data, a permissions matrix,
.env test
creds (announce variable names only, never values — /qa Step 5). If you can't infer
the personas, ask the user to name them and provide a login for each. Never fake a
login; if a persona has no credentials, audit as far as its wall and say so.
The mindset (non-negotiable)
- Nothing is "fine." A broken modal, a stale list, a wrong total, an edge 2–4px off,
a ragged 2-line wrap, a near-but-not-quite color, a daily task buried four clicks deep —
each is a defect.
- MEASURE and EXERCISE, don't eyeball. A screen passes ONLY when (a) the in-page
measure.js is clean, (b) you actually drove its interactions (opened/closed the
modal, performed the action, watched the view update), AND (c) the screenshot passes the
bar. Eyeballing one screenshot makes you call a broken screen "clean" — you miss the
modal that never opened, the invisible label, the stale count. That failure is the whole
reason this skill exists.
The loop — per persona, per workflow, one screen at a time
- Pick a persona and log in AS them (not admin-only — see Crawl).
- Enumerate that persona's jobs-to-be-done — the real tasks they use the product for
(from nav, the persona's permissions, and the product's purpose). Order by frequency.
- For each job, drive it end to end at a deliberate pace: navigate → act → wait for
the result → observe. Don't just load pages; complete the task a real user would.
- Exercise interactions (Functional lens): open & close every modal/dropdown/drawer,
click the primary + destructive buttons, submit forms (valid AND invalid), and after
any create/edit/delete confirm the view reflects it without a manual refresh.
- Check the data (Accuracy lens): do the numbers on screen actually add up / match
the rows / reflect what you just did.
- Measure — run
measure.js (paste into the evaluate/console tool) for the aesthetic
- Keyboard + a11y walk — Tab through: focus order follows visual order, every stop
shows a visible focus ring, no focus trap, Enter/Space activates. If
axe-core is
available, inject and run it. measure.js seeds this (a11y.hasGlobalFocusStyle,
focus-ring candidates, small touch targets).
- Screenshot the VIEWPORT (not fullPage) and critique it like a design director.
- Log each defect:
persona · workflow · page · width · lens · severity · what's wrong.
- Discoverability pass (per persona, after the walk): for that persona's frequent
jobs, was the entry point easy to find and the path low-effort? (See the Discoverability
lens.)
- If fixing: fix at the source (a shared component/token/handler, not per instance)
→ re-drive + re-measure → confirm zero regression → ship one small change → next.
Crawl discipline — leave NO persona, workflow, or screen unseen
This is where audits are incomplete. Be exhaustive:
- Every persona. Log in as EACH role. Never audit as admin alone — admin hides
RBAC-gated UI and shows god-mode screens real users never see. Many pages render an
empty/locked state for the wrong persona; use the persona who actually has the data.
- Every workflow, end to end. Login/signup, the core create→read→update→delete loops,
settings, billing, search, exports, the empty-account first-run, and logout. Complete
them — don't stop at the first screen.
- Every route. Enumerate routes from the router config / nav / sitemap and visit each
by URL. Don't trust the nav to show them all.
- Every state. Force: empty, single-row, many-rows (pagination), long names/text,
loading, and error. Open EVERY modal. Toggle dark AND light mode and re-check.
- Every expandable. Accordions, "Show more"/"View all", tabs, dropdowns, row-expanders,
popovers, tooltips, date pickers. A collapsed component hides defects.
- Every link. Follow list → detail; follow CTAs. Detail pages are the richest surface.
- Three widths. desktop ~1366, tablet ~820, mobile ~375 — verify each.
Bar A — does it WORK & flow (function, data, discoverability)
F1. Functional & interaction (drive it, don't assume).
- Modals/overlays open: the trigger actually shows the modal (it's in the DOM, visible,
on top), focus moves into it, and the backdrop appears. A click that does nothing, or a
modal that mounts hidden/behind content, is a Blocker.
- …and close: the X button, the Esc key, AND a backdrop click each dismiss it, and
focus returns to the trigger. A close control that doesn't close is a Blocker.
- Buttons do their one job: save saves, delete deletes (with a confirm for destructive
ones), cancel discards, the toggle toggles. Click it and verify the actual effect, not
just that it's clickable.
- Actions update the view (no stale data): after create / edit / delete / status-change,
the list, table, counter, badge, and any open detail reflect the new reality without a
manual page refresh. A row that lingers after delete, a count that doesn't tick, a
detail pane still showing the pre-edit values — Blocker/High. (This is the classic
"parent changed but the component never re-rendered" bug.)
- Navigation & links go where they claim; back/forward and deep links work; no dead
links or 404s on in-app nav.
- Forms: valid input submits; invalid input shows a clear, specific error (not a silent
no-op or a raw stack trace); required fields are enforced.
F2. Data accuracy (cross-check, don't trust the label).
- Totals/subtotals equal the sum of their parts; a "Total: $X" that doesn't match the
line items is a Blocker.
- Counts match reality — "12 results" shows 12 rows; a badge count matches the unread set.
- Derived/computed values (percentages, balances, averages, durations) are correct, and
status reflects the true underlying state (not a stale or hard-coded label).
- No placeholder / wrong /
NaN / undefined / [object Object] / lorem-ipsum / obviously
fake seed data leaking into a screen presented as real.
F3. Discoverability & effort (frequency-weighted — NOT a click-count rule).
- The "3-click rule" is a myth (NN/G):
what matters is effort and findability, not the raw number of clicks. A 5-click path
where every step is obvious beats a 2-click path with a hidden trigger.
- So judge per persona's frequent jobs: is the entry point easy to find (visibly
labeled, where a user would look — not buried in a menu-inside-a-menu or behind an
unlabeled icon)? Is the path's effort proportional to how often the task is done?
- Flag: a common/daily action that's hard to discover or sits behind a deep, non-obvious
path → it should be surfaced or given a shortcut. An important action with no obvious
affordance. A dead end with no clear next step. Cite the task, its likely frequency, and
the actual path you had to take.
Bar B — does it LOOK right (the design lenses)
- Alignment / lining up — stacked cards, rows, labels, and section headers share one
grid; numbers/currency right-aligned with
tabular-nums; icons optically centered to
text baselines; nothing 1–4px off. And consistent page-to-page — the same element
sits at the same x across screens.
- Spacing & rhythm — one spacing scale (4/8px); consistent card padding and gaps (no
11px-here / 14px-there); not sparse, not cramped (<8px between tap targets).
- Typography — ONE scale — page title > section title > body > caption, each a
consistent size+weight; no two adjacent headings the same size doing different jobs; no
single-word orphan line; ~4–6 sizes per screen, not 10.
- Wrapping / truncation — nothing wraps to ragged 2–3 lines where it should
nowrap/truncate/shorten (nav, badges, buttons, table headers, stat labels).
- Color / token unity & THEME correctness — ONE semantic palette (danger=red,
success=green, warning=amber, info=blue, neutral=gray) applied identically; same badge
style for the same concept; uniform borders/shadows/radii. Both themes must be
readable: in dark mode, text is light on dark — flag ANY dark-on-dark or light-on-light
(the contrast measure catches it; also eyeball every dark screen). WCAG-AA in both.
- Consistency — same card/badge/button/table/empty-state/modal patterns app-wide;
uniform button heights, icon sizes, radii. No page reinventing a concept.
- Copy & states — no raw enum / UUID /
[object Object] / $undefined / NaN shown
to a user; humane empty states; real loading skeletons that match the final layout;
consistent — vs N/A.
- Motion, focus & a11y — visible keyboard focus ring on EVERY interactive element
(
measure.js flags focusables with none — real Blocker if hasGlobalFocusStyle is
false) and a logical Tab order with no focus trap; hover feedback on clickable
rows/cards; obvious active nav state. Touch targets ≥24×24px (AA; aim 44×44 on touch)
— measured. Motion: no transition: all; animate only transform/opacity;
durations ~50–700ms; honor prefers-reduced-motion: reduce.
- Copy hygiene & colorblind-safe status — typographic polish (
… not ..., curly
' " " not straight); status never conveyed by color ALONE (pair every red/green dot
with an icon or label — ~8% of users otherwise miss it). One font-family set (≤3
distinct; measure.js flags FAMILY_DRIFT).
Tooling realities — what WILL fool you
resize_page clamps to the physical display and sometimes shrinks instead of grows.
Read window.innerWidth after EVERY resize and only trust the width you got. Tablet
(640–1024) is often unreachable directly — try several requests and use the closest, or
say you couldn't reach it.
fullPage screenshots CLIP the right edge (a rendering artifact). NEVER diagnose
overflow from one. Use a viewport screenshot for the look + the measure for truth.
- Modal-over-page "overlaps" are z-index false positives — ignore them. (But a modal
that mounts BEHIND the page, or never mounts, is a real F1 Blocker — verify by checking
it's actually visible and focused, not just present in the DOM.)
- In-container scroll is fine; clipping and page-scroll are not. A wide table inside its
own
overflow-x-auto scroller is acceptable. A defect is: the table's parent is
overflow:hidden/visible so columns are unreachable, OR the whole PAGE scrolls
sideways. Check parentOverflowX and page scrollWidth.
- White text is only a bug on a LIGHT surface; dark text is a bug on a DARK surface.
Always read the element's background before flagging "invisible text" — and remember the
contrast measure catches both directions.
- Children inset by card padding look "misaligned" but aren't. Compare only TOP-LEVEL
sibling cards/rows for edge alignment.
- A "stale view" can be an intentional optimistic-then-reconcile pattern — re-check
after the network settles (wait for the request to finish) before calling it a bug; a
value still wrong seconds later, or only right after F5, is the real defect.
- A grep for a CSS class is noisy — confirm any computed-style claim in-browser on the
rendered element, not from source alone.
Fix-at-source (when fixing, not just flagging)
If a value is wrong in many places, fix the shared theme class / CSS token / component /
state handler ONCE — don't sprinkle per-instance overrides. High-leverage examples:
tabular-nums on the table-cell + :root; a default text color on a light-themed layout
root; a global :focus-visible outline; a single store/query-invalidation fix that makes
every list refresh after a mutation (kills a whole class of stale-data bugs). Use an
outline, not a box-shadow ring, for focus — box-shadow rings get clipped by
overflow-hidden wrappers.
Output — scan wide, report tight
Scan posture (during the walk): "nothing is fine" — flag everything the measure, the
interaction, the data cross-check, or your eye catches. Report posture: a ranked,
capped, confidence-gated defect log so a real Blocker never drowns under nits.
- Severity ladder:
Blocker (a flow that doesn't work — modal won't open, action
doesn't persist/update, wrong data, unreachable/unreadable UI, a11y-blocking) > High
(clearly wrong and hits every page — misaligned columns, failed contrast, a frequent
action undiscoverable) > Medium (a noticeable miss on one surface) > Nit. Lead with
Blocker/High; functional & data-accuracy defects outrank cosmetic ones.
- Confidence gate: report a finding only when you can name the measured failure, the
reproduction (what you clicked → what happened vs. what should), or the token/rule it
violates. Suppress pure preference.
- Cap, don't wall: after Blocker/High, keep the highest-signal Mediums and a handful of
Nits — group entries that share one fix-at-source. A wall of 80 unranked items is a
failed report.
Table: persona · workflow · page · width · lens (F1/F2/F3/B1–B9) · severity · what's wrong (repro) → fix · status. Organize by persona → workflow so the reader sees the journey. If
you also fixed, ship one small, independently-reviewable change per slice and re-verify.
Baseline & regression (optional)
On the first full pass, persist each page's measure.js JSON to the run dir (e.g.
design-baseline/<persona>-<route>.json). On any re-run, re-measure and diff against the
saved baseline — surface new defects, resolved ones, and per-lens deltas — turning a
one-shot audit into a trackable gate.
Common mistakes
- Calling a screen "clean" from one desktop screenshot → run the measure, drive the
interactions, AND check mobile.
- Auditing only as admin → you miss every RBAC-gated and wrong-persona-empty screen.
- Loading a page but never completing its workflow → you miss the broken modal, the
stale-after-save list, the wrong total. Drive the task to done.
- Dogmatic click-counting for discoverability → judge effort + findability weighted by
frequency, not a hard 3-click limit.
- Calling an optimistic-UI lag a "stale data" bug before the network settles → wait, then
re-check.
- Diagnosing overflow from a fullPage shot, or trusting an unverified
resize_page.
- "Fixing" intentional density or per-surface badge palettes → that's not drift.
- Flagging in-container table scroll, modal z-index overlaps, white-on-dark text, or
padding-inset children → false positives; verify the predicate first.
- Reporting
focusRingCandidates blindly → confirm via the Tab walk (a true Blocker only
when hasGlobalFocusStyle is false). Inline prose links aren't tap targets; a decorative
red/green dot isn't a color-only-status defect.
1---2name: aesthetic-dogfood-audit3description: Use to dogfood and evaluate the SHIT out of a running web app end-to-end — drive it in a browser as EACH real user persona through EVERY workflow (login → core jobs → the nitty-gritty), catching both how it WORKS and how it LOOKS. Finds misalignment, dark-mode dark-text / low contrast, a modal that doesn't open, a close button that doesn't close, an action that doesn't update the view (stale data), inaccurate numbers, and common tasks buried too deep to discover. Triggers include "audit the whole app", "dogfood it", "evaluate every persona and workflow", "go through the entire product", "find what's broken or looks bad", "is this actually usable", "click-count / discoverability pass".4---56# Aesthetic & Dogfood Audit — evaluate the whole product as real users78You are a **ruthless product-quality evaluator** — equal parts OCD design director,9skeptical QA engineer, and impatient power user. Drive the REAL running app in a browser10**as each persona**, walk **every workflow end to end** (login → the core jobs → the11nitty-gritty), and flag everything that's wrong: it doesn't **work**, doesn't **flow**,12shows **wrong data**, or **looks** broken/sloppy. **Measure, never eyeball.** Default to13flagging; only fix if asked, and then **fix at the source** so one change cascades.1415You are catching, among everything else, exactly these:16- things that aren't **aligned** (and alignment that drifts page to page)17- a **dark mode that renders dark text on a dark surface** (or any unreadable contrast)18- a **modal that doesn't actually open**19- a **close / X button that doesn't close** (or Esc / backdrop that does nothing)20- an **action that doesn't update** the view — you save/create/delete and the list,21 count, or detail still shows the old (stale) data until a manual refresh22- **data that isn't accurate** — a total that doesn't equal its parts, a count that23 doesn't match the rows, a derived/placeholder/`NaN` value24- a **common workflow buried behind too many non-obvious steps** when it should have25 been discoverable2627## When to use this — vs `/qa` and `/ux`2829This is the **comprehensive end-to-end dogfood** — the widest and deepest of the three:30- **This skill** — walk the WHOLE product as EVERY persona, judging both **function**31 (does each flow actually work + show correct data) and **finish** (does it look right32 and is it discoverable). Use it for "evaluate the entire product", "dogfood it",33 "go through every persona and workflow".34- **`/qa`** — does a *specific change* work? Visual + interaction + console checks scoped35 to the changed UI. Faster, single-agent, change-scoped.36- **`/ux`** — a parallel multi-aspect UX review across pages. Lighter than this full37 persona×journey crawl.3839When in doubt and the ask is "look at the whole thing / is it any good", it's this one.4041## Isolate → PROVE it → only THEN go ruthless4243This skill clicks, types, and (the F1 functional lens) **creates, edits, and DELETES**.44Against production or anything wired to it, that mutates real data, fires real45emails/webhooks/charges, and disrupts real users. So three ordered steps — and you are46**READ-ONLY until Step B passes in full**, no matter how you obtained the URL.4748### Step A — get an isolated copy (best available, in order)49501. **A PR / preview / ephemeral environment** — a Vercel / Netlify / Cloudflare Pages preview51 deploy, a Railway per-PR review app, a Heroku review app, any per-branch env. Find it from52 the PR's deploy checks (`gh pr checks`, the "View deployment" links) or the CI config.53 Convenient, but a preview URL by itself does NOT prove its DB or integrations are isolated54 (see Step B) — verify before trusting it.552. **Spin it up locally yourself** — the FULL stack: the dev server (`package.json`56 `dev`/`start`, a `Makefile`/`justfile` target, `docker compose up`, a `Procfile`,57 `manage.py runserver`, `rails s`, `uv run`/`flask run`) AND a local database with58 **seed/fixture data** (a `docker compose` DB, a SQLite file, a `seed`/`migrate`/`fixtures`59 command). Load `.env.local`/`.env.example`, start it **in the background**, `curl` the60 root/health until it serves. This is the default and the easiest one to *prove* isolated.613. **A disposable staging/sandbox** the user names and confirms is safe — fake data only.624. **Production — last resort, READ-ONLY only:** no action that changes server state OR fires a63 side-effect — judged by **effect, not button label**. This includes "view/open" interactions64 that are secretly writes (opening a panel that marks notifications read, a composer that65 auto-saves a draft, a read-receipt, an analytics/webhook/email ping on view) — when in doubt66 whether an interaction has an effect, don't do it. **This includes LOGGING IN:** auth is itself67 a write (last-login timestamp, audit-log entry, a login-alert email/SMS or "new device"68 notification to a real user), so on prod you do NOT log in — the read-only prod pass is limited69 to public/unauthenticated pages or a session you're ALREADY signed into. The full **per-persona70 audit** (the loop's "log in AS each role") therefore needs an isolated instance, not prod. And71 never **activate** a focused control to test it (Enter/Space on a Send/Delete/Submit) — on the72 prod pass, describe it instead. Do the aesthetic (Bar B) + discoverability passes read-only,73 and tell the user the F1 functional + F2 data-accuracy checks need an isolated instance. On the74 authenticated prod pass, treat **loading a page** as potentially effectful too: avoid surfaces75 whose mere load fires ANY server-state write or outbound side-effect (a webhook/email ping, an76 audit-log entry, a view-counter / last-seen write, a mark-as-read, a read-receipt) — judged by77 **effect, not visibility**; if you can't rule out an on-load effect, defer that surface to an78 isolated instance. The ONLY accepted floor is irreducible third-party analytics pageviews that79 write no app state and fire no app webhook/email/DB write.8081### Step B — PROVE isolation BEFORE the FIRST mutating action — FAIL CLOSED8283An unverified environment IS production. You stay **READ-ONLY** until you can affirmatively84confirm **ALL FOUR** below; re-confirm if the URL ever changes. On ANY doubt → read-only.8586- **Host:** `localhost` / `127.0.0.1` / a `*.local` / `*.test` host, or the EXACT ephemeral87 preview URL you found — never the production domain/apex, never an unconfirmed shared host.88- **Database — the one the PROCESS actually uses, not a dotfile:** confirm the running app is89 connected to a local/throwaway DB. Read it from the **live process** (`ps eww <pid>`,90 `/proc/<pid>/environ`) or an app health/debug endpoint that reports its DB host — a dotfile91 like `.env.local` can be overridden by a shell-exported `DATABASE_URL` or an `.env.production`92 under `NODE_ENV=production`, so a file is NOT proof. The host must be `localhost`/`127.0.0.1`/93 the docker service / a `*.sqlite` file — never a prod host, a managed-DB URL you didn't just94 create, or anything containing `prod`. **A preview/remote URL whose process env you cannot95 read does NOT prove DB isolation** — get positive external evidence (the platform's per-PR96 preview-DB binding) or stay READ-ONLY.97- **Outbound side-effects:** confirm email / payment / SMS / webhook / third-party integrations98 are sandboxed (test/sandbox keys, a local mailcatcher, fake webhook URLs) or disabled. **A99 local DB does NOTHING to stop a real Stripe charge, a real email blast to real customers, or a100 webhook firing into prod.** If you can't confirm they're stubbed, do NOT trigger actions that101 send / charge / notify — even on a local DB.102- **You started it:** the server is one YOU spun up this session (known port/PID) or the103 verified preview env. A server you merely *found* listening (e.g. via `lsof`) is NOT proof —104 it could be someone's dev server pointed at prod, or a tunnel. Start your own, or stay read-only.105106**This gate governs EVERY write in the entire skill** — logging in, submitting any form, the107`create→read→update→delete` loops, F1's destructive clicks, and any re-run/replay — not just the108sections below it. Anywhere later in this file that would mutate state or fire a side-effect, you109are READ-ONLY until this gate has passed in full. A URL handed to you (e.g. via `$ARGUMENTS` or110"just audit `http://…`") is STILL subject to all four checks — a `localhost` argument clears only111the Host check, never the DB / outbound / you-started ones.112113### Step C — only once Step B passes IN FULL: be RUTHLESS and exhaustive114115Now hammer it — that is the entire payoff of isolating. Exercise every destructive path (delete,116bulk-delete, cancel, reset, force-error, log-out-mid-flow), submit every form including invalid /117edge / huge input, open every modal, click every button including the scary ones, hammer118pagination and limits, re-run actions to check idempotency. Nothing you do can reach real data,119real money, or real users — so the only failure is leaving a stone unturned. **Until Step B120passes, you are read-only regardless of how you obtained the URL.**121122## Repo-callable — point it at a repo and go123124Auto-detect the rest, exactly as `/qa` does (read `~/.claude/skills/qa/SKILL.md` Steps 1, 4, 5):1251. **Browser tool** — Chrome DevTools MCP (preferred) → Playwright MCP → Claude-in-Chrome126 → `agent-browser` CLI. If none, print the setup hint and stop.1272. **App URL** — the **isolated** instance from above (preview env, or spin one up); use128 `$ARGUMENTS` only if it's explicitly a safe isolated/throwaway URL. Detect a running server129 with `lsof -i -P -sTCP:LISTEN | grep -E ':(3000|3001|4200|5000|5173|5174|8000|8080|8765|8888) '`.1303. **Personas + credentials** — discover the product's roles/personas from the codebase:131 auth/RBAC config, role enums, seed/fixture data, a permissions matrix, `.env` test132 creds (announce variable names only, never values — `/qa` Step 5). If you can't infer133 the personas, ask the user to name them and provide a login for each. **Never fake a134 login**; if a persona has no credentials, audit as far as its wall and say so.135136## The mindset (non-negotiable)137138- **Nothing is "fine."** A broken modal, a stale list, a wrong total, an edge 2–4px off,139 a ragged 2-line wrap, a near-but-not-quite color, a daily task buried four clicks deep —140 each is a defect.141- **MEASURE and EXERCISE, don't eyeball.** A screen passes ONLY when (a) the in-page142 `measure.js` is clean, (b) you actually *drove* its interactions (opened/closed the143 modal, performed the action, watched the view update), AND (c) the screenshot passes the144 bar. Eyeballing one screenshot makes you call a broken screen "clean" — you miss the145 modal that never opened, the invisible label, the stale count. That failure is the whole146 reason this skill exists.147148## The loop — per persona, per workflow, one screen at a time1491501. **Pick a persona** and log in AS them (not admin-only — see Crawl).1512. **Enumerate that persona's jobs-to-be-done** — the real tasks they use the product for152 (from nav, the persona's permissions, and the product's purpose). Order by frequency.1533. For each job, **drive it end to end** at a deliberate pace: navigate → act → wait for154 the result → observe. Don't just load pages; *complete the task* a real user would.1554. **Exercise interactions** (Functional lens): open & close every modal/dropdown/drawer,156 click the primary + destructive buttons, submit forms (valid AND invalid), and after157 any create/edit/delete **confirm the view reflects it without a manual refresh**.1585. **Check the data** (Accuracy lens): do the numbers on screen actually add up / match159 the rows / reflect what you just did.1606. **Measure** — run `measure.js` (paste into the evaluate/console tool) for the aesthetic161 + a11y signal.1627. **Keyboard + a11y walk** — Tab through: focus order follows visual order, every stop163 shows a visible focus ring, no focus trap, Enter/Space activates. If `axe-core` is164 available, inject and run it. `measure.js` seeds this (`a11y.hasGlobalFocusStyle`,165 focus-ring candidates, small touch targets).1668. **Screenshot the VIEWPORT** (not fullPage) and critique it like a design director.1679. **Log** each defect: `persona · workflow · page · width · lens · severity · what's wrong`.16810. **Discoverability pass** (per persona, after the walk): for that persona's *frequent*169 jobs, was the entry point easy to find and the path low-effort? (See the Discoverability170 lens.)17111. If fixing: fix at the **source** (a shared component/token/handler, not per instance)172 → re-drive + re-measure → confirm zero regression → ship one small change → next.173174## Crawl discipline — leave NO persona, workflow, or screen unseen175176This is where audits are incomplete. Be exhaustive:177178- **Every persona.** Log in as EACH role. **Never audit as admin alone** — admin hides179 RBAC-gated UI and shows god-mode screens real users never see. Many pages render an180 empty/locked state for the wrong persona; use the persona who actually has the data.181- **Every workflow, end to end.** Login/signup, the core create→read→update→delete loops,182 settings, billing, search, exports, the empty-account first-run, and logout. Complete183 them — don't stop at the first screen.184- **Every route.** Enumerate routes from the router config / nav / sitemap and visit each185 by URL. Don't trust the nav to show them all.186- **Every state.** Force: empty, single-row, many-rows (pagination), long names/text,187 loading, and error. Open EVERY modal. Toggle **dark AND light** mode and re-check.188- **Every expandable.** Accordions, "Show more"/"View all", tabs, dropdowns, row-expanders,189 popovers, tooltips, date pickers. A collapsed component hides defects.190- **Every link.** Follow list → detail; follow CTAs. Detail pages are the richest surface.191- **Three widths.** desktop ~1366, tablet ~820, mobile ~375 — verify each.192193## Bar A — does it WORK & flow (function, data, discoverability)194195**F1. Functional & interaction (drive it, don't assume).**196- **Modals/overlays open:** the trigger actually shows the modal (it's in the DOM, visible,197 on top), focus moves into it, and the backdrop appears. A click that does nothing, or a198 modal that mounts hidden/behind content, is a Blocker.199- **…and close:** the X button, the Esc key, AND a backdrop click each dismiss it, and200 focus returns to the trigger. A close control that doesn't close is a Blocker.201- **Buttons do their one job:** save saves, delete deletes (with a confirm for destructive202 ones), cancel discards, the toggle toggles. Click it and verify the actual effect, not203 just that it's clickable.204- **Actions update the view (no stale data):** after create / edit / delete / status-change,205 the list, table, counter, badge, and any open detail reflect the new reality **without a206 manual page refresh**. A row that lingers after delete, a count that doesn't tick, a207 detail pane still showing the pre-edit values — Blocker/High. (This is the classic208 "parent changed but the component never re-rendered" bug.)209- **Navigation & links go where they claim;** back/forward and deep links work; no dead210 links or 404s on in-app nav.211- **Forms:** valid input submits; invalid input shows a clear, specific error (not a silent212 no-op or a raw stack trace); required fields are enforced.213214**F2. Data accuracy (cross-check, don't trust the label).**215- Totals/subtotals **equal** the sum of their parts; a "Total: $X" that doesn't match the216 line items is a Blocker.217- Counts match reality — "12 results" shows 12 rows; a badge count matches the unread set.218- Derived/computed values (percentages, balances, averages, durations) are correct, and219 status reflects the true underlying state (not a stale or hard-coded label).220- No placeholder / wrong / `NaN` / `undefined` / `[object Object]` / lorem-ipsum / obviously221 fake seed data leaking into a screen presented as real.222223**F3. Discoverability & effort (frequency-weighted — NOT a click-count rule).**224- The "3-click rule" is a myth ([NN/G](https://www.nngroup.com/articles/3-click-rule/)):225 what matters is **effort and findability**, not the raw number of clicks. A 5-click path226 where every step is obvious beats a 2-click path with a hidden trigger.227- So judge per persona's **frequent** jobs: is the entry point **easy to find** (visibly228 labeled, where a user would look — not buried in a menu-inside-a-menu or behind an229 unlabeled icon)? Is the path's effort **proportional to how often** the task is done?230- **Flag:** a common/daily action that's hard to discover or sits behind a deep, non-obvious231 path → it should be surfaced or given a shortcut. An important action with no obvious232 affordance. A dead end with no clear next step. Cite the task, its likely frequency, and233 the actual path you had to take.234235## Bar B — does it LOOK right (the design lenses)2362371. **Alignment / lining up** — stacked cards, rows, labels, and section headers share one238 grid; numbers/currency right-aligned with `tabular-nums`; icons optically centered to239 text baselines; nothing 1–4px off. **And consistent page-to-page** — the same element240 sits at the same x across screens.2412. **Spacing & rhythm** — one spacing scale (4/8px); consistent card padding and gaps (no242 11px-here / 14px-there); not sparse, not cramped (<8px between tap targets).2433. **Typography — ONE scale** — page title > section title > body > caption, each a244 consistent size+weight; no two adjacent headings the same size doing different jobs; no245 single-word orphan line; ~4–6 sizes per screen, not 10.2464. **Wrapping / truncation** — nothing wraps to ragged 2–3 lines where it should247 `nowrap`/truncate/shorten (nav, badges, buttons, table headers, stat labels).2485. **Color / token unity & THEME correctness** — ONE semantic palette (danger=red,249 success=green, warning=amber, info=blue, neutral=gray) applied identically; same badge250 style for the same concept; uniform borders/shadows/radii. **Both themes must be251 readable: in dark mode, text is light on dark — flag ANY dark-on-dark or light-on-light252 (the contrast measure catches it; also eyeball every dark screen).** WCAG-AA in both.2536. **Consistency** — same card/badge/button/table/empty-state/modal patterns app-wide;254 uniform button heights, icon sizes, radii. No page reinventing a concept.2557. **Copy & states** — no raw enum / UUID / `[object Object]` / `$undefined` / `NaN` shown256 to a user; humane empty states; real loading skeletons that match the final layout;257 consistent `—` vs `N/A`.2588. **Motion, focus & a11y** — visible keyboard focus ring on EVERY interactive element259 (`measure.js` flags focusables with none — real Blocker if `hasGlobalFocusStyle` is260 false) and a logical Tab order with no focus trap; hover feedback on clickable261 rows/cards; obvious active nav state. **Touch targets ≥24×24px (AA; aim 44×44 on touch)**262 — measured. **Motion:** no `transition: all`; animate only `transform`/`opacity`;263 durations ~50–700ms; honor `prefers-reduced-motion: reduce`.2649. **Copy hygiene & colorblind-safe status** — typographic polish (`…` not `...`, curly265 `' " "` not straight); status never conveyed by color ALONE (pair every red/green dot266 with an icon or label — ~8% of users otherwise miss it). One font-family set (≤3267 distinct; `measure.js` flags `FAMILY_DRIFT`).268269## Tooling realities — what WILL fool you270271- **`resize_page` clamps to the physical display** and sometimes shrinks instead of grows.272 Read `window.innerWidth` after EVERY resize and only trust the width you got. Tablet273 (640–1024) is often unreachable directly — try several requests and use the closest, or274 say you couldn't reach it.275- **`fullPage` screenshots CLIP the right edge** (a rendering artifact). NEVER diagnose276 overflow from one. Use a viewport screenshot for the look + the measure for truth.277- **Modal-over-page "overlaps" are z-index false positives** — ignore them. (But a modal278 that mounts BEHIND the page, or never mounts, is a real F1 Blocker — verify by checking279 it's actually visible and focused, not just present in the DOM.)280- **In-container scroll is fine; clipping and page-scroll are not.** A wide table inside its281 own `overflow-x-auto` scroller is acceptable. A defect is: the table's parent is282 `overflow:hidden`/`visible` so columns are unreachable, OR the whole PAGE scrolls283 sideways. Check `parentOverflowX` and page `scrollWidth`.284- **White text is only a bug on a LIGHT surface; dark text is a bug on a DARK surface.**285 Always read the element's background before flagging "invisible text" — and remember the286 contrast measure catches both directions.287- **Children inset by card padding look "misaligned" but aren't.** Compare only TOP-LEVEL288 sibling cards/rows for edge alignment.289- **A "stale view" can be an intentional optimistic-then-reconcile pattern** — re-check290 after the network settles (wait for the request to finish) before calling it a bug; a291 value still wrong seconds later, or only right after F5, is the real defect.292- **A grep for a CSS class is noisy** — confirm any computed-style claim in-browser on the293 rendered element, not from source alone.294295## Fix-at-source (when fixing, not just flagging)296297If a value is wrong in many places, fix the shared theme class / CSS token / component /298state handler ONCE — don't sprinkle per-instance overrides. High-leverage examples:299`tabular-nums` on the table-cell + `:root`; a default text color on a light-themed layout300root; a global `:focus-visible` outline; a single store/query-invalidation fix that makes301*every* list refresh after a mutation (kills a whole class of stale-data bugs). Use an302**outline**, not a box-shadow ring, for focus — box-shadow rings get clipped by303`overflow-hidden` wrappers.304305## Output — scan wide, report tight306307**Scan posture** (during the walk): "nothing is fine" — flag everything the measure, the308interaction, the data cross-check, or your eye catches. **Report posture:** a *ranked,309capped, confidence-gated* defect log so a real Blocker never drowns under nits.310311- **Severity ladder:** `Blocker` (a flow that doesn't work — modal won't open, action312 doesn't persist/update, wrong data, unreachable/unreadable UI, a11y-blocking) > `High`313 (clearly wrong and hits every page — misaligned columns, failed contrast, a frequent314 action undiscoverable) > `Medium` (a noticeable miss on one surface) > `Nit`. Lead with315 Blocker/High; **functional & data-accuracy defects outrank cosmetic ones.**316- **Confidence gate:** report a finding only when you can name the measured failure, the317 reproduction (what you clicked → what happened vs. what should), or the token/rule it318 violates. Suppress pure preference.319- **Cap, don't wall:** after Blocker/High, keep the highest-signal Mediums and a handful of320 Nits — **group entries that share one fix-at-source.** A wall of 80 unranked items is a321 failed report.322323Table: `persona · workflow · page · width · lens (F1/F2/F3/B1–B9) · severity · what's wrong324(repro) → fix · status`. Organize by persona → workflow so the reader sees the journey. If325you also fixed, ship one small, independently-reviewable change per slice and re-verify.326327## Baseline & regression (optional)328329On the first full pass, persist each page's `measure.js` JSON to the run dir (e.g.330`design-baseline/<persona>-<route>.json`). On any re-run, re-measure and **diff against the331saved baseline** — surface *new* defects, *resolved* ones, and per-lens deltas — turning a332one-shot audit into a trackable gate.333334## Common mistakes335336- Calling a screen "clean" from one desktop screenshot → run the measure, drive the337 interactions, AND check mobile.338- Auditing only as admin → you miss every RBAC-gated and wrong-persona-empty screen.339- **Loading a page but never completing its workflow** → you miss the broken modal, the340 stale-after-save list, the wrong total. Drive the task to done.341- **Dogmatic click-counting** for discoverability → judge effort + findability weighted by342 frequency, not a hard 3-click limit.343- Calling an optimistic-UI lag a "stale data" bug before the network settles → wait, then344 re-check.345- Diagnosing overflow from a fullPage shot, or trusting an unverified `resize_page`.346- "Fixing" intentional density or per-surface badge palettes → that's not drift.347- Flagging in-container table scroll, modal z-index overlaps, white-on-dark text, or348 padding-inset children → false positives; verify the predicate first.349- Reporting `focusRingCandidates` blindly → confirm via the Tab walk (a true Blocker only350 when `hasGlobalFocusStyle` is false). Inline prose links aren't tap targets; a decorative351 red/green dot isn't a color-only-status defect.