oh-my-opencode 5.0.0-beta.21 → 5.0.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli/index.js +290 -288
  2. package/dist/cli-node/index.js +290 -288
  3. package/dist/index.js +1675 -1657
  4. package/dist/skills/debugging/SKILL.md +2 -0
  5. package/dist/skills/debugging/references/methodology/00-setup.md +12 -0
  6. package/dist/skills/debugging/references/methodology/02-investigate.md +21 -0
  7. package/dist/skills/debugging/references/runtimes/go.md +15 -1
  8. package/dist/skills/debugging/references/runtimes/native-binary.md +38 -0
  9. package/dist/skills/debugging/references/runtimes/node.md +25 -0
  10. package/dist/skills/debugging/references/runtimes/python.md +13 -0
  11. package/dist/skills/debugging/references/runtimes/rust.md +41 -0
  12. package/dist/skills/debugging/references/scripts/dap.mjs +267 -0
  13. package/dist/skills/debugging/references/scripts/dap.test.ts +86 -0
  14. package/dist/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  15. package/dist/skills/debugging/references/tools/dap.md +103 -0
  16. package/dist/skills/debugging/references/tools/frida.md +193 -0
  17. package/dist/tools/delegate-task/types.d.ts +3 -0
  18. package/dist/tui.js +131 -147
  19. package/package.json +18 -15
  20. package/packages/lsp-core/src/index.ts +1 -0
  21. package/packages/lsp-core/src/lsp/client-wrapper.ts +25 -5
  22. package/packages/lsp-core/src/lsp/client.ts +26 -0
  23. package/packages/lsp-core/src/lsp/connection.ts +16 -0
  24. package/packages/lsp-core/src/lsp/constants.ts +1 -0
  25. package/packages/lsp-core/src/lsp/format-document.test.ts +143 -0
  26. package/packages/lsp-core/src/lsp/format-document.ts +90 -0
  27. package/packages/lsp-core/src/lsp/manager-max-clients.test.ts +151 -0
  28. package/packages/lsp-core/src/lsp/manager.ts +34 -1
  29. package/packages/lsp-core/src/lsp/server-definitions.ts +29 -0
  30. package/packages/lsp-core/src/lsp/server-installation.test.ts +376 -0
  31. package/packages/lsp-core/src/lsp/server-installation.ts +200 -27
  32. package/packages/lsp-core/src/lsp/server-resolution-local-binary.test.ts +170 -0
  33. package/packages/lsp-core/src/lsp/server-resolution.ts +18 -5
  34. package/packages/lsp-core/src/lsp/transport.ts +9 -0
  35. package/packages/lsp-core/src/lsp/types.ts +8 -0
  36. package/packages/lsp-core/src/tool-surface.test.ts +13 -1
  37. package/packages/lsp-core/src/tools/definitions.ts +14 -0
  38. package/packages/lsp-core/src/tools/format.test.ts +56 -0
  39. package/packages/lsp-core/src/tools/format.ts +56 -0
  40. package/packages/lsp-core/src/tools/index.ts +1 -0
  41. package/packages/lsp-core/src/tools/types.ts +10 -0
  42. package/packages/lsp-daemon/dist/cli.js +432 -144
  43. package/packages/lsp-daemon/dist/client.d.ts +10 -0
  44. package/packages/lsp-daemon/dist/client.js +401 -107
  45. package/packages/lsp-daemon/dist/daemon-client.d.ts +1 -0
  46. package/packages/lsp-daemon/dist/daemon-client.js +3 -0
  47. package/packages/lsp-daemon/dist/index.js +395 -107
  48. package/packages/lsp-daemon/package.json +1 -1
  49. package/packages/lsp-tools-mcp/dist/cli.js +384 -99
  50. package/packages/lsp-tools-mcp/dist/lsp/manager.js +53 -0
  51. package/packages/lsp-tools-mcp/dist/mcp.js +384 -99
  52. package/packages/lsp-tools-mcp/dist/tools.js +385 -99
  53. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  54. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
  55. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +69 -35
  58. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +21 -2
  59. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  60. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  63. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  64. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  65. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  67. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +404 -121
  68. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  69. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  70. package/packages/omo-codex/plugin/components/rules/dist/cli.js +3 -3
  71. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  72. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  73. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +3 -3
  74. package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +4 -4
  75. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +30 -15
  78. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +30 -15
  79. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  80. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  81. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  82. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  83. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  84. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +5 -1
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +2 -1
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +11 -1
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +22 -10
  89. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  90. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  91. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +5 -1
  92. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +13 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +3 -5
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +26 -2
  95. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  118. package/packages/omo-codex/plugin/model-catalog.json +3 -3
  119. package/packages/omo-codex/plugin/package-lock.json +13 -13
  120. package/packages/omo-codex/plugin/package.json +1 -1
  121. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +3 -3
  122. package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -0
  123. package/packages/omo-codex/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
  124. package/packages/omo-codex/plugin/skills/debugging/references/methodology/02-investigate.md +21 -0
  125. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/go.md +15 -1
  126. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
  127. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/node.md +25 -0
  128. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/python.md +13 -0
  129. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/rust.md +41 -0
  130. package/packages/omo-codex/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
  131. package/packages/omo-codex/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  132. package/packages/omo-codex/plugin/skills/debugging/references/tools/dap.md +103 -0
  133. package/packages/omo-codex/plugin/skills/debugging/references/tools/frida.md +193 -0
  134. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +1 -1
  135. package/packages/omo-codex/plugin/test/auto-update.test.mjs +2 -2
  136. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +8 -8
  137. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +2 -2
  138. package/packages/omo-codex/scripts/install-dist/install-local.mjs +38 -23
  139. package/packages/shared-skills/skills/debugging/SKILL.md +2 -0
  140. package/packages/shared-skills/skills/debugging/references/methodology/00-setup.md +12 -0
  141. package/packages/shared-skills/skills/debugging/references/methodology/02-investigate.md +21 -0
  142. package/packages/shared-skills/skills/debugging/references/runtimes/go.md +15 -1
  143. package/packages/shared-skills/skills/debugging/references/runtimes/native-binary.md +38 -0
  144. package/packages/shared-skills/skills/debugging/references/runtimes/node.md +25 -0
  145. package/packages/shared-skills/skills/debugging/references/runtimes/python.md +13 -0
  146. package/packages/shared-skills/skills/debugging/references/runtimes/rust.md +41 -0
  147. package/packages/shared-skills/skills/debugging/references/scripts/dap.mjs +267 -0
  148. package/packages/shared-skills/skills/debugging/references/scripts/dap.test.ts +86 -0
  149. package/packages/shared-skills/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  150. package/packages/shared-skills/skills/debugging/references/tools/dap.md +103 -0
  151. package/packages/shared-skills/skills/debugging/references/tools/frida.md +193 -0
