| name | diagnosing-bugs |
| description | A discipline for hard bugs, flaky tests, CI hangs, and performance regressions in this SDK. Use when the user says "diagnose" or "debug this", reports something broken/throwing/failing/hanging/slow, or a test is flaky. Builds a tight, red-capable feedback loop before hypothesizing. |
A discipline for hard bugs. Skip a phase only when you can say why.
The whole skill is Phase 1: get a loop that goes red on this bug. Everything after is mechanical once you have it. If you catch yourself reading code to form a theory before that loop exists, stop — jumping to a hypothesis without a red-capable loop is the exact failure this prevents.
For trivial bugs with an obvious fix and an obvious test, skip this and just write the failing test — this skill is for the bugs that resist.
Phase 1 — Build a feedback loop
A tight loop is fast, deterministic, and goes red on this bug. Build one and the bug is 90% solved; bisection, hypothesis-testing, and instrumentation all just consume it. Be aggressive and creative here — spend disproportionate effort.
Ways to construct one — try roughly in this order
- Failing test at the seam —
dart test path/to/test.dart or flutter test, unit or widget, at whatever seam reaches the bug. Tightest loop there is.
fakeAsync harness — for timer, microtask, and timing-dependent bugs (most flakes). Pin the clock via options.clock, elapse time deterministically.
testWidgets harness — for Flutter UI bugs: pump the widget tree, drive the gesture, assert on the tree (e.g. a hit-test or navigation regression).
- Integration test —
flutter test integration_test/... when the bug crosses the native (JNI/FFI) boundary, which cannot be faked (see packages/flutter/AGENTS.md).
- Replay a captured payload — save a real envelope / event / span / network payload to disk and replay it through the code path in isolation.
- Throwaway harness — a minimal
void main() that exercises the bug path with one call and mocked deps.
- Stress/repetition loop — for non-deterministic bugs: run the trigger 100×, parallelise, narrow timing windows, inject delays. The goal is a higher reproduction rate, not a clean repro.
- Differential loop — run the same input through two versions or two configs (e.g. before/after a dependency bump) and diff the output. For regressions that "appeared after X".
- Bisection harness — if it appeared between two commits, script the red/green check and
git bisect run it.
Tighten the loop
Once you have a loop, treat it as a product and tighten it:
- Faster — narrow the test scope, skip unrelated init.
- Sharper — assert the exact symptom the user described, not "didn't crash".
- More deterministic — pin the clock (
options.clock), use fakeAsync, seed any randomness, avoid real network/filesystem (per test-guidelines). A 2-second deterministic loop beats a 30-second flaky one.
For non-deterministic bugs the goal is a high enough reproduction rate to debug against — a 50% flake is debuggable, 1% is not. Keep raising the rate.
Completion criterion
Phase 1 is done when you can name one command — a test invocation or script path — that you have already run at least once (paste the invocation and its output), and that is:
No red-capable command, no Phase 2.
When you genuinely cannot build a loop
Say so explicitly, list what you tried, and ask the user for: access to an environment that reproduces it, a captured artifact (envelope dump, log, CI run, screen recording with timestamps), or permission for temporary instrumentation. Do not hypothesise without a loop.
Phase 2 — Reproduce and minimise
Run the loop, watch it go red. Confirm it produces the failure mode the user described — not a nearby one (wrong bug = wrong fix). Then shrink the repro to the smallest scenario that still goes red: cut inputs, callers, config, and steps one at a time, re-running after each cut. Done when every remaining element is load-bearing — removing any one turns it green. The minimal repro shrinks the hypothesis space and becomes the regression test.
Phase 3 — Hypothesise
Generate 3–5 ranked, falsifiable hypotheses before testing any — a single hypothesis anchors you on the first plausible idea. Each must state its prediction: "If X is the cause, then changing Y makes the bug vanish." If you can't state the prediction, it's a vibe — sharpen or discard it. Show the ranked list to the user before testing; they often re-rank it instantly ("we just changed #3"). Don't block on it if they're away.
Phase 4 — Instrument
Each probe maps to one prediction from Phase 3. Change one variable at a time.
- Prefer a debugger / breakpoint over logs where the env supports it. One breakpoint beats ten logs.
- Otherwise add targeted logs at the boundaries that distinguish hypotheses — never "log everything and grep".
- Tag every debug log with a unique prefix like
[DEBUG-a4f2] so cleanup is one grep.
- Performance regressions: logs are the wrong tool. Establish a baseline measurement (
Stopwatch, a timing harness, DevTools, the observatory), then bisect. Measure first, fix second.
Phase 5 — Fix and regression test
Write the regression test before the fix — but only at a correct seam, one that exercises the real bug pattern as it occurs at the call site (per test-guidelines). A too-shallow seam (a unit test that can't replicate the chain that triggered it) gives false confidence. If no correct seam exists, that is itself the finding — note it; the architecture is preventing the bug from being locked down.
With a correct seam: turn the minimised repro into a failing test, watch it fail, apply the fix, watch it pass, then re-run the Phase 1 loop against the original un-minimised scenario.
Phase 6 — Cleanup and post-mortem
Before declaring done:
Then ask: what would have prevented this bug? If the answer is architectural — no good test seam, tangled callers, a shallow module hiding the real bug behind it — hand off to the design-first skill with the specifics. Make that call after the fix is in, when you know the most.