@siuver/omp-debug-mode 0.1.2 → 0.1.3
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 +9 -0
- package/README.md +30 -14
- package/package.json +1 -1
- package/src/debug-mode.ts +638 -0
- package/src/gate.ts +36 -0
- package/src/log-files.ts +69 -0
- package/src/main.ts +12 -672
- package/src/methodology.ts +140 -0
- package/src/probes.ts +97 -0
- package/src/review-actions.ts +12 -1
- package/src/state.ts +147 -0
- package/src/tools.ts +78 -0
- package/src/ui.ts +78 -0
- package/src/workspace.ts +79 -0
package/src/main.ts
CHANGED
|
@@ -1,684 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Debug Mode Extension — Cursor-style human-in-the-loop debugging for omp.
|
|
3
3
|
*
|
|
4
|
-
* State machine:
|
|
4
|
+
* State machine (matches Cursor Debug Mode):
|
|
5
5
|
* IDLE → /debug-mode <problem>
|
|
6
|
-
* → agent
|
|
6
|
+
* → agent writes 3-5 hypotheses and @omp-probe instrumentation (no product fix)
|
|
7
7
|
* → WAITING_REPRO: agent stops; user reproduces out-of-band
|
|
8
|
+
* → Proceed/add details → agent reads logs, evaluates hypotheses, fixes only
|
|
9
|
+
* with evidence, keeps probes, and asks for a verification reproduce → loop
|
|
8
10
|
* → Mark as fixed → agent removes probes + summarizes → teardown (logs deleted)
|
|
9
|
-
* → Proceed/add details → agent analyzes logs, re-instruments + fixes → loop
|
|
10
11
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
* - Commands: /debug-mode, /debug-review, /debug-note, /debug-status, /debug-done, /debug-abort
|
|
12
|
+
* - `state.ts` — persisted round state and the injected blackboard
|
|
13
|
+
* - `probes.ts` — `@omp-probe` ledger and its on-disk ground truth
|
|
14
|
+
* - `log-files.ts` — stable `<cwd>/.omp/debug/current.jsonl` and run archival
|
|
15
|
+
* - `methodology.ts` — the Cursor Debug Mode prompt contract
|
|
16
|
+
* - `debug-mode.ts` — state machine, commands, and lifecycle wiring
|
|
17
17
|
*/
|
|
18
|
-
import type { ExtensionAPI
|
|
19
|
-
import
|
|
20
|
-
import * as path from "node:path";
|
|
21
|
-
import { ACTIVE_LOG_FILE, prepareRunLog, readJsonlLines, resolveRunLogFile } from "./log-files";
|
|
22
|
-
import { REVIEW_ABORT, REVIEW_ADD_DETAILS, REVIEW_MARK_FIXED, REVIEW_OPTIONS, REVIEW_PROCEED } from "./review-actions";
|
|
23
|
-
|
|
24
|
-
const DEBUG_ENTRY = "com.omp.debug-mode.state";
|
|
25
|
-
const PROBE_MARK = /@omp-probe\s+([A-Za-z0-9_-]+)/g;
|
|
26
|
-
type Phase = "idle" | "round" | "waiting" | "cleanup";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
interface Probe {
|
|
30
|
-
id: string;
|
|
31
|
-
file: string;
|
|
32
|
-
round: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface DebugState {
|
|
36
|
-
active: boolean;
|
|
37
|
-
phase: Phase;
|
|
38
|
-
problem: string;
|
|
39
|
-
round: number;
|
|
40
|
-
runId: string | null;
|
|
41
|
-
probes: Probe[];
|
|
42
|
-
debugDir: string | null;
|
|
43
|
-
logCounts: Record<string, number>;
|
|
44
|
-
hasRoundContent: boolean;
|
|
45
|
-
cleanupReady: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function freshState(): DebugState {
|
|
50
|
-
return {
|
|
51
|
-
active: false,
|
|
52
|
-
problem: "",
|
|
53
|
-
round: 0,
|
|
54
|
-
runId: null,
|
|
55
|
-
probes: [],
|
|
56
|
-
debugDir: null,
|
|
57
|
-
logCounts: {},
|
|
58
|
-
hasRoundContent: false,
|
|
59
|
-
cleanupReady: false,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function probeIdsIn(text: unknown): string[] {
|
|
64
|
-
if (typeof text !== "string") return [];
|
|
65
|
-
const ids: string[] = [];
|
|
66
|
-
for (const m of text.matchAll(PROBE_MARK)) ids.push(m[1]);
|
|
67
|
-
return ids;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function logFileFor(s: DebugState, run = s.runId): string | null {
|
|
71
|
-
return resolveRunLogFile(s.debugDir, run, s.runId);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function blackboard(s: DebugState): string {
|
|
75
|
-
const probes = s.probes.map(p => `${p.id} (${p.file}, round ${p.round})`).join("\n ") || "(none)";
|
|
76
|
-
const counts = Object.entries(s.logCounts)
|
|
77
|
-
.map(([run, n]) => `${run}: ${n}`)
|
|
78
|
-
.join(", ") || "(none yet)";
|
|
79
|
-
return `\
|
|
80
|
-
[DEBUG MODE ACTIVE — round ${s.round}]
|
|
81
|
-
|
|
82
|
-
Problem under investigation:
|
|
83
|
-
${s.problem}
|
|
84
|
-
|
|
85
|
-
Deployed probes (ground truth, maintained by the extension):
|
|
86
|
-
${probes}
|
|
87
|
-
|
|
88
|
-
Current run log file (absolute path): ${logFileFor(s) ?? "(not initialized)"}
|
|
89
|
-
Runtime probes MUST append JSONL directly to that exact file using the target environment's native file APIs.
|
|
90
|
-
Console output such as Unity Debug.Log may supplement diagnostics but is never the runtime evidence for this workflow. Never ask the user to transcribe console output.
|
|
91
|
-
Logs by run: ${counts}
|
|
92
|
-
Current run id: ${s.runId ?? "(not started)"}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const METHODOLOGY = `\
|
|
96
|
-
[DEBUG MODE METHODOLOGY — follow strictly]
|
|
97
|
-
Each round consists of, in order:
|
|
98
|
-
1. Restate hypotheses (mark each: pending / ruled-out / confirmed via runtime evidence).
|
|
99
|
-
2. Update instrumentation: remove probes that yielded no information, add probes that
|
|
100
|
-
discriminate between remaining hypotheses. Every runtime probe MUST append exactly
|
|
101
|
-
one compact JSON object plus a newline to the exact absolute log file shown above.
|
|
102
|
-
Use {"probe":"<id>","ts":<epoch-ms>,"data":<JSON-serializable-observation>}.
|
|
103
|
-
Append; never overwrite or truncate. Open, append, flush, and close promptly for each
|
|
104
|
-
observation. Do not use HTTP, POST, localhost, sockets, or any network transport.
|
|
105
|
-
Use the target environment's native file API and ensure the code path writes the file.
|
|
106
|
-
Console logging may supplement the file but never replaces it. Do not ask the user to
|
|
107
|
-
copy or summarize console output.
|
|
108
|
-
3. ATTEMPT A FIX for your leading hypothesis in the same round. Instrumentation without
|
|
109
|
-
a fix is an incomplete round — the loop only advances when you fix something.
|
|
110
|
-
4. End the round by writing concise reproduction steps for the user (exact commands or
|
|
111
|
-
actions, what to observe). Then STOP — the user reproduces out-of-band and uses
|
|
112
|
-
the review menu, /debug-note, or /debug-done fixed|proceed.
|
|
113
|
-
When told the fix is confirmed: remove every probe, verify with the list_debug_probes
|
|
114
|
-
tool that the ledger is empty, then summarize root cause and the final fix.`;
|
|
18
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
19
|
+
import { registerDebugMode } from "./debug-mode";
|
|
115
20
|
|
|
116
21
|
export default function debugModeExtension(pi: ExtensionAPI) {
|
|
117
|
-
const z = pi.zod;
|
|
118
|
-
const state: DebugState = freshState();
|
|
119
|
-
let reviewMenuOpen = false;
|
|
120
|
-
let uiCtx: ExtensionContext | null = null;
|
|
121
|
-
|
|
122
|
-
// ============================== log files ==============================
|
|
123
|
-
|
|
124
|
-
function initializeLogDirectory(ctx: ExtensionContext): boolean {
|
|
125
|
-
const dir = path.resolve(ctx.cwd, ".omp", "debug");
|
|
126
|
-
try {
|
|
127
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
128
|
-
state.debugDir = dir;
|
|
129
|
-
return true;
|
|
130
|
-
} catch (err) {
|
|
131
|
-
pi.logger.error("debug-mode: cannot create log dir", { dir, err });
|
|
132
|
-
state.debugDir = null;
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readRunLines(run: string): string[] {
|
|
138
|
-
return readJsonlLines(logFileFor(state, run));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function refreshLogCounts(): void {
|
|
142
|
-
const runs = new Set(Object.keys(state.logCounts));
|
|
143
|
-
if (state.debugDir) {
|
|
144
|
-
try {
|
|
145
|
-
for (const file of fs.readdirSync(state.debugDir)) {
|
|
146
|
-
if (!file.endsWith(".jsonl")) continue;
|
|
147
|
-
if (file === ACTIVE_LOG_FILE) {
|
|
148
|
-
if (state.runId) runs.add(state.runId);
|
|
149
|
-
} else {
|
|
150
|
-
runs.add(file.replace(/\.jsonl$/, ""));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
} catch {}
|
|
154
|
-
}
|
|
155
|
-
for (const run of runs) state.logCounts[run] = readRunLines(run).length;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function newRun(): string | null {
|
|
159
|
-
if (!state.debugDir) return null;
|
|
160
|
-
const run = `run${state.round}-${Date.now().toString(36)}`;
|
|
161
|
-
try {
|
|
162
|
-
prepareRunLog(state.debugDir, state.runId);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
pi.logger.error("debug-mode: cannot initialize run log", {
|
|
165
|
-
file: path.join(state.debugDir, ACTIVE_LOG_FILE),
|
|
166
|
-
err,
|
|
167
|
-
});
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
state.runId = run;
|
|
171
|
-
state.logCounts[run] = 0;
|
|
172
|
-
return run;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// ============================== probe ledger ==============================
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
pi.on("tool_call", async (event) => {
|
|
180
|
-
if (!state.active) return;
|
|
181
|
-
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
182
|
-
const input = event.input as Record<string, unknown>;
|
|
183
|
-
const file =
|
|
184
|
-
typeof input.path === "string" ? input.path : typeof input.file_path === "string" ? input.file_path : "(unknown)";
|
|
185
|
-
for (const v of Object.values(input)) {
|
|
186
|
-
for (const id of probeIdsIn(v)) {
|
|
187
|
-
if (!state.probes.some(p => p.id === id)) state.probes.push({ id, file, round: state.round });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// Removal detection rescans files for ground truth on demand.
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
/** Ground truth: which registered probes still exist in code. */
|
|
194
|
-
async function checkLedger(): Promise<Probe[]> {
|
|
195
|
-
const alive: Probe[] = [];
|
|
196
|
-
const byFile = new Map<string, Probe[]>();
|
|
197
|
-
for (const p of state.probes) {
|
|
198
|
-
const list = byFile.get(p.file) ?? [];
|
|
199
|
-
list.push(p);
|
|
200
|
-
byFile.set(p.file, list);
|
|
201
|
-
}
|
|
202
|
-
for (const [file, probes] of byFile) {
|
|
203
|
-
let text: string;
|
|
204
|
-
try {
|
|
205
|
-
text = await Bun.file(file).text();
|
|
206
|
-
} catch {
|
|
207
|
-
continue; // file gone → probe gone
|
|
208
|
-
}
|
|
209
|
-
for (const p of probes) {
|
|
210
|
-
if (text.includes(`@omp-probe ${p.id}`)) alive.push(p);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return alive;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ============================== UI ==============================
|
|
217
|
-
|
|
218
|
-
function refreshUi(): void {
|
|
219
|
-
const ctx = uiCtx;
|
|
220
|
-
if (!ctx?.hasUI) return;
|
|
221
|
-
if (!state.active) {
|
|
222
|
-
ctx.ui.setStatus("debug-mode", undefined);
|
|
223
|
-
ctx.ui.setWidget("debug-mode", undefined);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const label =
|
|
227
|
-
state.phase === "waiting" ? "🐞 waiting-repro" : state.phase === "round" ? `🐞 round ${state.round}` : "🐞 cleanup";
|
|
228
|
-
ctx.ui.setStatus("debug-mode", ctx.ui.theme.fg("warning", label));
|
|
229
|
-
const lines: string[] = [];
|
|
230
|
-
if (state.phase === "waiting") {
|
|
231
|
-
refreshLogCounts();
|
|
232
|
-
const n = state.runId ? (state.logCounts[state.runId] ?? 0) : 0;
|
|
233
|
-
lines.push(ctx.ui.theme.fg("accent", "reproduce the bug, then: /debug-review"));
|
|
234
|
-
lines.push(ctx.ui.theme.fg("dim", `run ${state.runId} — ${n} log entries`));
|
|
235
|
-
}
|
|
236
|
-
ctx.ui.setWidget("debug-mode", lines.length ? lines : undefined);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ============================== prompt injection ==============================
|
|
240
|
-
|
|
241
|
-
pi.on("before_agent_start", async () => {
|
|
242
|
-
if (!state.active || (state.phase !== "round" && state.phase !== "cleanup")) return;
|
|
243
|
-
return {
|
|
244
|
-
message: {
|
|
245
|
-
customType: "debug-mode-context",
|
|
246
|
-
content: `${blackboard(state)}\n\n${METHODOLOGY}`,
|
|
247
|
-
display: false,
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// Drop stale blackboard messages; the freshest is re-injected each round.
|
|
253
|
-
pi.on("context", async (event) => {
|
|
254
|
-
const filtered = event.messages.filter(
|
|
255
|
-
m => !(m.role === "custom" && (m as { customType?: string }).customType === "debug-mode-context"),
|
|
256
|
-
);
|
|
257
|
-
if (filtered.length !== event.messages.length) return { messages: filtered };
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ============================== round lifecycle ==============================
|
|
261
|
-
|
|
262
|
-
pi.on("agent_start", async () => {
|
|
263
|
-
if (state.active) state.hasRoundContent = false;
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
pi.on("message_end", async (event) => {
|
|
267
|
-
if (!state.active) return;
|
|
268
|
-
const msg = event.message as { role?: string };
|
|
269
|
-
if (msg?.role === "assistant") {
|
|
270
|
-
if (state.phase === "cleanup") state.cleanupReady = true;
|
|
271
|
-
state.hasRoundContent = true;
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
pi.on("session_stop", async (_event, ctx) => {
|
|
276
|
-
if (!state.active) return;
|
|
277
|
-
if (state.phase === "cleanup") {
|
|
278
|
-
// Cleanup turn settled: keep fixes and remove the temporary logs.
|
|
279
|
-
if (state.cleanupReady) await teardown(ctx, "finished");
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (state.phase !== "round" || !state.hasRoundContent) return;
|
|
283
|
-
state.phase = "waiting";
|
|
284
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
285
|
-
refreshUi();
|
|
286
|
-
ctx.ui.notify(
|
|
287
|
-
`Debug round ${state.round} paused. Reproduce the bug, then choose an action or run /debug-review.`,
|
|
288
|
-
"info",
|
|
289
|
-
);
|
|
290
|
-
if (ctx.hasUI) {
|
|
291
|
-
void openReviewMenu(ctx).catch(err => pi.logger.warn("debug-mode: review menu failed", { err }));
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
pi.registerCommand("debug-mode", {
|
|
296
|
-
description: "Start debug mode: /debug-mode <problem description>",
|
|
297
|
-
handler: async (args, ctx) => {
|
|
298
|
-
uiCtx = ctx;
|
|
299
|
-
if (state.active) {
|
|
300
|
-
ctx.ui.notify("debug-mode: already active (use /debug-review, /debug-done, or /debug-abort)", "error");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const problem = args.trim();
|
|
304
|
-
if (!problem) {
|
|
305
|
-
ctx.ui.notify("Usage: /debug-mode <problem description — symptoms, expected vs actual, how to reproduce>", "error");
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
startDebug(ctx, problem);
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
function startDebug(ctx: ExtensionContext, problem: string): void {
|
|
313
|
-
if (!initializeLogDirectory(ctx)) {
|
|
314
|
-
ctx.ui.notify("debug-mode: could not create the debug log directory; debug mode was not started", "error");
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
state.active = true;
|
|
318
|
-
state.phase = "round";
|
|
319
|
-
state.problem = problem;
|
|
320
|
-
state.round = 1;
|
|
321
|
-
state.probes = [];
|
|
322
|
-
state.logCounts = {};
|
|
323
|
-
state.hasRoundContent = false;
|
|
324
|
-
if (!newRun()) {
|
|
325
|
-
Object.assign(state, freshState());
|
|
326
|
-
ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
refreshUi();
|
|
330
|
-
pi.sendMessage(
|
|
331
|
-
{
|
|
332
|
-
customType: "debug-mode-start",
|
|
333
|
-
content: `Starting debug mode. Problem report:\n\n${problem}\n\nBegin round 1: explore, hypothesize, instrument with direct file writes, and attempt a fix. Then give reproduction steps and stop.`,
|
|
334
|
-
display: true,
|
|
335
|
-
},
|
|
336
|
-
{ triggerTurn: true },
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function ensureWaiting(ctx: ExtensionContext): boolean {
|
|
341
|
-
if (!state.active) {
|
|
342
|
-
ctx.ui.notify("debug-mode: not active", "error");
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
if (state.phase !== "waiting") {
|
|
346
|
-
ctx.ui.notify(`debug-mode: not waiting for reproduction (phase: ${state.phase})`, "error");
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
|
|
353
|
-
if (!ensureWaiting(ctx)) return;
|
|
354
|
-
refreshLogCounts();
|
|
355
|
-
const logCount = state.runId ? (state.logCounts[state.runId] ?? 0) : 0;
|
|
356
|
-
if (logCount === 0 && ctx.hasUI) {
|
|
357
|
-
const confirmed = await ctx.ui.confirm(
|
|
358
|
-
"Mark as fixed without runtime logs?",
|
|
359
|
-
"No runtime observations were captured for this round. Mark the problem as fixed anyway?",
|
|
360
|
-
);
|
|
361
|
-
if (!confirmed) return;
|
|
362
|
-
}
|
|
363
|
-
if (!state.active || state.phase !== "waiting") return;
|
|
364
|
-
|
|
365
|
-
state.phase = "cleanup";
|
|
366
|
-
state.cleanupReady = false;
|
|
367
|
-
refreshUi();
|
|
368
|
-
pi.sendMessage(
|
|
369
|
-
{
|
|
370
|
-
customType: "debug-mode-fixed",
|
|
371
|
-
content:
|
|
372
|
-
"User marked the problem FIXED.\n" +
|
|
373
|
-
"1. Remove every debug probe from the code (probe ledger below; verify with list_debug_probes after edits).\n" +
|
|
374
|
-
"2. Then summarize: root cause, the fix applied, what remains in the working diff.\n" +
|
|
375
|
-
`Probe ledger: ${JSON.stringify(state.probes)}`,
|
|
376
|
-
display: true,
|
|
377
|
-
},
|
|
378
|
-
{ triggerTurn: true },
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function advanceDebug(ctx: ExtensionContext, reproductionDetails?: string): Promise<void> {
|
|
383
|
-
if (!ensureWaiting(ctx)) return;
|
|
384
|
-
refreshLogCounts();
|
|
385
|
-
const run = state.runId ?? "(none)";
|
|
386
|
-
const logCount = state.logCounts[run] ?? 0;
|
|
387
|
-
if (logCount === 0 && !reproductionDetails && ctx.hasUI) {
|
|
388
|
-
const confirmed = await ctx.ui.confirm(
|
|
389
|
-
"Proceed without runtime logs?",
|
|
390
|
-
"No runtime observations were captured for this round. Continue to another analysis and fix round anyway?",
|
|
391
|
-
);
|
|
392
|
-
if (!confirmed) return;
|
|
393
|
-
}
|
|
394
|
-
if (!state.active || state.phase !== "waiting") return;
|
|
395
|
-
|
|
396
|
-
const previousRound = state.round;
|
|
397
|
-
state.round += 1;
|
|
398
|
-
state.phase = "round";
|
|
399
|
-
state.hasRoundContent = false;
|
|
400
|
-
if (!newRun()) {
|
|
401
|
-
state.round = previousRound;
|
|
402
|
-
state.phase = "waiting";
|
|
403
|
-
ctx.ui.notify("debug-mode: could not initialize the next run log file; staying at the reproduction gate", "error");
|
|
404
|
-
refreshUi();
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
refreshUi();
|
|
408
|
-
|
|
409
|
-
const userEvidence = reproductionDetails
|
|
410
|
-
? `User added reproduction details after run ${run}:\n\n${reproductionDetails}\n\nTreat these details as evidence alongside the captured logs.\n`
|
|
411
|
-
: `User chose PROCEED — the fix did not resolve it (run ${run} captured ${logCount} log entries).\n`;
|
|
412
|
-
pi.sendMessage(
|
|
413
|
-
{
|
|
414
|
-
customType: reproductionDetails ? "debug-mode-note" : "debug-mode-proceed",
|
|
415
|
-
content:
|
|
416
|
-
userEvidence +
|
|
417
|
-
(logCount === 0
|
|
418
|
-
? "No logs were captured: the instrumented code path may not have executed, the build may be stale, the path may be wrong, or the file append may have failed — treat that as a signal.\n"
|
|
419
|
-
: `Run ${run} captured ${logCount} log entries.\n`) +
|
|
420
|
-
"Read the previous run with get_debug_logs, update the hypotheses, re-instrument, attempt the next fix, give reproduction steps, and stop.",
|
|
421
|
-
display: true,
|
|
422
|
-
},
|
|
423
|
-
{ triggerTurn: true },
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function teardown(ctx: ExtensionContext, outcome: "finished" | "aborted"): Promise<void> {
|
|
428
|
-
if (state.debugDir) {
|
|
429
|
-
try {
|
|
430
|
-
fs.rmSync(state.debugDir, { recursive: true, force: true });
|
|
431
|
-
} catch (err) {
|
|
432
|
-
pi.logger.warn("debug-mode: failed to remove debug dir", { err });
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const probesLeft = await checkLedger();
|
|
436
|
-
Object.assign(state, freshState());
|
|
437
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
438
|
-
refreshUi();
|
|
439
|
-
const resultLabel = outcome === "finished" ? "finished" : "aborted";
|
|
440
|
-
if (probesLeft.length > 0) {
|
|
441
|
-
ctx.ui.notify(
|
|
442
|
-
`Debug mode ${resultLabel}, but ${probesLeft.length} probe(s) remain in code: ${probesLeft.map(p => p.id).join(", ")} — remove manually. Applied fixes remain in the working diff.`,
|
|
443
|
-
"warning",
|
|
444
|
-
);
|
|
445
|
-
} else {
|
|
446
|
-
ctx.ui.notify(
|
|
447
|
-
`Debug mode ${resultLabel}. Log files removed; applied fixes remain in the working diff for review.`,
|
|
448
|
-
"info",
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function abortDebug(ctx: ExtensionContext): Promise<void> {
|
|
454
|
-
if (!state.active) {
|
|
455
|
-
ctx.ui.notify("debug-mode: not active", "error");
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (ctx.hasUI) {
|
|
459
|
-
const confirmed = await ctx.ui.confirm(
|
|
460
|
-
"Abort debug mode?",
|
|
461
|
-
"Delete captured debug logs and stop the workflow? Applied code changes will remain in the working diff.",
|
|
462
|
-
);
|
|
463
|
-
if (!confirmed) return;
|
|
464
|
-
}
|
|
465
|
-
if (!state.active) return;
|
|
466
|
-
await teardown(ctx, "aborted");
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async function openReviewMenu(ctx: ExtensionContext): Promise<void> {
|
|
470
|
-
if (!ensureWaiting(ctx)) return;
|
|
471
|
-
if (!ctx.hasUI) {
|
|
472
|
-
ctx.ui.notify("/debug-review requires an interactive UI; use /debug-done fixed|proceed instead.", "warning");
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (reviewMenuOpen) {
|
|
476
|
-
ctx.ui.notify("debug-mode: review menu is already open", "info");
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
reviewMenuOpen = true;
|
|
481
|
-
try {
|
|
482
|
-
const choice = await ctx.ui.select(`Review debug round ${state.round}`, [...REVIEW_OPTIONS]);
|
|
483
|
-
if (!choice) return;
|
|
484
|
-
if (choice === REVIEW_MARK_FIXED) {
|
|
485
|
-
await markDebugFixed(ctx);
|
|
486
|
-
} else if (choice === REVIEW_PROCEED) {
|
|
487
|
-
await advanceDebug(ctx);
|
|
488
|
-
} else if (choice === REVIEW_ADD_DETAILS) {
|
|
489
|
-
ctx.ui.setEditorText("/debug-note ");
|
|
490
|
-
ctx.ui.notify("Add reproduction details in the editor, then submit /debug-note.", "info");
|
|
491
|
-
} else if (choice === REVIEW_ABORT) {
|
|
492
|
-
await abortDebug(ctx);
|
|
493
|
-
}
|
|
494
|
-
} finally {
|
|
495
|
-
reviewMenuOpen = false;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ============================== commands ==============================
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
pi.registerCommand("debug-done", {
|
|
503
|
-
description: "Answer the reproduction gate: /debug-done fixed|proceed",
|
|
504
|
-
handler: async (args, ctx) => {
|
|
505
|
-
uiCtx = ctx;
|
|
506
|
-
const verdict = args.trim().toLowerCase();
|
|
507
|
-
if (verdict === "fixed") {
|
|
508
|
-
await markDebugFixed(ctx);
|
|
509
|
-
} else if (verdict === "proceed") {
|
|
510
|
-
await advanceDebug(ctx);
|
|
511
|
-
} else {
|
|
512
|
-
ctx.ui.notify("Usage: /debug-done fixed|proceed", "error");
|
|
513
|
-
}
|
|
514
|
-
},
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
pi.registerCommand("debug-review", {
|
|
518
|
-
description: "Open the interactive action menu for a completed debug round",
|
|
519
|
-
handler: async (_args, ctx) => {
|
|
520
|
-
uiCtx = ctx;
|
|
521
|
-
await openReviewMenu(ctx);
|
|
522
|
-
},
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
pi.registerCommand("debug-note", {
|
|
526
|
-
description: "Add reproduction details and continue: /debug-note <details>",
|
|
527
|
-
handler: async (args, ctx) => {
|
|
528
|
-
uiCtx = ctx;
|
|
529
|
-
const details = args.trim();
|
|
530
|
-
if (!details) {
|
|
531
|
-
ctx.ui.notify("Usage: /debug-note <reproduction details>", "error");
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
await advanceDebug(ctx, details);
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
pi.registerCommand("debug-abort", {
|
|
539
|
-
description: "Abort debug mode: delete logs (fixes stay in the working diff)",
|
|
540
|
-
handler: async (_args, ctx) => {
|
|
541
|
-
uiCtx = ctx;
|
|
542
|
-
await abortDebug(ctx);
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
pi.registerCommand("debug-status", {
|
|
547
|
-
description: "Show debug mode state",
|
|
548
|
-
handler: async (_args, ctx) => {
|
|
549
|
-
uiCtx = ctx;
|
|
550
|
-
if (!state.active) {
|
|
551
|
-
ctx.ui.notify("debug-mode: idle", "info");
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
refreshLogCounts();
|
|
555
|
-
const alive = await checkLedger();
|
|
556
|
-
ctx.ui.notify(
|
|
557
|
-
`debug-mode: phase=${state.phase} round=${state.round} run=${state.runId}\n` +
|
|
558
|
-
`probes (ledger ${state.probes.length}, alive ${alive.length}):\n` +
|
|
559
|
-
(alive.map(p => ` ${p.id} — ${p.file}`).join("\n") || " (none)") +
|
|
560
|
-
`\nlogs: ${Object.entries(state.logCounts).map(([r, n]) => `${r}=${n}`).join(", ") || "(none)"}` +
|
|
561
|
-
`\ncurrent log file: ${logFileFor(state) ?? "(not initialized)"}`,
|
|
562
|
-
"info",
|
|
563
|
-
);
|
|
564
|
-
},
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
// ============================== tools ==============================
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
pi.registerTool({
|
|
571
|
-
name: "get_debug_logs",
|
|
572
|
-
label: "Get Debug Logs",
|
|
573
|
-
description:
|
|
574
|
-
"Read JSONL observations appended directly by debug-mode runtime probes. Each entry: {probe, ts, data}. Call with previous=true to analyze the last completed reproduction run.",
|
|
575
|
-
parameters: z.object({
|
|
576
|
-
run: z.string().optional().describe("Run id filter (default: current run)"),
|
|
577
|
-
probe: z.string().optional().describe("Probe id filter"),
|
|
578
|
-
previous: z.boolean().optional().describe("Use the previous (completed) run instead of the current one"),
|
|
579
|
-
}),
|
|
580
|
-
approval: "read",
|
|
581
|
-
async execute(_toolCallId, params) {
|
|
582
|
-
refreshLogCounts();
|
|
583
|
-
const runs = Object.keys(state.logCounts);
|
|
584
|
-
let run = params.run;
|
|
585
|
-
if (!run && params.previous) {
|
|
586
|
-
run = runs[runs.length - 2];
|
|
587
|
-
if (!run) {
|
|
588
|
-
return {
|
|
589
|
-
content: [{ type: "text", text: "(no completed previous debug run is available)" }],
|
|
590
|
-
details: { run: null, file: null, count: 0 },
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (!run) run = state.runId ?? undefined;
|
|
595
|
-
|
|
596
|
-
let lines = run ? readRunLines(run) : [];
|
|
597
|
-
if (params.probe) {
|
|
598
|
-
lines = lines.filter(line => {
|
|
599
|
-
try {
|
|
600
|
-
const entry = JSON.parse(line) as { probe?: unknown };
|
|
601
|
-
return entry.probe === params.probe;
|
|
602
|
-
} catch {
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
const text = lines.join("\n");
|
|
608
|
-
return {
|
|
609
|
-
content: [
|
|
610
|
-
{
|
|
611
|
-
type: "text",
|
|
612
|
-
text:
|
|
613
|
-
text ||
|
|
614
|
-
"(no logs captured — the instrumented path may not have executed, the build may be stale, the path may be wrong, or the file append may have failed)",
|
|
615
|
-
},
|
|
616
|
-
],
|
|
617
|
-
details: { run: run ?? null, file: run ? logFileFor(state, run) : null, count: lines.length },
|
|
618
|
-
};
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
pi.registerTool({
|
|
623
|
-
name: "list_debug_probes",
|
|
624
|
-
label: "List Debug Probes",
|
|
625
|
-
description:
|
|
626
|
-
"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.",
|
|
627
|
-
parameters: z.object({}),
|
|
628
|
-
approval: "read",
|
|
629
|
-
async execute() {
|
|
630
|
-
const alive = await checkLedger();
|
|
631
|
-
return {
|
|
632
|
-
content: [
|
|
633
|
-
{
|
|
634
|
-
type: "text",
|
|
635
|
-
text:
|
|
636
|
-
alive.length === 0
|
|
637
|
-
? "Probe ledger is EMPTY — all probes removed."
|
|
638
|
-
: `Alive probes (${alive.length}):\n` + alive.map(p => `${p.id} — ${p.file}`).join("\n"),
|
|
639
|
-
},
|
|
640
|
-
],
|
|
641
|
-
details: { alive },
|
|
642
|
-
};
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
// ============================== lifecycle ==============================
|
|
647
|
-
|
|
648
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
649
|
-
uiCtx = ctx;
|
|
650
|
-
const entries = ctx.sessionManager.getEntries();
|
|
651
|
-
const last = entries
|
|
652
|
-
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === DEBUG_ENTRY)
|
|
653
|
-
.pop() as { data?: DebugState } | undefined;
|
|
654
|
-
if (last?.data?.active) {
|
|
655
|
-
Object.assign(state, last.data);
|
|
656
|
-
if (!state.debugDir || !fs.existsSync(state.debugDir)) initializeLogDirectory(ctx);
|
|
657
|
-
const currentFile = logFileFor(state);
|
|
658
|
-
if (currentFile) {
|
|
659
|
-
try {
|
|
660
|
-
const legacyRunFile = state.debugDir && state.runId ? path.join(state.debugDir, `${state.runId}.jsonl`) : null;
|
|
661
|
-
if (!fs.existsSync(currentFile) && legacyRunFile && fs.existsSync(legacyRunFile)) {
|
|
662
|
-
fs.renameSync(legacyRunFile, currentFile);
|
|
663
|
-
}
|
|
664
|
-
fs.writeFileSync(currentFile, "", { flag: "a" });
|
|
665
|
-
} catch (err) {
|
|
666
|
-
pi.logger.error("debug-mode: cannot restore run log", { file: currentFile, err });
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
refreshLogCounts();
|
|
670
|
-
ctx.ui.notify(
|
|
671
|
-
`debug-mode resumed: phase=${state.phase} round=${state.round}, log=${currentFile ?? "unavailable"}. Use /debug-status, /debug-review, /debug-done fixed|proceed, or /debug-abort.`,
|
|
672
|
-
"info",
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
refreshUi();
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
pi.on("turn_start", async () => {
|
|
679
|
-
if (state.active) pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
|
|
683
22
|
pi.setLabel("Debug Mode");
|
|
23
|
+
registerDebugMode(pi);
|
|
684
24
|
}
|