@siuver/omp-debug-mode 0.1.3 → 0.1.4
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 +133 -94
- package/package.json +3 -3
- package/src/debug-mode.ts +215 -105
- package/src/evidence.ts +196 -0
- package/src/gate.ts +32 -14
- package/src/methodology.ts +80 -32
- package/src/state.ts +96 -3
- package/src/tools.ts +58 -0
- package/src/ui.ts +41 -16
- package/src/review-actions.ts +0 -22
package/src/debug-mode.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Text } from "@oh-my-pi/pi-coding-agent";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, MessageRenderer } from "@oh-my-pi/pi-coding-agent";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { addEvidenceArtifact, describeEvidence, parseEvidencePlan } from "./evidence";
|
|
5
6
|
import { decideGate } from "./gate";
|
|
6
7
|
import {
|
|
7
8
|
ACTIVE_LOG_FILE,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
readJsonlLines,
|
|
12
13
|
summarizeHypotheses,
|
|
13
14
|
} from "./log-files";
|
|
15
|
+
import { describeLedger, recordProbes, syncLedger } from "./probes";
|
|
14
16
|
import {
|
|
15
17
|
CLEANUP_CONTRACT,
|
|
16
18
|
METHODOLOGY,
|
|
@@ -21,27 +23,28 @@ import {
|
|
|
21
23
|
extractAssistantText,
|
|
22
24
|
extractReproductionSteps,
|
|
23
25
|
} from "./methodology";
|
|
24
|
-
import { describeLedger, recordProbes, syncLedger } from "./probes";
|
|
25
|
-
import { REVIEW_ABORT, REVIEW_ADD_DETAILS, REVIEW_MARK_FIXED, REVIEW_PROCEED, reviewMenuOptions } from "./review-actions";
|
|
26
26
|
import {
|
|
27
27
|
DEBUG_CONTEXT_TYPE,
|
|
28
28
|
DEBUG_ENTRY,
|
|
29
29
|
type DebugState,
|
|
30
|
+
type EvidenceObservation,
|
|
30
31
|
blackboard,
|
|
31
32
|
compareRunIds,
|
|
33
|
+
evidenceSummary,
|
|
32
34
|
freshState,
|
|
33
35
|
keepLatestCustomType,
|
|
34
36
|
logFileFor,
|
|
37
|
+
pendingEvidenceRequests,
|
|
38
|
+
replaceRoundEvidenceRequests,
|
|
35
39
|
} from "./state";
|
|
36
40
|
import { registerDebugTools } from "./tools";
|
|
37
|
-
import { applyUi
|
|
41
|
+
import { applyUi } from "./ui";
|
|
38
42
|
import { debugDirFor, excludeDebugLogsFromGit, pruneDebugRoot } from "./workspace";
|
|
39
43
|
|
|
40
44
|
const COMMAND_MODE = "debug-mode";
|
|
41
|
-
const COMMAND_MENU = "debug-menu";
|
|
42
45
|
const COMMAND_DONE = "debug-done";
|
|
43
46
|
const COMMAND_PROCEED = "debug-proceed";
|
|
44
|
-
const
|
|
47
|
+
const COMMAND_EVIDENCE = "debug-evidence";
|
|
45
48
|
const COMMAND_ABORT = "debug-abort";
|
|
46
49
|
const COMMAND_STATUS = "debug-status";
|
|
47
50
|
|
|
@@ -49,7 +52,6 @@ const COMMAND_STATUS = "debug-status";
|
|
|
49
52
|
const MESSAGE_SUMMARIES: Record<string, string> = {
|
|
50
53
|
"debug-mode-start": "debug mode started — hypotheses and instrumentation",
|
|
51
54
|
"debug-mode-proceed": "proceed — analyzing captured logs",
|
|
52
|
-
"debug-mode-note": "reproduction details added — analyzing captured logs",
|
|
53
55
|
"debug-mode-fixed": "marked fixed — removing probes and summarizing",
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -59,7 +61,6 @@ interface DebugMessageDetails {
|
|
|
59
61
|
|
|
60
62
|
export function registerDebugMode(pi: ExtensionAPI): void {
|
|
61
63
|
const state: DebugState = freshState();
|
|
62
|
-
let reviewMenuOpen = false;
|
|
63
64
|
let uiCtx: ExtensionContext | null = null;
|
|
64
65
|
let watchedLogFile: string | null = null;
|
|
65
66
|
const lineCounter = new JsonlLineCounter();
|
|
@@ -203,8 +204,8 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
203
204
|
return {
|
|
204
205
|
message: {
|
|
205
206
|
customType: DEBUG_CONTEXT_TYPE,
|
|
206
|
-
content: `${blackboard(state)}\n\n${contract}`,
|
|
207
|
-
|
|
207
|
+
content: `${blackboard(state, describeEvidence(state))}\n\n${contract}`,
|
|
208
|
+
display: false,
|
|
208
209
|
},
|
|
209
210
|
};
|
|
210
211
|
});
|
|
@@ -229,23 +230,33 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
229
230
|
if (msg?.role === "assistant") {
|
|
230
231
|
if (state.phase === "cleanup") state.cleanupReady = true;
|
|
231
232
|
state.hasRoundContent = true;
|
|
232
|
-
const
|
|
233
|
+
const text = extractAssistantText(msg.content);
|
|
234
|
+
const steps = extractReproductionSteps(text);
|
|
233
235
|
if (steps.length > 0) state.reproductionSteps = steps;
|
|
236
|
+
if (state.phase === "round") {
|
|
237
|
+
const plan = parseEvidencePlan(text, state.round);
|
|
238
|
+
if (plan.found) {
|
|
239
|
+
replaceRoundEvidenceRequests(state, state.round, plan.valid ? plan.requests : []);
|
|
240
|
+
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
234
243
|
}
|
|
235
244
|
});
|
|
236
245
|
|
|
237
246
|
pi.on("session_stop", async (_event, ctx) => {
|
|
238
247
|
if (!state.active) return;
|
|
239
248
|
if (state.phase === "cleanup") {
|
|
240
|
-
// Cleanup turn settled: keep fixes and remove the temporary logs.
|
|
241
249
|
if (state.cleanupReady) await teardown(ctx, "finished");
|
|
242
250
|
return;
|
|
243
251
|
}
|
|
252
|
+
// Ordinary editor messages while waiting are conversational only. Only an
|
|
253
|
+
// explicit debug command advances the run or leaves the gate.
|
|
244
254
|
if (state.phase !== "round" || !state.hasRoundContent) return;
|
|
245
|
-
|
|
246
255
|
const probesThisRound = state.probes.filter(p => p.round === state.round).length;
|
|
256
|
+
const hasEvidencePlan = state.evidenceRequests.some(request => request.round === state.round);
|
|
247
257
|
const decision = decideGate({
|
|
248
258
|
hasReproductionSteps: state.reproductionSteps.length > 0,
|
|
259
|
+
hasEvidencePlan,
|
|
249
260
|
probesThisRound,
|
|
250
261
|
nudgesUsed: state.gateNudges,
|
|
251
262
|
});
|
|
@@ -254,29 +265,47 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
254
265
|
state.gateNudges += 1;
|
|
255
266
|
return { continue: true, additionalContext: decision.context };
|
|
256
267
|
}
|
|
257
|
-
enterGate(ctx, probesThisRound, decision.missingSteps);
|
|
268
|
+
enterGate(ctx, probesThisRound, decision.missingSteps, decision.missingEvidencePlan);
|
|
258
269
|
});
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
function enterGate(
|
|
272
|
+
ctx: ExtensionContext,
|
|
273
|
+
probesThisRound: number,
|
|
274
|
+
missingSteps: boolean,
|
|
275
|
+
missingEvidencePlan: boolean,
|
|
276
|
+
): void {
|
|
277
|
+
state.phase = "waiting";
|
|
278
|
+
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
279
|
+
refreshUi();
|
|
280
|
+
watchLogFile();
|
|
281
|
+
// Commands are the only interaction at the gate: the widget lists them,
|
|
282
|
+
// the user picks one. No menu is opened automatically.
|
|
283
|
+
const pending = pendingEvidenceRequests(state, state.round);
|
|
284
|
+
const hasValidPlan = state.evidenceRequests.some(request => request.round === state.round);
|
|
285
|
+
if (probesThisRound === 0 && !hasValidPlan) {
|
|
286
|
+
ctx.ui.notify(
|
|
287
|
+
`Debug round ${state.round} paused, but it added no probes and declared no evidence plan — this round cannot produce runtime evidence. Use /${COMMAND_PROCEED} to ask for instrumentation.`,
|
|
288
|
+
"warning",
|
|
289
|
+
);
|
|
290
|
+
} else if (missingSteps) {
|
|
291
|
+
ctx.ui.notify(
|
|
292
|
+
`Debug round ${state.round} paused without reproduction steps. Exercise the instrumented path, then ${PROCEED_REMINDER}`,
|
|
293
|
+
"warning",
|
|
294
|
+
);
|
|
295
|
+
} else if (pending.length > 0) {
|
|
296
|
+
ctx.ui.notify(
|
|
297
|
+
`Debug round ${state.round} paused. User evidence requested (${pending.length} pending: ${pending.map(r => r.id).join(", ")}) — attach via /${COMMAND_EVIDENCE} <request-id> <path>, then ${PROCEED_REMINDER}`,
|
|
298
|
+
"info",
|
|
299
|
+
);
|
|
300
|
+
} else if (missingEvidencePlan) {
|
|
301
|
+
ctx.ui.notify(
|
|
302
|
+
`Debug round ${state.round} paused. The gate is usable, but the next Proceed will ask the model to declare an evidence method.`,
|
|
303
|
+
"info",
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
ctx.ui.notify(`Debug round ${state.round} paused. Reproduce the bug, then ${PROCEED_REMINDER}`, "info");
|
|
279
307
|
}
|
|
308
|
+
}
|
|
280
309
|
|
|
281
310
|
// ============================== round transitions ==============================
|
|
282
311
|
|
|
@@ -296,6 +325,9 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
296
325
|
state.cleanupReady = false;
|
|
297
326
|
state.reproductionSteps = [];
|
|
298
327
|
state.gateNudges = 0;
|
|
328
|
+
state.evidenceRequests = [];
|
|
329
|
+
state.evidenceArtifacts = [];
|
|
330
|
+
state.evidenceObservations = [];
|
|
299
331
|
if (!newRun()) {
|
|
300
332
|
Object.assign(state, freshState());
|
|
301
333
|
ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
|
|
@@ -331,10 +363,13 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
331
363
|
return true;
|
|
332
364
|
}
|
|
333
365
|
|
|
334
|
-
|
|
366
|
+
async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
|
|
335
367
|
if (!ensureWaiting(ctx)) return;
|
|
336
368
|
const logCount = currentLogCount();
|
|
337
|
-
|
|
369
|
+
const hasNonProbeEvidence = state.evidenceRequests.some(
|
|
370
|
+
request => request.round === state.round && request.method !== "runtime_probe",
|
|
371
|
+
);
|
|
372
|
+
if (logCount === 0 && !hasNonProbeEvidence && ctx.hasUI) {
|
|
338
373
|
const confirmed = await ctx.ui.confirm(
|
|
339
374
|
"Mark as fixed without runtime logs?",
|
|
340
375
|
"No runtime observations were captured for this round. Mark the problem as fixed anyway?",
|
|
@@ -347,23 +382,30 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
347
382
|
state.cleanupReady = false;
|
|
348
383
|
unwatchLogFile();
|
|
349
384
|
refreshUi();
|
|
385
|
+
const evidenceJson = JSON.stringify({
|
|
386
|
+
requests: state.evidenceRequests,
|
|
387
|
+
observations: state.evidenceObservations,
|
|
388
|
+
artifacts: state.evidenceArtifacts,
|
|
389
|
+
});
|
|
350
390
|
pi.sendMessage(
|
|
351
391
|
{
|
|
352
392
|
customType: "debug-mode-fixed",
|
|
353
|
-
content: buildFixedMessage(JSON.stringify(state.probes)),
|
|
354
|
-
display: true,
|
|
393
|
+
content: buildFixedMessage(JSON.stringify(state.probes), evidenceJson),
|
|
355
394
|
details: { summary: `marked fixed — removing ${state.probes.length} probe(s)` } satisfies DebugMessageDetails,
|
|
356
395
|
},
|
|
357
396
|
{ triggerTurn: true },
|
|
358
397
|
);
|
|
359
|
-
|
|
398
|
+
}
|
|
360
399
|
|
|
361
|
-
async function advanceDebug(ctx: ExtensionContext,
|
|
400
|
+
async function advanceDebug(ctx: ExtensionContext, userDetails?: string): Promise<void> {
|
|
362
401
|
if (!ensureWaiting(ctx)) return;
|
|
363
402
|
refreshLogCounts();
|
|
364
403
|
const run = state.runId ?? "(none)";
|
|
365
404
|
const logCount = state.logCounts[run] ?? 0;
|
|
366
|
-
|
|
405
|
+
const hasNonProbeEvidence = state.evidenceRequests.some(
|
|
406
|
+
request => request.round === state.round && request.method !== "runtime_probe",
|
|
407
|
+
);
|
|
408
|
+
if (logCount === 0 && !userDetails && !hasNonProbeEvidence && ctx.hasUI) {
|
|
367
409
|
const confirmed = await ctx.ui.confirm(
|
|
368
410
|
"Proceed without runtime logs?",
|
|
369
411
|
"No runtime observations were captured for this round. Continue to log analysis anyway?",
|
|
@@ -372,6 +414,34 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
372
414
|
}
|
|
373
415
|
if (!state.active || state.phase !== "waiting") return;
|
|
374
416
|
|
|
417
|
+
// Persist optional /debug-proceed details as a batched user observation
|
|
418
|
+
// tied to every current-round user_report request.
|
|
419
|
+
if (userDetails && userDetails.trim().length > 0) {
|
|
420
|
+
const reportIds = state.evidenceRequests
|
|
421
|
+
.filter(request => request.round === state.round && request.method === "user_report")
|
|
422
|
+
.map(request => request.id);
|
|
423
|
+
const observation: EvidenceObservation = {
|
|
424
|
+
id: `observation-${Date.now().toString(36)}`,
|
|
425
|
+
requestIds: reportIds,
|
|
426
|
+
text: userDetails.trim(),
|
|
427
|
+
round: state.round,
|
|
428
|
+
addedAt: Date.now(),
|
|
429
|
+
};
|
|
430
|
+
state.evidenceObservations = [...state.evidenceObservations, observation];
|
|
431
|
+
}
|
|
432
|
+
const stillPending = pendingEvidenceRequests(state, state.round);
|
|
433
|
+
if (stillPending.length > 0 && ctx.hasUI) {
|
|
434
|
+
const confirmed = await ctx.ui.confirm(
|
|
435
|
+
"Proceed without all requested evidence?",
|
|
436
|
+
`${stillPending.length} evidence request(s) still pending (${stillPending.map(r => r.id).join(", ")}). Continue anyway?`,
|
|
437
|
+
);
|
|
438
|
+
if (!confirmed) {
|
|
439
|
+
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
440
|
+
refreshUi();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
375
445
|
const hypotheses = describeHypotheses(summarizeHypotheses(readRunLines(run)));
|
|
376
446
|
const previousRound = state.round;
|
|
377
447
|
const previousSteps = state.reproductionSteps;
|
|
@@ -391,13 +461,19 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
391
461
|
unwatchLogFile();
|
|
392
462
|
refreshUi();
|
|
393
463
|
|
|
394
|
-
const summary =
|
|
395
|
-
? `
|
|
464
|
+
const summary = userDetails
|
|
465
|
+
? `proceed with user details — analyzing run ${run} (${logCount} entries)`
|
|
396
466
|
: `proceed — analyzing run ${run} (${logCount} entries)`;
|
|
397
467
|
pi.sendMessage(
|
|
398
468
|
{
|
|
399
|
-
customType:
|
|
400
|
-
content: buildProceedMessage({
|
|
469
|
+
customType: "debug-mode-proceed",
|
|
470
|
+
content: buildProceedMessage({
|
|
471
|
+
run,
|
|
472
|
+
logCount,
|
|
473
|
+
userDetails,
|
|
474
|
+
hypotheses,
|
|
475
|
+
evidenceSummary: evidenceSummary(state, previousRound),
|
|
476
|
+
}),
|
|
401
477
|
display: true,
|
|
402
478
|
details: { summary } satisfies DebugMessageDetails,
|
|
403
479
|
},
|
|
@@ -452,50 +528,56 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
452
528
|
}
|
|
453
529
|
|
|
454
530
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
531
|
+
* Attach one user-provided evidence file to the current waiting round. The
|
|
532
|
+
* file is referenced in place — never copied, moved or deleted — and the
|
|
533
|
+
* action stays at the gate: no model-visible message, no agent turn.
|
|
458
534
|
*/
|
|
459
|
-
function
|
|
460
|
-
if (!ctx
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
if (reviewMenuOpen) {
|
|
475
|
-
ctx.ui.notify("debug-mode: review menu is already open", "info");
|
|
476
|
-
return;
|
|
535
|
+
async function attachEvidence(ctx: ExtensionContext, rawPath?: string, requestId: string | null = null): Promise<boolean> {
|
|
536
|
+
if (!ensureWaiting(ctx)) return false;
|
|
537
|
+
let input = rawPath?.trim() ?? "";
|
|
538
|
+
if (!input) {
|
|
539
|
+
if (!ctx.hasUI) {
|
|
540
|
+
ctx.ui.notify(`Usage: /${COMMAND_EVIDENCE} <path>`, "error");
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
input = (await ctx.ui.input("Path to debug evidence file", "absolute or cwd-relative path")) ?? "";
|
|
544
|
+
if (!input.trim()) {
|
|
545
|
+
ctx.ui.notify("debug-mode: no evidence file path provided", "error");
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
477
548
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
} else if (choice === REVIEW_ADD_DETAILS) {
|
|
490
|
-
ctx.ui.setEditorText("/debug-note ");
|
|
491
|
-
ctx.ui.notify("Add reproduction details in the editor, then submit /debug-note.", "info");
|
|
492
|
-
} else if (choice === REVIEW_ABORT) {
|
|
493
|
-
await abortDebug(ctx);
|
|
549
|
+
const trimmed = input.trim();
|
|
550
|
+
const cwdRoot = path.resolve(ctx.cwd);
|
|
551
|
+
const resolved = path.resolve(cwdRoot, trimmed);
|
|
552
|
+
const outsideCwd = resolved !== cwdRoot && !resolved.startsWith(`${cwdRoot}${path.sep}`);
|
|
553
|
+
if (outsideCwd) {
|
|
554
|
+
if (!ctx.hasUI) {
|
|
555
|
+
ctx.ui.notify(
|
|
556
|
+
`debug-mode: ${resolved} is outside the session working directory and cannot be confirmed without a UI`,
|
|
557
|
+
"error",
|
|
558
|
+
);
|
|
559
|
+
return false;
|
|
494
560
|
}
|
|
495
|
-
|
|
496
|
-
|
|
561
|
+
const confirmed = await ctx.ui.confirm(
|
|
562
|
+
"Attach evidence outside the working directory?",
|
|
563
|
+
`Record ${resolved} as debug evidence? The file is referenced in place and never modified.`,
|
|
564
|
+
);
|
|
565
|
+
if (!confirmed) return false;
|
|
497
566
|
}
|
|
498
|
-
|
|
567
|
+
const result = addEvidenceArtifact(state, trimmed, ctx.cwd, requestId);
|
|
568
|
+
if ("error" in result) {
|
|
569
|
+
ctx.ui.notify(`debug-mode: evidence rejected — ${result.error}`, "error");
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
Object.assign(state, result.state);
|
|
573
|
+
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
574
|
+
refreshUi();
|
|
575
|
+
ctx.ui.notify(
|
|
576
|
+
`debug-mode: attached ${result.artifact.id} → ${result.artifact.path} (${result.artifact.size} bytes)`,
|
|
577
|
+
"info",
|
|
578
|
+
);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
499
581
|
|
|
500
582
|
// ============================== commands ==============================
|
|
501
583
|
|
|
@@ -504,7 +586,7 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
504
586
|
handler: async (args, ctx) => {
|
|
505
587
|
uiCtx = ctx;
|
|
506
588
|
if (state.active) {
|
|
507
|
-
ctx.ui.notify(`debug-mode: already active (use /${
|
|
589
|
+
ctx.ui.notify(`debug-mode: already active (use /${COMMAND_STATUS}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT})`, "error");
|
|
508
590
|
return;
|
|
509
591
|
}
|
|
510
592
|
const problem = args.trim();
|
|
@@ -528,34 +610,44 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
528
610
|
});
|
|
529
611
|
|
|
530
612
|
pi.registerCommand(COMMAND_PROCEED, {
|
|
531
|
-
description: "Continue with captured
|
|
532
|
-
handler: async (
|
|
613
|
+
description: "Continue with captured evidence and optional user details: /debug-proceed [details]",
|
|
614
|
+
handler: async (args, ctx) => {
|
|
533
615
|
uiCtx = ctx;
|
|
534
|
-
|
|
616
|
+
const details = args.trim();
|
|
617
|
+
await advanceDebug(ctx, details || undefined);
|
|
535
618
|
},
|
|
536
619
|
});
|
|
537
620
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Parse `/debug-evidence [<request-id>] <path>`: when the first
|
|
623
|
+
* whitespace-delimited token names a pending current-round user_artifact
|
|
624
|
+
* request, link to it and treat the remainder (spaces intact) as the path;
|
|
625
|
+
* otherwise the whole argument is an unlinked path.
|
|
626
|
+
*/
|
|
627
|
+
function parseEvidenceArgument(args: string): { requestId: string | null; rawPath: string | undefined } {
|
|
628
|
+
const trimmed = args.trim();
|
|
629
|
+
if (!trimmed) return { requestId: null, rawPath: undefined };
|
|
630
|
+
const firstToken = trimmed.split(/\s+/, 1)[0];
|
|
631
|
+
const isPending = pendingEvidenceRequests(state, state.round).some(
|
|
632
|
+
request => request.method === "user_artifact" && request.id === firstToken,
|
|
633
|
+
);
|
|
634
|
+
if (isPending) {
|
|
635
|
+
const rest = trimmed.slice(firstToken.length).trim();
|
|
636
|
+
return rest ? { requestId: firstToken, rawPath: rest } : { requestId: firstToken, rawPath: undefined };
|
|
637
|
+
}
|
|
638
|
+
return { requestId: null, rawPath: trimmed };
|
|
639
|
+
}
|
|
545
640
|
|
|
546
|
-
pi.registerCommand(
|
|
547
|
-
description: "
|
|
641
|
+
pi.registerCommand(COMMAND_EVIDENCE, {
|
|
642
|
+
description: "Attach one user-provided evidence file: /debug-evidence [<request-id>] <path> (no argument opens a path prompt)",
|
|
548
643
|
handler: async (args, ctx) => {
|
|
549
644
|
uiCtx = ctx;
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
ctx.ui.notify("Usage: /debug-note <reproduction details>", "error");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
await advanceDebug(ctx, details);
|
|
645
|
+
const { requestId, rawPath } = parseEvidenceArgument(args);
|
|
646
|
+
await attachEvidence(ctx, rawPath, requestId);
|
|
556
647
|
},
|
|
557
648
|
});
|
|
558
649
|
|
|
650
|
+
|
|
559
651
|
pi.registerCommand(COMMAND_ABORT, {
|
|
560
652
|
description: "Abort debug mode: delete logs (fixes stay in the working diff)",
|
|
561
653
|
handler: async (_args, ctx) => {
|
|
@@ -575,12 +667,28 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
575
667
|
refreshLogCounts();
|
|
576
668
|
const scan = await syncLedger(state);
|
|
577
669
|
const tallies = state.runId ? summarizeHypotheses(readRunLines(state.runId)) : [];
|
|
670
|
+
const roundRequests = state.evidenceRequests.filter(request => request.round === state.round);
|
|
671
|
+
const pending = pendingEvidenceRequests(state, state.round);
|
|
672
|
+
const unavailable = state.evidenceArtifacts.filter(artifact => {
|
|
673
|
+
try {
|
|
674
|
+
const stats = fs.statSync(artifact.path);
|
|
675
|
+
fs.accessSync(artifact.path, fs.constants.R_OK);
|
|
676
|
+
return !stats.isFile();
|
|
677
|
+
} catch {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
578
681
|
ctx.ui.notify(
|
|
579
682
|
`debug-mode: phase=${state.phase} round=${state.round} run=${state.runId}\n` +
|
|
580
683
|
`${describeLedger(scan)}\n` +
|
|
581
684
|
`logs: ${state.runHistory.map(r => `${r}=${state.logCounts[r] ?? 0}`).join(", ") || "(none)"}\n` +
|
|
582
685
|
`this run by hypothesis: ${describeHypotheses(tallies)}\n` +
|
|
583
|
-
`current log file: ${logFileFor(state) ?? "(not initialized)"}
|
|
686
|
+
`current log file: ${logFileFor(state) ?? "(not initialized)"}\n` +
|
|
687
|
+
`evidence requests: ${roundRequests.map(r => `${r.id}[${r.method}] ${r.title}`).join("; ") || "(none)"}\n` +
|
|
688
|
+
`pending: ${pending.map(r => r.id).join(", ") || "(none)"}\n` +
|
|
689
|
+
`observations: ${state.evidenceObservations.map(o => o.id).join(", ") || "(none)"}\n` +
|
|
690
|
+
`artifacts: ${state.evidenceArtifacts.map(a => a.id).join(", ") || "(none)"}` +
|
|
691
|
+
(unavailable.length > 0 ? `\nunavailable artifacts: ${unavailable.map(a => `${a.id} ${a.path}`).join(", ")}` : ""),
|
|
584
692
|
"info",
|
|
585
693
|
);
|
|
586
694
|
},
|
|
@@ -601,6 +709,9 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
601
709
|
if (last?.data?.active) {
|
|
602
710
|
Object.assign(state, last.data);
|
|
603
711
|
if (!Array.isArray(state.reproductionSteps)) state.reproductionSteps = [];
|
|
712
|
+
if (!Array.isArray(state.evidenceRequests)) state.evidenceRequests = [];
|
|
713
|
+
if (!Array.isArray(state.evidenceArtifacts)) state.evidenceArtifacts = [];
|
|
714
|
+
if (!Array.isArray(state.evidenceObservations)) state.evidenceObservations = [];
|
|
604
715
|
if (!Array.isArray(state.runHistory)) state.runHistory = state.runId ? [state.runId] : [];
|
|
605
716
|
if (typeof state.gateNudges !== "number") state.gateNudges = 0;
|
|
606
717
|
if (!state.phase) state.phase = "waiting";
|
|
@@ -619,13 +730,12 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
619
730
|
}
|
|
620
731
|
refreshLogCounts();
|
|
621
732
|
ctx.ui.notify(
|
|
622
|
-
`debug-mode resumed: phase=${state.phase} round=${state.round}, log=${currentFile ?? "unavailable"}. Use /${COMMAND_STATUS}, /${
|
|
733
|
+
`debug-mode resumed: phase=${state.phase} round=${state.round}, log=${currentFile ?? "unavailable"}; evidence: ${evidenceSummary(state, state.round)}. Use /${COMMAND_STATUS}, /${COMMAND_EVIDENCE}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT}.`,
|
|
623
734
|
"info",
|
|
624
735
|
);
|
|
625
736
|
}
|
|
626
737
|
refreshUi();
|
|
627
738
|
watchLogFile();
|
|
628
|
-
if (state.active && state.phase === "waiting") scheduleReviewMenu(ctx);
|
|
629
739
|
});
|
|
630
740
|
|
|
631
741
|
pi.on("turn_start", async () => {
|