@@ -53,6 +53,8 @@ These are not "optional extras". They are the correct tool in their domain, and
53
53
  | **Ghidra** | Any binary without trustworthy source — third-party closed libs, malware, vendored binaries whose behavior contradicts docs, CTF, firmware. **Use Ghidra's decompiler before `strings`/`objdump` guessing. It turns machine code into readable C.** | 📖 **[references/tools/ghidra.md](references/tools/ghidra.md)** |
54
54
  | **pwndbg** | Any native binary debugging session. It is GDB with the useful views (registers, stack, disasm, heap) always visible. **If you'd reach for plain `gdb`, reach for `pwndbg` instead — it is strictly a superset.** | 📖 **[references/tools/pwndbg.md](references/tools/pwndbg.md)** |
55
55
  | **pwntools** | Any time you need a reproducible interaction with a binary or network service — crafted payloads, exploit automation, fuzz harness, CTF scripting. | 📖 **[references/tools/pwntools.md](references/tools/pwntools.md)** |
56
+ | **Frida** | Any running process you must instrument live without source or symbols — hook a function and print real argument values, trace calls, stub a return. Complements Ghidra: Ghidra reads the bytes, Frida watches them execute. **If Ghidra's static decompile has hit its limit, reach for Frida.** | 📖 **[references/tools/frida.md](references/tools/frida.md)** |
57
+ | **DAP client (`dap.mjs`)** | Any time you would drive a debugger through a PTY and screen-scrape its text. Debuggers already speak the machine-readable Debug Adapter Protocol (debugpy, `dlv dap`, lldb-dap, js-debug); this bundled script drives it with bounded, monitorable output. **Prefer it over scraping `gdb`/`pdb` whenever the debugger speaks DAP.** Design modeled on oh-my-pi's `debug` tool (github.com/can1357/oh-my-pi). | 📖 **[references/tools/dap.md](references/tools/dap.md)** |
56
58
 
57
59
  **Failing to use these tools in their domain is a process failure, not a stylistic choice.** If the bug is in a browser and you did Phase 8 without Playwright, you are doing it wrong. If the bug is in a stripped binary and you read hex with `xxd`, you are doing it wrong. The references tell you how. Read them.
58
60
 
@@ -35,6 +35,18 @@ The shape of the answers you need (commands in the runtime reference):
35
35
  | Is there an existing failing test or known repro? | Prefer amplifying an existing repro over inventing one. |
36
36
  | Are watchers (file watchers, hot reloaders, supervisors) going to restart the process mid-session? | If yes, turn them off before attaching. Restarts drop inspector connections and invalidate breakpoints. |
37
37
 
38
+ ### Output budget
39
+
40
+ **Bound every inspection before you run it; unbounded output destroys the investigation's working context.**
41
+ - Set a frame count, element count, or byte range for every query.
42
+ - Prefer one targeted query over one broad dump: ask for the value that distinguishes hypotheses.
43
+ - Page through more data only when the first bounded result says it is necessary; do not re-dump the whole object.
44
+ - Treat any command whose output size you cannot predict as a command that needs a limit.
45
+ - Before: GDB `bt` — After: GDB `bt 10` to inspect only the top ten frames.
46
+ - Before: LLDB `memory read 0x1000` — After: `memory read 0x1000 -c 32` for a bounded byte range.
47
+ - Before: LLDB `frame variable` — After: `frame variable --children-count 10 user` for one variable's children.
48
+ - This is not presentation etiquette: a flooded context hides the decisive observation and can end the investigation.
49
+
38
50
  ### 4. Gate check
39
51
 
40
52
  If any answer is "I'm not sure", you are not ready for Phase 1. Investigate until certain. Guessing here cascades into false-positive hypotheses in Phase 2.
@@ -39,6 +39,27 @@ If two hypotheses have identical distinguishing evidence, they aren't actually d
39
39
 
40
40
  ## Phase 3 — Parallel Investigation
41
41
 
42
+ ### State freshness invariant
43
+
44
+ **A debug session's state is a snapshot with a lifetime, not a fact you can carry forward indefinitely.**
45
+ - Before any action that mutates the debuggee, re-observe the session's current state.
46
+ - This includes continue, step, side-effecting evaluation, setting or removing breakpoints, and termination.
47
+ - Record the current thread state and stop reason; those are the concrete tells for whether the old plan still applies.
48
+ - An observation from an earlier turn may already be stale when the next action is issued.
49
+ - Turns can be minutes apart while the debuggee runs at full speed, so this failure mode is normal, not exceptional.
50
+ - If the thread state or stop reason has moved, re-observe the new state and do not replay the old plan.
51
+ - Treat every mutation as conditional on the state you just observed, not on a remembered stop.
52
+
53
+ ### Failure-to-recovery taxonomy
54
+
55
+ | Failure signature | Most likely cause | Next move |
56
+ |---|---|---|
57
+ | Adapter or debugger process failed to start | Wrong binary or missing installation | Verify the tool exists, then re-launch; do not retry blindly. |
58
+ | Breakpoint accepted but never bound or verified | Source path mismatch, optimized-out code, or missing debug symbols | Check the binary was built with symbols and that the debugger-resolved path matches your file. |
59
+ | Attach refused | Permissions, ptrace scope, SIP, or wrong PID | Fix the environment or target identity; a retry will not change it. |
60
+ | No stop event within the expected window | Process is running, the breakpoint is unreachable, or the wait was too short | Pause and inspect threads rather than waiting longer. |
61
+ | Session terminated unexpectedly | The debuggee crashed or exited | Capture the exit and crash evidence before restarting. |
62
+
42
63
  Branch depending on what's available.
