# Remote Debug

> Autonomous physics debugging against nudge.exe (app/scene-based) and nudge_tests.exe (unit-test pause points). Scientific method with deterministic repros, mechanical metrics, varied-input verification, and bias guards. Activate when the user asks to debug a physics bug end-to-end and wants the agent to drive from repro to verified fix.

- Skill: `randygaul/remote-debug` (Agent Skill)
- Install (CLI): `npx skillmds@latest add randygaul/remote-debug`
- Raw SKILL.md: https://api.skillmd.com/api/skills/randygaul/remote-debug/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: randygaul (https://skillmd.com/u/randygaul)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/randygaul/remote-debug

---


# Remote Debug -- Autonomous Physics Debugging

End-to-end loop: find a deterministic repro, isolate one variable at a time, measure against expectations, propose a minimal fix, and re-verify across many varied inputs. The agent drives; the human watches.

## When to activate

- User points at a bug and asks Claude to find + fix it.
- User says things like "debug this", "why is X happening", "find the repro", or invokes `/remote-debug <bug>`.
- A unit test is failing and the user wants pause-point inspection instead of printf spelunking.

## When NOT to activate

- The user wants a feature, refactor, or design discussion.
- The user is asking how an algorithm works — that is research, not debugging.
- You already have a verified repro and root cause and just need to implement the fix.

## Two modes

The viewer (`nudge_viewer.exe`) talks to either target over TCP and inspects memory via `ReadProcessMemory`. Type tables are sent by the engine on connect, so all command syntax is identical -- the difference is the simulation host.

| Mode | Target | When to use |
|------|--------|-------------|
| **App mode** | `nudge.exe` | Scene-based bugs: scene loads, interactive push/drag, rendering-tied repros, mouse-recording playback. |
| **Test mode** | `nudge_tests.exe --debug --break=<pattern>` | Unit-test bugs: a specific test fails and you want to pause at a known point and inspect state -- an alternative to printf debugging. |

Both modes support the same inspection commands (`summary`, `get`, `table`, `contacts`, `warm`, `filter`, `snap`, `diff`, `stable`, `raw`). Test mode additionally supports pause points (`DBG_BREAK(name, world)`) and the control commands `continue` / `where` / `break-filter`.

## Core principles

1. **Find the repro FIRST.** Can't debug what you can't reproduce. Don't investigate until you have a deterministic repro that shows the bug.
2. **Use the user's actual interaction.** If the user triggers it with the mouse, use `playrecording` to replay their mouse recording. Don't substitute with `push` -- it's a different interaction pattern.
3. **Determinism is required for comparison.** Same script + same binary = same result. Fresh engine per run. Non-deterministic comparisons are INVALID.
4. **ONE variable per experiment.** Never change two things between comparisons.
5. **The fix is usually small.** If you're writing 50+ lines, you're probably fixing a symptom. The actual fix for this engine's capsule bug was 2 lines.
6. **Band-aids create new bugs.** Speculative contacts, warm cache thresholds, substep increases -- these mask the real bug and introduce new problems.
7. **Listen to the user.** When they say "narrowphase", look at the narrowphase. When they say "it's not the solver", stop modifying the solver.
8. **Verify with VARIED inputs.** One passing test is not proof. Run 15+ varied scenarios. Failure to find ANY repro after the fix = confirmed.

## Phase 0: Find the repro

**This is the MOST IMPORTANT phase. Do NOT skip or rush it.**

**Step 1: Try the user's exact interaction.**
- If the user has a mouse recording: `playrecording` command
- If the user describes an interaction: replicate with `drag`/`push`
- If the user says "knock capsules over": knock them over at various angles and forces

**Step 2: Vary inputs until you find a failure.**
- Try different push forces: gentle (1), moderate (3), hard (5)
- Try different angles: sideways, diagonal, upward+sideways
- Try different application points: top, middle, offset
- Try different scenes: single body, multiple bodies, mixed shapes
- The bug may only appear at specific angular velocities or tilt angles

**Step 3: Confirm the repro is deterministic.**
```bash
# Run the SAME commands 3 times with fresh engines
for run in 1 2 3; do
  ./build/Debug/nudge.exe &
  sleep 2
  echo -e "scene X\npause\nstep N\npush ...\nstep M\nget body_hot/K\nexit" | timeout T ./build/Debug/nudge_viewer.exe localhost 9999
  taskkill //F //IM nudge.exe
  sleep 1
done
# All 3 outputs must be IDENTICAL
```

**Step 4: If you can't find a repro with `push`, use `playrecording`.**
The user's mouse constraint creates sustained force over many frames -- fundamentally different from an instant impulse. Add commands to replay user recordings rather than trying to approximate the interaction.

