debug — find the cause before you touch the fix

SkillDev tools

Use when something is broken — a failing or flaky test, crash, wrong result or regression — and the root cause must be reproduced and proven before any fix. On-demand; callable mid-`implement`. NOT a feature to spec or build (that is `specify`/`implement`), NOT the lint/test gate (that is `verify`), NOT adversarial diff reading (that is `review`).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the debug — find the cause before you touch the fix skill

What this skill tells your AI

The instructions your AI receives, as published by ericrisco/rsc-harness in skills/debug/SKILL.md and read by ahel’s review.

debug is the on-demand diagnosis discipline of the rsc-sdd chain. Something is broken — a test went red, a crash landed, a result is wrong, a regression appeared — and the strongest pull in the room is to change code until the symptom disappears. That pull is the enemy. A symptom that vanishes under a guessed edit usually moved; it rarely died. This skill replaces guess-and-patch with a short, evidence-driven loop that ends only when one confirmed cause has been named, fixed, and proven gone.

The one rule everything else serves: no fix before a reproduced, isolated, confirmed cause. If you cannot make the bug happen on demand, you cannot know you fixed it — you can only know the symptom stopped showing, which is not the same thing.

This is a process skill: it owns the method, never the instruments. The test runner, debugger, race detector and profiler belong to whichever stack the failure lives in — pull them from the delegation table below.

Model tier — heavy (opt-in routing)

This phase's default model tier is heavy — root-cause diagnosis is deep reasoning. Routing is off unless models.enabled: true in 02-DOCS/wiki/sdd/config.yaml. When on: resolve this phase's tier (models.overrides wins over models.phases), map it to a model via models.tiers, and apply per ../sdd/references/model-routing.md — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any Task/parallel subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.

Read the room first (accompaniment dial)

Before diagnosing, read 02-DOCS/wiki/harness/user-profile.md for the technical + accompaniment level and match it; with no profile yet, assume non-technical — narrate the reasoning plainly and never apply a behavior-changing fix without a quick confirm. The method never changes with the dial — the volume does.

LevelWhile diagnosing you show…Questions you ask
L0 tersethe confirmed cause and the one-line fix, once foundnone unless you need a missing repro detail
L1 briefthe cause + one line of why it produced this symptomonly what you can't observe yourself (e.g. exact error text)
L2 decisionseach step's finding (repro, the half that isolated it, the cause)confirm before a fix that changes behavior beyond the bug
L3 fullnarrate the whole loop, teach the binary-search reasoning aloudask to contextualize the environment, recent changes, expectations

The loop — five steps, never skipped, never reordered

REPRODUCE   → make the bug happen on demand. A reliable repro (or a quantified flake rate) is the
              entry ticket. No repro → you are not debugging yet, you are guessing. Capture the
              exact command, input, env, and the verbatim error/stack.
ISOLATE     → binary-search the cause. Halve the surface each step — git bisect across commits,
              comment/branch to split code paths, remove inputs until the minimal failing case
              remains. Change ONE variable at a time; note what each change does to the symptom.
HYPOTHESIZE → from the isolated evidence, state ONE falsifiable cause: "X happens because Y, and if
              so, changing Z will flip the result." A hypothesis you can't disprove isn't one.
FIX         → make the smallest change that addresses the *cause* (not the symptom). First, encode
              the bug as a failing test (it should now go red for the real reason) — then fix until
              it's green. Treat the test as the proof the cause was real.
VERIFY      → re-run the repro: symptom gone. Re-run the new test: green. Re-run the surrounding
              suite: still green (no new red). For a flake, run it enough times to show the rate
              dropped to zero. Only now is it fixed.

Reproduce — the entry ticket

