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.
- package/dist/cli/index.js +290 -288
- package/dist/cli-node/index.js +290 -288
- package/dist/index.js +1675 -1657
- package/dist/skills/debugging/SKILL.md +2 -0
- package/dist/skills/debugging/references/methodology/00-setup.md +12 -0
- package/dist/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/dist/skills/debugging/references/runtimes/go.md +15 -1
- package/dist/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/dist/skills/debugging/references/runtimes/node.md +25 -0
- package/dist/skills/debugging/references/runtimes/python.md +13 -0
- package/dist/skills/debugging/references/runtimes/rust.md +41 -0
- package/dist/skills/debugging/references/scripts/dap.mjs +267 -0
- package/dist/skills/debugging/references/scripts/dap.test.ts +86 -0
- package/dist/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/dist/skills/debugging/references/tools/dap.md +103 -0
- package/dist/skills/debugging/references/tools/frida.md +193 -0
- package/dist/tools/delegate-task/types.d.ts +3 -0
- package/dist/tui.js +131 -147
- package/package.json +18 -15
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +25 -5
- package/packages/lsp-core/src/lsp/client.ts +26 -0
- package/packages/lsp-core/src/lsp/connection.ts +16 -0
- package/packages/lsp-core/src/lsp/constants.ts +1 -0
- package/packages/lsp-core/src/lsp/format-document.test.ts +143 -0
- package/packages/lsp-core/src/lsp/format-document.ts +90 -0
- package/packages/lsp-core/src/lsp/manager-max-clients.test.ts +151 -0
- package/packages/lsp-core/src/lsp/manager.ts +34 -1
- package/packages/lsp-core/src/lsp/server-definitions.ts +29 -0
- package/packages/lsp-core/src/lsp/server-installation.test.ts +376 -0
- package/packages/lsp-core/src/lsp/server-installation.ts +200 -27
- package/packages/lsp-core/src/lsp/server-resolution-local-binary.test.ts +170 -0
- package/packages/lsp-core/src/lsp/server-resolution.ts +18 -5
- package/packages/lsp-core/src/lsp/transport.ts +9 -0
- package/packages/lsp-core/src/lsp/types.ts +8 -0
- package/packages/lsp-core/src/tool-surface.test.ts +13 -1
- package/packages/lsp-core/src/tools/definitions.ts +14 -0
- package/packages/lsp-core/src/tools/format.test.ts +56 -0
- package/packages/lsp-core/src/tools/format.ts +56 -0
- package/packages/lsp-core/src/tools/index.ts +1 -0
- package/packages/lsp-core/src/tools/types.ts +10 -0
- package/packages/lsp-daemon/dist/cli.js +432 -144
- package/packages/lsp-daemon/dist/client.d.ts +10 -0
- package/packages/lsp-daemon/dist/client.js +401 -107
- package/packages/lsp-daemon/dist/daemon-client.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-client.js +3 -0
- package/packages/lsp-daemon/dist/index.js +395 -107
- package/packages/lsp-daemon/package.json +1 -1
- package/packages/lsp-tools-mcp/dist/cli.js +384 -99
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +53 -0
- package/packages/lsp-tools-mcp/dist/mcp.js +384 -99
- package/packages/lsp-tools-mcp/dist/tools.js +385 -99
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +69 -35
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +21 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +404 -121
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +3 -3
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +3 -3
- package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +4 -4
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +30 -15
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +30 -15
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +22 -10
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +3 -5
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +26 -2
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/model-catalog.json +3 -3
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +3 -3
- package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/go.md +15 -1
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/node.md +25 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/python.md +13 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/rust.md +41 -0
- package/packages/omo-codex/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
- package/packages/omo-codex/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/dap.md +103 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/frida.md +193 -0
- package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +2 -2
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +8 -8
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +2 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +38 -23
- package/packages/shared-skills/skills/debugging/SKILL.md +2 -0
- package/packages/shared-skills/skills/debugging/references/methodology/00-setup.md +12 -0
- package/packages/shared-skills/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/go.md +15 -1
- package/packages/shared-skills/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/node.md +25 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/python.md +13 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/rust.md +41 -0
- package/packages/shared-skills/skills/debugging/references/scripts/dap.mjs +267 -0
- package/packages/shared-skills/skills/debugging/references/scripts/dap.test.ts +86 -0
- package/packages/shared-skills/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/packages/shared-skills/skills/debugging/references/tools/dap.md +103 -0
- package/packages/shared-skills/skills/debugging/references/tools/frida.md +193 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# DAP Client (`dap.mjs`) — Drive the Debugger's Protocol, Not Its Text
|
|
2
|
+
|
|
3
|
+
**Design modeled on the `debug` tool of oh-my-pi (https://github.com/can1357/oh-my-pi, branch omp2).** Their harness proved the shape: one structured debug surface with bounded output, stop snapshots, and classified errors. This script brings the same discipline to any agent with a shell — no harness tool registration required.
|
|
4
|
+
|
|
5
|
+
Debuggers already speak the Debug Adapter Protocol (DAP), a machine-readable JSON protocol. Driving DAP beats screen-scraping a PTY for the same reason an API beats OCR: structured stops, structured variables, structured errors. **If the debugger speaks DAP, use this script instead of parsing `gdb`/`pdb` output.**
|
|
6
|
+
|
|
7
|
+
The script is at `references/scripts/dap.mjs` — zero dependencies, runs under Bun or Node.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## When to use which interface
|
|
12
|
+
|
|
13
|
+
| Situation | Use |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Source-level debugging of Python / Go / Node / native code, and you need breakpoints, stepping, variables | `dap.mjs` (this file) |
|
|
16
|
+
| Browser-served JS, or anything already in Chrome | Scripted CDP — see `references/runtimes/node.md` |
|
|
17
|
+
| Stripped binary, no source, no symbols | Ghidra (static) + Frida (live) — see `references/tools/` |
|
|
18
|
+
| The debugger has no DAP mode (plain gdb on an exotic target) | pwndbg, with the output-budget rule from `references/methodology/00-setup.md` |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How to drive it
|
|
23
|
+
|
|
24
|
+
`dap.mjs` is a **REPL**, not a one-shot command: a debug session is long-lived, so the script runs as a persistent process reading one command per line on stdin and writing bounded text on stdout. Run it as a background shell session, send commands to its stdin, and subscribe to its output.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
bun references/scripts/dap.mjs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every execution-control answer prints a single snapshot line beginning with `STOP:` — subscribe your watcher to that exact prefix so a breakpoint hit wakes you instead of you polling.
|
|
31
|
+
|
|
32
|
+
### Commands
|
|
33
|
+
|
|
34
|
+
| Command | Effect |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `launch <adapter> <program> [args...]` | Spawn a stdio adapter (path to executable or `.mjs` adapter) and launch the program under it |
|
|
37
|
+
| `attach <host:port>` | Connect to a listening adapter over TCP and attach |
|
|
38
|
+
| `break <file>:<line>` / `rmbreak <file>:<line>` | Set / remove a source breakpoint |
|
|
39
|
+
| `continue` / `step` / `next` / `stepin` / `stepout` / `pause` | Execution control; each prints a `STOP:` snapshot when the debuggee next stops |
|
|
40
|
+
| `stack [limit]` | TSV backtrace, bounded |
|
|
41
|
+
| `scopes` | Scopes of the top frame with their variablesReferences |
|
|
42
|
+
| `vars <ref>` | Variables under a variablesReference from `scopes` — refs are session-scoped; always chain `scopes` first |
|
|
43
|
+
| `eval <expr>` | Evaluate in the top frame |
|
|
44
|
+
| `threads` / `sessions` | Thread list / session state |
|
|
45
|
+
| `terminate` / `quit` | End the debuggee / exit the REPL |
|
|
46
|
+
|
|
47
|
+
### Output contract (what keeps your context alive)
|
|
48
|
+
|
|
49
|
+
- Tabular results are TSV with a header row.
|
|
50
|
+
- Hard caps: `MAX_ROWS = 100`, `MAX_OUTPUT_BYTES = 32 KB`. Overflow prints an explicit `TRUNCATED: rows dropped=N bytes dropped=M` line — never a silent cut.
|
|
51
|
+
- After every continue/step: `STOP: stopped reason=<why> threadId=<id> <frame> at <file>:<line>:<col>`. On debuggee exit: `EXIT: terminated`.
|
|
52
|
+
- Errors are one line, classified: `ERR: invalid-args | no-session | adapter-failed | unverified-breakpoint | timeout | terminated | adapter-error`. The token tells you the recovery — see the failure taxonomy in `references/methodology/02-investigate.md`.
|
|
53
|
+
- Every request times out after 15 s (override with `DAP_TIMEOUT_MS`) and the session stays alive.
|
|
54
|
+
- `DAP_DEBUG=1` logs raw protocol traffic to stderr for diagnosing a misbehaving adapter.
|
|
55
|
+
|
|
56
|
+
### Adapter quirks this client already handles
|
|
57
|
+
|
|
58
|
+
- **debugpy** requires `console: "internalConsole"` in the launch request, and withholds the `initialized` event for unrecognized `adapterID`s — the client sends both correctly. It also flushes `initialized` lazily, so the client launches first and awaits the event after.
|
|
59
|
+
- **lldb-dap** reports the launch response's `success` unreliably (`false` even when the process started and stopped) and reports `threadId: 0` on the entry stop. The events are the real signal; the client tolerates the response flag.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Adapter matrix
|
|
64
|
+
|
|
65
|
+
Translated from oh-my-pi's `builtin_adapters()` (omp2 `crates/docserver/src/dap_adapter.rs`). Install the one matching your runtime; the client speaks to all of them the same way.
|
|
66
|
+
|
|
67
|
+
| Runtime | Adapter | Install | Launch mode |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| Python | debugpy | `pip install debugpy` | `python -m debugpy.adapter` (stdio) |
|
|
70
|
+
| Go | dlv | `go install github.com/go-delve/delve/cmd/dlv@latest` | `dlv dap` (stdio) |
|
|
71
|
+
| C/C++/Swift/Rust/Zig | lldb-dap | ships with Xcode CLT / LLVM | `lldb-dap` (stdio) |
|
|
72
|
+
| C/C++/Rust/Zig | codelldb | VS Code extension binary | stdio |
|
|
73
|
+
| C/C++/Rust | gdb | `brew install gdb` / apt | via gdb's DAP mode where available |
|
|
74
|
+
| JS/TS | js-debug | `npm i -g @vscode/js-debug` | stdio |
|
|
75
|
+
| .NET | netcoredbg | GitHub releases | stdio |
|
|
76
|
+
| Ruby | rdbg | `gem install debug` | stdio |
|
|
77
|
+
| Kotlin | kotlin-debug-adapter | GitHub releases | stdio |
|
|
78
|
+
| PHP | php-debug-adapter | composer | stdio |
|
|
79
|
+
| Bash | bash-debug-adapter | GitHub releases | stdio |
|
|
80
|
+
| Dart/Flutter | dart debug adapter | ships with Dart SDK | `dart` (stdio) |
|
|
81
|
+
| Elixir | elixir-ls debugger | GitHub releases | stdio |
|
|
82
|
+
|
|
83
|
+
If an adapter is missing, the install line is the fix — do not fall back to scraping a REPL.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Worked example (Python)
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
launch /path/to/python -m debugpy.adapter myscript.py # or a wrapper script path
|
|
91
|
+
break myscript.py:12
|
|
92
|
+
continue
|
|
93
|
+
STOP: stopped reason=breakpoint threadId=1 compute at myscript.py:12:1
|
|
94
|
+
stack
|
|
95
|
+
scopes
|
|
96
|
+
vars 6 # the variablesReference scopes just printed for Locals
|
|
97
|
+
eval total
|
|
98
|
+
continue
|
|
99
|
+
terminate
|
|
100
|
+
quit
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Cleanup is part of the session: `terminate` the debuggee, `quit` the REPL, and journal any wrapper scripts you created per the skill's cleanup phase.
|