**A repro that settles after a few seconds is NOT a repro of "persistent erratic bouncing."** Keep varying inputs until you find one that shows the EXACT symptom the user describes.

## Phase 1: Isolate -- one variable at a time

**Disable everything, then add back one at a time:**
- N^2 broadphase (eliminates BVH)
- Disable sleep, warm start, incremental NP
- Single body + ground (eliminates multi-body)

**For EACH toggle, run the SAME deterministic repro and compare.**
If the result changes when toggling X, X is involved.
If the result is identical, X is not involved -- move on.

## Phase 2: Measure

Record at the frame of anomaly:
- pen depth, lambda, velocity, feature_id, contact count
- Expected pen = velocity * dt / substeps
- Expected lambda for resting ≈ 0.08
- If measured >> expected, the discrepancy IS the bug

**Track the full contact cycle:** contact frame -> gap frames -> re-contact.
Healthy contacts persist many frames. Single-frame contacts are the red flag.

## Phase 3: Hypothesize -- in the right file

**Priority:**
1. Narrowphase early-exit conditions (hard boundaries without slop margin)
2. Broadphase pair filtering (tight AABB check discarding near-contact pairs)
3. Contact filtering between NP and solver
4. Solver convergence (only after 1-3 are ruled out)

**Key lesson learned:** The capsule bounce bug was a 2-line fix in the narrowphase early exit -- `r.distance > a.radius` needed `+ LINEAR_SLOP`. Not in the solver, not in warm start, not in the broadphase.

**If 3 experiments in the same file don't help, you're in the wrong file.**

## Phase 4: Test -- one atomic experiment

- Add ONE printf, rebuild, run repro, read output
- Revert the printf after reading
- ONE experiment per iteration

## Phase 5: Fix -- minimal

- Fix the MEASURED root cause
- **DO NOT** tweak solver parameters, thresholds, substep counts, or margins
- **DO NOT** add speculative contacts or change eviction thresholds
- The fix should be in the narrowphase or broadphase code path, not the solver

## Phase 6: Verify -- varied inputs (critical)

**Running the original repro is NOT ENOUGH. You must search for a new repro with varied inputs and FAIL to find one.**

Run at minimum 15 tests covering:
```
# Push variations (6+ tests)
push (1,0,0), (2,0,0), (3,0,0), (0,0,2), (1,0,1), (2,1,1)

# Push with torque (3+ tests)
push with offset application points

# Drag interactions (3+ tests)
gentle drag, moderate drag, diagonal drag

# Multi-body (2+ tests)
bowling pins, capsule-capsule collision

# Regression (2+ tests)
sphere, box -- must still work

# Test suite
./build/Debug/nudge_tests.exe -- ALL tests pass
```

**For each test: check |vy| < 0.01 after 10 seconds (600 frames).**

**Only declare FIXED when:** Zero failures across all varied tests + all unit tests pass.

## Progress logging

Every 3 iterations:
```
=== Debug Progress (iteration N) ===
Hypotheses: X tested (Y confirmed, Z disproven)
Repro status: [found / searching / confirmed deterministic]
Variables ruled out: [list]
Current file under investigation: [path]
```

## Key lessons (from capsule bounce debugging)

| Lesson | Detail |
|--------|--------|
| The fix was 2 lines | `r.distance > a.radius` -> `r.distance > a.radius + LINEAR_SLOP` in two functions |
| Band-aids created new bugs | Speculative contacts caused negative penetrations and energy injection |
| The user's recording was the real repro | `push` couldn't reproduce the exact mouse-drag dynamics |
| 10+ iterations were wasted in the solver | The bug was in the narrowphase all along |
| N^2 vs BVH comparison was not deterministic | Different frame timing invalidated the comparison |
| "Settled after 10s" is not "no bug" | The user sees erratic bouncing in the FIRST few seconds |
| Contact toggling at tangent boundary | GJK distance oscillating around `radius` caused 1-frame contacts |

## Anti-patterns

| Don't | Why |
|-------|-----|
| Substitute `push` for mouse interaction | Different dynamics -- use `playrecording` |
| Change multiple variables per experiment | Can't attribute results |
| Declare victory on one test | Must fail to find repro across 15+ varied inputs |
| Spend >3 iterations in wrong file | Switch to the file the user pointed at |
| Fix symptoms (big lambda) | Find WHY the contact is intermittent |
| Add speculative contacts | Creates ghost contacts with negative penetration |
| Increase substeps as a fix | Masks temporal aliasing, doubles perf cost |
| Compare non-deterministic runs | Use fresh engine per run, same script |

