@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/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, reviewMenuTitle } from "./ui";
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 COMMAND_NOTE = "debug-note";
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
- display: false,
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 steps = extractReproductionSteps(extractAssistantText(msg.content));
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
- function enterGate(ctx: ExtensionContext, probesThisRound: number, missingSteps: boolean): void {
261
- state.phase = "waiting";
262
- pi.appendEntry(DEBUG_ENTRY, { ...state });
263
- refreshUi();
264
- watchLogFile();
265
- scheduleReviewMenu(ctx);
266
- if (probesThisRound === 0) {
267
- ctx.ui.notify(
268
- `Debug round ${state.round} paused, but it added no probes — this round cannot produce runtime evidence. Use /${COMMAND_MENU} and Proceed to ask for instrumentation.`,
269
- "warning",
270
- );
271
- } else if (missingSteps) {
272
- ctx.ui.notify(
273
- `Debug round ${state.round} paused without reproduction steps. Exercise the instrumented path, then ${PROCEED_REMINDER}`,
274
- "warning",
275
- );
276
- } else {
277
- ctx.ui.notify(`Debug round ${state.round} paused. Reproduce the bug, then ${PROCEED_REMINDER}`, "info");
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
- async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
366
+ async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
335
367
  if (!ensureWaiting(ctx)) return;
336
368
  const logCount = currentLogCount();
337
- if (logCount === 0 && ctx.hasUI) {
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, reproductionDetails?: string): Promise<void> {
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
- if (logCount === 0 && !reproductionDetails && ctx.hasUI) {
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 = reproductionDetails
395
- ? `reproduction details added — analyzing run ${run} (${logCount} entries)`
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: reproductionDetails ? "debug-mode-note" : "debug-mode-proceed",
400
- content: buildProceedMessage({ run, logCount, reproductionDetails, hypotheses }),
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
- * Open the action menu after `session_stop` has returned. Awaiting it
456
- * inside that handler would hold the agent-loop lock for the whole
457
- * reproduction; the microtask lets the round settle first.
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 scheduleReviewMenu(ctx: ExtensionContext): void {
460
- if (!ctx.hasUI) return;
461
- queueMicrotask(() => {
462
- openReviewMenu(ctx).catch(err => {
463
- pi.logger.warn("debug-mode: review menu failed to open", { err });
464
- });
465
- });
466
- }
467
-
468
- async function openReviewMenu(ctx: ExtensionContext): Promise<void> {
469
- if (!ensureWaiting(ctx)) return;
470
- if (!ctx.hasUI) {
471
- ctx.ui.notify(`/${COMMAND_MENU} requires an interactive UI; use /${COMMAND_DONE} or /${COMMAND_PROCEED} instead.`, "warning");
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
- reviewMenuOpen = true;
480
- try {
481
- const probesThisRound = state.probes.filter(p => p.round === state.round).length;
482
- const title = reviewMenuTitle(state.round, currentLogCount(), probesThisRound);
483
- const choice = await ctx.ui.select(title, reviewMenuOptions());
484
- if (!choice) return;
485
- if (choice === REVIEW_MARK_FIXED) {
486
- await markDebugFixed(ctx);
487
- } else if (choice === REVIEW_PROCEED) {
488
- await advanceDebug(ctx);
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
- } finally {
496
- reviewMenuOpen = false;
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 /${COMMAND_MENU}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT})`, "error");
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 logs: evaluate hypotheses, fix only with evidence",
532
- handler: async (_args, ctx) => {
613
+ description: "Continue with captured evidence and optional user details: /debug-proceed [details]",
614
+ handler: async (args, ctx) => {
533
615
  uiCtx = ctx;
534
- await advanceDebug(ctx);
616
+ const details = args.trim();
617
+ await advanceDebug(ctx, details || undefined);
535
618
  },
536
619
  });
537
620
 
538
- pi.registerCommand(COMMAND_MENU, {
539
- description: "Open the interactive action menu for a completed debug round",
540
- handler: async (_args, ctx) => {
541
- uiCtx = ctx;
542
- await openReviewMenu(ctx);
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(COMMAND_NOTE, {
547
- description: "Add reproduction details and continue: /debug-note <details>",
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 details = args.trim();
551
- if (!details) {
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}, /${COMMAND_MENU}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT}.`,
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 () => {