Diagnosing Bugs
Discipline for tricky bugs. Only skip phases if you explicitly state why.
When exploring the codebase, first read
(if it exists) to build a clear mental model of relevant modules, and check the ADRs for the areas you will touch.
Phase 1 - Build a feedback loop
This is the core of this skill. Everything else is mechanical steps. If you have a tight pass/fail signal for the bug—one that turns red for this bug—you can find the cause; bisection, hypothesis-testing, and instrumentation all just consume this signal. Without it, staring at code for hours won’t save you.
Invest disproportionate effort here. Be tough, be creative, refuse to give up.
Ways to construct one - try them in roughly this order
- Failing test, placed at a seam that reaches the bug: unit, integration, e2e are all acceptable.
- Curl / HTTP script, hitting a running dev server.
- CLI invocation, using fixture input, and diff stdout against a known-good snapshot.
- Headless browser script (Playwright / Puppeteer), driving the UI, and asserting on DOM/console/network.
- Replay a captured trace. Save real network requests / payloads / event logs to disk, and replay them into the code path in an isolated environment.
- Throwaway harness. Start the minimal subset of the system (one service, mocked dependencies), and trigger the bug code path with a single function call.
- Property / fuzz loop. If the bug is "sometimes wrong output", run 1000 random inputs and look for failure modes.
- Bisection harness. If the bug appears between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can use .
- Differential loop. Run old-version vs new-version (or two configurations) with the same input, then diff the outputs.
- HITL bash script. Last resort. If a human must click, use
scripts/hitl-loop.template.sh
to drive the human, keeping the loop structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
Tighten the loop
Treat the loop like a product. Once you have one loop, keep tightening it:
- Can I make it faster? (Cache setup, skip irrelevant initialization, narrow test scope.)
- Can I make the signal sharper? (Assert the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop; a 2-second deterministic loop is a tight debugging superpower.
Non-deterministic bugs
The goal isn’t a clean repro, but higher reproduction rate. Trigger the loop 100x, parallelize, add stress, narrow timing windows, inject sleeps. A 50%-flake bug can be debugged; 1% cannot. Keep increasing the reproduction rate until it’s debuggable.
When you genuinely cannot build a loop
Stop and state this explicitly. List what you’ve tried. Ask the user for: (a) access to a reproducible environment, (b) captured artifacts (HAR file, log dump, core dump, timestamped screen recording), or (c) permission to add temporary production instrumentation. Do not continue hypothesizing without a loop.
Completion criterion - a tight loop that goes red
Phase 1 completion criteria: the loop is tight and red-capable. You can point to one command (script path, test invocation, curl), and you’ve run it at least once (post the invocation and output), and it meets:
If you find yourself reading code to build theories before the command exists, stop; jumping straight to hypothesis is the failure this skill is designed to prevent. Without a red-capable command, there is no Phase 2.
Phase 2 - Reproduce + minimise
Run the loop. Watch it turn red—i.e., the bug appears.
Confirm:
Minimise
Once it’s red, shrink the repro to the smallest possible scenario that still turns red. Cut inputs, callers, config, data, and steps one by one, re-running the loop after each cut; only keep the load-bearing parts of the failure.
Why: A minimal repro narrows the hypothesis space for Phase 3 (fewer moving parts to suspect), and becomes a clean regression test in Phase 5.
Completion criterion: Every remaining element is load-bearing—removing any of them makes the loop turn green.
Do not proceed until you’ve reproduced and minimised.
Phase 3 - Hypothesise
Before testing any hypotheses, generate 3-5 ranked hypotheses. A single hypothesis anchors you to the first plausible idea.
Each hypothesis must be falsifiable: state what prediction it makes.
Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you can’t state the prediction, it’s a vibe; discard or refine it.
Show the ranked list to the user before testing. Users often have domain knowledge that can immediately reorder it ("we just deployed a change to #3"), or know which hypotheses have already been ruled out. Cheap checkpoint that saves massive time. Don’t block on this; if the user is AFK, proceed with your ranking.
Phase 4 - Instrument
Each probe must map to a specific prediction from Phase 3. Change only one variable at a time.
Tool preference:
- Debugger / REPL inspection, if the environment supports it. One breakpoint is worth ten logs.
- Targeted logs, placed at boundaries that distinguish hypotheses.
- Never "log everything and grep".
Add a unique prefix to each debug log, e.g.,
. Cleanup at the end can grep them all at once. Untagged logs will linger; tagged logs must be deleted.
Perf branch. For performance regressions, logs are usually not helpful. Instead, first establish a baseline measurement (timing harness,
, profiler, query plan), then bisect. Measure first, then fix.
Phase 5 - Fix + regression test
Write the regression test before fixing, but only if a correct seam exists.
A correct seam is where the test can trigger the real bug pattern in the way it actually occurs at the call site. If the only available seam is too shallow (the bug requires multiple callers, but the test has only a single caller; a unit test can’t replicate the chain that triggers the bug), the regression test there will give false confidence.
If no correct seam exists, this is a finding in itself. Document it. The codebase architecture prevents you from locking in the bug. Flag it for the next phase.
If a correct seam exists:
- Turn the minimised repro into a failing test at that seam.
- Watch it fail.
- Apply the fix.
- Watch it pass.
- Re-run the Phase 1 feedback loop against the original (unminimised) scenario.
Phase 6 - Cleanup + post-mortem
Must do before declaring completion:
Then ask: What could have prevented this bug? If the answer involves architecture changes (no good test seam, tangled callers, hidden coupling), pass this to the
/improve-codebase-architecture
skill with specific details. This suggestion should come after the fix, not before; you know much more now than you did at the start.