## Commands

Run `help` in the viewer for all viewer commands (RPM inspection).
Run `help` as a driver command for all engine commands (TCP control).

Key commands for debugging:
- `summary` -- scene overview
- `contacts <body>` -- solver contacts for a body
- `stable [threshold] [frames]` -- mechanical pass/fail stability check
- `push <idx> <f> [r]` -- impulse at body (app mode only)
- `playrecording` -- replay user's mouse recording (app mode only)
- `npviz` + `npvset <A|B> <kind> <pos> <rot> <params>` -- load shapes into NP Viz for visual narrowphase debugging (app mode only)
- `continue` / `c` -- resume from a pause point (test mode)
- `where` -- show current pause point name + file:line (test mode)
- `break-filter [pattern]` -- view or change the break filter at runtime (test mode)
- `np-debug <0|1> [body_a] [body_b]` -- toggle narrowphase snapshot capture + filter (either body index, `-1` = any). After enabling, step one frame (`step 1` in app mode, or `continue` past a DBG_BREAK in test mode), then `get np_debug` dumps the SAT intermediates and emitted contacts for the first matching hull-hull pair. Force `--threads 1` when capturing -- the slot is single-entry and races under parallel NP.

## Test mode -- pause points instead of printf

Use this when a unit test is misbehaving and you need to inspect live engine state mid-test without recompiling with printfs.

### Adding a pause point

Place `DBG_BREAK("name", world)` anywhere in a test, inside the loop or right before an assertion. The macro is defined by `src/debug_server.c` (included into the test binary) and is a zero-cost predicate check unless the binary is started with `--debug`. Example:

```c
for (int f = 0; f < 60; f++) {
    world_step(w, 1.0f / 60.0f);
    if (f == 30) DBG_BREAK("my_test:frame30", w);   // pauses here when armed
}
```

Name pattern: pick something greppable and scope-prefixed. `my_test:frame30` beats `break1`. The viewer glob-matches against this.

### Running the test under the debugger

```bash
# Start the test binary with --debug. --break arms one or more named points.
# Without --break, DBG_BREAK is a no-op. --break=* catches every pause point.
powershell Start-Process -FilePath 'build/Debug/nudge_tests.exe' \
    -ArgumentList '--break=my_test:*', '--unit' -PassThru

# Or run it directly in foreground and attach a viewer from another shell.
./build/Debug/nudge_tests.exe --debug --break=my_test:* --unit
```

When a matching `DBG_BREAK` is reached the test thread blocks. The binary prints `[dbg] break "<name>" at <file:line> (world=0x...)` to stderr and keeps servicing TCP until a viewer sends `continue`.

### Driving the viewer

```bash
# Interactive
./build/Debug/nudge_viewer.exe localhost 9999

# Scripted (pipe commands via stdin)
(echo "where"; echo "summary"; echo "get body_state"; echo "continue"; echo "quit") \
    | ./build/Debug/nudge_viewer.exe localhost 9999
```

Typical inspection loop at a break:
```
> where
OK break="my_test:frame30" at src/tests_weld_bridge_unit.c:83 world=0x...
> summary                    # frame, body count, contact count, islands
> get body_hot 1             # velocities / angular velocities of body 1
> get body_state 2           # position + rotation of body 2
> table body_hot             # spreadsheet view of all bodies
> contacts 1                 # solver contacts touching body 1
> continue                   # resume; fires again at the next matching break
```

### Flow between breaks

After `continue` the test resumes and runs until the next matching pause point (or the test ends). The viewer prompt returns immediately; send `info` or `where` to check whether you're at a new break, still running, or the binary has exited. Tighten the filter at runtime with `break-filter <pattern>` if you only care about later breaks in the same run.

### Disabling without editing

Leaving `DBG_BREAK(...)` calls in checked-in tests is fine -- they cost one predicate check when `--debug` isn't passed. You don't need to `#ifdef` them out. Skipping past a compiled break without editing the source: set the filter so the name doesn't match (`--break=somethingElse`) or omit `--break` entirely.

## Companion modules

- `physics-debug` -- deeper methodology reference (5-phase breakdown, break-one-frame-before rule, cdb step-through recipes, impulse-ledger-vs-velocity check, bias guards). Reach for it when the bug is inside a function body and reflection alone isn't enough.
- `win-debugger` MCP -- tool-level cdb access for function locals and callstacks.

## Now begin

Find the repro first. Use `playrecording` if the user has a recording. Vary inputs. One variable at a time. For unit-test bugs, prefer `DBG_BREAK` + viewer over adding printfs.