43
64
 
44
65
  ### Path A: Team mode ENABLED
@@ -42,10 +42,24 @@ dlv exec ./bin/myserver
42
42
  # Attach to a running process
43
43
  dlv attach $(pgrep myserver)
44
44
 
45
- # Headless mode (IDE / remote attach) — default port 2345
45
+ # Delve's JSON-RPC API (IDE / remote attach) — default port 2345
46
46
  dlv debug --headless --listen=:2345 --api-version=2 ./cmd/server
47
47
  ```
48
48
 
49
+ Delve exposes two distinct machine-readable interfaces:
50
+
51
+ ```bash
52
+ # Debug Adapter Protocol: use this for editors and DAP-aware automation
53
+ dlv dap --listen=127.0.0.1:2345
54
+
55
+ # Delve's JSON-RPC API: use this for clients built directly against Delve's API
56
+ dlv debug --headless --listen=127.0.0.1:2345 --api-version=2 ./cmd/server
57
+ ```
58
+
59
+ Pick `dlv dap` when the client speaks DAP; pick headless JSON-RPC when you need Delve's
60
+ native API or an existing JSON-RPC client. Both expose structured state, unlike scraping
61
+ an interactive `(dlv)` session.
62
+
49
63
  ### Building a debuggable binary
50
64
 
51
65
  The compiler inlines and optimizes aggressively in normal builds, which makes stepping confusing. For serious debugging:
@@ -107,6 +107,24 @@ print(repr(data[max(0,pos-100):pos+200]))
107
107
 
108
108
  If you must keep using `strings`, lower the threshold: `strings -n 1 -t x ./target | rg ...`. The signal-to-noise drops sharply but short content is preserved.
109
109
 
110
+ ### Sanitizer build for a crashing C/C++ binary
111
+
112
+ If the binary can be rebuilt from C/C++ sources, reproduce the crash with sanitizers before
113
+ trying to infer the fault from stripped machine code. AddressSanitizer catches out-of-bounds
114
+ accesses and use-after-free; UndefinedBehaviorSanitizer reports undefined operations.
115
+
116
+ ```bash
117
+ # Rebuild with source locations and runtime checks enabled
118
+ CFLAGS='-g -O1 -fsanitize=address,undefined' \
119
+ CXXFLAGS='-g -O1 -fsanitize=address,undefined' \
120
+ make clean all
121
+ ./target
122
+ ```
123
+
124
+ The sanitizer report identifies the invalid access and its allocation/free or undefined
125
+ operation stack, often making the root cause clear before dynamic analysis. For a binary
126
+ that cannot be rebuilt, continue with the native tracing and debugger workflow below.
127
+
110
128
  Write the triage summary to the journal:
111
129
 
112
130
  ```markdown
@@ -229,6 +247,26 @@ lldb ./target
229
247
  (lldb) image dump symtab ./target
230
248
  ```
231
249
 
250
+ ### Data watchpoints — catch the corruption, not just the crash
251
+
252
+ When a value is already wrong by the time execution reaches the crash, watch the memory
253
+ address for writes and stop at the instruction that corrupts it. Use the address of the
254
+ field or buffer identified from the static/dynamic hypothesis; see [tools/pwndbg.md](../tools/pwndbg.md)
255
+ for the richer pwndbg watchpoint workflow rather than duplicating it here.
256
+
257
+ ```text
258
+ # lldb: watch writes to an address (replace with a valid expression/address)
259
+ (lldb) watchpoint set expression -- <addr>
260
+
261
+ # gdb: stop on writes, or on reads when a read is the first observable symptom
262
+ (gdb) watch *(<type> *)<addr>
263
+ (gdb) rwatch *(<type> *)<addr>
264
+ ```
265
+
266
+ A watchpoint is most useful after narrowing the suspect object: continue from its
267
+ allocation or initialization, and inspect the backtrace and writer when it triggers.
268
+ Hardware watchpoint slots are limited, so remove broad watchpoints before adding more.
269
+
232
270
  **Function interception via `DYLD_INSERT_LIBRARIES`** (macOS equivalent of `LD_PRELOAD`):
233
271
 
234
272
  ```bash
@@ -98,6 +98,31 @@ bun test --inspect-brk # debug test runner
98
98
 
99
99
  **Critical**: Bun uses WebKit Inspector Protocol, not V8. `chrome://inspect` cannot connect directly. Use `debug.bun.sh` or the (currently buggy, per Bun docs) VS Code extension.
100
100
 
101
+ ### Scripted V8 inspector / CDP attach
102
+
103
+ The inspector port also serves machine-readable HTTP discovery endpoints. `/json` lists
104
+ inspectable targets; `/json/version` includes the WebSocket debugger URL. Connect that
105
+ URL with the Chrome DevTools Protocol (CDP), rather than scraping `node inspect` text.
106
+ `chrome-remote-interface` is the canonical scriptable npm client. When browser automation
107
+ is already in play, use Playwright's `CDPSession` instead.
108
+
109
+ ```bash
110
+ curl -s http://127.0.0.1:9229/json/version
111
+ ```
112
+
113
+ ```js
114
+ const CDP = require('chrome-remote-interface');
115
+ const version = await fetch('http://127.0.0.1:9229/json/version').then(r => r.json());
116
+ const client = await CDP({ target: version.webSocketDebuggerUrl });
117
+ await client.Runtime.enable();
118
+ await client.Debugger.pause();
119
+ ```
120
+
121
+ The same CDP commands can be sent through `chrome-remote-interface`; with Playwright,
122
+ create a session with `await context.newCDPSession(page)` and call
123
+ `await session.send('Runtime.enable')`. These V8/CDP examples do not apply to Bun's
124
+ WebKit inspector protocol.
125
+
101
126
  ### Deno (native V8, Chrome DevTools / VS Code compatible)