A bug you cannot reproduce is not a bug you can fix; it's a rumor. Pin it down:

  • The exact invocation (command, request, UI steps), the input that triggers it, the environment (OS, versions, env vars, branch/commit), and the verbatim error + stack trace — not a paraphrase.
  • For a flake, reproduction means a rate: run it N times, record k/N failures. "Intermittent" is a measurement to take, not a property to accept. A flake almost always means shared state, order dependence, a real race, time/timezone, or network — name which.
  • If you genuinely cannot reproduce, that is the finding. Say so, gather more signal (logs, a failing CI run, the user's exact steps), and do not apply a speculative fix to a bug you can't trigger.

Isolate — binary-search the surface

The cause is somewhere in a large space; cut it in half, repeatedly, with evidence.

  • Across history: git bisect between a known-good and known-bad commit to land on the exact change that introduced it. Let the repro be the bisect's good/bad oracle.
  • Across code: disable/short-circuit half the suspect path; see which half keeps the symptom.
  • Across inputs/data: shrink the failing input to the minimal case that still fails.
  • One variable per step. Two changes at once and you've learned nothing about either. Write down what each step did to the symptom — the trail is the diagnosis.

Delegate the stack-specific tooling (debugger, race detector, profiler, query plan) to the stack skill below; debug decides what to halve, the stack skill provides the instrument.

Hypothesize → Fix the cause, not the symptom

State the cause as a sentence you could be wrong about, then act on it:

  • Fix the cause, not the symptom. A try/except that swallows the error, a retry that hides a race, a sleep that papers over an ordering bug, a bumped timeout — these relocate the symptom and leave the cause armed. If your fix doesn't reference the cause you named, it's a patch on a guess.
  • Encode the bug as a test first. The failing test that reproduces the bug is your regression guard; watch it go red for the real reason, then make it green. A fix with no test means the next change can resurrect the bug silently.
  • Keep the change minimal. Resist "while I'm here" refactors — they confound the verification and bury the one line that mattered.

Verify — the symptom and the test, both

The fix is unproven until the original repro no longer fires and the new test is green and the surrounding suite stayed green. For a flake, re-run enough times to show the failure rate is zero, not merely lower. Then hand the whole-gate re-run (lint/type/full suite/audit) to verify — that gate, not this skill, is what licenses the word "done".

Delegating the stack tooling (don't reinvent the instruments)

debug owns the loop; the stack skill owns the debugger, the flake mechanics, and the profiler.

Stack / layerWhere the instruments liveWhat you pull
FastAPI / async Python../fastapi/references/testing.mdpytest -x --lf, pdb/breakpoint(), async task/race traps, transactional-rollback fixtures to kill state bleed
Go services../go/references/testing.mdgo test -race, -run/-count=1 to force a flake, delve, pprof, errors.Is/As unwrapping
Next.js / React../nextjs/references/testing.mdVitest --no-isolate/.only, Playwright trace viewer, RSC vs client boundary errors, hydration mismatches
Flutter / Dart../flutter/references/testing.mdflutter test --plain-name, widget pump/settle timing, DevTools, golden diffs
Postgres / data layer../postgresdb/SKILL.mdEXPLAIN ANALYZE, isolation-level/locking races, constraint violations, migration-order bugs
Security-shaped failure../secure-coding/SKILL.mdauth/authz edge cases, injection, secret/leak paths surfacing as "weird" failures

If the failure spans two stacks (a Next.js call into a FastAPI endpoint), reproduce at the boundary first — isolate which side actually fails before you open either stack's debugger.

Log the diagnosis (the 02-DOCS trail)

When the cause is non-obvious — a race, an order dependency, an environment-only failure, a regression a reviewer would otherwise have to rediscover — append a short entry to 02-DOCS/wiki/sdd/decisions.md (append-only; create it if absent and add a row to the root CLAUDE.md ## Knowledge map under the sdd/ topic). One entry:

## YYYY-MM-DD — bug: <symptom in five words>  (feature: <slug>)
Repro      — the exact command/input that triggered it
Cause      — the ONE confirmed root cause (not the symptom)
Fix        — the minimal change + the regression test that guards it
Why missed — what let it through, so the class of bug doesn't recur

Skip the trivial ones (a typo'd variable). Log the cause a future debugger would pay to know.

Anti-patterns

Anti-patternWhy it fails
"I see the likely line — let me just change it and see."That's guess-and-patch. Reproduce first; a fix to an untriggered bug proves nothing.
"It's intermittent, you can't really reproduce it.""Intermittent" is a rate to measure (k/N), not an excuse. Flakes have causes: state, order, races, time.
"Wrapping it in try/except makes the error go away."You hid the symptom and left the cause armed. Fix the cause, not the crash site.
"Adding a sleep/retry/bigger timeout fixes the flake."It relocates the race. Name the shared state or ordering bug; fix that.
"I'll fix it now and add a test later if there's time."The bug-reproducing test IS the fix's proof. No red-for-the-right-reason test = unproven fix.
"I changed three things and now it works."You can't say which mattered or why. One variable per isolate step; revert the rest.
"The repro's gone, ship it."Gone how? Re-run the repro AND the new test AND the suite. Symptom-absent ≠ cause-dead.
"It works on my machine, so it's fixed."Environment IS a variable. Reproduce where it actually fails (CI, prod-like) before claiming done.
"This bug is obvious, skip the loop."Obvious causes are the ones that turn out to be a second bug masking the first. Run the loop.

Red flags — stop and re-route

  • The "bug" is a spec contradiction or unclear requirement, not a defect → route to clarify / analyze; debugging won't fix an ambiguity.
  • The fix grows past the cause (you're refactoring "while you're here") → split it out; keep the fix minimal and verifiable. Improvements go through implement.
  • You're tempted to disable, skip, or delete the failing test to make the suite green → that's the bug winning. Never ship around a red test; diagnose it.
  • You "fixed" it but can't point to the cause you named → you patched a symptom. Resume at HYPOTHESIZE.
  • The cause violates the constitution (e.g. it only "works" by breaking a quality bar) → surface it; don't smuggle a violation in under the banner of a bugfix.

Checklist (copy per bug)

- [ ] REPRODUCE: exact command/input/env + verbatim error captured; repro reliable (or flake rate k/N measured)
- [ ] ISOLATE: surface halved with evidence (bisect / code split / minimal input); ONE variable per step
- [ ] HYPOTHESIZE: one falsifiable cause stated ("X because Y; changing Z flips it")
- [ ] FIX: bug encoded as a failing test (red for the RIGHT reason); smallest change to the CAUSE
- [ ] VERIFY: repro no longer fires; new test green; surrounding suite still green; flake rate → 0
- [ ] Non-obvious cause logged to 02-DOCS/wiki/sdd/decisions.md
- [ ] Handed the whole-gate re-run back to verify; resumed implement where the failure interrupted it

Where you are in the chain

debug is on-demand, callable from any phase — most often pulled in mid-implement when a test fails for a reason you don't understand, or by verify when its gate reports a failure it won't diagnose. It is not a fixed step in the line constitution → specify → clarify → plan → tasks → analyze → implement → verify → review → ship.

Next: when the one confirmed cause is fixed and proven gone, hand back. Return to implement to resume the interrupted task, or to verify to re-run the full gate and let evidence — not the relief of a quiet terminal — declare the work done.

Signals

GitHub stars
82
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
debug-ericrisco
Source
github.com/ericrisco/rsc-harness