systematic-debugging
For when you are actively fixing a bug, not documenting it. Replaces guess-and-check with a loop that isolates the cause before touching code.
Distinct from identify-bug: that skill documents a bug for someone else to fix and stops there. This one drives the fix. Use identify-bug to hand off; use this to resolve.
When to use
- A test or endpoint fails and the cause is not obvious.
- You have already tried one fix and it did not work (this is the strongest signal to switch to a systematic loop).
- Behavior differs between environments (local vs staging) or between similar code paths.
Phase 1 — Reproduce
Get a reliable, minimal reproduction before changing anything.
- Reduce to the smallest input/steps that still fail.
- If it only reproduces in staging/prod, use
obs-logsto pull the evidence; do not guess from a description. - If you cannot reproduce, you cannot verify a fix — invest here first.
Write the reproduction down (a failing test is ideal — see test-driven-development).
Phase 2 — Isolate
Narrow where the failure originates before asking why. In this stack, instrument at the boundaries data crosses:
| Boundary | What to check |
|---|---|
| Gateway controller | Is the request shape correct on arrival? Log the DTO. |
@MessagePattern RPC | Is the payload intact across the service hop? |
| Service layer | Is the business input correct before the DB call? |
| Mongo / Aurora | Is the query/filter what you think? Log the actual filter + result count. |
Bisect: confirm the data is correct at boundary N, wrong at boundary N+1. The bug lives between them. This turns "somewhere in the flow" into "these 20 lines".
Phase 3 — Hypothesize + instrument
State a specific, falsifiable hypothesis: "the filter drops the communityId because it's a string, not a number." Then add the instrumentation that would prove or disprove it (a log line with the actual value + type). Run. Read the evidence.
Do not apply a fix yet. Confirm the hypothesis first. A confirmed cause makes the fix obvious; an unconfirmed one makes it a guess.
Phase 4 — Fix + verify
- Apply the minimal fix that addresses the confirmed cause.
- Verify against the Phase-1 reproduction: it must now pass.
- Remove temporary instrumentation (or convert genuinely useful lines to proper
this.loggercalls per the error-logging rules). - If you wrote a reproduction test, keep it as a regression guard.
The 3-attempt stop
After 3 failed fix attempts, STOP. Do not try a 4th variation. Three failures mean the model of the problem is wrong, not the fix. When you hit this:
- Discard the assumptions from the failed attempts — write them down as "ruled out".
- Return to Phase 2 and re-isolate with fresh instrumentation.
- If still stuck, escalate: hand off via
identify-bugwith the ruled-out list, or ask a domain agent (payments, auth-security, database) for the subsystem you're stuck in.
Repeated failure is a signal to change approach, not to try harder at the same one.
Red-flags table
| Rationalization | The discipline |
|---|---|
| "I think I know what it is, let me just fix it" | State the hypothesis, instrument, confirm — then fix. |
| "Let me try changing this and see" | That's attempt-and-pray. Isolate first (Phase 2). |
| "It works now, not sure why" | You did not find the cause. Re-run the reproduction 3× and confirm you understand the fix. |
| "One more fix idea" (after 3 tries) | Stop. The problem model is wrong. Re-isolate. |
| "I'll read the whole log" | grep for the boundary you're bisecting; delegate a wide log search to a subagent. |
Token note
Instrumentation output and log dumps are heavy. When a phase produces large output (a full log tail, a wide query result), run it in a subagent per delegate-heavy-context so only the relevant evidence returns to the main thread.