Debug Mode
Most debugging fails because the agent guesses from source code and ships a plausible fix that does not address the real cause. This skill forbids that. No fix may be proposed until runtime evidence has confirmed a specific hypothesis.
The loop: hypothesize → instrument → reproduce → read evidence → fix → verify → clean up.
Everything runs through one dependency-free CLI:
scripts/debugctl.py
Set DBG="python3 <abs path to>/scripts/debugctl.py" once and reuse it. Run it from inside the project being debugged.
Non-negotiables
- Hypotheses before probes. A probe exists to discriminate between competing explanations. If you cannot say which hypothesis a probe kills, do not add it.
- Never guess from source once instrumented. If zero logs arrived, run
doctorand fix the pipeline. Do not fall back to reading code and calling it a root cause. - The user reproduces, not you — unless they have handed you a command that triggers the bug. Stop and wait at the repro gate.
- A rejected hypothesis is a result. Record it. Do not quietly drop it.
- The session is not done until
verify-cleanexits 0. Leaving probes in the tree is the single most common failure of this style of debugging. - Probes must be inert: never block, never throw, never mutate program state, never swallow an exception belonging to the code under test.
Phase 0 — Open an isolated session
$DBG start --goal "<one-line bug description>"
Prints a session id, a sentinel (e.g. DBG_A1B2C3D4), an ingest URL, and a log file. Everything is scoped to .debugsessions/<session-id>/ and auto-gitignored. Concurrent sessions get separate ports, separate logs, and separate sentinels, so they never interfere.
Flags that matter:
| Situation | Flag |
|---|---|
| Android emulator | --host 10.0.2.2 |
| Docker / compose | --host host.docker.internal |
| Physical device | --host <your LAN IP> |
| No network, sandbox, or bind denied | --no-server (file sink) |
If start reports that listening sockets are forbidden, re-run with --no-server — the rest of the loop is identical.
Phase 1 — Generate competing hypotheses
Read the relevant code first. Then record 3–5 hypotheses that disagree with each other. One hypothesis is not a hypothesis, it is a hunch.
$DBG hypo add "<what is actually wrong>" --signal "<the log line that would confirm or reject this>"
The --signal is the contract. Writing it forces the probe design: if you cannot name the observation that would settle the question, the hypothesis is too vague to test.
Aim for hypotheses at different layers — bad input, wrong branch taken, wrong order of operations, stale or shared state, boundary/type coercion, async timing. See references/hypotheses.md.
Phase 2 — Instrument
Get a correct emitter for the language, already carrying the right URL and sentinel:
$DBG snippet python # with notes and a usage example
$DBG snippet python --helper # just the helper block, safe to pipe into a file
Languages: python node browser typescript go java kotlin swift ruby php rust csharp c cpp shell dart elixir file, plus aliases (js, ts, react, android, ios, flutter, bash, golang, dotnet, …).
Read the NOTE the snippet prints — several runtimes send detached, and a short-lived process can exit before the probe lands.
Paste the helper once, then place one probe per hypothesis at the decision point that discriminates it. Rules:
Every probe line must contain the sentinel — that is how cleanup finds it. Multi-line helpers go between
<SENTINEL>:beginand<SENTINEL>:end.Put every probe on its own line. Cleanup deletes sentinel-bearing lines in full, so a probe appended to a line that also holds program logic would take that logic with it. Never do this:
if (user.plan === "free") { return (_dbg("H1","b",{x}),"basic"); } // DBG_XXXX ← deletes the branchExpand the block and give the probe its own line instead.
cleanuprefuses (exit 3) when it detects this and leaves the file untouched, but it is far cheaper to avoid.Tag every probe with its hypothesis id (
H1,H2, …).Log values, not prose: the actual variable, the branch taken, the length, the id, the timestamp.
"got here"proves nothing.Capture both sides of a comparison — the expected and the actual.
Probe the boundary and the interior, so a silent probe tells you the code never reached it.
Then record what you touched:
$DBG instrumented # lists every line carrying this session's sentinel
Phase 3 — The reproduce gate
Write a marker so this attempt's evidence is separable from the last:
$DBG mark "repro-1"
Then give the user exact, numbered reproduction steps and stop. Tell them what you expect to see. Wait.
The two actions that drive every cycle
The user comes back with exactly one of these:
A. "The issue reproduced" → the bug happened, evidence was captured.
$DBG reproduced --note "<what the user observed>"
Go to Phase 4.
B. "The issue is fixed" → the bug no longer happens.
$DBG fixed --note "<what the user observed>"
Go to Phase 6 — cleanup. This is a terminal action: stop investigating and remove the instrumentation.
If reproduced reports 0 log entries, the probes never reached the sink. Run $DBG doctor and repair the pipeline before drawing any conclusion.
Phase 4 — Read the evidence
$DBG summary # which probes fired, which stayed silent
$DBG logs --since-last-mark # only this repro attempt
$DBG logs --hypothesis H2 # evidence for one hypothesis
$DBG logs --grep "user_id|null" # regex across the payload
summary lists silent hypotheses — probes that never fired. A silent probe is evidence: that code path did not execute. That frequently is the bug.
Adjudicate every hypothesis, quoting the actual log values:
$DBG hypo set H1 confirmed --evidence "tax_base=25.0 == subtotal while discounted=22.5"
$DBG hypo set H2 rejected --evidence "apply_discount returned 22.5, the correct total"
$DBG hypo set H3 inconclusive --evidence "probe never fired; path not reached"
If every hypothesis is rejected, that is progress, not failure. You have eliminated the obvious. Form a new round from what the logs did show, add probes, and return to Phase 3. Do not start guessing.
Phase 5 — Root cause and targeted fix
State the root cause in one sentence, grounded in a specific log line. Then make the smallest change that addresses that cause. Do not refactor, do not fix adjacent things you noticed, do not "improve" the code while you are in there.
Leave the instrumentation in place — you need it to verify.
Mark a fresh attempt and hand the user verification steps:
$DBG mark "verify-1"
Then wait for one of the two actions again. If the bug still reproduces, the logs now tell you why the fix missed; iterate.
Phase 6 — Clean up (mandatory)
$DBG cleanup # dry run: exactly what will be removed
$DBG cleanup --apply # removes only THIS session's sentinel lines
$DBG verify-clean # exits 0 only if the tree is clean
$DBG end # refuses to close while probes remain
cleanup backs up every modified file under the session directory first. It matches only this session's sentinel, so a concurrent debug session's probes are untouched.
Exit codes: cleanup returns 3 and changes nothing if any probe is mixed into a line of real code — hand-edit those lines to separate the probe, then re-run. verify-clean returns 2 while any probe remains, 0 when the tree is clean.
Read the final diff before reporting done. verify-clean proves the sentinel is gone; it does not prove the surrounding code is intact. Confirm the diff contains your fix and nothing else.
Finish by reporting: the root cause, the evidence that proved it, the hypotheses you rejected, and the diff you are shipping. Confirm verify-clean passed.
Command reference
| Command | Purpose |
|---|---|
start --goal "..." |
open an isolated session |
status / list |
current state / all sessions |
snippet <lang> [--helper] |
ready-to-paste probe for that language |
hypo add/set/list |
the hypothesis ledger |
mark <label> |
split one repro attempt from the next |
reproduced / fixed |
the two cycle actions |
logs / summary |
read and aggregate evidence |
instrumented |
list every probe in the tree |
doctor |
why no logs are arriving |
cleanup --apply / verify-clean |
remove probes, prove it |
stop [--all] / end |
stop one sink (or every one in the project) / close the session |
Global flags work on either side of the subcommand: --session <id> targets a specific session, --json gives machine-readable output, --root <dir> sets the project root.
When logs do not arrive
Run $DBG doctor first — it checks the sink, the health endpoint, the capture count, and whether probes are present, then prints targeted hints. Full matrix in references/transports.md.
The usual culprits: the app was never restarted after instrumenting; a browser page on https blocked from posting to an http sink; an Android emulator dialing 127.0.0.1 (itself) instead of 10.0.2.2; a container with no route to the host; or a process too short-lived for an async send to land — use --no-server and the file sink for that last one.
Further reading
references/hypotheses.md— how to generate hypotheses that actually discriminatereferences/instrumentation.md— probe placement patterns, and what to log for races, state corruption, and performancereferences/transports.md— getting logs out of browsers, emulators, containers, remote hosts, and CI