| name | descartes-debug |
| description | Debug live Java applications through JDWP using Descartes MCP debugger tools. Use when the user asks to investigate runtime behavior, diagnose a Java bug, inspect threads or variables, set breakpoints, step through code, evaluate expressions, detect deadlocks, or prove a causal chain in either Descartes embedded mode or remote-proxy mode. |
Descartes MCP Debugger Skill
You are debugging Java applications via JDWP (Java Debug Wire Protocol) using 11 Descartes MCP tools. You operate like an IDE debugger — attaching to a live JVM, setting breakpoints, stepping through code, inspecting variables, and evaluating expressions — but you do it autonomously through tool calls.
Tool Reference
| Tool | Purpose | Key Operations |
|---|
debugger_session | Session lifecycle | start, stop, status, threads, suspend, resume, resume_all |
debugger_breakpoints | Breakpoint management | set, upsert, resolve_line, remove, remove_all, list, enable, disable |
debugger_step | Step execution | step_over, step_into, step_out |
debugger_threads | Thread list/control | list, inspect, suspend, resume, resume_all |
debugger_stacktrace | Stack frames | capture, capture_filtered, get_frame, get_current_frame |
debugger_variables | Variable inspection | get_variables, get_child_variables, get_static_fields |
debugger_evaluate | Expression evaluation | evaluate |
debugger_watch | Watch expressions | add, remove, remove_all, list, enable, disable, evaluate |
debugger_events | Event polling | wait, fetch, clear |
thread_analyzer | Thread analysis | thread_list, thread_inspect, thread_search, deadlocks, thread_dump |
object_inspector | Context object inspection | inspect, fields, methods, type, value |
Runtime Reality Checks
- Breakpoint
condition is accepted and stored, but current runtime behavior does not reliably enforce it as a hit filter. Treat it as metadata; filter manually with debugger_evaluate at breakpoint hits.
debugger.method_entry, debugger.method_exit, and debugger.exception events require corresponding JDI event requests. Current public debugger tools do not expose method/exception request setup.
object_inspector evaluates JShell expressions in the proxy JVM against the proxy's
shared context map — not in the debuggee. Breakpoints set in the debuggee will never
fire on code executed via object_inspector. The proxy context map is separate from any
application-level context in the debuggee. For inspecting state at a suspended frame in the
debuggee, use debugger_variables and debugger_evaluate.
Install Preflight (Run Once)
After copying/installing this skill, validate launcher dependencies:
.agents/skills/descartes-debug/scripts/preflight.sh
If scripts/launch-managed-nontty.sh is missing, preflight fails with exact remediation steps.
Quick Start Loop (Use This First)
When the user asks to debug behavior drift, follow this generic loop before deep-diving:
- Define two concrete statements: expected state and observed state
- Build a decision-gate map from code/logs (branch points, policy gates, budget gates)
- Set only 3-5 high-signal breakpoints on those gates
- Reproduce once and capture a Decision Snapshot at each hit
- Refine fast: if a breakpoint is noisy twice, move/disable it quickly
- Validate the causal chain from first divergence to final symptom
- Separate debugger-induced effects (pause/timeout) from product behavior
- Report root cause as: line + branch/predicate value + runtime state + causal chain
The Debugging Methodology
Follow these 10 steps for every debugging task:
Step 1: Understand the Problem
Read the user's description and classify it:
- Crash: Exception, NPE, StackOverflow, ClassCast
- Wrong result: Incorrect output, off-by-one, logic error
- Hang: Deadlock, infinite loop, blocked thread
- Performance: Slow execution, high CPU, memory growth
Before tooling, write:
- Expected state: what should happen (one sentence)
- Observed state: what actually happened (one sentence)
- First possible divergence: where the two could start to differ
Read the source code BEFORE setting any breakpoints. Examine the relevant class or method with the available file-reading tools. Know which lines are executable, where methods begin and end, and what the expected behavior should be.
Step 2: Detect Mode and Connect
Check session status first:
debugger_session(operation: "status")
Record whether you started the session. Inventory existing breakpoints, watches, and suspended threads before adding anything so cleanup preserves another client's debugger state.
Artifact parity check (required for jar-launched targets):
- Before attaching, verify your debug artifact is at least as new as the source you are inspecting.
- If source is newer than the launched jar/class artifact, rebuild and relaunch target first.
- If
resolve_line resolves to an unrelated method, assume stale bytecode/source mismatch and stop to rebuild+relaunch.
If no active session, start one:
debugger_session(operation: "start", jdwp_timeout: 10000)
Detect your operational mode — this determines how you trigger workloads:
- Embedded mode:
jshell_async is available. You have all tools (20+). You can trigger workloads programmatically.
- Proxy mode: Only debugger tools,
thread_analyzer, and object_inspector are available (11 tools). You must ask the user to trigger the workload, use curl/HTTP, or instruct manual action.
If debugger_session start fails with JDWP_CONNECTION_FAILED, triage in order:
- Is the target JVM running with
-agentlib:jdwp=...? If not, see "Launching a Debug Target" below.
- Are host and port correct? Embedded mode auto-detects; proxy mode configures JDWP host/port at proxy launch time (via
run-remote-proxy.sh --jdwp-host --jdwp-port), not via debugger_session start.
- Is the JDWP port reachable? Check firewall rules, bind scope (
address=*:5005 vs address=5005), and network path.
- Is another debugger already attached? JDWP allows only one client at a time.
Establish an event baseline immediately after connecting. This drains stale breakpoint events from any previous session and gives you a sequence cursor for later waits:
debugger_events(operation: "fetch", types: ["debugger.breakpoint_hit"], max_events: 100)
Store latest_sequence from the response. You will pass it as since_sequence when waiting for events in Step 6.
Step 3: Form a Hypothesis
Based on the problem classification and source code reading, hypothesize where the bug is:
- Which method is suspect?
- Which line(s) should you observe?
- What variable values would confirm or refute your hypothesis?
Use a lightweight hypothesis ledger:
H1: <expected relation> at <class:line> confirmed by <signal>
H2: ...
After each breakpoint hit, mark each hypothesis as confirmed, refuted, or inconclusive.
Step 4: Set Strategic Breakpoints
Always preflight with resolve_line before setting breakpoints on uncertain lines:
debugger_breakpoints(operation: "resolve_line", class_name: "com.example.MyClass", line_number: 42)
Then set the breakpoint:
debugger_breakpoints(operation: "set", class_name: "com.example.MyClass", line_number: 42)
condition is currently best treated as metadata, not guaranteed hit filtering. Use manual gating at hit time:
debugger_breakpoints(operation: "set", class_name: "com.example.MyClass", line_number: 48, condition: "i >= 98")
After a hit on that breakpoint:
debugger_evaluate(operation: "evaluate", thread_id: <tid>, expression: "i >= 98")
Prioritize breakpoints by quality:
- High-signal: captures a branch/gate/policy decision directly
- Medium-signal: captures inputs that feed a later decision
- Low-signal: generic state with no clear decision impact
Rules:
- Start with high-signal breakpoints only
- Keep <= 5 active breakpoints unless investigating concurrency
- If a breakpoint is low-signal on 2 hits, disable/refine it immediately
See references/breakpoint-strategy.md for strategic placement guidance by bug type.
Step 5: Trigger the Workload
CRITICAL: Never use synchronous jshell_repl for code that will hit a breakpoint. The breakpoint suspends the thread executing the MCP call, deadlocking the entire MCP channel.
Embedded mode — use jshell_async:
jshell_async(operation: "start", code: "new MyClass().myMethod()")
This returns immediately with a task_id.
Proxy mode — trigger externally:
- Ask the user: "Please trigger the operation now (e.g., send the HTTP request, click the button)"
- Use
curl through an available shell tool if the target exposes an HTTP endpoint
- The user exercises the application manually
Step 6: Wait for Breakpoint Events
Wait for a breakpoint hit using your sequence cursor (established in Step 2, or carried forward from the previous iteration's wait response):
debugger_events(operation: "wait", types: ["debugger.breakpoint_hit"], since_sequence: <latest_sequence>, timeout_ms: 30000)
Update latest_sequence from the response for the next iteration.
If the wait times out, check: Is the breakpoint active? Was the workload triggered? Is this the correct class/line/path? If you set condition, remember it does not reliably gate hits.
Do not assume timeout root cause from a paused run. Track timeout attribution explicitly:
debugger-influenced: long pauses/suspensions consumed available time budget
product-only: reproduced without meaningful debug pauses
See references/orchestration.md for the full async trigger + event polling pattern.
Step 7: Inspect the Suspended State
Once a breakpoint hits, inspect in this order:
1. Stack trace — understand where you are:
debugger_stacktrace(operation: "capture_filtered", thread_id: <tid>, exclude_patterns: ["java.*", "javax.*", "jdk.*", "sun.*"])
2. Variables — see local state (progressive disclosure):
debugger_variables(operation: "get_variables", thread_id: <tid>, frame_index: 0)
Only expand objects you need:
debugger_variables(operation: "get_child_variables", thread_id: <tid>, variable_reference: <ref>)
3. Evaluate expressions — test your hypothesis:
debugger_evaluate(operation: "evaluate", thread_id: <tid>, expression: "list.size()")
Check multiple frames by varying frame_index to see caller context.
Capture a Decision Snapshot at each high-signal hit before deep object expansion:
- Runtime context: request/execution ID, thread ID/name, class/method/line
- Decision inputs: booleans/enums/reason strings used by the current branch
- Resource inputs: counters/sizes/budgets/time windows used by the decision
- Decision outcome: which branch was taken and what action follows
- Hypothesis result: confirmed/refuted/inconclusive, plus next step
If these five are captured, resume unless you need one targeted step.
Step 8: Step and Iterate
Step through code to observe execution flow:
debugger_step(operation: "step_over", thread_id: <tid>)
debugger_step(operation: "step_into", thread_id: <tid>)
debugger_step(operation: "step_out", thread_id: <tid>)
After each step, re-inspect variables and evaluate expressions. Use watches for values you check repeatedly:
debugger_watch(operation: "add", expression: "sum", display_name: "Running total")
debugger_watch(operation: "evaluate", thread_id: <tid>)
Step 9: Resume and Repeat
When done inspecting at this breakpoint hit:
debugger_session(operation: "resume_all")
Default behavior is to resume all threads after inspection. For deliberate suspend_policy: "all" concurrency snapshots, keep pauses brief and still resume_all before leaving the loop.
Update your sequence cursor from the last event response, then loop back to Step 6 if you expect more breakpoint hits.
In embedded mode, check async task status:
jshell_async(operation: "status", task_id: "<task_id>")
Step 10: Diagnose and Report
Synthesize your findings:
- State the root cause — exact line, exact branch/predicate value, exact variable state
- Explain why the bug occurs
- Show the causal chain — first divergence -> intermediate gates -> final symptom
- Suggest the fix with specific code changes
- Clean up: resume all threads, remove only breakpoints and watches created during this run, and stop the session only if you started it. Use
remove_all only for a clean session you exclusively own.
Definition of done:
- Root cause is proven with concrete runtime state (not just suspected)
- At least one end-to-end causal chain is validated
- A regression target is identified (test or reproducible scenario)
The Critical Orchestration Pattern
Steps 4-9 above form an async-safe loop. The key constraint: MCP is synchronous, so a synchronous trigger that hits a breakpoint deadlocks the channel. The loop separates triggering (async) from inspection (sync polling).
For the full annotated walkthrough with exact tool parameters for each phase, timeout layer interactions, and error recovery patterns, see references/orchestration.md.
Variable Inspection Strategy
Use progressive disclosure to minimize noise:
- Start at frame 0:
get_variables at the top stack frame first
- Expand selectively: Only call
get_child_variables for objects you need to inspect
- Evaluate for computed values: Use
debugger_evaluate for list.size(), map.containsKey("x"), obj.getClass().getName() — faster than expanding
- Check caller context: Vary
frame_index (0 = top, 1 = caller, 2 = caller's caller) to see how arguments were passed
- Static fields: Use
get_static_fields with class_name for constants, singletons, counters
Expression fallback ladder (avoid repeated dead-end evaluations):
- Evaluate simple primitives/booleans first (
count, flag, x == null)
- If method resolution fails, cast explicitly (
((java.util.List)items).size())
- If still failing, switch to
get_variables + targeted get_child_variables
- Do not retry the same failing expression more than twice without changing approach
Thread Management Rules
- Default to
resume_all after inspection. If you intentionally hold suspend_policy: "all" for concurrency analysis, do it briefly and resume before continuing.
- Default
suspend_policy is "thread" — only the triggering thread suspends. This is correct for most debugging.
- Use
suspend_policy: "all" only for race conditions where you need to freeze the entire JVM.
- Use
suspend_policy: "none" for logging-only breakpoints (no suspension, just event).
- Find suspended threads:
debugger_threads(operation: "list", suspended_only: true)
- Check for accidentally suspended threads before concluding "the app is hung."
Causal Chain Validation
Do not stop after the first anomaly. Validate the chain:
- Identify the first divergent state
- Confirm downstream gate(s) consume that divergent state
- Confirm those gate outcomes produce the final symptom
If step 2 or 3 is missing, keep debugging; you have correlation, not root cause.
Timeout Attribution Rules
When timeouts are involved, classify them explicitly:
- Debugger-influenced timeout: observed while threads are paused/suspended long enough to consume runtime budget
- Product timeout: reproduced in a run with minimal/no debugger pauses
Required evidence:
- Wall-clock timing around breakpoints
- Application timeout/budget values at decision points
- A low-pause confirmation run before concluding product timeout root cause
Mode Detection
Determine your mode at session start:
Embedded mode indicators:
jshell_async tool is available
- Full tool set (20+ tools including profiler, monitoring, log files)
- Can trigger workloads programmatically
Proxy mode indicators:
- Only 11 tools available:
debugger_session, debugger_breakpoints, debugger_step, debugger_stacktrace, debugger_variables, debugger_evaluate, debugger_watch, debugger_events, debugger_threads, thread_analyzer, object_inspector
- No
jshell_async, no jshell_repl
- Must use alternative trigger strategies
object_inspector is context-scoped JShell inspection, not suspended-thread/frame inspection
Proxy mode trigger alternatives:
- Ask the user to perform the action that exercises the code
- Use
curl or HTTP client via Bash to hit an endpoint
- Have the user click a button, submit a form, or run a test
- Set breakpoints first, then tell the user to trigger
- If the app is idle with all threads in native frames (e.g.,
Unsafe.park,
ServerSocket.accept) and no HTTP endpoints, autonomous code triggering is not
possible. Tell the user: "All debuggee threads are in native frames with no reachable
endpoints — please trigger the operation manually so breakpoints can fire."
Launching a Debug Target
The target JVM must be started with JDWP enabled — it cannot be attached dynamically. If the user's application is not already running with JDWP, help them launch it.
Using the bundled launch script (recommended for agent-driven workflows)
Use the canonical repo script scripts/launch-managed-nontty.sh — a supervised non-TTY process launcher with signal forwarding and PID management. It is designed for agent-driven debug target startup.
scripts/launch-managed-nontty.sh \
--name myapp-debug \
-- java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \
-jar target/your-application.jar
If you copied this skill without the repo scripts/ directory, scripts/preflight.sh (and the wrapper) will fail fast with remediation steps. Fix by either:
- copying
scripts/launch-managed-nontty.sh into your repository, or
- setting
DESCARTES_LAUNCH_SCRIPT=/absolute/path/launch-managed-nontty.sh.
Options:
--name <name> — logical name for PID file (default: debug-target, stored at .pids/<name>.pid)
--pid-file <path> — custom PID file location
--cwd <dir> — working directory for the launched command
--json — emit machine-readable startup metadata (useful for automation)
This script requires non-TTY stdin/stdout/stderr (standard when launched by agents).
Using the Descartes embedded mode script
For local development with full Descartes tooling (debugger + JShell + hot reload + profiling):
scripts/run-with-hotreload.sh
This auto-builds the shaded JAR if needed, finds a free MCP port (9080-9084), and starts the Descartes embedded server with Java agent flags. You still need a separate JDWP-enabled target process — the embedded server connects to it via JDWP, the same way an IDE debugger does. Use scripts/launch-managed-nontty.sh or add -agentlib:jdwp=... to your application's launch command to start the target.
Using the Descartes remote proxy script
For debugging a remote or already-running JDWP target:
scripts/run-remote-proxy.sh --jdwp-host localhost --jdwp-port 5005 --mcp-port 9090
Defaults to localhost:5005 → MCP :9090. Use --auto-discover to scan for JDWP processes. Use --rebuild to force a fresh JAR build.
Manual JDWP launch (any Java application)
Add this JVM flag to any Java launch command:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
For JDK 21+ applications where you need virtual threads to appear in thread-list
snapshots, append includevirtualthreads=y:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005,includevirtualthreads=y
Keep breakpoints narrow on virtual-thread-heavy targets. Suspending a virtual
thread at a breakpoint pins its carrier while it is stopped, so broad breakpoints
can make a debug run appear stalled. When debugging Descartes itself, set
DESCARTES_FORCE_PLATFORM_THREADS=true or
-Dtools.executor.virtualThreads.enabled=false to run shared tool tasks on the
bounded platform-thread fallback.
For JDK 17+, also add:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
Key JDWP options:
suspend=n — JVM starts normally (use suspend=y to pause at startup, useful for debugging initialization code)
address=*:5005 — listen on all interfaces, port 5005 (use address=5005 for localhost-only)
Common Java Bug Patterns
Quick-reference for mapping bug types to debugging strategy:
| Bug Type | Where to Break | What to Inspect | Key Expression |
|---|
| Off-by-one | Loop body near boundary | Loop counter, bounds | i < n vs i <= n |
| NPE | One line before crash | The null candidate | variable == null |
| Integer overflow | Before the overflow op | Operands and result | (long)a * b |
| Wrong conditional | First line inside each branch | Which branch taken | The boolean condition |
| Collection issue | After .add()/.put() | Collection size/contents | collection.size() |
| Infinite loop | Inside body with hit count | Loop variable mutation | Counter or termination var |
| Deadlock | N/A — use thread_analyzer | Lock ownership | thread_analyzer deadlocks |
| Race condition | Shared state access, suspend_policy: "all" | Which threads access | Thread.currentThread().getName() |
| ClassCastException | Before the cast | Actual runtime type | obj.getClass().getName() |
| StackOverflow | Recursive method entry | Recursion depth | Stack frame count |
See references/java-debug-patterns.md for full playbooks.
Anti-Patterns
These rules are critical. Violating them causes deadlocks, frozen JVMs, stale conclusions, or false root causes.
-
NEVER use jshell_repl for code that hits a breakpoint. Deadlocks the MCP channel. Use jshell_async instead. (See Step 5.)
-
NEVER leave the session with threads paused. If you intentionally hold a global pause for concurrency inspection, still resume_all before continuing. (See Step 9.)
-
NEVER ignore since_sequence when polling events. Causes stale-event processing. Carry latest_sequence forward from every wait/fetch response. (See Step 6.)
-
NEVER use debugger_events clear as routine practice. Both fetch and wait already consume events — use cursor-based polling instead. Reserve clear for recovery. (See references/orchestration.md.)
-
NEVER set breakpoints without reading source code first. You need the fully qualified class name, executable line numbers, and method boundaries. (See Step 1 and Step 4.)
-
NEVER use breakpoint spray. Too many broad breakpoints produce noise and hide causal flow.
-
NEVER deep-dive object graphs before capturing decision variables. Record branch inputs/outcomes first.
-
NEVER repeat the same failing evaluation expression. Change strategy (cast, different frame, variable inspection).
-
NEVER treat timeout during paused debugging as proof of product timeout behavior. Run low-pause confirmation.
-
NEVER keep low-signal breakpoints active after repeated noisy hits. Disable or move them quickly.
-
NEVER stop at the first anomaly without validating downstream impact. Correlation is not root cause.
References