@siuver/omp-debug-mode 0.1.0
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/CHANGELOG.md +7 -0
- package/README.md +56 -0
- package/package.json +41 -0
- package/src/main.ts +594 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-08-20
|
|
4
|
+
|
|
5
|
+
- Initial npm release of the Cursor Debug Mode replica for OMP.
|
|
6
|
+
- Added iterative hypothesis, instrumentation, fix, and user-reproduction rounds.
|
|
7
|
+
- Added local probe logging, session resume, probe-ledger cleanup, debug commands, and evidence-reading tools.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @siuver/omp-debug-mode
|
|
2
|
+
|
|
3
|
+
A Cursor Debug Mode replica for oh-my-pi. It recreates Cursor's evidence-driven, human-in-the-loop debugging workflow for OMP; it is an independent implementation and is not affiliated with Cursor.
|
|
4
|
+
|
|
5
|
+
The plugin makes the agent form hypotheses, add temporary runtime probes, attempt a fix, and then pause while you reproduce the problem outside the agent loop. Your result either starts another evidence-driven round or triggers probe cleanup and a final summary.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
| Command | Purpose |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `/debug-mode <problem>` | Starts a debugging session from a symptom, expected result, actual result, and reproduction description. |
|
|
12
|
+
| `/debug-status` | Shows the current phase, round, run, live probes, and captured log counts. |
|
|
13
|
+
| `/debug-done fixed` | Confirms the fix and asks the agent to remove every probe and summarize the root cause and final change. |
|
|
14
|
+
| `/debug-done proceed` | Reports that the issue remains, asks the agent to analyze the captured logs, and starts another hypothesis/instrument/fix round. |
|
|
15
|
+
| `/debug-abort` | Stops debug mode and removes its logs while leaving code changes in the working tree. |
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. Run `/debug-mode <problem description>`.
|
|
20
|
+
2. The agent investigates, records hypotheses, inserts minimal probes marked with `@omp-probe <id>`, and attempts a fix.
|
|
21
|
+
3. When the agent stops, reproduce the issue in the real application.
|
|
22
|
+
4. Run `/debug-done fixed` if the issue is resolved, or `/debug-done proceed` to analyze evidence and continue.
|
|
23
|
+
5. On success, the agent removes all probes, verifies the probe ledger is empty, and summarizes the root cause and fix.
|
|
24
|
+
|
|
25
|
+
The extension also provides the read-only `get_debug_logs` and `list_debug_probes` tools so the agent can inspect runtime evidence and verify cleanup.
|
|
26
|
+
|
|
27
|
+
## Runtime Data
|
|
28
|
+
|
|
29
|
+
During an active session, the plugin starts a local HTTP server on `127.0.0.1:7842`. Instrumentation posts `{ "probe": "...", "data": ... }` to `/log`, and the plugin appends the entries to `<project>/.omp/debug/<run>.jsonl`.
|
|
30
|
+
|
|
31
|
+
The server only listens on localhost. Debug data may still contain values captured from your application, so review probe payloads before reproducing sensitive workflows. Finishing or aborting debug mode removes the debug log directory. Applied fixes remain in the working tree for review.
|
|
32
|
+
|
|
33
|
+
Debug state is recorded in the OMP session so an active workflow can resume after reopening the session. Port `7842` must be available for probes to report data.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
omp plugin install @siuver/omp-debug-mode
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Restart omp after installation, then run `/debug-mode <problem>`.
|
|
42
|
+
|
|
43
|
+
## Local Development
|
|
44
|
+
|
|
45
|
+
From the repository root:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bun run check
|
|
49
|
+
omp plugin link ./plugins/debug-mode
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Restart omp after linking.
|
|
53
|
+
|
|
54
|
+
## Changelog
|
|
55
|
+
|
|
56
|
+
See `CHANGELOG.md` for release notes included in the npm package.
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@siuver/omp-debug-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A Cursor Debug Mode replica for evidence-driven, human-in-the-loop debugging in oh-my-pi.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/Siuver/blackhole/tree/master/plugins/debug-mode#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Siuver/blackhole.git",
|
|
11
|
+
"directory": "plugins/debug-mode"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"omp",
|
|
15
|
+
"oh-my-pi",
|
|
16
|
+
"extension",
|
|
17
|
+
"debug",
|
|
18
|
+
"cursor",
|
|
19
|
+
"human-in-the-loop"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/main.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"!src/**/*.test.ts",
|
|
27
|
+
"package.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"omp": {
|
|
32
|
+
"name": "debug-mode",
|
|
33
|
+
"description": "Cursor-style human-in-the-loop debugging with runtime probes and reproduction gates.",
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./src/main.ts"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@oh-my-pi/pi-coding-agent": "*"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Mode Extension — Cursor-style human-in-the-loop debugging for omp.
|
|
3
|
+
*
|
|
4
|
+
* State machine:
|
|
5
|
+
* IDLE → /debug-mode <problem>
|
|
6
|
+
* → agent hypothesizes, adds @omp-probe instrumentation, attempts a fix
|
|
7
|
+
* → WAITING_REPRO: agent stops; user reproduces out-of-band
|
|
8
|
+
* → /debug-done fixed → agent removes probes + summarizes → teardown (logs deleted)
|
|
9
|
+
* → /debug-done proceed → agent analyzes captured logs, re-instruments + fixes → loop
|
|
10
|
+
*
|
|
11
|
+
* Components:
|
|
12
|
+
* - Local log server (Bun.serve, 127.0.0.1:7842) — probes POST {probe, data};
|
|
13
|
+
* logs append to <cwd>/.omp/debug/<run>.jsonl
|
|
14
|
+
* - Probe ledger: tool_call interception on edit/write records @omp-probe ids;
|
|
15
|
+
* ground truth is rescanned from disk (checkLedger)
|
|
16
|
+
* - Tools: get_debug_logs, list_debug_probes
|
|
17
|
+
* - Commands: /debug-mode, /debug-status, /debug-done fixed|proceed, /debug-abort
|
|
18
|
+
*/
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
20
|
+
import type { Server } from "bun";
|
|
21
|
+
import type { WriteStream } from "node:fs";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
|
|
25
|
+
const DEBUG_ENTRY = "com.omp.debug-mode.state";
|
|
26
|
+
const PROBE_MARK = /@omp-probe\s+([A-Za-z0-9_-]+)/g;
|
|
27
|
+
const DEFAULT_PORT = 7842;
|
|
28
|
+
|
|
29
|
+
type Phase = "idle" | "round" | "waiting" | "cleanup";
|
|
30
|
+
|
|
31
|
+
interface Probe {
|
|
32
|
+
id: string;
|
|
33
|
+
file: string;
|
|
34
|
+
round: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DebugState {
|
|
38
|
+
active: boolean;
|
|
39
|
+
phase: Phase;
|
|
40
|
+
problem: string;
|
|
41
|
+
round: number;
|
|
42
|
+
runId: string | null;
|
|
43
|
+
probes: Probe[];
|
|
44
|
+
debugDir: string | null;
|
|
45
|
+
logCounts: Record<string, number>;
|
|
46
|
+
hasRoundContent: boolean;
|
|
47
|
+
cleanupReady: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface LogEntry {
|
|
51
|
+
run: string;
|
|
52
|
+
probe: string;
|
|
53
|
+
ts: number;
|
|
54
|
+
data: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function freshState(): DebugState {
|
|
58
|
+
return {
|
|
59
|
+
active: false,
|
|
60
|
+
phase: "idle",
|
|
61
|
+
problem: "",
|
|
62
|
+
round: 0,
|
|
63
|
+
runId: null,
|
|
64
|
+
probes: [],
|
|
65
|
+
debugDir: null,
|
|
66
|
+
logCounts: {},
|
|
67
|
+
hasRoundContent: false,
|
|
68
|
+
cleanupReady: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function probeIdsIn(text: unknown): string[] {
|
|
73
|
+
if (typeof text !== "string") return [];
|
|
74
|
+
const ids: string[] = [];
|
|
75
|
+
for (const m of text.matchAll(PROBE_MARK)) ids.push(m[1]);
|
|
76
|
+
return ids;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function blackboard(s: DebugState): string {
|
|
80
|
+
const probes = s.probes.map(p => `${p.id} (${p.file}, round ${p.round})`).join("\n ") || "(none)";
|
|
81
|
+
const counts = Object.entries(s.logCounts)
|
|
82
|
+
.map(([run, n]) => `${run}: ${n}`)
|
|
83
|
+
.join(", ") || "(none yet)";
|
|
84
|
+
return `\
|
|
85
|
+
[DEBUG MODE ACTIVE — round ${s.round}]
|
|
86
|
+
|
|
87
|
+
Problem under investigation:
|
|
88
|
+
${s.problem}
|
|
89
|
+
|
|
90
|
+
Deployed probes (ground truth, maintained by the extension):
|
|
91
|
+
${probes}
|
|
92
|
+
|
|
93
|
+
Log endpoint for probes: POST JSON {probe, data} to http://127.0.0.1:${DEFAULT_PORT}/log
|
|
94
|
+
Probe code MUST carry the marker comment \`// @omp-probe <id>\` adjacent to it.
|
|
95
|
+
Logs by run: ${counts}
|
|
96
|
+
Current run id: ${s.runId ?? "(not started)"}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const METHODOLOGY = `\
|
|
100
|
+
[DEBUG MODE METHODOLOGY — follow strictly]
|
|
101
|
+
Each round consists of, in order:
|
|
102
|
+
1. Restate hypotheses (mark each: pending / ruled-out / confirmed via runtime evidence).
|
|
103
|
+
2. Update instrumentation: remove probes that yielded no information, add probes that
|
|
104
|
+
discriminate between remaining hypotheses. Each probe is one minimal statement sending
|
|
105
|
+
JSON via HTTP POST to the log endpoint, with the marker comment \`// @omp-probe <id>\`
|
|
106
|
+
adjacent (language-appropriate; keep the block small and easy to remove).
|
|
107
|
+
3. ATTEMPT A FIX for your leading hypothesis in the same round. Instrumentation without
|
|
108
|
+
a fix is an incomplete round — the loop only advances when you fix something.
|
|
109
|
+
4. End the round by writing concise reproduction steps for the user (exact commands or
|
|
110
|
+
actions, what to observe). Then STOP — the user reproduces out-of-band and answers
|
|
111
|
+
with /debug-done fixed or /debug-done proceed.
|
|
112
|
+
When told the fix is confirmed: remove every probe, verify with the list_debug_probes
|
|
113
|
+
tool that the ledger is empty, then summarize root cause and the final fix.`;
|
|
114
|
+
|
|
115
|
+
export default function debugModeExtension(pi: ExtensionAPI) {
|
|
116
|
+
const z = pi.zod;
|
|
117
|
+
const state: DebugState = freshState();
|
|
118
|
+
let server: Server | null = null;
|
|
119
|
+
let logBuffer: LogEntry[] = [];
|
|
120
|
+
let pendingWrite: WriteStream | null = null;
|
|
121
|
+
let uiCtx: ExtensionContext | null = null;
|
|
122
|
+
|
|
123
|
+
// ============================== log server ==============================
|
|
124
|
+
|
|
125
|
+
function startServer(ctx: ExtensionContext, port: number): boolean {
|
|
126
|
+
if (server) return true;
|
|
127
|
+
const dir = path.join(ctx.cwd, ".omp", "debug");
|
|
128
|
+
try {
|
|
129
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
pi.logger.error("debug-mode: cannot create log dir", { dir, err });
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
state.debugDir = dir;
|
|
135
|
+
logBuffer = [];
|
|
136
|
+
try {
|
|
137
|
+
server = Bun.serve({
|
|
138
|
+
port,
|
|
139
|
+
hostname: "127.0.0.1",
|
|
140
|
+
idleTimeout: 5,
|
|
141
|
+
async fetch(req) {
|
|
142
|
+
const url = new URL(req.url);
|
|
143
|
+
if (req.method === "POST" && url.pathname === "/log") {
|
|
144
|
+
try {
|
|
145
|
+
const body = (await req.json()) as { probe?: unknown; data?: unknown };
|
|
146
|
+
if (typeof body.probe !== "string") return new Response("bad probe", { status: 400 });
|
|
147
|
+
const entry: LogEntry = {
|
|
148
|
+
run: state.runId ?? "orphan",
|
|
149
|
+
probe: body.probe,
|
|
150
|
+
ts: Date.now(),
|
|
151
|
+
data: body.data ?? null,
|
|
152
|
+
};
|
|
153
|
+
logBuffer.push(entry);
|
|
154
|
+
state.logCounts[entry.run] = (state.logCounts[entry.run] ?? 0) + 1;
|
|
155
|
+
pendingWrite?.write(JSON.stringify(entry) + "\n");
|
|
156
|
+
refreshUi();
|
|
157
|
+
return new Response("ok");
|
|
158
|
+
} catch {
|
|
159
|
+
return new Response("bad json", { status: 400 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
163
|
+
return new Response("ok");
|
|
164
|
+
}
|
|
165
|
+
return new Response("not found", { status: 404 });
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
pi.logger.error("debug-mode: log server failed to start", { err });
|
|
171
|
+
server = null;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function stopServer(): void {
|
|
177
|
+
pendingWrite?.end();
|
|
178
|
+
pendingWrite = null;
|
|
179
|
+
server?.stop(true);
|
|
180
|
+
server = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function newRun(): string {
|
|
184
|
+
pendingWrite?.end();
|
|
185
|
+
pendingWrite = null;
|
|
186
|
+
const run = `run${state.round}-${Date.now().toString(36)}`;
|
|
187
|
+
state.runId = run;
|
|
188
|
+
state.logCounts[run] = 0;
|
|
189
|
+
if (state.debugDir) {
|
|
190
|
+
pendingWrite = fs.createWriteStream(path.join(state.debugDir, `${run}.jsonl`), { flags: "a" });
|
|
191
|
+
}
|
|
192
|
+
return run;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================== probe ledger ==============================
|
|
196
|
+
|
|
197
|
+
pi.on("tool_call", async (event) => {
|
|
198
|
+
if (!state.active) return;
|
|
199
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
200
|
+
const input = event.input as Record<string, unknown>;
|
|
201
|
+
const file =
|
|
202
|
+
typeof input.path === "string" ? input.path : typeof input.file_path === "string" ? input.file_path : "(unknown)";
|
|
203
|
+
for (const v of Object.values(input)) {
|
|
204
|
+
for (const id of probeIdsIn(v)) {
|
|
205
|
+
if (!state.probes.some(p => p.id === id)) state.probes.push({ id, file, round: state.round });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// removal detection: edit inputs alone can't prove deletion — checkLedger()
|
|
209
|
+
// rescans files for ground truth on demand.
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/** Ground truth: which registered probes still exist in code. */
|
|
213
|
+
async function checkLedger(): Promise<Probe[]> {
|
|
214
|
+
const alive: Probe[] = [];
|
|
215
|
+
const byFile = new Map<string, Probe[]>();
|
|
216
|
+
for (const p of state.probes) {
|
|
217
|
+
const list = byFile.get(p.file) ?? [];
|
|
218
|
+
list.push(p);
|
|
219
|
+
byFile.set(p.file, list);
|
|
220
|
+
}
|
|
221
|
+
for (const [file, probes] of byFile) {
|
|
222
|
+
let text: string;
|
|
223
|
+
try {
|
|
224
|
+
text = await Bun.file(file).text();
|
|
225
|
+
} catch {
|
|
226
|
+
continue; // file gone → probe gone
|
|
227
|
+
}
|
|
228
|
+
for (const p of probes) {
|
|
229
|
+
if (text.includes(`@omp-probe ${p.id}`)) alive.push(p);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return alive;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================== UI ==============================
|
|
236
|
+
|
|
237
|
+
function refreshUi(): void {
|
|
238
|
+
const ctx = uiCtx;
|
|
239
|
+
if (!ctx?.hasUI) return;
|
|
240
|
+
if (!state.active) {
|
|
241
|
+
ctx.ui.setStatus("debug-mode", undefined);
|
|
242
|
+
ctx.ui.setWidget("debug-mode", undefined);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const label =
|
|
246
|
+
state.phase === "waiting" ? "🐞 waiting-repro" : state.phase === "round" ? `🐞 round ${state.round}` : "🐞 cleanup";
|
|
247
|
+
ctx.ui.setStatus("debug-mode", ctx.ui.theme.fg("warning", label));
|
|
248
|
+
const lines: string[] = [];
|
|
249
|
+
if (state.phase === "waiting") {
|
|
250
|
+
const n = state.runId ? (state.logCounts[state.runId] ?? 0) : 0;
|
|
251
|
+
lines.push(ctx.ui.theme.fg("accent", "reproduce the bug, then: /debug-done fixed | proceed"));
|
|
252
|
+
lines.push(ctx.ui.theme.fg("dim", `run ${state.runId} — ${n} log entries`));
|
|
253
|
+
}
|
|
254
|
+
ctx.ui.setWidget("debug-mode", lines.length ? lines : undefined);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================== prompt injection ==============================
|
|
258
|
+
|
|
259
|
+
pi.on("before_agent_start", async () => {
|
|
260
|
+
if (!state.active || (state.phase !== "round" && state.phase !== "cleanup")) return;
|
|
261
|
+
return {
|
|
262
|
+
message: {
|
|
263
|
+
customType: "debug-mode-context",
|
|
264
|
+
content: `${blackboard(state)}\n\n${METHODOLOGY}`,
|
|
265
|
+
display: false,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Drop stale blackboard messages; the freshest is re-injected each round.
|
|
271
|
+
pi.on("context", async (event) => {
|
|
272
|
+
const filtered = event.messages.filter(
|
|
273
|
+
m => !(m.role === "custom" && (m as { customType?: string }).customType === "debug-mode-context"),
|
|
274
|
+
);
|
|
275
|
+
if (filtered.length !== event.messages.length) return { messages: filtered };
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ============================== round lifecycle ==============================
|
|
279
|
+
|
|
280
|
+
pi.on("agent_start", async () => {
|
|
281
|
+
if (state.active) state.hasRoundContent = false;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
pi.on("message_end", async (event) => {
|
|
285
|
+
if (!state.active) return;
|
|
286
|
+
const msg = event.message as { role?: string };
|
|
287
|
+
if (msg?.role === "assistant") {
|
|
288
|
+
if (state.phase === "cleanup") state.cleanupReady = true;
|
|
289
|
+
state.hasRoundContent = true;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
pi.on("session_stop", async (_event, ctx) => {
|
|
294
|
+
if (!state.active) return;
|
|
295
|
+
if (state.phase === "cleanup") {
|
|
296
|
+
// cleanup turn settled → teardown (fixes kept, logs + server removed)
|
|
297
|
+
if (state.cleanupReady) await teardown(ctx, true);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (state.phase !== "round" || !state.hasRoundContent) return;
|
|
301
|
+
state.phase = "waiting";
|
|
302
|
+
refreshUi();
|
|
303
|
+
ctx.ui.notify(
|
|
304
|
+
`Debug round ${state.round} paused.\nReproduce the bug now, then run:\n /debug-done fixed — fix confirmed, clean up & summarize\n /debug-done proceed — analyze logs, next round`,
|
|
305
|
+
"info",
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
pi.registerCommand("debug-mode", {
|
|
310
|
+
description: "Start debug mode: /debug-mode <problem description>",
|
|
311
|
+
handler: async (args, ctx) => {
|
|
312
|
+
uiCtx = ctx;
|
|
313
|
+
if (state.active) {
|
|
314
|
+
ctx.ui.notify("debug-mode: already active (use /debug-done or /debug-abort)", "error");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const problem = args.trim();
|
|
318
|
+
if (!problem) {
|
|
319
|
+
ctx.ui.notify("Usage: /debug-mode <problem description — symptoms, expected vs actual, how to reproduce>", "error");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
startDebug(ctx, problem);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
function startDebug(ctx: ExtensionContext, problem: string): void {
|
|
327
|
+
state.active = true;
|
|
328
|
+
state.phase = "round";
|
|
329
|
+
state.problem = problem;
|
|
330
|
+
state.round = 1;
|
|
331
|
+
state.probes = [];
|
|
332
|
+
state.logCounts = {};
|
|
333
|
+
state.hasRoundContent = false;
|
|
334
|
+
if (!startServer(ctx, DEFAULT_PORT)) {
|
|
335
|
+
ctx.ui.notify("debug-mode: log server failed to start; probes will have nowhere to report", "error");
|
|
336
|
+
}
|
|
337
|
+
newRun();
|
|
338
|
+
refreshUi();
|
|
339
|
+
pi.sendMessage(
|
|
340
|
+
{
|
|
341
|
+
customType: "debug-mode-start",
|
|
342
|
+
content: `Starting debug mode. Problem report:\n\n${problem}\n\nBegin round 1: explore, hypothesize, instrument, and attempt a fix. Then give reproduction steps and stop.`,
|
|
343
|
+
display: true,
|
|
344
|
+
},
|
|
345
|
+
{ triggerTurn: true },
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function finishDebug(ctx: ExtensionContext, verdict: "fixed" | "proceed"): void {
|
|
350
|
+
if (!state.active) {
|
|
351
|
+
ctx.ui.notify("debug-mode: not active", "error");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (state.phase !== "waiting") {
|
|
355
|
+
ctx.ui.notify(`debug-mode: not waiting for reproduction (phase: ${state.phase})`, "error");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (verdict === "fixed") {
|
|
359
|
+
state.phase = "cleanup";
|
|
360
|
+
state.cleanupReady = false;
|
|
361
|
+
refreshUi();
|
|
362
|
+
pi.sendMessage(
|
|
363
|
+
{
|
|
364
|
+
customType: "debug-mode-fixed",
|
|
365
|
+
content:
|
|
366
|
+
"User marked the problem FIXED.\n" +
|
|
367
|
+
"1. Remove every debug probe from the code (probe ledger below; verify with list_debug_probes after edits).\n" +
|
|
368
|
+
"2. Then summarize: root cause, the fix applied, what remains in the working diff.\n" +
|
|
369
|
+
`Probe ledger: ${JSON.stringify(state.probes)}`,
|
|
370
|
+
display: true,
|
|
371
|
+
},
|
|
372
|
+
{ triggerTurn: true },
|
|
373
|
+
);
|
|
374
|
+
// teardown happens when the cleanup turn settles (session_stop above)
|
|
375
|
+
} else {
|
|
376
|
+
const run = state.runId ?? "(none)";
|
|
377
|
+
const n = state.logCounts[run] ?? 0;
|
|
378
|
+
state.round += 1;
|
|
379
|
+
state.phase = "round";
|
|
380
|
+
state.hasRoundContent = false;
|
|
381
|
+
newRun();
|
|
382
|
+
refreshUi();
|
|
383
|
+
pi.sendMessage(
|
|
384
|
+
{
|
|
385
|
+
customType: "debug-mode-proceed",
|
|
386
|
+
content:
|
|
387
|
+
`User chose PROCEED — the fix did not resolve it (run ${run} captured ${n} log entries).\n` +
|
|
388
|
+
(n === 0
|
|
389
|
+
? "No logs were captured: the probes may not have executed (wrong code path, not rebuilt, or server unreachable) — treat that as a signal.\n"
|
|
390
|
+
: "") +
|
|
391
|
+
"Read the logs with get_debug_logs, rule out / strengthen hypotheses, re-instrument, attempt the next fix, give reproduction steps, and stop.",
|
|
392
|
+
display: true,
|
|
393
|
+
},
|
|
394
|
+
{ triggerTurn: true },
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function teardown(ctx: ExtensionContext, keepFixes: boolean): Promise<void> {
|
|
400
|
+
if (state.debugDir) {
|
|
401
|
+
try {
|
|
402
|
+
fs.rmSync(state.debugDir, { recursive: true, force: true });
|
|
403
|
+
} catch (err) {
|
|
404
|
+
pi.logger.warn("debug-mode: failed to remove debug dir", { err });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
stopServer();
|
|
408
|
+
const probesLeft = await checkLedger();
|
|
409
|
+
Object.assign(state, freshState());
|
|
410
|
+
refreshUi();
|
|
411
|
+
if (probesLeft.length > 0) {
|
|
412
|
+
ctx.ui.notify(
|
|
413
|
+
`debug-mode ended, but ${probesLeft.length} probe(s) remain in code: ${probesLeft.map(p => p.id).join(", ")} — remove manually.`,
|
|
414
|
+
"warning",
|
|
415
|
+
);
|
|
416
|
+
} else if (keepFixes) {
|
|
417
|
+
ctx.ui.notify("Debug mode finished. Log files removed; working diff contains the fix — review with git diff.", "info");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ============================== commands ==============================
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
pi.registerCommand("debug-done", {
|
|
425
|
+
description: "Answer the reproduction gate: /debug-done fixed|proceed",
|
|
426
|
+
handler: async (args, ctx) => {
|
|
427
|
+
uiCtx = ctx;
|
|
428
|
+
const verdict = args.trim().toLowerCase();
|
|
429
|
+
if (verdict !== "fixed" && verdict !== "proceed") {
|
|
430
|
+
ctx.ui.notify("Usage: /debug-done fixed|proceed", "error");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
finishDebug(ctx, verdict);
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
pi.registerCommand("debug-abort", {
|
|
438
|
+
description: "Abort debug mode: delete logs (fixes stay in the working diff)",
|
|
439
|
+
handler: async (_args, ctx) => {
|
|
440
|
+
uiCtx = ctx;
|
|
441
|
+
if (!state.active) {
|
|
442
|
+
ctx.ui.notify("debug-mode: not active", "error");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const ledger = await checkLedger();
|
|
446
|
+
await teardown(ctx, true);
|
|
447
|
+
if (ledger.length > 0) {
|
|
448
|
+
ctx.ui.notify(
|
|
449
|
+
`Debug mode aborted. These probes remain (remove manually or ask the agent):\n${ledger.map(p => ` ${p.id} — ${p.file}`).join("\n")}\nApplied fixes are kept in the working diff.`,
|
|
450
|
+
"warning",
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
ctx.ui.notify("Debug mode aborted. No probes in code; applied fixes are kept in the working diff.", "info");
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
pi.registerCommand("debug-status", {
|
|
459
|
+
description: "Show debug mode state",
|
|
460
|
+
handler: async (_args, ctx) => {
|
|
461
|
+
uiCtx = ctx;
|
|
462
|
+
if (!state.active) {
|
|
463
|
+
ctx.ui.notify("debug-mode: idle", "info");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const alive = await checkLedger();
|
|
467
|
+
ctx.ui.notify(
|
|
468
|
+
`debug-mode: phase=${state.phase} round=${state.round} run=${state.runId}\n` +
|
|
469
|
+
`probes (ledger ${state.probes.length}, alive ${alive.length}):\n` +
|
|
470
|
+
(alive.map(p => ` ${p.id} — ${p.file}`).join("\n") || " (none)") +
|
|
471
|
+
`\nlogs: ${Object.entries(state.logCounts).map(([r, n]) => `${r}=${n}`).join(", ") || "(none)"}`,
|
|
472
|
+
"info",
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ============================== tools ==============================
|
|
478
|
+
|
|
479
|
+
pi.registerTool({
|
|
480
|
+
name: "get_debug_logs",
|
|
481
|
+
label: "Get Debug Logs",
|
|
482
|
+
description:
|
|
483
|
+
"Read logs captured by debug-mode probes. Each entry: {run, probe, ts, data}. Returns JSONL text. Call with previous=true to analyze the last completed reproduction run.",
|
|
484
|
+
parameters: z.object({
|
|
485
|
+
run: z.string().optional().describe("Run id filter (default: current run)"),
|
|
486
|
+
probe: z.string().optional().describe("Probe id filter"),
|
|
487
|
+
previous: z.boolean().optional().describe("Use the previous (completed) run instead of the current one"),
|
|
488
|
+
}),
|
|
489
|
+
approval: "read",
|
|
490
|
+
async execute(_toolCallId, params) {
|
|
491
|
+
const runs = Object.keys(state.logCounts);
|
|
492
|
+
let run = params.run;
|
|
493
|
+
if (!run && params.previous) run = runs[runs.length - 2];
|
|
494
|
+
if (!run) run = state.runId ?? undefined;
|
|
495
|
+
// disk is authoritative (survives resume); memory buffer is fallback
|
|
496
|
+
let text = "";
|
|
497
|
+
if (state.debugDir && run) {
|
|
498
|
+
try {
|
|
499
|
+
text = await Bun.file(path.join(state.debugDir, `${run}.jsonl`)).text();
|
|
500
|
+
} catch {
|
|
501
|
+
text = "";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (!text) {
|
|
505
|
+
text = logBuffer
|
|
506
|
+
.filter(e => (!run || e.run === run) && (!params.probe || e.probe === params.probe))
|
|
507
|
+
.map(e => JSON.stringify(e))
|
|
508
|
+
.join("\n");
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
content: [
|
|
512
|
+
{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: text || "(no logs captured — probes did not report; wrong code path, stale build, or server unreachable)",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
details: { run: run ?? null, count: text.split("\n").filter(Boolean).length },
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
pi.registerTool({
|
|
523
|
+
name: "list_debug_probes",
|
|
524
|
+
label: "List Debug Probes",
|
|
525
|
+
description:
|
|
526
|
+
"Ground-truth probe ledger for debug mode: rescans files for `@omp-probe <id>` markers and reports which probes are actually present in code, with their files. Use to verify cleanup is complete.",
|
|
527
|
+
parameters: z.object({}),
|
|
528
|
+
approval: "read",
|
|
529
|
+
async execute() {
|
|
530
|
+
const alive = await checkLedger();
|
|
531
|
+
return {
|
|
532
|
+
content: [
|
|
533
|
+
{
|
|
534
|
+
type: "text",
|
|
535
|
+
text:
|
|
536
|
+
alive.length === 0
|
|
537
|
+
? "Probe ledger is EMPTY — all probes removed."
|
|
538
|
+
: `Alive probes (${alive.length}):\n` + alive.map(p => `${p.id} — ${p.file}`).join("\n"),
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
details: { alive },
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ============================== lifecycle ==============================
|
|
547
|
+
|
|
548
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
549
|
+
uiCtx = ctx;
|
|
550
|
+
const entries = ctx.sessionManager.getEntries();
|
|
551
|
+
const last = entries
|
|
552
|
+
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === DEBUG_ENTRY)
|
|
553
|
+
.pop() as { data?: DebugState } | undefined;
|
|
554
|
+
if (last?.data?.active) {
|
|
555
|
+
Object.assign(state, last.data);
|
|
556
|
+
if (state.debugDir && startServer(ctx, DEFAULT_PORT)) {
|
|
557
|
+
// replay disk logs for analysis continuity across resume
|
|
558
|
+
if (fs.existsSync(state.debugDir)) {
|
|
559
|
+
for (const f of fs.readdirSync(state.debugDir)) {
|
|
560
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
561
|
+
const run = f.replace(/\.jsonl$/, "");
|
|
562
|
+
try {
|
|
563
|
+
const lines = (await Bun.file(path.join(state.debugDir!, f)).text()).split("\n").filter(Boolean);
|
|
564
|
+
state.logCounts[run] = lines.length;
|
|
565
|
+
for (const l of lines) {
|
|
566
|
+
try {
|
|
567
|
+
logBuffer.push(JSON.parse(l) as LogEntry);
|
|
568
|
+
} catch {}
|
|
569
|
+
}
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (state.runId) {
|
|
574
|
+
pendingWrite = fs.createWriteStream(path.join(state.debugDir, `${state.runId}.jsonl`), { flags: "a" });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
ctx.ui.notify(
|
|
578
|
+
`debug-mode resumed: phase=${state.phase} round=${state.round}. Use /debug-status, /debug-done fixed|proceed, or /debug-abort.`,
|
|
579
|
+
"info",
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
refreshUi();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
pi.on("turn_start", async () => {
|
|
586
|
+
if (state.active) pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
pi.on("session_shutdown", async () => {
|
|
590
|
+
stopServer();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
pi.setLabel("Debug Mode");
|
|
594
|
+
}
|