Follow EXACTLY. No deviation.
On the FIRST use of ANY tool:
You MUST validate what it is doing and its output
Required:
- Cross-check against another source
- Verify structure AND content
- Confirm assumptions
Never:
- Trust output blindly
- Assume correctness
If not validated: -> ALL reasoning is INVALID
- Tool validation (first use only)
- Sync state (NOT frame number)
- Dump full state (native + oracle) via TCP debug server
- Diff bytes
- Find FIRST divergence
- Trace writer (function + instruction + call path)
- Classify (codegen / runner / timing / config)
- Fix the tool (never generated output)
If ANY step is skipped: -> STOP and restart
All inspection must use TCP (Phase 2+). Before TCP exists (Phase 1), use structured JSON artifacts only.
When comparing native vs DuckStation oracle:
- CPU registers (32 GPRs, HI, LO, PC, COP0)
- RAM contents at known kernel addresses (PCB, TCB, EvCB tables)
- VRAM contents (byte-for-byte)
- MMIO read/write sequences
- Timer counter values
- Interrupt fire ordering
- DMA transfer sequences
- GPU command streams
codegen:
- Wrong C emitted by strict_translator
- Fix: recompiler/src/strict_translator.cpp
runner:
- Wrong hardware simulation (MMIO, DMA, IRQ, GPU, timers)
- Fix: runtime/src/*.c
timing:
- Events fire at wrong cycle / wrong order
- Fix: runtime timing logic
config:
- Missing function seed, wrong address alias
- Fix: seed files, address_aliases.json
discovery:
- Function not found, dispatch miss
- Fix: function finder pipeline
NEVER fix generated output. Fix the tool that produced it.
When debugging boot/runtime flows, never accept an internal milestone as success unless the full user-visible loop closes.
Examples of FALSE success:
- "counter reached 0" is NOT success if the shell still retries
- "IRQ fired" is NOT success if the waiting code still loops
- "callback returned" is NOT success if the owning state machine does not advance
- "data structure written" is NOT success if the consumer still rejects it
- "handler installed" is NOT success if the VBlank counter never increments
- "chain head populated" is NOT success if pixels don't reach the screen
Required method:
- Define full end-to-end success criteria FIRST — what does the USER see when it works?
- Trace producer AND consumer — follow the data from origin to final consumer
- Find the first semantic divergence — where does the consumer's view differ from expected?
- Fix subsystem behaviour — correct the root cause in the producing subsystem
- Rerun the full flow — verify the entire loop closes, not just the intermediate step
- Reject any patch that only changes intermediate state without closing the loop
If a handoff contains stale claims contradicted by new logs, update the problem statement before changing code. Never continue debugging from an invalidated hypothesis. If there are contradictions or stale data, STOP and inform the user immediately before proceeding.
(Seen in the 2026-04-13 VBlank stall: the handoff said "handler is never installed"
but live TCP inspection proved the handler WAS installed at 0x800DFEE0 — the real
gap was one level up, in the kernel root-counter chain head at *(0xA000E014). The
entire static-analysis cascade below that stale claim was wasted effort.)
If at ANY point during debugging the TCP debug server cannot answer a question you need answered — STOP IMMEDIATELY. Do not:
- Guess the answer
- Work around it with printf/fprintf
- Proceed with partial information
- Make assumptions based on code reading alone
Instead:
- STOP all current work
- INFORM the user exactly what data is missing and what command is needed
- ASK the user how to proceed (build the command? change approach?)
- BUILD the tooling if approved, then resume
Adding a new command is cheap — the pattern is one handler in runtime/src/debug_server.c
(native) and a matching handler in duckstation/src/core/psxrecomp_debug_server.cpp
(DuckStation oracle, then regenerate tools/duckstation/psxrecomp_oracle.patch).
See TCP_COMMANDS.md for the protocol.
A wrong answer from missing tooling costs MORE than the time to build the tool. Every workaround becomes technical debt that hides the real bug longer.
Claude builds, launches, and closes ALL tools. The user NEVER gatekeeps this.
- Build native:
(cd runtime && cmake --build build)(withPATH=/c/msys64/mingw64/bin:$PATH) - Build DuckStation oracle:
bash tools/duckstation/build.sh - Launch native:
./runtime/build/psx-runtime.exe & - Launch DuckStation oracle:
./duckstation/build/bin/duckstation-qt.exe -bios -nogui -fastboot & - Close / restart:
taskkill //F //IM psx-runtime.exeor//IM duckstation-qt.exe
Do NOT ask the user to launch, close, or restart anything. Do NOT tell the user what commands to run — just run them.
The exe file lock can persist after process exit (AV, delayed handle release). Always use this sequence:
# 1. Kill any running instance
taskkill //F //IM psx-runtime.exe 2>/dev/null
taskkill //F //IM duckstation-qt.exe 2>/dev/null
# 2. If linker still fails with "Permission denied", delete the exe first
rm -f runtime/build/psx-runtime.exe
# 3. Rebuild
PATH=/c/msys64/mingw64/bin:$PATH
(cd runtime && cmake --build build -j)
# 4. Relaunch (one instance at a time — memory policy)
./runtime/build/psx-runtime.exe &Never use C:/msys64/msys2_shell.cmd — it spawns orphan processes. Use bash with
PATH=/c/msys64/mingw64/bin:$PATH inline.