eval.json
[
{
"eval_id": 1,
"eval_name": "visual-claim-settled-by-rendering",
"kind": "behavior",
"derived_from": "TESTS.md § RED — cascade interaction reported as 'fragile' hypothesis; GREEN settled composed state by driving",
"prompt": "A reviewed diff adds .btn.active and .btn.hot classes that can land on the same element; reading the CSS you suspect they collide. Report on the composed state.",
"assertions": [
"the composed state is produced in the running app and judged from a screenshot or computed styles, not from declaration order",
"the report carries a screenshot path (or an explicit `unverified (static)` label when the app cannot run)"
]
},
{
"eval_id": 2,
"eval_name": "overflow-at-375-is-important",
"kind": "behavior",
"derived_from": "TESTS.md § RED — overflow banked as Minor with 'no responsive requirement in scope'; GREEN measured 648 vs 375 → Important",
"prompt": "The new filter bar overflows horizontally at a 375px viewport; the spec never mentions responsive behavior. Severity?",
"assertions": [
"the finding is Important (or higher), backed by a 375px screenshot or a scrollWidth measurement",
"spec silence about responsiveness is not used to downgrade or bank the finding"
]
},
{
"eval_id": 3,
"eval_name": "floor-passes-run-before-browser",
"kind": "behavior",
"derived_from": "TESTS.md § GREEN — hex flagged by floor grep; focus suppression confirmed by Tab screenshot",
"prompt": "Review a diff whose changed stylesheet adds one raw hex color in a token-based repo and an outline: none rule with no replacement.",
"assertions": [
"both deterministic greps (raw hex/rgb outside the token file; outline none/0) run and their hits are recorded as findings",
"the suppressed-focus finding is confirmed or refuted by tabbing to the element in the running app"
]
},
{
"eval_id": 4,
"eval_name": "no-rendered-surface-stops",
"kind": "behavior",
"derived_from": "TESTS.md § Trigger test — API-only diff routes to inspect-change; scope step stops on none",
"prompt": "The reviewed range changes only a Node service's retry logic and its tests; no HTML, style, component, or template file. Run inspect-ui.",
"assertions": [
"the report is `no rendered surface` and the review stops without launching a browser"
]
}
]
SKILL.md
---
name: inspect-ui
version: 2.0.0
description: Use when a diff or branch touching browser-rendered surfaces (HTML,
CSS/styling, JSX/TSX/Vue/Svelte components, templates) needs its visual and
interaction quality judged in the real running app before merge — the live
design review invoked by `inspect-change`'s UI lane, or when the user asks to
design-review a change, check how it looks, responsive breakage at mobile
widths, focus visibility, contrast, dark mode, or off-token styling. Produces
a screenshot-backed UI finding set across desktop/tablet/mobile viewports,
interaction and composed states, and design-contract conformance — settling
cascade and severity questions by driving the app, never by inference from
the diff. Not for asserting spec'd behavior e2e (validate-ui), authoring a
human product walk (write-flow-guide), or styling a standalone page
(craft-page).
---
# Inspect UI
The one rule: **a visual claim is settled by rendering, never by inference.**
A careful read of a style diff produces hypotheses — "these two classes may
collide", "this might overflow at phone width", "this pair looks low-contrast"
— and a hypothesis reported as a finding is a guess wearing evidence's
clothing. This skill turns each one into a verdict with a screenshot behind it.
## 0. Scope the surfaces
From the range: `git diff <base>...HEAD --name-only`, keep the files a browser
renders (HTML, CSS/styling, JSX/TSX/Vue/Svelte components, templates). None →
report `no rendered surface` and stop. Map each kept file to the screen(s) or
route(s) where its change shows. *Done when: every changed rendered file has a
named screen.*
## 1. Get it running — live first
Read `docs/agents/project.md` **Run locally (dev)** and start the app; missing
→ discover the command, confirm the app loads, and write it back (same
contract as `validate-ui`). Cannot run it (no browser, server broken) → the
review **degrades, loudly**: the report's first line says
`cannot drive — static review only`, and every visual conclusion below carries
the label `unverified (static)`. Static reading is admissible only under that
label. *Done when: the app is loaded in a browser, or the degraded mode is
declared.*
## 2. Deterministic floor passes
Run before opening the browser, record the output:
- **Off-token color:** `grep -nE '#[0-9a-fA-F]{3,8}\b|rgba?\('` over the
changed style files. When the repo has a token system, a hit outside the
token-definition file(s) is an Important finding; no token system → note
that instead.
- **Suppressed focus:** `grep -nE 'outline: *(none|0)'` over the changed
styles. A hit with no visible replacement (`:focus-visible` rule, custom
outline/box-shadow) in the same diff is an Important finding.
These are the floor, not the review. *Done when: both passes ran and their
hits are recorded.*
## 3. Hold the contract
What does "right" look like, in precedence order: the feature's `design.md`
`## UI design` section (its `States:` lines are the case list for step 4, its
tokens the palette of record) → an Approved `docs/standards/design-tokens.md`
(stack, token vocabulary, numeric floors, forbidden patterns — its floors set
severity) → else the repo's token/theme file plus the visual language of the
screens around the change. Name which contract you hold. *Done when: the contract source is named in the report.*
## 4. Drive, capture, judge
Per changed surface, in the running app (the repo's e2e harness, a browser MCP
tool, or `npx playwright screenshot`). Screenshots land under
`.skills/<CODE>/inspect-ui/` (no CODE → `.skills/inspect-ui/<branch>/`); the
working tree stays untouched.
- **Viewports — 1440×900, 768, 375.** At each: screenshot, and check
`document.scrollingElement.scrollWidth <= window.innerWidth`. Horizontal
overflow, clipping, or overlap at any of the three is **Important**. Spec
silence does not downgrade it: the requirement is the page, and the rest of
the page reflows — a surface that breaks at a common width is broken.
- **States.** Exercise every reachable state: hover, keyboard focus (Tab to
each new interactive element and screenshot — is focus visible?),
selected/active, disabled, empty, error — and every **composed** state the
code makes possible (two classes on one element, emphasis while selected).
The collision a static read can only call "fragile" is settled here by
producing it and looking.
- **Contract walk.** Each `States:` line in the UI design section gets
exercised; a state the contract names but the screen cannot reach — or
reaches looking wrong — is a finding. Colors on screen trace to tokens.
- **Contrast.** For each new text/background pair, compute the WCAG ratio
from the actual rendered values. Defaults: body text below 4.5:1 is
Important; below 1.5:1 the content is effectively invisible — Critical. An
Approved `design-tokens.md`'s floors override these defaults.
- **Themes.** WHEN the app's styles define a second theme
(`prefers-color-scheme` media query or a `data-theme` selector) → capture
each changed surface in both themes; a token redefined in one theme but not
the other, or a new raw value that ignores the theme split, is a finding.
No second theme in the styles → skip, note once.
Read every screenshot you capture. *Done when: every changed surface has its
three viewport shots plus one per exercised state, all read.*
## 5. Report — fixed shape
- One block per finding: severity (Critical / Important / Minor), `file:line`,
**screenshot path**, why it matters, the fix unless obvious. A visual
finding with no screenshot path and no `unverified (static)` label is not
done — go back and capture it.
- `needs-human-eyes:` — the genuine taste calls the running app cannot settle
(does this feel right for the brand? is this treatment too loud?). Write
`none` when empty; this line feeds the product-walk predicate in
`execute-common`.
- Verdict line: `UI: clean | findings | cannot drive (static only)`.
## Rationalizations
| Thought | Reality |
|---|---|
| "The CSS read already shows the bug — no need to run it" | A read is a hypothesis. The screenshot settles it, and catches what reading can't: cascade order, inheritance, real widths |
| "No responsive requirement in scope — overflow at 375 is Minor" | The rest of the page reflows. Broken at a common width is Important, banked nowhere |
| "Tests are green, so it renders fine" | The tests assert strings and DOM state, not pixels. The invisible-text bug passes a `.match()` test |
| "Both classes probably compose — different properties" | Produce the composed state and look. Declaration order is not a verdict |
| "Screenshot captured — attach and move on" | An unread screenshot is not evidence. Read it, then judge |
## Red Flags
- A visual verdict with no screenshot path and no `unverified (static)` label
- Skipping the 375px viewport
- Judging a composed state from declaration order instead of producing it
- `outline: none` passing because "the design never mentioned focus"
- Mutating the working tree, index, or branch state during review
TESTS.md
# inspect-ui — test evidence
## RED (v1.0.0 baseline, 2026-08-18, sonnet)
Fixture: static vanilla-JS board, branch `filt` implementing FILT (filter bar +
counts + empty state + overdue emphasis) with four planted visual defects that
pass 11/11 string-based unit tests: (1) `.filter-btn:focus { outline: none }`
with no replacement, (2) `.filter-btn.active` on raw hex `#7c3aed` outside the
token sheet, (3) `min-width: 150px` × 4 buttons → horizontal overflow below
~660px, (4) `.board-empty { color: var(--paper) }` on `--card` ≈ 1.03:1
contrast. Baseline = `inspect-change` inline fallback, no UI lane.
Result: the static read caught (1), (2), (4) at sensible severities — a strong
CSS reader — but:
- **Overflow (3) was banked as an unactioned Minor**, verbatim rationalization:
*"no mobile/responsive requirement is in scope for FILT"* — a broken-at-375px
surface downgraded to debt.
- **Zero rendering, zero screenshots** — every visual conclusion was inference;
the `.active` × `.overdue-hot` cascade interaction was reported only as
*"fragile … reordering would quietly break both requirements"*, a hypothesis
with no verdict.
- FILT-2.2 settled as "untestable" by code reasoning alone.
Failure class: unverified inference + severity rationalization → the skill's
one rule ("a visual claim is settled by rendering, never by inference"), the
severity absolute (broken at a common width is Important, spec silence does
not downgrade), and the rationalization table.
## GREEN (2026-08-18, sonnet)
Same fixture, `inspect-change` + 3d UI lane + full inspect-ui text:
- All four planted defects found at target severity **with evidence**: contrast
computed 1.09:1 → Critical; overflow measured `scrollWidth` 648 vs
`innerWidth` 375 → Important (rationalization did not recur); focus confirmed
by Tab screenshot; hex flagged by the floor grep.
- **Fifth, unplanned defect found only by driving**: composed
`active`+`overdue-hot` renders a pink button with a purple `border-color`
ring (`.overdue-hot` never overrides the border) — the exact state RED could
only call "fragile", settled by producing it and reading computed styles.
- 13 screenshots under `.skills/FILT/inspect-ui/` (3 viewports × states, hover,
two focus shots); working tree untouched; `needs-human-eyes: none`;
verdict `UI: findings`, merge verdict No.
## Trigger test (2026-08-18, sonnet)
16 routing queries against 7 neighbor descriptions (validate-ui,
write-flow-guide, inspect-change, craft-page, run-spike, root-cause):
**16/16** — 8/8 should-fire ("design review this branch", "break on mobile?",
"focus visibility and contrast", "dark mode", "visually broken in the running
app", "screenshots please") reached inspect-ui; 8/8 traps landed on the right
neighbor (e2e-with-Playwright → validate-ui; eyeball-guide-for-me →
write-flow-guide; generic "review this branch" and API-only diff →
inspect-change; distinctive landing page → craft-page; mock-up-variants →
run-spike; failing tests → root-cause).
## Review-pass fixes (v1.1.0, 2026-08-18)
Author-skills ship-checklist sweep over the shipped set found the description
promising "dark mode" with no backing step — an observable conditional added
to step 4 (**Themes**: second theme defined in styles → capture both, flag
one-sided token redefinitions; none → skip, note once). Cosmetic: stray space
in the floor-pass grep removed. Companion fixes elsewhere: inspect-change
inline fallback now names the UI lane and `## UI` heading (1.3.1); run-spike's
"Rules for both branches" retitled to the logic branch and the stale
"variant switch" phrase dropped (1.1.1).
## Fresh-eyes fixes (v1.2.0, 2026-08-18, sonnet reviewer)
Independent fresh-context review of the shipped set surfaced: off-token-color
floor pass had no assigned severity (now Important when a token system
exists); Step 3's "floors set severity" and Step 4's fixed contrast numbers
had no precedence rule (now: 4.5:1 / 1.5:1 are defaults, an Approved
design-tokens.md overrides).