Debugging with DAP Tools
Use this skill when you need runtime state — a value at a line, the
exception that actually threw, which code path ran — and reading the source
no longer answers the question.
Per-language launch config, source mapping, and expression syntax rules live
in reference files alongside this skill. Load the one matching your debuggee
before launching:
skill_view name="dap-debugging" file_path="references/go.md",
references/python.md, references/typescript.md, or
references/native.md.
The debugging loop
Debugging is a search: you are locating the first place observable state
diverges from intended state. Each iteration narrows the search.
- Reproduce and name the symptom. Run the program normally first
(
bash). Write down the visible wrong thing: wrong output, exception,
hang. The symptom anchors every breakpoint you set.
- State your one-line hypothesis. "The cache returns a stale entry
after eviction." One line, about data. If you cannot, read more code —
debugging without a hypothesis is random walking.
- Probe the boundary where the symptom appears. Breakpoint at the
observable wrong behavior (see placement rules below), inspect state.
Ask: does it match expectations here?
- Move backward along the data flow. State is wrong at your probe →
the corruption happened earlier. Find where the bad value was written
(assignment site, mutation, argument pass) and probe there. Repeat until
you reach a probe where state is correct — the divergence is between
the last correct and first incorrect probe.
- Minimize the step distance. Steps between your last-correct and
first-wrong probes may still be large. Step (
step_over / step_in)
through that window while re-evaluating the suspect expression. The
statement where the expression flips from right to wrong is usually the
bug — or one line away from it.
- Fix, then verify at the same breakpoint. Re-run to the same probe
with the fix and confirm state is now correct. Do not skip the
verification pass.
Give up on a thread after ~3 empty iterations: if three probes reveal no
new divergence, your hypothesis is stale — stop, re-read the code around
the data flow, form a new one, or report what you ruled out. Never
escalate into longer debugging sessions with fewer details each time;
that pattern produces cost without signal.
Where to set breakpoints
Placement is the highest-leverage decision. Prefer the probe that splits
the remaining search space, not the nearest line.
- The symptom line itself — the
return, print, write, or UI
update that surfaces the wrong behavior. Confirms inputs to the symptom.
- The decision point — the
if/switch/loop where behavior diverges.
Evaluating the condition plus its operands tells you which side of the
truth table the program took and why.
- Function entry of the wrong-returning function — check arguments
in against expectation. If arguments are already wrong, the bug is in
the caller; move up the stack (see
debug_backtrace frames).
- The mutation point — the line that writes the corrupted value:
assignment,
append/push, map insert, struct field set, callback
registration. If you don't know which of several writers is guilty, use
debug_watch_change on the expression to catch the changer red-handed.
- Error handling boundaries —
catch/except blocks, if err != nil
guards, .catch() chains where the error path diverges from the happy
path.
- Loop boundaries — first and last iteration where behavior changes;
evaluate the loop invariant and the index.
- Async/join boundaries — where a promise resolves, a goroutine joins,
a message is received: the value crossing the boundary is the suspect.
Avoid:
- Library/framework interiors you didn't write — probe your call into
them and their call back into you.
- Lines that execute millions of times (inner hot loops) — a conditional
probe via
debug_watch_change, or a breakpoint at the loop's exit, is
cheaper than stepping through.
- Comment-only or declaration-only lines — many adapters resolve those to
the next statement, which may be a different block than you expect.
Finding the interesting state
Once stopped, the question is "which of the 40 visible variables is the
interesting one?"
- Start from the hypothesis variable. The one you named in step 2.
debug_eval it first. Wrong → keep digging here. Right → the interesting
state is elsewhere; don't wander.
- Follow the data backward, not the code forward. For each value that
is wrong, ask who could have written it. Evaluate its immediate
producers (arguments at call site, fields of its container). This walks
you along the causality chain instead of dumping everything.
- Inspect structures, not just primitives. The bug is usually a field
you didn't think to check.
debug_locals expands nested fields one to
two levels; for deeper, eval explicit field paths (a.b.c); check the
language reference for expansion limits (dlv: 64 elements, 2 levels).
- Compare expected vs actual, explicitly. At every probe, write both
down. The shape of the discrepancy is the clue: off-by-one → loop
bounds; stale value → caching/ordering; truncated → pagination/limits;
wrong type → conversion site;
nil/None/undefined → initialization
or error swallow.
- Cross the frame boundary when the caller is suspect. Use
debug_backtrace, then inspect a higher frame's locals to see the
arguments that produced this state.
- When you don't know where the mutation happens, don't random-
breakpoint:
debug_watch_change({file, line, expression}) at a stable
line and let it report the changer.
- When you don't know what should run, probe the dispatcher: eval
the flag/route/method table that selects the code path at the decision
point.
- Structured values and
variables_reference. debug_locals and
debug_eval tag expandable values with [ref N]. Refs are valid only
while paused at the current stop and adapters may expire them on
resume — call debug_set_variable(..., variables_reference: N) or
expand children immediately, not after debug_continue.
Tool selection cheat sheet
One-shot (fire-and-forget) — default choice
debug_state_at({file, line, evaluated?}) — the workhorse probe:
breakpoints + run + locals + backtrace + evaluated expressions + output
in one result. Use it for loop steps 3–4 above.
debug_last_error({program}) — runs with exception breakpoints;
returns exception type/message plus locals + backtrace at the throw
site. Start here when the symptom is a crash.
debug_watch_change({file, line, expression}) — reports old vs new
for an expression across stops: "who changed this".
debug_trace_calls({program}) — marker parsing only: returns
records the program printed as __KIMCHI_TRACE__<json>. Nothing is
instrumented for you; no markers means "not instrumented" — add markers
or use debug_state_at.
Interactive (stateful) — multi-stop investigations
debug_launch({program}) → session_id.
debug_set_breakpoint({session_id, source, line}) — set several along
the suspected data flow at once; cheaper than relaunching.
debug_continue({session_id}) → next stop.
debug_locals / debug_eval / debug_backtrace — inspect.
step_over / step_in / step_out — narrow the window (loop step 5).
debug_terminate({session_id}) when done — always terminate; orphan
sessions hold adapter processes.
Rules: step_* auto-completes a pending launch. js-debug nested sessions
(startDebugging) route to the child transparently. Default timeout is
30 s; cold builds (first Go launch compiles the stdlib) need a larger
timeout_ms.
Failure playbooks
- "Debuggee terminated before reaching a stop" — breakpoint never hit:
check source mapping in the language reference (compiled paths vs build
paths), verify the line actually executes (the code may be inlined or
dead), or run
debug_last_error if the process crashed on the way.
- Breakpoint
verified: false — path mismatch between what you passed
and what the adapter sees; use the concrete path forms in the language
reference.
debug_eval errors but the variable exists — expression-syntax
limits in that adapter (dlv: no method calls, especially on unexported
fields). Simplify to a bare field path, or inspect via debug_locals.
- Values look optimized-out / locals missing (native, release builds)
— rebuild with debug info (
-g, debug profile); see references/native.md.
- Empty or contradictory sessions — terminate and report what you've
ruled out rather than launching again with less detail.
Humility rules
- The debugger shows state, not cause. Infer cause from the difference
between two probes, never from one.
- A failed hypothesis is useful output — record what you ruled out so the
next attempt (human or agent) doesn't repeat it.
- Do not present guesses from reading locals as confirmed behavior; say
"state at file:line showed X, which means Y because Z was W".
1---2name: dap-debugging3description: Diagnose runtime state with persistent DAP debugger sessions — breakpoints, expression eval, and stepping across Go, Python, TypeScript/JavaScript, and native binaries4---5# Debugging with DAP Tools67Use this skill when you need **runtime state** — a value at a line, the8exception that actually threw, which code path ran — and reading the source9no longer answers the question.1011Per-language launch config, source mapping, and expression syntax rules live12in reference files alongside this skill. Load the one matching your debuggee13before launching:14`skill_view name="dap-debugging" file_path="references/go.md"`,15`references/python.md`, `references/typescript.md`, or16`references/native.md`.1718## The debugging loop1920Debugging is a search: you are locating the first place observable state21diverges from intended state. Each iteration narrows the search.22231. **Reproduce and name the symptom.** Run the program normally first24 (`bash`). Write down the *visible wrong thing*: wrong output, exception,25 hang. The symptom anchors every breakpoint you set.262. **State your one-line hypothesis.** "The cache returns a stale entry27 after eviction." One line, about *data*. If you cannot, read more code —28 debugging without a hypothesis is random walking.293. **Probe the boundary where the symptom appears.** Breakpoint at the30 observable wrong behavior (see placement rules below), inspect state.31 Ask: does it match expectations here?324. **Move backward along the data flow.** State is wrong at your probe →33 the corruption happened earlier. Find where the bad value was *written*34 (assignment site, mutation, argument pass) and probe there. Repeat until35 you reach a probe where state is **correct** — the divergence is between36 the last correct and first incorrect probe.375. **Minimize the step distance.** Steps between your last-correct and38 first-wrong probes may still be large. Step (`step_over` / `step_in`)39 through that window while re-evaluating the suspect expression. The40 statement where the expression flips from right to wrong is usually the41 bug — or one line away from it.426. **Fix, then verify at the same breakpoint.** Re-run to the same probe43 with the fix and confirm state is now correct. Do not skip the44 verification pass.4546Give up on a thread after ~3 empty iterations: if three probes reveal no47new divergence, your hypothesis is stale — stop, re-read the code around48the data flow, form a new one, or report what you ruled out. **Never49escalate into longer debugging sessions with fewer details each time**;50that pattern produces cost without signal.5152## Where to set breakpoints5354Placement is the highest-leverage decision. Prefer the probe that splits55the remaining search space, not the nearest line.5657- **The symptom line itself** — the `return`, `print`, `write`, or UI58 update that surfaces the wrong behavior. Confirms inputs to the symptom.59- **The decision point** — the `if`/`switch`/loop where behavior diverges.60 Evaluating the condition plus its operands tells you which side of the61 truth table the program took and why.62- **Function entry of the wrong-returning function** — check *arguments63 in* against *expectation*. If arguments are already wrong, the bug is in64 the caller; move up the stack (see `debug_backtrace` frames).65- **The mutation point** — the line that writes the corrupted value:66 assignment, `append`/`push`, map insert, struct field set, callback67 registration. If you don't know which of several writers is guilty, use68 `debug_watch_change` on the expression to catch the changer red-handed.69- **Error handling boundaries** — `catch`/`except` blocks, `if err != nil`70 guards, `.catch()` chains where the error path diverges from the happy71 path.72- **Loop boundaries** — first and last iteration where behavior changes;73 evaluate the loop invariant and the index.74- **Async/join boundaries** — where a promise resolves, a goroutine joins,75 a message is received: the value crossing the boundary is the suspect.7677Avoid:78- Library/framework interiors you didn't write — probe *your* call into79 them and *their* call back into you.80- Lines that execute millions of times (inner hot loops) — a conditional81 probe via `debug_watch_change`, or a breakpoint at the loop's *exit*, is82 cheaper than stepping through.83- Comment-only or declaration-only lines — many adapters resolve those to84 the next statement, which may be a different block than you expect.8586## Finding the interesting state8788Once stopped, the question is "which of the 40 visible variables is the89interesting one?"9091- **Start from the hypothesis variable.** The one you named in step 2.92 `debug_eval` it first. Wrong → keep digging here. Right → the interesting93 state is elsewhere; don't wander.94- **Follow the data backward, not the code forward.** For each value that95 is wrong, ask *who could have written it*. Evaluate its immediate96 producers (arguments at call site, fields of its container). This walks97 you along the causality chain instead of dumping everything.98- **Inspect structures, not just primitives.** The bug is usually a field99 you didn't think to check. `debug_locals` expands nested fields one to100 two levels; for deeper, eval explicit field paths (`a.b.c`); check the101 language reference for expansion limits (dlv: 64 elements, 2 levels).102- **Compare expected vs actual, explicitly.** At every probe, write both103 down. The *shape* of the discrepancy is the clue: off-by-one → loop104 bounds; stale value → caching/ordering; truncated → pagination/limits;105 wrong type → conversion site; `nil`/`None`/`undefined` → initialization106 or error swallow.107- **Cross the frame boundary when the caller is suspect.** Use108 `debug_backtrace`, then inspect a higher frame's locals to see the109 arguments that produced this state.110- **When you don't know *where*** the mutation happens, don't random-111 breakpoint: `debug_watch_change({file, line, expression})` at a stable112 line and let it report the changer.113- **When you don't know *what* should run**, probe the dispatcher: eval114 the flag/route/method table that selects the code path at the decision115 point.116- **Structured values and `variables_reference`.** `debug_locals` and117 `debug_eval` tag expandable values with `[ref N]`. Refs are valid only118 while paused at the *current* stop and adapters may expire them on119 resume — call `debug_set_variable(..., variables_reference: N)` or120 expand children immediately, not after `debug_continue`.121122## Tool selection cheat sheet123124### One-shot (fire-and-forget) — default choice125126- **`debug_state_at({file, line, evaluated?})`** — the workhorse probe:127 breakpoints + run + locals + backtrace + evaluated expressions + output128 in one result. Use it for loop steps 3–4 above.129- **`debug_last_error({program})`** — runs with exception breakpoints;130 returns exception type/message plus locals + backtrace at the throw131 site. Start here when the symptom is a crash.132- **`debug_watch_change({file, line, expression})`** — reports old vs new133 for an expression across stops: "who changed this".134- **`debug_trace_calls({program})`** — marker parsing **only**: returns135 records the program printed as `__KIMCHI_TRACE__<json>`. Nothing is136 instrumented for you; no markers means "not instrumented" — add markers137 or use `debug_state_at`.138139### Interactive (stateful) — multi-stop investigations1401411. `debug_launch({program})` → `session_id`.1422. `debug_set_breakpoint({session_id, source, line})` — set several along143 the suspected data flow at once; cheaper than relaunching.1443. `debug_continue({session_id})` → next stop.1454. `debug_locals` / `debug_eval` / `debug_backtrace` — inspect.1465. `step_over` / `step_in` / `step_out` — narrow the window (loop step 5).1476. `debug_terminate({session_id})` when done — **always terminate**; orphan148 sessions hold adapter processes.149150Rules: `step_*` auto-completes a pending launch. js-debug nested sessions151(`startDebugging`) route to the child transparently. Default timeout is15230 s; cold builds (first Go launch compiles the stdlib) need a larger153`timeout_ms`.154155## Failure playbooks156157- **"Debuggee terminated before reaching a stop"** — breakpoint never hit:158 check source mapping in the language reference (compiled paths vs build159 paths), verify the line actually executes (the code may be inlined or160 dead), or run `debug_last_error` if the process crashed on the way.161- **Breakpoint `verified: false`** — path mismatch between what you passed162 and what the adapter sees; use the concrete path forms in the language163 reference.164- **`debug_eval` errors but the variable exists** — expression-syntax165 limits in that adapter (dlv: no method calls, especially on unexported166 fields). Simplify to a bare field path, or inspect via `debug_locals`.167- **Values look optimized-out / locals missing (native, release builds)**168 — rebuild with debug info (`-g`, debug profile); see references/native.md.169- **Empty or contradictory sessions** — terminate and report what you've170 ruled out rather than launching again with less detail.171172## Humility rules173174- The debugger shows state, not cause. Infer cause from the *difference*175 between two probes, never from one.176- A failed hypothesis is useful output — record what you ruled out so the177 next attempt (human or agent) doesn't repeat it.178- Do not present guesses from reading locals as confirmed behavior; say179 "state at file:line showed X, which means Y because Z was W".