Flake Triage
Determine if preview changes are real regressions or just unstable rendering.
Installation
- Make sure Claude is on your device and in your terminal.
Skills load from
~/.claude/skills/when Claude Code starts up β so you need it on your machine first. If you don't have it yet, install it once with the command below, then runclaudein any terminal to verify.One-time setupnpm i -g @anthropic-ai/claude-codeAlready have it? Skip ahead.
- Paste into Claude Code or into your terminal.
This copies the whole skill folder into
~/.claude/skills/flake-triage-yschimke/β the SKILL.md plus any scripts, reference docs, or templates the skill ships with. Safe default: works for every skill.Faster alternative (instruction-only skills)
Skips the clone and grabs only the SKILL.md file. Don't use this if the skill ships Python scripts, reference markdowns, or asset templates β they won't be downloaded and the skill will fail when it tries to load them.
Quick install (SKILL.md only)Sign up to copy - Restart Claude Code.
Quit and reopen Claude Code (or any other agent that loads from
~/.claude/skills/). New skills are picked up on startup. - Just ask Claude.
Skills auto-activate when your request matches the skill's description β no slash command needed. Trigger phrases live in the skill's own frontmatter; you can read them in the βWhat this skill doesβ section above.
Prefer to read the source first? Open on GitHub.
When Claude uses it
Decide whether a preview the visual-diff bot flagged actually regressed or is simply nondeterministic, using a repeat-render oracle at a single commit. Use when a PR's preview diff reports a changed preview whose source the PR does not touch, or when a render, GIF or filmstrip is suspected of being unstable.
What this skill does
Is it a regression, or is the preview unstable?
A changed preview whose source the PR does not touch β clocks, timestamps, randomness, animation frames, network-loaded images β is instability to triage, not a regression to rubber-stamp and not a fix to write blind. The distinction is decidable in a few minutes, at one commit, with no reference to the base branch.
The oracle
Render the same preview N times at the same commit and compare the bytes. If two renders of one commit differ, nothing about the PR's diff is evidence of anything.
for i in 1 2 3; do
./gradlew :samples:android:composePreviewRender --rerun \
-PcomposePreview.filter=<PreviewFunctionName>
cp <module>/build/compose-previews/renders/<Preview>.png run-$i.png
done
md5sum run-*.png
--rerun is load-bearing: a render task goes UP-TO-DATE off its own outputs
(including .error.json sidecars from a failed run), so a plain re-invocation
compares a file against itself and always "passes".
A harness capture has the same oracle, and it is cheaper. The captures the
serve-preview-diff bot compares are Playwright shots of committed static
fixtures, so N runs cost seconds and need no JVM. Two harnesses write into that
one baseline set, and they do not share an invocation β find which one owns your
capture (git grep "<fixture>" -- '*/preview-harness/*') and use its own:
# preview-server/preview-harness β the `serve` web surfaces (serve-*, viewer-*).
cd preview-server/preview-harness
HARNESS_CHROMIUM=/path/to/chromium HARNESS_THEME=light \
npx playwright test -c playwright.config.mjs pages-snapshot.spec.mjs -g "<fixture>"
md5sum out/<capture>.light.png
# compose-preview-vscode/preview-harness β the VS Code panel's own fixtures.
# Run from `compose-preview-vscode/`, not from inside the harness directory, and build
# the webview bundle first: the fixtures load it, so a stale one moves pixels for
# reasons no commit explains.
cd ../compose-preview-vscode
node esbuild.webview.mjs
HARNESS_CHROMIUM=/path/to/chromium HARNESS_FIXTURE=<fixture> HARNESS_THEME=light \
npx playwright test -c preview-harness/playwright.config.mjs snapshot.spec.mjs
md5sum preview-harness/out/<capture>.light.png
The spec filenames differ (pages-snapshot.spec.mjs vs snapshot.spec.mjs), so
the two commands are not interchangeable. HARNESS_THEME narrows to one theme in
both; HARNESS_FIXTURE is the extension harness's own selector and is cleaner
than -g there.
There is no --rerun equivalent to remember: each invocation rewrites out/.
Read the hashes:
- All identical β the preview is deterministic at this commit. A diff against the base is real; go find it in the source.
- They differ β the preview is unstable. The bot flagging it on unrelated PRs is a symptom, not a coincidence. Continue below.
Three runs is usually enough to catch it; five identical runs is a reasonable bar for calling a fix proven.
Localising an unstable render
Once you know it moves, find what moves:
- Still PNGs β diff the pair as images rather than eyeballing them; the percentage of changed pixels and where they sit names the culprit (a clock face, one panel of a filmstrip, a gradient band).
- GIFs and filmstrips β decode to frames and compare frame by frame. A filmstrip
whose panels re-freeze somewhere new on each run differs in one to three panels
while the rest is byte-identical, which reads as "small diff" until you split it.
docs/design/evidence/filmstrip-determinism/is the worked example: two same-commit renders differing across 2.99% / 7.97% / 10.96% of the image depending on the pair, root-caused to panels freezing at unpinned points rather than at their labelled transition fractions. - Animated images in a harness capture β an APNG or GIF a spec swaps in plays
on its own clock, and
img.completesays decoded, not finished. A shot held only for the decode lands on an arbitrary frame.docs/design/evidence/motion-index-playing-determinism/is the worked example: eight same-commit runs, three distinct hashes, 0.11% of the image moving in one 19-row band per card. Decode the stub'sacTL/fcTLchunks to learn its frame count and delays, then hold for rest β poll the container's pixels until two reads spaced wider than one frame agree, rather than hard-coding the duration.
The usual sources, in rough order of frequency: an unpinned animation clock, a system-time variable, unseeded randomness, a network- or disk-loaded image, and layout that depends on measurement order.
Fixing it, and proving the fix
Pin the nondeterminism at its source β hold the clock, seed the randomness, pin the panel to its labelled fraction β rather than loosening a comparison threshold. Then:
- Re-run the oracle. Consecutive
--rerunrenders should be byte-identical; quote the shared hash. - Add the pixel test that keeps it pinned (
SharedElementFilmstripPixelTestis the pattern) so the next regression fails a test rather than a review. - Commit the evidence β the two differing before-renders and the stable after β
under
docs/design/evidence/<slug>/with a README stating the commit, the command, and the hashes. That is what makes the next occurrence a lookup instead of a rediscovery.
Reporting an unstable preview without the hashes is an assertion, not a triage. The hashes are cheap; include them.
Related: render-evidence for the capture mechanics, and the stability reference
in the published compose-preview-review
skill for how to flag instability on someone else's PR.
Related skills
Documentation Co-Authoring
anthropics
Guide structured workflows for writing docs, proposals, and technical specs collaboratively.
MCP Server Builder
anthropics
Build protocol servers that connect language models to external APIs and services.
Skill Builder & Optimizer
anthropics
Create, edit, and optimize Claude skills with performance testing and benchmarking.
Multi-Component Web Artifacts
anthropics
Build complex React artifacts with Tailwind CSS and shadcn/ui components.