@siuver/omp-debug-mode 0.1.4 → 0.1.6

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/machine.ts ADDED
@@ -0,0 +1,421 @@
1
+ import { parseEvidencePlan } from "./evidence";
2
+ import { type GateFacts, MAX_HANDOFF_NUDGES, MAX_PROBE_NUDGES, decideGate, describeHandoff } from "./gate";
3
+ import {
4
+ CLEANUP_NUDGE,
5
+ buildFixedMessage,
6
+ buildProceedMessage,
7
+ buildStartMessage,
8
+ extractReproductionSteps,
9
+ } from "./methodology";
10
+ import {
11
+ type DebugSession,
12
+ type DebugState,
13
+ type EvidenceArtifact,
14
+ type EvidenceObservation,
15
+ type EvidenceRequest,
16
+ type HandoffMode,
17
+ type Probe,
18
+ type Round,
19
+ INACTIVE,
20
+ allRequests,
21
+ blankRound,
22
+ currentRound,
23
+ declaresRuntimeProbe,
24
+ evidenceSummary,
25
+ freshSession,
26
+ liveProbeCount,
27
+ needsUserCapture,
28
+ pendingRequests,
29
+ roundProbeIds,
30
+ } from "./state";
31
+
32
+ export const PROMPT_START = "debug-mode-start";
33
+ export const PROMPT_PROCEED = "debug-mode-proceed";
34
+ export const PROMPT_FIXED = "debug-mode-fixed";
35
+
36
+ /** Artifact metadata gathered by the shell; the reducer only files it. */
37
+ export type ArtifactCandidate = Omit<EvidenceArtifact, "id" | "addedAt" | "requestId">;
38
+
39
+ /** No nudges left: used when there is no agent turn to send anything back to. */
40
+ const SPENT_NUDGES = { handoff: MAX_HANDOFF_NUDGES, probes: MAX_PROBE_NUDGES };
41
+
42
+ export type DebugEvent =
43
+ | { t: "start"; problem: string; debugDir: string; runId: string; logFile: string }
44
+ | { t: "turn_started" }
45
+ /** A user message landed in the transcript — the only thing that ends a user turn. */
46
+ | { t: "user_replied" }
47
+ | { t: "assistant_message"; text: string }
48
+ /** Any tool call: the progress signal the reminder guard waits for. */
49
+ | { t: "tool_used" }
50
+ | { t: "probes_found"; probes: Probe[] }
51
+ | { t: "ledger_synced"; probes: Probe[] }
52
+ | { t: "runs_observed"; runHistory: string[]; logCounts: Record<string, number> }
53
+ | { t: "turn_settled" }
54
+ /** An explicit `hand_off_to_user` call: intent the machine can trust. */
55
+ | { t: "handoff"; mode: HandoffMode; steps: string[]; plan: EvidenceRequest[] | null }
56
+ /** The user takes the turn back from a round that never settled. */
57
+ | { t: "reclaim" }
58
+ | { t: "proceed"; runId: string; logCount: number; hypotheses: string; details?: string; now: number }
59
+ | { t: "mark_fixed" }
60
+ | { t: "attach_artifact"; candidate: ArtifactCandidate; requestId: string | null; now: number }
61
+ | { t: "abort" };
62
+
63
+ export type Effect =
64
+ | { kind: "notify"; level: "info" | "warning" | "error"; text: string }
65
+ /** Inject a prompt and start an agent turn. */
66
+ | { kind: "prompt"; customType: string; content: string; summary: string }
67
+ /** Keep the current turn going instead of settling it. */
68
+ | { kind: "continue"; context: string }
69
+ | { kind: "teardown"; outcome: "finished" | "aborted"; probesLeft: Probe[]; debugDir: string | null };
70
+
71
+ export interface Transition {
72
+ state: DebugState;
73
+ effects: Effect[];
74
+ }
75
+
76
+ /** A narrowed active state. Kept as one type so no-ops can return it verbatim. */
77
+ type ActiveState = { active: true } & DebugSession;
78
+
79
+ function unchanged(state: DebugState): Transition {
80
+ return { state, effects: [] };
81
+ }
82
+
83
+ function withRound(session: ActiveState, round: Round): ActiveState {
84
+ return { ...session, rounds: [...session.rounds.slice(0, -1), round] };
85
+ }
86
+
87
+ /**
88
+ * The whole state machine. Pure and total: every transition is a function of
89
+ * the previous state plus one event, and anything that touches the filesystem,
90
+ * the host UI or the model is returned as an effect for the shell to apply.
91
+ */
92
+ export function reduce(state: DebugState, event: DebugEvent): Transition {
93
+ if (event.t === "start") {
94
+ if (state.active) return unchanged(state);
95
+ return {
96
+ state: { active: true, ...freshSession(event.problem, event.debugDir, event.runId) },
97
+ effects: [
98
+ {
99
+ kind: "prompt",
100
+ customType: PROMPT_START,
101
+ content: buildStartMessage(event.problem, event.logFile),
102
+ summary: "debug mode started — hypotheses and instrumentation",
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ if (!state.active) return unchanged(state);
108
+ const session: ActiveState = state;
109
+
110
+ switch (event.t) {
111
+ case "turn_started":
112
+ // Deliberately stage-blind. A turn can start without the user doing
113
+ // anything — a todo reminder, a plan nudge or any other host
114
+ // continuation resumes the agent loop, which starts a turn while the
115
+ // round is still with the user. Reading "a turn began" as "the user
116
+ // replied" is what let those continuations take the user's turn away.
117
+ return session.turnProduced ? unchanged({ ...session, turnProduced: false }) : unchanged(session);
118
+
119
+ case "user_replied":
120
+ // A reply at a handoff hands the round back to the agent. Keeping the
121
+ // user stage through the turn would make the entire turn invisible to
122
+ // the machine and silently discard the plan and steps it produced.
123
+ return unchanged(session.stage === "user_turn" ? reopenRound(session) : session);
124
+
125
+ case "assistant_message":
126
+ return unchanged(absorbAssistantText(session, event.text));
127
+
128
+ case "tool_used":
129
+ return unchanged(clearNudgeWait(session));
130
+
131
+ case "probes_found": {
132
+ const fresh = event.probes.filter(probe => !session.probes.some(known => known.id === probe.id));
133
+ if (fresh.length === 0) return unchanged(session);
134
+ const probes = [...session.probes, ...fresh];
135
+ if (session.stage !== "investigating") return unchanged({ ...session, probes });
136
+ const round = currentRound(session);
137
+ return unchanged(
138
+ withRound({ ...session, probes }, { ...round, probeIds: [...round.probeIds, ...fresh.map(p => p.id)] }),
139
+ );
140
+ }
141
+
142
+ case "ledger_synced": {
143
+ const same =
144
+ event.probes.length === session.probes.length &&
145
+ event.probes.every((probe, i) => probe.id === session.probes[i]?.id);
146
+ return unchanged(same ? session : { ...session, probes: event.probes });
147
+ }
148
+
149
+ case "runs_observed":
150
+ return unchanged({ ...session, runHistory: event.runHistory, logCounts: event.logCounts });
151
+
152
+ case "turn_settled":
153
+ return settle(session);
154
+
155
+ case "handoff":
156
+ return applyHandoff(session, event);
157
+
158
+ case "reclaim":
159
+ return reclaim(session);
160
+
161
+ case "proceed":
162
+ return advance(session, event);
163
+
164
+ case "mark_fixed":
165
+ return startCleanup(session);
166
+
167
+ case "attach_artifact":
168
+ return attach(session, event);
169
+
170
+ case "abort":
171
+ return {
172
+ state: INACTIVE,
173
+ effects: [
174
+ { kind: "teardown", outcome: "aborted", probesLeft: session.probes, debugDir: session.debugDir },
175
+ ],
176
+ };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Give the round back to the agent. Nudge budgets are per agent turn rather
182
+ * than per round: a round the user replied to must still be repairable, or one
183
+ * spent budget would silence every later turn in the same round.
184
+ */
185
+ function reopenRound(session: ActiveState): ActiveState {
186
+ const round = currentRound(session);
187
+ return withRound(
188
+ { ...session, stage: "investigating", turnProduced: false },
189
+ { ...round, handoff: null, nudges: { handoff: 0, probes: 0 }, awaitingProgress: false },
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Clear the reminder guard. A tool call means the agent acted on the reminder
195
+ * instead of restating itself, so the next settle may remind it again; a turn
196
+ * that only produces more prose keeps the guard set and is handed over.
197
+ */
198
+ function clearNudgeWait(session: ActiveState): ActiveState {
199
+ if (session.stage !== "investigating") return session;
200
+ const round = currentRound(session);
201
+ if (!round.awaitingProgress) return session;
202
+ return withRound(session, { ...round, awaitingProgress: false });
203
+ }
204
+
205
+ /** Record the round artefacts a settled-but-not-yet-judged turn produced. */
206
+ function absorbAssistantText(session: ActiveState, text: string): ActiveState {
207
+ const round = currentRound(session);
208
+ const steps = session.stage === "investigating" ? extractReproductionSteps(text) : [];
209
+ const plan = session.stage === "investigating" ? parseEvidencePlan(text) : { found: false, valid: false, requests: [] };
210
+ const next: Round = {
211
+ ...round,
212
+ reproductionSteps: steps.length > 0 ? steps : round.reproductionSteps,
213
+ // A later malformed block must never erase a plan that already validated:
214
+ // the round would lose evidence requests the user was already shown.
215
+ plan: plan.valid ? plan.requests : round.plan,
216
+ };
217
+ const roundChanged = next.reproductionSteps !== round.reproductionSteps || next.plan !== round.plan;
218
+ if (!roundChanged) return session.turnProduced ? session : { ...session, turnProduced: true };
219
+ return withRound({ ...session, turnProduced: true }, next);
220
+ }
221
+
222
+ function gateFacts(session: ActiveState, round: Round): GateFacts {
223
+ return {
224
+ round: round.index,
225
+ hasReproductionSteps: round.reproductionSteps.length > 0,
226
+ hasEvidencePlan: round.plan !== null,
227
+ declaresRuntimeProbe: declaresRuntimeProbe(round),
228
+ needsUserCapture: needsUserCapture(round),
229
+ liveProbes: liveProbeCount(session),
230
+ probesAddedThisRound: roundProbeIds(session, round).length,
231
+ nudges: round.nudges,
232
+ awaitingProgress: round.awaitingProgress,
233
+ };
234
+ }
235
+
236
+ function settle(session: ActiveState): Transition {
237
+ if (session.stage === "cleaning_up") return finishCleanup(session);
238
+ if (session.stage !== "investigating" || !session.turnProduced) return unchanged(session);
239
+
240
+ const round = currentRound(session);
241
+ const decision = decideGate(gateFacts(session, round));
242
+
243
+ if (decision.kind === "nudge") {
244
+ const nudges =
245
+ decision.budget === "probes"
246
+ ? { ...round.nudges, probes: round.nudges.probes + 1 }
247
+ : { ...round.nudges, handoff: round.nudges.handoff + 1 };
248
+ return {
249
+ state: withRound(session, { ...round, nudges, awaitingProgress: true }),
250
+ effects: [{ kind: "continue", context: decision.context }],
251
+ };
252
+ }
253
+
254
+ return handOff(session, decision.mode, decision.missingPlan);
255
+ }
256
+
257
+ /**
258
+ * Move the round to the user. Every settled turn ends here: the mode only
259
+ * changes the wording, so a mode the agent misjudged can never make a command
260
+ * illegal. The notify text leads with the same sentence as the widget so the
261
+ * transcript and the widget cannot disagree about whose move it is.
262
+ */
263
+ function handOff(session: ActiveState, mode: HandoffMode, missingPlan: boolean): Transition {
264
+ const round = currentRound(session);
265
+ const next = withRound({ ...session, stage: "user_turn" }, { ...round, handoff: mode });
266
+ const pending = pendingRequests(next);
267
+ const detail = pending.length > 0 ? ` Pending: ${pending.map(request => request.id).join(", ")}.` : "";
268
+ return {
269
+ state: next,
270
+ effects: [
271
+ {
272
+ kind: "notify",
273
+ level: mode === "incomplete" || missingPlan ? "warning" : "info",
274
+ text: `${describeHandoff(mode, round.index, missingPlan)}${detail}`,
275
+ },
276
+ ],
277
+ };
278
+ }
279
+
280
+ /**
281
+ * An explicit handoff call. The tool has already validated its arguments, so
282
+ * this records the declared plan and steps and hands over without consulting
283
+ * the gate: the agent said what it wants, and guessing from its prose is the
284
+ * failure mode the tool exists to remove.
285
+ */
286
+ function applyHandoff(session: ActiveState, event: Extract<DebugEvent, { t: "handoff" }>): Transition {
287
+ if (session.stage !== "investigating") return unchanged(session);
288
+ const round = currentRound(session);
289
+ const updated: Round = {
290
+ ...round,
291
+ plan: event.plan ?? round.plan,
292
+ reproductionSteps: event.steps.length > 0 ? event.steps : round.reproductionSteps,
293
+ };
294
+ const staged = withRound({ ...session, turnProduced: true }, updated);
295
+ const expectsPlan = event.mode !== "question";
296
+ return handOff(staged, event.mode, expectsPlan && updated.plan === null);
297
+ }
298
+
299
+ /**
300
+ * Classify a round whose turn never settled. An aborted turn never reaches
301
+ * `session_stop`, so without this the stage would stay `investigating` and
302
+ * every debug command would refuse for the rest of the session.
303
+ */
304
+ function reclaim(session: ActiveState): Transition {
305
+ if (session.stage !== "investigating") return unchanged(session);
306
+ const round = currentRound(session);
307
+ // There is no turn left to continue, so no nudge is available.
308
+ const decision = decideGate({ ...gateFacts(session, round), nudges: SPENT_NUDGES });
309
+ if (decision.kind === "nudge") return handOff(session, "incomplete", round.plan === null);
310
+ return handOff(session, decision.mode, decision.missingPlan);
311
+ }
312
+
313
+ /** Cleanup may only finish once the ledger is actually empty. */
314
+ function finishCleanup(session: ActiveState): Transition {
315
+ if (!session.turnProduced) return unchanged(session);
316
+ if (session.probes.length > 0 && session.cleanupNudges < 1) {
317
+ return {
318
+ state: { ...session, cleanupNudges: session.cleanupNudges + 1 },
319
+ effects: [{ kind: "continue", context: `${CLEANUP_NUDGE}\nRemaining probes: ${JSON.stringify(session.probes)}` }],
320
+ };
321
+ }
322
+ return {
323
+ state: INACTIVE,
324
+ effects: [{ kind: "teardown", outcome: "finished", probesLeft: session.probes, debugDir: session.debugDir }],
325
+ };
326
+ }
327
+
328
+ function advance(session: ActiveState, event: Extract<DebugEvent, { t: "proceed" }>): Transition {
329
+ const closing = currentRound(session);
330
+ const details = event.details?.trim() ?? "";
331
+ let withObservation: ActiveState = session;
332
+ if (details.length > 0) {
333
+ const reportIds = (closing.plan ?? [])
334
+ .filter(request => request.method === "user_report")
335
+ .map(request => request.id);
336
+ const observation: EvidenceObservation = {
337
+ id: `observation-${event.now.toString(36)}`,
338
+ requestIds: reportIds,
339
+ text: details,
340
+ round: closing.index,
341
+ addedAt: event.now,
342
+ };
343
+ withObservation = { ...session, observations: [...session.observations, observation] };
344
+ }
345
+
346
+ const summary = evidenceSummary(withObservation, closing);
347
+ const next: ActiveState = {
348
+ ...withObservation,
349
+ stage: "investigating",
350
+ turnProduced: false,
351
+ rounds: [...withObservation.rounds, blankRound(closing.index + 1, event.runId)],
352
+ runHistory: [...withObservation.runHistory, event.runId],
353
+ logCounts: { ...withObservation.logCounts, [event.runId]: 0 },
354
+ };
355
+ const label = details.length > 0 ? "proceed with user details" : "proceed";
356
+ return {
357
+ state: next,
358
+ effects: [
359
+ {
360
+ kind: "prompt",
361
+ customType: PROMPT_PROCEED,
362
+ content: buildProceedMessage({
363
+ run: closing.runId ?? "(none)",
364
+ logCount: event.logCount,
365
+ userDetails: details.length > 0 ? details : undefined,
366
+ hypotheses: event.hypotheses,
367
+ evidenceSummary: summary,
368
+ }),
369
+ summary: `${label} — analyzing run ${closing.runId ?? "(none)"} (${event.logCount} entries)`,
370
+ },
371
+ ],
372
+ };
373
+ }
374
+
375
+ function startCleanup(session: ActiveState): Transition {
376
+ const next: ActiveState = { ...session, stage: "cleaning_up", turnProduced: false, cleanupNudges: 0 };
377
+ const evidenceJson = JSON.stringify({
378
+ requests: allRequests(session),
379
+ observations: session.observations,
380
+ artifacts: session.artifacts,
381
+ });
382
+ return {
383
+ state: next,
384
+ effects: [
385
+ {
386
+ kind: "prompt",
387
+ customType: PROMPT_FIXED,
388
+ content: buildFixedMessage(JSON.stringify(session.probes), evidenceJson),
389
+ summary: `marked fixed — removing ${session.probes.length} probe(s)`,
390
+ },
391
+ ],
392
+ };
393
+ }
394
+
395
+ function attach(session: ActiveState, event: Extract<DebugEvent, { t: "attach_artifact" }>): Transition {
396
+ if (event.requestId !== null && !allRequests(session).some(request => request.id === event.requestId)) {
397
+ return {
398
+ state: session,
399
+ effects: [
400
+ { kind: "notify", level: "error", text: `debug-mode: unknown evidence request id ${event.requestId}` },
401
+ ],
402
+ };
403
+ }
404
+ const existing = session.artifacts.find(artifact => artifact.path === event.candidate.path);
405
+ const artifact: EvidenceArtifact = existing
406
+ ? { ...existing, ...event.candidate, requestId: event.requestId }
407
+ : { id: `artifact-${event.now.toString(36)}`, requestId: event.requestId, ...event.candidate, addedAt: event.now };
408
+ const artifacts = existing
409
+ ? session.artifacts.map(a => (a.path === artifact.path ? artifact : a))
410
+ : [...session.artifacts, artifact];
411
+ return {
412
+ state: { ...session, artifacts },
413
+ effects: [
414
+ {
415
+ kind: "notify",
416
+ level: "info",
417
+ text: `debug-mode: attached ${artifact.id} → ${artifact.path} (${artifact.size} bytes)`,
418
+ },
419
+ ],
420
+ };
421
+ }
package/src/main.ts CHANGED
@@ -1,24 +1,24 @@
1
- /**
2
- * Debug Mode Extension — Cursor-style human-in-the-loop debugging for omp.
3
- *
4
- * State machine (matches Cursor Debug Mode):
5
- * IDLE → /debug-mode <problem>
6
- * → agent writes 3-5 hypotheses and @omp-probe instrumentation (no product fix)
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
10
- * → Mark as fixed → agent removes probes + summarizes → teardown (logs deleted)
11
- *
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
- */
18
- import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
19
- import { registerDebugMode } from "./debug-mode";
20
-
21
- export default function debugModeExtension(pi: ExtensionAPI) {
22
- pi.setLabel("Debug Mode");
23
- registerDebugMode(pi);
24
- }
1
+ /**
2
+ * Debug Mode Extension — Cursor-style human-in-the-loop debugging for omp.
3
+ *
4
+ * State machine (matches Cursor Debug Mode):
5
+ * IDLE → /debug-mode <problem>
6
+ * → agent writes 3-5 hypotheses and @omp-probe instrumentation (no product fix)
7
+ * → WAITING_REPRO: agent stops; user reproduces out-of-band
8
+ * → /debug-proceed [details] → agent reads logs, evaluates hypotheses, fixes only
9
+ * with evidence, keeps probes, and asks for a verification reproduce → loop
10
+ * → /debug-done → agent removes probes + summarizes → teardown (logs deleted)
11
+ *
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
+ */
18
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
19
+ import { registerDebugMode } from "./debug-mode";
20
+
21
+ export default function debugModeExtension(pi: ExtensionAPI) {
22
+ pi.setLabel("Debug Mode");
23
+ registerDebugMode(pi);
24
+ }
@@ -1,7 +1,26 @@
1
- export const PROCEED_REMINDER = "Press Proceed/Mark as fixed when done.";
1
+ export const PROCEED_REMINDER =
2
+ "When done, run /debug-proceed to analyze this round, or /debug-done if the bug is fixed.";
2
3
 
3
4
  export const EVIDENCE_PLAN_TAG = "evidence_plan";
4
5
 
6
+ /** The Agent tool that ends an agent turn and hands the round to the user. */
7
+ export const HANDOFF_TOOL = "hand_off_to_user";
8
+
9
+ /**
10
+ * Stated wherever a round is closed. Prose cannot move the session: the model
11
+ * announcing a reproduction is not the same event as the session reaching the
12
+ * user, and conflating them is what leaves a round unclosed while the user has
13
+ * already been told to act.
14
+ */
15
+ export const HANDOFF_RULE =
16
+ `Close every round by calling ${HANDOFF_TOOL} as your last action. Pass mode ("reproduce" when the user must run ` +
17
+ 'the app, "capture" when they only supply a report or file, "question" when you need an answer before you can ' +
18
+ "plan), the evidence plan array, and the numbered steps the USER performs now. That tool call is the only thing " +
19
+ "that hands the session to the user, and it replies with exactly what to fix when an argument is wrong, so a " +
20
+ `rejected call is repaired in this turn. <${EVIDENCE_PLAN_TAG}> and <reproduction_steps> in prose are still read ` +
21
+ "for their contents, but they never close a round: a turn that stops without the tool call is sent straight back " +
22
+ "to you to make it.";
23
+
5
24
  /** The least-user-intervention evidence method ordering, verbatim for prompts. */
6
25
  export const MINIMIZE_USER_INTERVENTION =
7
26
  "MINIMIZE USER INTERVENTION. Choose the cheapest reliable evidence method per hypothesis, in this exact order: " +
@@ -12,6 +31,18 @@ export const MINIMIZE_USER_INTERVENTION =
12
31
  "NEVER ask the user to run a command you can run yourself. Batch all unavoidable user actions into the fewest reproductions/captures. " +
13
32
  "Do not choose a lower-priority method merely because it is familiar.";
14
33
 
34
+ /**
35
+ * Closing-tag order repeated in the methodology, start prompt, and proceed prompt.
36
+ * Probe edits must land before the round may close; reproduction_steps are user-only.
37
+ */
38
+ export const CLOSE_ROUND_RULES =
39
+ "If any selected method is runtime_probe, write @omp-probe instrumentation into the working tree in THIS turn before closing — probe edits are required and are not a product fix. " +
40
+ `Do not emit <${EVIDENCE_PLAN_TAG}> or <reproduction_steps> for a runtime_probe round until those probes are already on disk. ` +
41
+ "A runtime_probe plan without @omp-probe markers is an incomplete round: keep going and instrument first. " +
42
+ "<reproduction_steps> lists only actions the user performs now (reproduce, capture, restart). " +
43
+ "Never include future agent work such as installing probes, reading logs, or analyzing results. " +
44
+ "agent_inspection, user_report, and user_artifact rounds may close without adding probes.";
45
+
15
46
  export const METHODOLOGY = `\
16
47
  [DEBUG MODE METHODOLOGY — follow strictly]
17
48
  This is OMP Debug Mode. Follow the steps in order. Do not skip them.
@@ -27,8 +58,9 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
27
58
 
28
59
  Every plan entry needs a concrete rationale. A user_report or user_artifact rationale must explicitly name why BOTH autonomous inspection AND model-added probes cannot answer the hypotheses; a generic statement such as "need more information" is invalid.
29
60
 
30
- 3. For runtime_probe rounds, instrument code with probes that test ALL remaining
31
- hypotheses in parallel. Do not apply a product fix in this step. NEVER fix without evidence first. Always rely on runtime logs plus code — never code inspection alone. Unit tests are optional and never replace a user reproduction.
61
+ 3. For runtime_probe rounds, instrument code in THIS turn with probes that test
62
+ ALL remaining hypotheses in parallel. ${CLOSE_ROUND_RULES}
63
+ Do not apply a product fix in this step. NEVER fix without evidence first. Always rely on runtime logs plus code — never code inspection alone. Unit tests are optional and never replace a user reproduction.
32
64
 
33
65
  Probe rules:
34
66
  - Wrap EACH probe in a collapsible region (\`// #region agent log\` /
@@ -47,8 +79,10 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
47
79
  - The extension truncates the current log file at the start of each round.
48
80
  Do not delete, rename, or overwrite that file yourself.
49
81
 
50
- 4. Close the round. Emit exactly one <${EVIDENCE_PLAN_TAG}> block containing a
51
- non-empty JSON array covering EVERY hypothesis:
82
+ 4. Close the round only after step 3 is done for any runtime_probe plan.
83
+ ${HANDOFF_RULE}
84
+ The plan is a non-empty JSON array covering EVERY hypothesis, in the same
85
+ shape accepted inside a legacy <${EVIDENCE_PLAN_TAG}> block:
52
86
  <${EVIDENCE_PLAN_TAG}>
53
87
  [{"id":"E1","hypothesisIds":["A","B"],"method":"runtime_probe","title":"...","rationale":"The disputed runtime branches are not present in existing logs; one model-added probe set can capture both without a separate user artifact.","instructions":["..."],"artifactHint":"optional"}]
54
88
  </${EVIDENCE_PLAN_TAG}>
@@ -56,17 +90,22 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
56
90
  Use artifactHint only for user_artifact (expected file kind). Give actionable
57
91
  numbered capture/report instructions in instructions.
58
92
 
59
- 5. Ask the user to reproduce (or capture/report, per the plan). End your response
60
- with a <reproduction_steps> numbered list (no header inside the tag) describing
61
- the single combined reproduction/capture sequence, and this exact sentence
62
- after the tag: "${PROCEED_REMINDER}"
63
- Never say "click". Never ask the user to reply "done". Remind them to restart
64
- the app or service if the instrumented code would otherwise be stale.
93
+ 5. Ask the user to reproduce (or capture/report, per the plan). Pass the single
94
+ combined reproduction/capture sequence the USER performs now as the steps
95
+ argument of ${HANDOFF_TOOL} — the widget the user reads is built from that
96
+ call, not from your prose. A <reproduction_steps> numbered list (no header
97
+ inside the tag) followed by "${PROCEED_REMINDER}" is read as content but
98
+ closes nothing, so make the call even when you also wrote the prose.
99
+ ${CLOSE_ROUND_RULES}
100
+ Never say "click" or "press Proceed": those buttons do not exist. Name
101
+ /debug-proceed and /debug-done by those exact commands. Never ask the user to
102
+ reply "done". Remind them to restart the app or service if the instrumented
103
+ code would otherwise be stale.
65
104
  Then STOP. The user reproduces out-of-band.
66
105
  No logs may be expected for user_report/user_artifact plans: evaluate the
67
106
  requested user evidence instead of treating absent probes as a failed round.
68
107
 
69
- 6. After Proceed: call list_debug_evidence, then read logs with get_debug_logs
108
+ 6. After /debug-proceed: call list_debug_evidence, then read logs with get_debug_logs
70
109
  (previous=true for the completed run). Evaluate EACH hypothesis as CONFIRMED,
71
110
  REJECTED, or INCONCLUSIVE citing the selected evidence method: hypothesis ID
72
111
  plus log-line numbers, a submitted observation, or an attached artifact/report
@@ -83,7 +122,7 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
83
122
  8. After a fix, ask the user to reproduce again. Compare before/after logs with
84
123
  cited entries. Do not claim success without that proof.
85
124
 
86
- 9. If verification proves success and the user chooses Mark as fixed: remove
125
+ 9. If verification proves success and the user runs /debug-done: remove
87
126
  every probe, verify with list_debug_probes that the ledger is empty, then
88
127
  summarize the root cause and the final fix in 1-2 lines.
89
128
 
@@ -104,9 +143,14 @@ export function buildStartMessage(problem: string, logFile: string): string {
104
143
  MINIMIZE_USER_INTERVENTION +
105
144
  "\n\n" +
106
145
  "Begin round 1: generate 3-5 precise hypotheses, decide an evidence method for each, and do NOT apply a product fix yet. " +
146
+ CLOSE_ROUND_RULES +
147
+ " " +
148
+ HANDOFF_RULE +
149
+ " " +
107
150
  "Call list_debug_evidence whenever you need the request/observation/artifact ledger. " +
108
151
  "Cite evidence as hypothesis ID plus log-line number or attached observation/artifact/report path. " +
109
- `Close with exactly one <${EVIDENCE_PLAN_TAG}> JSON block, then <reproduction_steps> and "${PROCEED_REMINDER}" then STOP.`
152
+ `Then close with ${HANDOFF_TOOL} prose alone never closes a round, so make the call even if you also wrote ` +
153
+ `a <${EVIDENCE_PLAN_TAG}> block — tell the user "${PROCEED_REMINDER}" and STOP.`
110
154
  );
111
155
  }
112
156
 
@@ -137,8 +181,13 @@ export function buildProceedMessage(args: {
137
181
  "Call list_debug_evidence first, then read the previous run with get_debug_logs (previous=true). " +
138
182
  "Evaluate each hypothesis CONFIRMED/REJECTED/INCONCLUSIVE citing hypothesis ID plus log-line number or attached observation/artifact/report path. " +
139
183
  "Fix only if a hypothesis is confirmed with 100% confidence; keep all probes in place for a verification reproduce. " +
140
- "If not confident, re-instrument without a speculative patch; after an inconclusive round, reconsider a lower-burden method instead of repeating the same user request. " +
141
- `If a previous fix failed, first revert code changes from rejected hypotheses. End with <${EVIDENCE_PLAN_TAG}>, <reproduction_steps> and "${PROCEED_REMINDER}" then STOP.`
184
+ "If not confident, re-instrument in this turn without a speculative patch; after an inconclusive round, reconsider a lower-burden method instead of repeating the same user request. " +
185
+ "If a previous fix failed, first revert code changes from rejected hypotheses. " +
186
+ CLOSE_ROUND_RULES +
187
+ " " +
188
+ HANDOFF_RULE +
189
+ " " +
190
+ `Then end with ${HANDOFF_TOOL} — prose alone never closes a round — tell the user "${PROCEED_REMINDER}" and STOP.`
142
191
  );
143
192
  }
144
193
 
@@ -154,7 +203,15 @@ The user confirmed the fix. Only two things remain:
154
203
  \`#region agent log\` wrapper, then call list_debug_probes and confirm the
155
204
  ledger is empty. Keep the proven fix; remove nothing else.
156
205
  2. Summarize in 1-2 lines: the root cause and the fix that is staying.
157
- Do not add probes, form new hypotheses, or ask for another reproduction.`;
206
+ Do not add probes, form new hypotheses, or ask for another reproduction, and do
207
+ not call ${HANDOFF_TOOL}: cleanup ends when the ledger is empty and you have
208
+ summarized.`;
209
+
210
+ /** Sent back into a cleanup turn that ended with probes still in the code. */
211
+ export const CLEANUP_NUDGE =
212
+ "Cleanup is not finished: the probe ledger below is still not empty. Remove each remaining `@omp-probe` marker " +
213
+ "and its `#region agent log` wrapper from the code, keep the proven fix, then call list_debug_probes to confirm " +
214
+ "the ledger is empty before summarizing.";
158
215
 
159
216
  export function buildFixedMessage(probesJson: string, evidenceJson = "[]"): string {
160
217
  return (
package/src/probes.ts CHANGED
@@ -23,15 +23,17 @@ export function probeFileFromInput(input: Record<string, unknown>, cwd: string):
23
23
  return path.resolve(cwd, raw);
24
24
  }
25
25
 
26
- /** Register every `@omp-probe <id>` marker introduced by an edit/write tool call. */
27
- export function recordProbes(probes: Probe[], round: number, input: Record<string, unknown>, cwd: string): void {
26
+ /** Every `@omp-probe <id>` marker an edit/write tool call introduces, deduplicated by id. */
27
+ export function probesInInput(input: Record<string, unknown>, cwd: string, round: number): Probe[] {
28
28
  const file = probeFileFromInput(input, cwd);
29
+ const found: Probe[] = [];
29
30
  for (const [key, value] of Object.entries(input)) {
30
31
  if (PRE_EDIT_KEYS.test(key)) continue;
31
32
  for (const id of probeIdsIn(value)) {
32
- if (!probes.some(p => p.id === id)) probes.push({ id, file, round });
33
+ if (!found.some(p => p.id === id)) found.push({ id, file, round });
33
34
  }
34
35
  }
36
+ return found;
35
37
  }
36
38
 
37
39
  export interface LedgerScan {
@@ -73,13 +75,11 @@ export async function scanLedger(probes: readonly Probe[]): Promise<LedgerScan>
73
75
  }
74
76
 
75
77
  /**
76
- * Rescan and prune the ledger so it mirrors the code on disk. Unreadable files
77
- * keep their probes so a transient read error cannot fake a clean teardown.
78
+ * The ledger as it should be after a rescan. Unreadable files keep their probes
79
+ * so a transient read error cannot fake a clean teardown.
78
80
  */
79
- export async function syncLedger(state: { probes: Probe[] }): Promise<LedgerScan> {
80
- const scan = await scanLedger(state.probes);
81
- state.probes = [...scan.alive, ...scan.unknown];
82
- return scan;
81
+ export function survivingProbes(scan: LedgerScan): Probe[] {
82
+ return [...scan.alive, ...scan.unknown];
83
83
  }
84
84
 
85
85
  export function describeLedger(scan: LedgerScan): string {