102
127
 
103
128
  ```bash
@@ -41,6 +41,19 @@ python -c 'import asyncio; print(asyncio.__version__)'
41
41
  | **post-mortem `pdb.pm()`** | Exception already happened, you want to inspect state | In an exception-caught REPL: `import pdb; pdb.pm()` after the exception propagates |
42
42
  | **debugpy (remote / IDE)** | IDE attach, remote host, containerized process | `python -m debugpy --listen 5678 --wait-for-client script.py` then attach from VS Code / PyCharm |
43
43
 
44
+ `debugpy` is a Debug Adapter Protocol (DAP) adapter, not merely an IDE convenience.
45
+ Its listener exposes a machine-readable protocol that an agent or other client can drive
46
+ programmatically; it is a better boundary for automation than the human-oriented pdb REPL.
47
+
48
+ ```bash
49
+ # Listen on the requested interface and wait before executing the script
50
+ python -m debugpy --listen <host:port> --wait-for-client <script>
51
+ ```
52
+
53
+ Use a DAP client to send structured requests and receive structured stopped/continued
54
+ and stack/variable events. By contrast, `pdb` exposes prompt-oriented text: an agent
55
+ using it must screen-scrape output and parse a human-facing REPL.
56
+
44
57
  ### Prefer `ipdb` or `pudb` over plain `pdb` when available
45
58
 
46
59
  - **ipdb** — drop-in replacement with tab completion, syntax highlighting. `pip install ipdb`, then `PYTHONBREAKPOINT=ipdb.set_trace` or use `import ipdb; ipdb.set_trace()`.
@@ -144,6 +144,47 @@ Rust symbols are mangled. Use either:
144
144
 
145
145
  ---
146
146
 
147
+ ## Sanitizers — memory and race bugs
148
+
149
+ Nightly Rust exposes LLVM sanitizers through `-Z sanitizer`. Use them when a crash,
150
+ data race, or leak is otherwise too timing- or state-dependent to explain from a
151
+ backtrace; they replace guesswork with a report at the invalid operation.
152
+
153
+ ```bash
154
+ # AddressSanitizer: out-of-bounds accesses and use-after-free
155
+ cargo +nightly run -Z sanitizer=address
156
+
157
+ # ThreadSanitizer: data races between threads
158
+ cargo +nightly test -Z sanitizer=thread
159
+
160
+ # LeakSanitizer: allocations that remain reachable at process exit
161
+ cargo +nightly run -Z sanitizer=leak
162
+ ```
163
+
164
+ `address` catches invalid memory access and use-after-free, `thread` catches unsynchronized
165
+ concurrent accesses, and `leak` reports memory that was allocated but not released. Run
166
+ the invocation that matches the suspected class; sanitizer diagnostics are especially
167
+ useful before attaching a debugger to optimized or timing-sensitive code.
168
+
169
+ ---
170
+
171
+ ## rr — time-travel debugging
172
+
173
+ On supported Linux systems, record the failure once, then replay it deterministically and
174
+ run backwards from the failure with `reverse-continue` or `reverse-step`:
175
+
176
+ ```bash
177
+ rr record ./target/debug/my_binary
178
+ rr replay
179
+ # (rr) reverse-continue
180
+ # (rr) reverse-step
181
+ ```
182
+
183
+ `rr` is Linux/x86-64 only. It does not apply on Apple Silicon, including arm64 macOS;
184
+ use `rust-lldb` or a sanitizer there instead.
185
+
186
+ ---
187
+
147
188
  ## tokio-console — async task debugging
148
189
 
149
190
  For tokio-based async apps, this is essential when tasks are stuck or leaking.
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ // Zero-dependency DAP REPL, modeled on oh-my-pi's debug tool. DAP framing is UTF-8 byte based.
3
+ import { spawn } from "node:child_process";
4
+ import { createConnection } from "node:net";
5
+ import { createInterface } from "node:readline";
6
+ import { stdin, stdout, stderr, env } from "node:process";
7
+
8
+ export const MAX_ROWS = 100;
9
+ export const MAX_OUTPUT_BYTES = 32 * 1024;
10
+
11
+ export function isTcpAdapterSpec(spec) {
12
+ const separator = spec.lastIndexOf(":");
13
+ return separator > 0 &&
14
+ !spec.includes("/") &&
15
+ !spec.includes("\\") &&
16
+ /^\d+$/.test(spec.slice(separator + 1));
17
+ }
18
+
19
+ export class DapFrameParser {
20
+ constructor() { this.buffer = Buffer.alloc(0); }
21
+ push(chunk) {
22
+ this.buffer = Buffer.concat([this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
23
+ const messages = [];
24
+ while (true) {
25
+ const marker = this.buffer.indexOf(Buffer.from("\r\n\r\n"));
26
+ if (marker < 0) break;
27
+ const header = this.buffer.subarray(0, marker).toString("ascii");
28
+ const match = header.match(/(?:^|\r\n)Content-Length:\s*(\d+)/i);
29
+ if (!match) { this.buffer = this.buffer.subarray(marker + 4); continue; }
30
+ const length = Number(match[1]);
31
+ const start = marker + 4;
32
+ if (this.buffer.length < start + length) break;
33
+ const body = this.buffer.subarray(start, start + length);
34
+ this.buffer = this.buffer.subarray(start + length);
35
+ try { messages.push(JSON.parse(body.toString("utf8"))); } catch { /* malformed frames are ignored */ }
36
+ }
37
+ return messages;
38
+ }
39
+ }
40
+
41
+ const timeoutMs = Number(env.DAP_TIMEOUT_MS || 15000);
42
+ let transport = null;
43
+ let parser = null;
44
+ let nextSeq = 1;
45
+ let initialized = false;
46
+ let terminated = false;
47
+ let currentStop = null;
48
+ let topFrame = null;
49
+ const pending = new Map();
50
+ const breakpoints = new Map();
51
+ // Event-driven waits. A tight `await setImmediate` busy-loop starves Bun's socket
52
+ // data callbacks (the `initialized` event then never processes and launch deadlocks),
53
+ // so every wait subscribes to the actual DAP event instead of polling state.
54
+ const eventWaiters = new Map();
55
+ function onEvent(name) {
56
+ return new Promise(resolve => {
57
+ const list = eventWaiters.get(name) || [];
58
+ list.push(resolve);
59
+ eventWaiters.set(name, list);
60
+ });
61
+ }
62
+ function emitEvent(name, value) {
63
+ const list = eventWaiters.get(name) || [];
64
+ eventWaiters.delete(name);
65
+ for (const resolve of list) resolve(value);
66
+ }
67
+ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
68
+
69
+ const debugTraffic = Boolean(env.DAP_DEBUG);
70
+ function trace(direction, message) { if (debugTraffic) stderr.write(`DAP ${direction} ${JSON.stringify(message).slice(0, 300)}\n`); }
71
+ function line(text) { stdout.write(`${text}\n`); }
72
+ function error(kind, detail = "") { line(`ERR: ${kind}${detail ? ` ${detail}` : ""}`); }
73
+ function send(message) {
74
+ trace(">", message);
75
+ const body = Buffer.from(JSON.stringify(message));
76
+ transport.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]));
77
+ }
78
+ function request(command, args = {}) {
79
+ if (!transport || terminated) return Promise.reject(new Error("no-session"));
80
+ const seq = nextSeq++;
81
+ send({ type: "request", seq, command, arguments: args });
82
+ return new Promise((resolve, reject) => {
83
+ const timer = setTimeout(() => { pending.delete(seq); reject(new Error("timeout")); }, timeoutMs);
84
+ pending.set(seq, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: value => { clearTimeout(timer); reject(value); } });
85
+ });
86
+ }
87
+ function onMessage(message) {
88
+ trace("<", message);
89
+ if (message.type === "response") {
90
+ const wait = pending.get(message.request_seq);
91
+ if (!wait) return;
92
+ pending.delete(message.request_seq);
93
+ if (message.success === false) wait.reject(new Error("adapter-error")); else wait.resolve(message);
94
+ } else if (message.type === "event") {
95
+ if (message.event === "initialized") { initialized = true; emitEvent("initialized"); }
96
+ if (message.event === "stopped") { currentStop = message.body || {}; emitEvent("stopped", currentStop); }
97
+ if (message.event === "terminated" || message.event === "exited") { terminated = true; emitEvent("terminated"); }
98
+ }
99
+ }
100
+ function connectTransport(write, read, close = read) {
101
+ transport = { write: chunk => write.write(chunk), destroy: () => close.destroy?.() };
102
+ parser = new DapFrameParser();
103
+ read.on("data", chunk => { for (const message of parser.push(chunk)) onMessage(message); });
104
+ read.on("error", () => { if (!terminated) error("adapter-failed"); });
105
+ read.on("close", () => { if (!terminated) error("adapter-failed"); });
106
+ }
107
+ function connectSocket(socket) { connectTransport(socket, socket, socket); }
108
+ async function startAdapter(spec) {
109
+ if (isTcpAdapterSpec(spec)) {
110
+ const separator = spec.lastIndexOf(":");
111
+ const socket = createConnection(Number(spec.slice(separator + 1)), spec.slice(0, separator));
112
+ connectSocket(socket);
113
+ await new Promise((resolve, reject) => { socket.once("connect", resolve); socket.once("error", reject); });
114
+ } else {
115
+ // A .mjs/.cjs/.ts adapter spec runs under the current Bun/Node runtime (e.g. the
116
+ // fixture adapter); any other spec is an adapter executable (lldb-dap, debugpy, dlv)
117
+ // spawned directly with its own argv.
118
+ const isScript = /\.(mjs|cjs|ts)$/.test(spec);
119
+ const [cmd, argv] = isScript ? [process.execPath, [spec]] : [spec, []];
120
+ const child = spawn(cmd, argv, { stdio: ["pipe", "pipe", "inherit"] });
121
+ connectTransport(child.stdin, child.stdout, child);
122
+ child.on("error", () => error("adapter-failed"));
123
+ }
124
+ }
125
+ // Waits for a stopped event. When `required` is false this is a best-effort grace
126
+ // window (e.g. an optional stop-on-entry); it returns on the first stop OR on timeout
127
+ // without failing, so adapters that run straight to a breakpoint are not broken.
128
+ async function waitForStop(required = true, graceMs) {
129
+ if (currentStop || terminated) return Boolean(currentStop);
130
+ const budget = graceMs ?? timeoutMs;
131
+ await Promise.race([onEvent("stopped"), onEvent("terminated"), sleep(budget)]);
132
+ return Boolean(currentStop);
133
+ }
134
+ // Waits for the adapter's `initialized` event, event-driven (never a busy-loop).
135
+ async function waitInitialized() {
136
+ if (initialized) return;
137
+ await Promise.race([onEvent("initialized"), sleep(timeoutMs)]);
138
+ if (!initialized) throw new Error("timeout");
139
+ }
140
+ // Adapters gate the `initialized` event on a recognized adapterID (debugpy withholds
141
+ // it for unknown IDs, hanging the handshake). Infer a known ID from the adapter spec.
142
+ function adapterIdFor(spec) {
143
+ const base = String(spec).split("/").pop().toLowerCase();
144
+ if (base.includes("debugpy") || base.includes("python")) return "debugpy";
145
+ if (base.includes("lldb")) return "lldb-dap";
146
+ if (base.includes("dlv") || base.includes("delve")) return "dlv";
147
+ if (base.includes("js-debug")) return "js-debug";
148
+ if (base.includes("gdb")) return "gdb";
149
+ return "omo";
150
+ }
151
+ async function launch(adapter, program, args) {
152
+ if (transport) return error("adapter-error", "session already exists");
153
+ try {
154
+ await startAdapter(adapter);
155
+ await request("initialize", { clientID: "omo-dap", adapterID: adapterIdFor(adapter), linesStartAt1: true, columnsStartAt1: true, pathFormat: "path" });
156
+ // Send launch BEFORE awaiting `initialized`: debugpy's adapter defers/flushes the
157
+ // initialized event until further client activity, so waiting for it first
158
+ // deadlocks the handshake. DAP allows launch any time after the initialize
159
+ // response; `initialized` is only required before configurationDone.
160
+ // stopOnEntry yields an initial stopped event with a real threadId, so the client
161
+ // can set breakpoints and inspect from a valid stopped state before continuing.
162
+ // `console: "internalConsole"` is REQUIRED by debugpy (without it the launch never
163
+ // spawns the debug server -> "Server is not available"); lldb-dap ignores it.
164
+ // lldb-dap reports the launch response success unreliably (false even when the
165
+ // process started and stopped); the process/stopped EVENTS are the real signal,
166
+ // so a non-success launch response is tolerated, not fatal.
167
+ await request("launch", { program, args, console: "internalConsole", justMyCode: false, stopOnEntry: true }).catch(() => null);
168
+ await waitInitialized();
169
+ await request("configurationDone").catch(() => null);
170
+ // Best-effort: lldb-dap stops at entry, other adapters run to the first breakpoint.
171
+ await waitForStop(false, 2000);
172
+ line("READY: launch");
173
+ } catch (e) { error(e.message === "timeout" ? "timeout" : e.message === "adapter-error" ? "adapter-error" : "adapter-failed"); }
174
+ }
175
+ async function snapshot() {
176
+ if (terminated) { line("EXIT: terminated"); return; }
177
+ try {
178
+ const response = await request("stackTrace", { threadId: currentStop?.threadId || 1, levels: 1 });
179
+ topFrame = response.body?.stackFrames?.[0];
180
+ const f = topFrame;
181
+ if (f) line(`STOP: stopped reason=${currentStop?.reason || "unknown"} threadId=${currentStop?.threadId || ""} ${f.name} at ${f.source?.path || "?"}:${f.line || 0}:${f.column || 0}`);
182
+ else line(`STOP: stopped reason=${currentStop?.reason || "unknown"} threadId=${currentStop?.threadId || ""} ? at ?:0:0`);
183
+ } catch (e) { error(e.message === "timeout" ? "timeout" : "adapter-error"); }
184
+ }
185
+ function boundedTable(header, rows) {
186
+ const all = rows.map(row => row.join("\t"));
187
+ const chosen = all.slice(0, MAX_ROWS);
188
+ const prefix = `${header.join("\t")}\n`;
189
+ let output = prefix;
190
+ let count = 0;
191
+ for (const row of chosen) {
192
+ const candidate = `${row}\n`;
193
+ if (Buffer.byteLength(output + candidate) > MAX_OUTPUT_BYTES) break;
194
+ output += candidate; count++;
195
+ }
196
+ stdout.write(output);
197
+ const rowDropped = all.length - count;
198
+ const byteDropped = Buffer.byteLength(all.slice(count).join("\n") + (all.length > count ? "\n" : ""));
199
+ if (rowDropped > 0 || byteDropped > 0) line(`TRUNCATED: rows dropped=${rowDropped} bytes dropped=${byteDropped}`);
200
+ }
201
+ async function command(input) {
202
+ const parts = input.trim().split(/\s+/);
203
+ const cmd = parts.shift();
204
+ if (!cmd) return;
205
+ if (cmd === "quit") { transport?.destroy?.(); process.exit(0); }
206
+ if (cmd === "launch") return launch(parts.shift(), parts.shift(), parts);
207
+ if (cmd === "attach") {
208
+ const [host, port] = (parts.shift() || "").split(":");
209
+ try {
210
+ await startAdapter(`${host}:${port}`);
211
+ await request("initialize", { clientID: "omo-dap", adapterID: "debugpy", linesStartAt1: true, columnsStartAt1: true, pathFormat: "path" });
212
+ await waitInitialized();
213
+ await request("attach", { justMyCode: false });
214
+ await request("configurationDone");
215
+ line("READY: attach");
216
+ } catch (e) { error(e.message === "timeout" ? "timeout" : "adapter-failed"); }
217
+ return;
218
+ }
219
+ if (!transport) return error("no-session");
220
+ try {
221
+ if (cmd === "break" || cmd === "rmbreak") {
222
+ const [file, lineNo] = (parts[0] || "").split(":");
223
+ if (!file || !lineNo) return error("invalid-args");
224
+ const set = breakpoints.get(file) || [];
225
+ const next = cmd === "break" ? [...set.filter(x => x.line !== Number(lineNo)), { line: Number(lineNo) }] : set.filter(x => x.line !== Number(lineNo));
226
+ const response = await request("setBreakpoints", { source: { path: file }, breakpoints: next });
227
+ breakpoints.set(file, next);
228
+ if (response.body?.breakpoints?.some(bp => bp.verified === false)) error("unverified-breakpoint"); else line(`BREAK: ${file}:${lineNo}`);
229
+ } else if (["continue", "step", "next", "stepin", "stepout", "pause"].includes(cmd)) {
230
+ const dapCmd = { step: "next", stepin: "stepIn", stepout: "stepOut" }[cmd] || cmd;
231
+ const threadId = currentStop?.threadId || 1;
232
+ currentStop = null;
233
+ await request(dapCmd, { threadId });
234
+ // Wait for the resulting stopped event (breakpoint hit / step completed) before
235
+ // snapshotting; without this the stack is read while the debuggee is still running.
236
+ await waitForStop();
237
+ await snapshot();
238
+ } else if (cmd === "stack") {
239
+ const r = await request("stackTrace", { threadId: currentStop?.threadId || 1, levels: Number(parts[0]) || 100 });
240
+ boundedTable(["FRAME", "NAME", "FILE", "LINE", "COLUMN"], (r.body?.stackFrames || []).map(f => [String(f.id), f.name, f.source?.path || "", String(f.line || ""), String(f.column || "")]));
241
+ } else if (cmd === "scopes") {
242
+ const r = await request("scopes", { frameId: topFrame?.id || 42 });
243
+ boundedTable(["NAME", "VARIABLES_REFERENCE"], (r.body?.scopes || []).map(s => [s.name, String(s.variablesReference)]));
244
+ } else if (cmd === "vars") {
245
+ if (!parts[0]) return error("invalid-args");
246
+ const r = await request("variables", { variablesReference: Number(parts[0]) });
247
+ boundedTable(["NAME", "VALUE"], (r.body?.variables || []).map(v => [v.name, v.value ?? ""]));
248
+ } else if (cmd === "eval") {
249
+ if (!parts.length) return error("invalid-args");
250
+ const r = await request("evaluate", { expression: parts.join(" "), frameId: topFrame?.id || 42 });
251
+ line(`EVAL\t${r.body?.result || ""}`);
252
+ } else if (cmd === "threads") {
253
+ const r = await request("threads"); boundedTable(["ID", "NAME"], (r.body?.threads || []).map(t => [String(t.id), t.name]));
254
+ } else if (cmd === "sessions") line("SESSION\tSTATE\n1\tactive");
255
+ else if (cmd === "terminate") { await request("terminate", { restart: false }); line("EXIT: terminated"); terminated = true; }
256
+ else error("invalid-args");
257
+ } catch (e) { error(e.message === "timeout" ? "timeout" : e.message === "adapter-error" ? "adapter-error" : "adapter-failed"); }
258
+ }
259
+
260
+ if (import.meta.main) {
261
+ // Serialize stdin commands: fire-and-forget (`void command(...)`) lets a `continue`
262
+ // race ahead of the launch handshake's configurationDone, and the adapter then
263
+ // drops or rejects the out-of-order request. Each line awaits the previous command.
264
+ let chain = Promise.resolve();
265
+ const rl = createInterface({ input: stdin, crlfDelay: Infinity });
266
+ rl.on("line", value => { chain = chain.then(() => command(value)).catch(() => {}); });
267
+ }
@@ -0,0 +1,86 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { spawn } from "node:child_process";
3
+ import { join } from "node:path";
4
+
5
+ const dir = import.meta.dir;
6
+ const client = join(dir, "dap.mjs");
7
+ const fixture = join(dir, "fixture-adapter.mjs");
8
+
9
+ test("treats Windows drive-letter paths as executable specs, not host-port specs", async () => {
10
+ const { isTcpAdapterSpec } = await import("./dap.mjs");
11
+ expect(isTcpAdapterSpec("C:\\workspace\\fixture-adapter.mjs")).toBe(false);
12
+ expect(isTcpAdapterSpec("127.0.0.1:5678")).toBe(true);
13
+ });
14
+
15
+ function frame(message: unknown) {
16
+ const body = Buffer.from(JSON.stringify(message));
17
+ return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]);
18
+ }
19
+
20
+ describe("DAP framing", () => {
21
+ test("parses multiple frames and frames split across chunks", async () => {
22
+ const { DapFrameParser } = await import("./dap.mjs");
23
+ const parser = new DapFrameParser();
24
+ const first = frame({ type: "event", event: "one" });
25
+ const second = frame({ type: "event", event: "two", body: { ok: true } });
26
+ expect(parser.push(Buffer.concat([first, second]))).toEqual([{ type: "event", event: "one" }, { type: "event", event: "two", body: { ok: true } }]);
27
+ const third = frame({ type: "response", seq: 3 });
28
+ expect(parser.push(third.subarray(0, 9))).toEqual([]);
29
+ expect(parser.push(third.subarray(9, 17))).toEqual([]);
30
+ expect(parser.push(third.subarray(17))).toEqual([{ type: "response", seq: 3 }]);
31
+ });
32
+ });
33
+
34
+ function session(...fixtureArgs: string[]) {
35
+ // 300ms request timeout ONLY for the --no-answer session, whose test asserts the
36
+ // timeout classification and needs it to fire fast. Answering sessions get 5s:
37
+ // the vars-8 fixture response is ~130KB over a pipe, and on a loaded Windows CI
38
+ // runner that transfer exceeded 300ms, so the request timed out and TRUNCATED
39
+ // output could never appear (run 33065571760: byte-cap test burned its full
40
+ // 15s wait while vars-7's ~5KB response stayed under 300ms and passed).
41
+ const noAnswer = fixtureArgs.includes("--no-answer");
42
+ const child = spawn("bun", [client], { cwd: dir, env: { ...process.env, DAP_TIMEOUT_MS: noAnswer ? "300" : "5000", ...(noAnswer ? { DAP_FIXTURE_NO_ANSWER: "1" } : {}) } });
43
+ let output = "";
44
+ child.stdout.on("data", data => { output += data.toString(); });
45
+ const api = { child, get output() { return output; }, command(line: string) { child.stdin.write(`${line}\n`); } };
46
+ api.command(`launch ${fixture} /tmp/program.py`);
47
+ return api;
48
+ }
49
+ async function until(s: ReturnType<typeof session>, predicate: (out: string) => boolean) {
50
+ // 15s, not 3s: the file's first `spawn("bun", ...)` on a cold Windows CI runner
51
+ // (bun.exe cold launch + AV scan + fixture adapter spawn) was observed at just
52
+ // over 3s (dev run 33064234392: first READY wait failed at 3024ms while the
53
+ // warm sessions of later tests passed). `until` returns as soon as the
54
+ // predicate holds, so the raise costs nothing on the green path.
55
+ const deadline = Date.now() + 15_000;
56
+ while (!predicate(s.output) && Date.now() < deadline) await Bun.sleep(10);
57
+ expect(predicate(s.output)).toBe(true);
58
+ }
59
+
60
+ test("full session emits stop snapshot, stack and capped variables", async () => {
61
+ const s = session();
62
+ await until(s, out => out.includes("READY: launch"));
63
+ s.command("break /tmp/program.py:12"); await until(s, out => out.includes("BREAK:"));
64
+ s.command("continue"); await until(s, out => out.includes("STOP: stopped reason=breakpoint threadId=1 main at /tmp/program.py:12:3"));
65
+ s.command("stack"); await until(s, out => out.includes("FRAME\t"));
66
+ s.command("vars 7"); await until(s, out => out.includes("TRUNCATED: rows dropped=150"));
67
+ expect((s.output.match(/^v\d+\t/gm) ?? []).length).toBe(100);
68
+ s.command("terminate"); await until(s, out => out.includes("EXIT:"));
69
+ s.command("quit");
70
+ }, 60_000);
71
+
72
+ test("byte cap reports dropped bytes", async () => {
73
+ const s = session();
74
+ await until(s, out => out.includes("READY: launch"));
75
+ s.command("vars 8"); await until(s, out => out.includes("TRUNCATED: rows dropped=") && out.includes("bytes dropped="));
76
+ s.command("quit");
77
+ }, 60_000);
78
+
79
+ test("unverified breakpoint and timeout are classified", async () => {
80
+ const s = session();
81
+ await until(s, out => out.includes("READY: launch"));
82
+ s.command("break /tmp/unverified.py:9"); await until(s, out => out.includes("ERR: unverified-breakpoint"));
83
+ const t = session("--no-answer");
84
+ await until(t, out => out.includes("ERR: timeout"));
85
+ s.command("quit"); t.command("quit");
86
+ }, 60_000);
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic DAP fixture used by dap.test.ts.
3
+ import { stdin, stdout, argv } from "node:process";
4
+
5
+ let seq = 1;
6
+ let buffer = Buffer.alloc(0);
7
+ const noAnswer = argv.includes("--no-answer") || process.env.DAP_FIXTURE_NO_ANSWER === "1";
8
+
9
+ function send(message) {
10
+ const body = Buffer.from(JSON.stringify(message));
11
+ stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
12
+ stdout.write(body);
13
+ }
14
+ function response(request, body = {}, success = true, message) {
15
+ send({ type: "response", seq: seq++, request_seq: request.seq, success, command: request.command, body, ...(message ? { message } : {}) });
16
+ }
17
+ function event(event, body = {}) { send({ type: "event", seq: seq++, event, body }); }
18
+ function handle(request) {
19
+ if (noAnswer && request.command === "initialize") return;
20
+ switch (request.command) {
21
+ case "initialize":
22
+ response(request, { supportsConfigurationDoneRequest: true, supportsEvaluateForHovers: true });
23
+ event("initialized");
24
+ break;
25
+ case "setBreakpoints": {
26
+ const bps = request.arguments?.breakpoints ?? [];
27
+ response(request, { breakpoints: bps.map((bp, i) => ({ id: i + 1, verified: !request.arguments?.source?.path?.includes("unverified"), line: bp.line, column: bp.column ?? 1, message: "fixture" })) });
28
+ break;
29
+ }
30
+ case "configurationDone": response(request); break;
31
+ case "launch": response(request); break;
32
+ case "continue": response(request, { allThreadsContinued: true }); event("stopped", { reason: "breakpoint", threadId: 1, allThreadsStopped: true }); break;
33
+ case "next": case "stepIn": case "stepOut": response(request); event("stopped", { reason: "step", threadId: 1 }); break;
34
+ case "pause": response(request); event("stopped", { reason: "pause", threadId: 1 }); break;
35
+ case "threads": response(request, { threads: [{ id: 1, name: "main" }] }); break;
36
+ case "stackTrace": response(request, { stackFrames: [{ id: 42, name: "main", source: { path: "/tmp/program.py" }, line: 12, column: 3 }] }); break;
37
+ case "scopes": response(request, { scopes: [{ name: "Locals", variablesReference: 7, expensive: false }] }); break;
38
+ case "variables": response(request, { variables: Array.from({ length: 250 }, (_, i) => ({ name: `v${i + 1}`, value: request.arguments?.variablesReference === 8 ? `${String(i + 1)} ${"x".repeat(500)}` : String(i + 1), variablesReference: 0 })) }); break;
39
+ case "evaluate": response(request, { result: "42", type: "int", variablesReference: 0 }); break;
40
+ case "terminate": response(request); event("terminated", {}); break;
41
+ default: response(request, {}, false, `unsupported ${request.command}`);
42
+ }
43
+ }
44
+ stdin.on("data", chunk => {
45
+ buffer = Buffer.concat([buffer, chunk]);
46
+ while (true) {
47
+ const marker = buffer.indexOf(Buffer.from("\r\n\r\n"));
48
+ if (marker < 0) return;
49
+ const header = buffer.subarray(0, marker).toString();
50
+ const match = header.match(/Content-Length:\s*(\d+)/i);
51
+ if (!match) { buffer = buffer.subarray(marker + 4); continue; }
52
+ const length = Number(match[1]);
53
+ const start = marker + 4;
54
+ if (buffer.length < start + length) return;
55
+ const body = buffer.subarray(start, start + length);
56
+ buffer = buffer.subarray(start + length);
57
+ try { handle(JSON.parse(body.toString("utf8"))); } catch { /* fixture input is controlled */ }
58
+ }
59
+ });