The canon for serious errors. Skip phases only when clearly justified.
While exploring the codebase, use the project's domain glossary to build a clear mental model of the relevant modules, and check the ADRs in the area you are touching.
Phase One — Build a feedback loop
This is where the skill is. Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the error, you will find the cause—bisection, hypothesis testing, and detection all just consume that signal. If you don't have such a signal, no amount of staring at the code will save you.
Invest disproportionate effort here.Be proactive. Be creative. Refuse to give up.
Construction methods—try roughly in this order
- Failing testat any seam that can reach the error—unit test, integration test, end-to-end test.
- Curl / HTTP scriptagainst a running dev server.
- CLI invocationwith fixed input, diffing stdout against a known-good snapshot.
- Headless browser script(Playwright / Puppeteer)—drive the UI, assert on DOM/console/network.
- Replay captured traces. Save real network requests/payloads/event logs to disk; replay it through the code path in isolation.
- One-off harness. Start a minimal _subset_ of the system (one service, mocked dependencies), exercising the error code path through a single function call.
- Property/fuzz loop. If the error is "sometimes outputs wrong", run 1000 random inputs and look for a failure pattern.
- Bisection harness. If the error appears between two known states (commit, dataset, version), automate "start at state X, check, repeat" so that you can
git bisect runit. - Diff loop. Run the same input through the old version and the new version (or two configurations) and diff the output.
- HITL bash script. Last resort. If someone has to click, use
scripts/hitl-loop.template.shto drive _them_, so that the loop is still structured. The captured output is fed back to you.
Build the right feedback loop, and the error is already 90% fixed.
Iterate on the loop itself
Treat the loop as a product. Once you have _one_ loop, ask:
- Can I make it faster? (Cache setup, skip irrelevant initialization, narrow the test scope.)
- Can I make the signal clearer? (Assert on the specific symptom, not "no crash".)
- Can I make it more deterministic? (Pin time, seed the RNG, isolate the filesystem, freeze the network.)
A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.
Non-deterministic errors
The goal is not a clean reproduction, butHigher reproduction rate. Loop the trigger 100 times, parallelize, add stress, shrink the time window, inject sleeps. A 50% flaky bug is debuggable; 1% is not—keep raising the ratio until it is debuggable.
When you truly cannot build a loop
Stop and state it explicitly. List what you tried. Ask the user for: (a) access to any environment that reproduces it, (b) captured artifacts (HAR files, log dumps, core dumps, timestamped screen recordings), or (c) permission to add temporary production instrumentation. Without a loop,do notproceed on assumptions.
Do not enter Phase 2 before you believe you have a loop.
Phase 2 — Reproduce
Run the loop. Observe the bug appear.
Confirm:
- The loop produces theuser-described failure mode—not a different failure that happens to be nearby. Wrong bug = wrong fix.
- The failure is reproducible across multiple runs (or, for non-deterministic bugs, at a high enough rate to debug).
- You have captured the exact symptom (error message, wrong output, slow timing) so that later phases can verify whether the fix actually resolves it.
Do not continue before you reproduce the bug.
Phase 3 — Hypothesis
Before testing any hypothesis, generate3-5 ranked hypotheses. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must befalsifiable: state the prediction it makes.
Format: "If is the cause, then <change Y> will make the bug disappear / <change Z> will make it worse."
If you cannot state a prediction, the hypothesis is a feeling—discard or sharpen it.
Show the ranked list to the user before testing. They often have domain knowledge that can immediately re-rank it ("we just deployed a change to #3"), or know hypotheses they have already ruled out. Cheap checkpoint, big time saver. Do not block—if the user is away, continue with your ranking.
Phase 4 — Detection
Each probe must map to a specific prediction from Phase 3.Change one variable at a time.
Tool preference:
- Debugger/REPL inspectionif the environment supports it. One breakpoint beats ten logs.
- Targeted logsat the boundaries that distinguish hypotheses.
- Never "log everything and grep".
Tag each debug log with a unique prefix, for example [DEBUG-a4f2]. The final cleanup becomes a single grep. Untagged logs survive; tagged logs die.
Performance branch. For performance regressions, logs are usually the wrong tool. Instead: establish a baseline measurement (timing harness,performance.now(), profiler, query plan), then bisect. Measure first, fix later.
Phase 5 — Fix + Regression Test
Write the regression testbeforethe fix—but only when there is acorrect seam.
The correct seam is when the test exercises thereal bug patternplace. If the only available seam is too shallow (a single-caller test when the bug requires multiple callers, a unit test that cannot replicate the chain that triggers the bug), a regression test there will give false confidence.
If the correct seam does not exist, that itself is a finding. Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If the correct seam exists:
- Convert the minimized reproduction 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 (non-minimized) scenario.
Phase 6 — Cleanup + Post-mortem
Required before declaring completion:
- The original reproduction no longer reproduces (re-run the Phase 1 loop)
- The regression test passes (or the missing seam is documented)
- All
[DEBUG-...]probes have been removed (grepprefix) - One-off prototypes have been removed (or moved to a clearly marked debug location)
- The hypothesis about why the result is correct is stated in the commit/PR message — so the next debugger can learn from it
Then ask: what would prevent this bug? If the answer involves architectural changes (no good test seams, entangled callers, hidden coupling), hand the specifics over to the /improve-codebase-architecture skill. Put the fix in placeafterwardMake the suggestion, not before — you now have more information than you did at the start.