@siuver/omp-debug-mode 0.1.4 → 0.1.5
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 +13 -0
- package/README.md +31 -10
- package/package.json +1 -1
- package/src/debug-mode.ts +281 -375
- package/src/evidence.ts +9 -36
- package/src/gate.ts +59 -30
- package/src/machine.ts +334 -0
- package/src/methodology.ts +34 -8
- package/src/probes.ts +9 -9
- package/src/state.ts +233 -74
- package/src/tools.ts +39 -23
- package/src/ui.ts +43 -19
package/src/evidence.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { EvidenceArtifact, EvidenceMethod, EvidenceRequest, EvidenceView } from "./state";
|
|
4
4
|
|
|
5
5
|
/** Upper bound on accepted plan entries so one model reply cannot flood the gate. */
|
|
6
6
|
export const MAX_EVIDENCE_REQUESTS = 12;
|
|
@@ -31,7 +31,7 @@ function hasOnlyUniqueValues(values: readonly string[]): boolean {
|
|
|
31
31
|
* all-or-nothing: any malformed or invalid entry rejects the whole plan so a
|
|
32
32
|
* partial plan can never silently drop a hypothesis.
|
|
33
33
|
*/
|
|
34
|
-
export function parseEvidencePlan(text: string
|
|
34
|
+
export function parseEvidencePlan(text: string): EvidencePlanParseResult {
|
|
35
35
|
const match = /<evidence_plan>([\s\S]*?)<\/evidence_plan>/.exec(text);
|
|
36
36
|
if (!match) return { found: false, valid: false, requests: [] };
|
|
37
37
|
const fail = (error: string): EvidencePlanParseResult => ({ found: true, valid: false, requests: [], error });
|
|
@@ -80,7 +80,6 @@ export function parseEvidencePlan(text: string, round: number): EvidencePlanPars
|
|
|
80
80
|
rationale: record.rationale,
|
|
81
81
|
instructions: record.instructions,
|
|
82
82
|
...(record.artifactHint === undefined ? {} : { artifactHint: record.artifactHint }),
|
|
83
|
-
round,
|
|
84
83
|
});
|
|
85
84
|
}
|
|
86
85
|
return { found: true, valid: true, requests };
|
|
@@ -116,46 +115,20 @@ export function validateEvidenceArtifact(rawPath: string, cwd: string): Artifact
|
|
|
116
115
|
};
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
/** Record a user-supplied artifact, deduplicating by exact absolute path. */
|
|
120
|
-
export function addEvidenceArtifact(
|
|
121
|
-
state: DebugState,
|
|
122
|
-
rawPath: string,
|
|
123
|
-
cwd: string,
|
|
124
|
-
requestId: string | null,
|
|
125
|
-
now = Date.now(),
|
|
126
|
-
): { state: DebugState; artifact: EvidenceArtifact } | { error: string } {
|
|
127
|
-
if (requestId !== null && !state.evidenceRequests.some(request => request.id === requestId)) {
|
|
128
|
-
return { error: `unknown evidence request id ${requestId}` };
|
|
129
|
-
}
|
|
130
|
-
const validation = validateEvidenceArtifact(rawPath, cwd);
|
|
131
|
-
if (!validation.ok) return { error: validation.reason };
|
|
132
|
-
const base = validation.artifact;
|
|
133
|
-
const existing = state.evidenceArtifacts.find(artifact => artifact.path === base.path);
|
|
134
|
-
if (existing) {
|
|
135
|
-
const artifact: EvidenceArtifact = { ...existing, ...base, requestId };
|
|
136
|
-
return {
|
|
137
|
-
state: { ...state, evidenceArtifacts: state.evidenceArtifacts.map(a => (a.path === base.path ? artifact : a)) },
|
|
138
|
-
artifact,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const artifact: EvidenceArtifact = { id: `artifact-${now.toString(36)}`, requestId, ...base, addedAt: now };
|
|
142
|
-
return { state: { ...state, evidenceArtifacts: [...state.evidenceArtifacts, artifact] }, artifact };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
118
|
/**
|
|
146
119
|
* Human-readable evidence description for the blackboard and the model-facing
|
|
147
120
|
* tool. Availability is re-checked on every call: a missing or unreadable file
|
|
148
121
|
* is never treated as captured evidence.
|
|
149
122
|
*/
|
|
150
|
-
export function describeEvidence(
|
|
151
|
-
const requests
|
|
152
|
-
if (requests.length === 0 &&
|
|
123
|
+
export function describeEvidence(view: EvidenceView): string {
|
|
124
|
+
const { requests, artifacts, observations } = view;
|
|
125
|
+
if (requests.length === 0 && artifacts.length === 0 && observations.length === 0) {
|
|
153
126
|
return "(none)";
|
|
154
127
|
}
|
|
155
128
|
const lines: string[] = [];
|
|
156
129
|
for (const request of requests) {
|
|
157
|
-
const linked =
|
|
158
|
-
const reports =
|
|
130
|
+
const linked = artifacts.filter(artifact => artifact.requestId === request.id);
|
|
131
|
+
const reports = observations.filter(observation => observation.requestIds.includes(request.id));
|
|
159
132
|
const hint = request.artifactHint ? `, artifactHint: ${request.artifactHint}` : "";
|
|
160
133
|
const coverage =
|
|
161
134
|
request.method === "user_artifact"
|
|
@@ -171,10 +144,10 @@ export function describeEvidence(state: DebugState, round?: number): string {
|
|
|
171
144
|
`- ${request.id} [${request.method}] ${request.title} (${coverage})${hint}\n hypotheses: ${request.hypothesisIds.join(", ")}\n rationale: ${request.rationale}\n instructions: ${request.instructions.join(" | ")}`,
|
|
172
145
|
);
|
|
173
146
|
}
|
|
174
|
-
for (const observation of
|
|
147
|
+
for (const observation of observations) {
|
|
175
148
|
lines.push(`- ${observation.id} [user observation, round ${observation.round}] ${observation.text}`);
|
|
176
149
|
}
|
|
177
|
-
for (const artifact of
|
|
150
|
+
for (const artifact of artifacts) {
|
|
178
151
|
let stats: fs.Stats;
|
|
179
152
|
try {
|
|
180
153
|
stats = fs.statSync(artifact.path);
|
package/src/gate.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { MINIMIZE_USER_INTERVENTION, PROCEED_REMINDER } from "./methodology";
|
|
1
|
+
import { CLOSE_ROUND_RULES, MINIMIZE_USER_INTERVENTION, PROCEED_REMINDER } from "./methodology";
|
|
2
|
+
import type { OpenReason } from "./state";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
export const
|
|
4
|
+
/** Per-reason nudge budgets: a probe nudge must not spend the closing-tag one. */
|
|
5
|
+
export const MAX_TAG_NUDGES = 1;
|
|
6
|
+
export const MAX_PROBE_NUDGES = 1;
|
|
5
7
|
|
|
6
8
|
export const GATE_NUDGE =
|
|
7
9
|
"This round did not close properly. It must end with BOTH a <evidence_plan> JSON block " +
|
|
@@ -14,41 +16,68 @@ export const GATE_NUDGE =
|
|
|
14
16
|
"A user_artifact request must state the file type, path/capture instructions, how you will inspect the file, " +
|
|
15
17
|
"and why inspection/probes are inadequate. Do not start new work.";
|
|
16
18
|
|
|
19
|
+
export const PROBE_NUDGE =
|
|
20
|
+
"Your evidence plan selects runtime_probe, but no @omp-probe marker for this round exists in the working tree. " +
|
|
21
|
+
"Instrument the code now with edit/write — that is required work for this round, not a product fix — and only then " +
|
|
22
|
+
`re-emit <evidence_plan> and <reproduction_steps> followed by "${PROCEED_REMINDER}". ` +
|
|
23
|
+
CLOSE_ROUND_RULES;
|
|
24
|
+
|
|
17
25
|
export type GateDecision =
|
|
18
26
|
/** Hand control to the user and wait for a reproduction/capture. */
|
|
19
|
-
| { kind: "gate";
|
|
27
|
+
| { kind: "gate"; missingPlan: boolean }
|
|
20
28
|
/** Let the agent finish the round properly before gating. */
|
|
21
|
-
| { kind: "nudge"; context: string }
|
|
22
|
-
/**
|
|
23
|
-
| { kind: "
|
|
29
|
+
| { kind: "nudge"; budget: "tags" | "probes"; context: string }
|
|
30
|
+
/** The round settled without closing: the ball is with the user, not the gate. */
|
|
31
|
+
| { kind: "open"; reason: OpenReason };
|
|
32
|
+
|
|
33
|
+
export interface GateFacts {
|
|
34
|
+
hasReproductionSteps: boolean;
|
|
35
|
+
hasEvidencePlan: boolean;
|
|
36
|
+
/** The plan selects runtime_probe for at least one hypothesis. */
|
|
37
|
+
declaresRuntimeProbe: boolean;
|
|
38
|
+
/** Probes this round introduced that the ledger still finds on disk. */
|
|
39
|
+
liveProbes: number;
|
|
40
|
+
nudges: { tags: number; probes: number };
|
|
41
|
+
}
|
|
24
42
|
|
|
25
43
|
/**
|
|
26
|
-
* Decide what a settled agent turn means
|
|
44
|
+
* Decide what a settled agent turn means.
|
|
27
45
|
*
|
|
28
|
-
* A
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* flags set so the workflow can never stall.
|
|
46
|
+
* A round may only close when its declarations are backed by observable facts:
|
|
47
|
+
* a `runtime_probe` plan needs real `@omp-probe` markers, and both closing tags
|
|
48
|
+
* must be present. Anything short of that is either one automatic nudge or an
|
|
49
|
+
* `open` round the user can talk to — never a silent stop that looks gated.
|
|
33
50
|
*/
|
|
34
|
-
export function decideGate(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
export function decideGate(facts: GateFacts): GateDecision {
|
|
52
|
+
// Declared instrumentation that never reached disk: the reproduction the
|
|
53
|
+
// user is about to be asked for could not record anything.
|
|
54
|
+
if (facts.declaresRuntimeProbe && facts.liveProbes === 0) {
|
|
55
|
+
if (facts.nudges.probes < MAX_PROBE_NUDGES) return { kind: "nudge", budget: "probes", context: PROBE_NUDGE };
|
|
56
|
+
return { kind: "open", reason: "probes_missing" };
|
|
57
|
+
}
|
|
58
|
+
if (facts.hasReproductionSteps && facts.hasEvidencePlan) {
|
|
59
|
+
return { kind: "gate", missingPlan: false };
|
|
42
60
|
}
|
|
43
61
|
// Legacy/no-probe closure: reproduction steps alone still gate the turn.
|
|
44
|
-
if (
|
|
45
|
-
return { kind: "gate",
|
|
62
|
+
if (facts.hasReproductionSteps && !facts.hasEvidencePlan && facts.liveProbes === 0) {
|
|
63
|
+
return { kind: "gate", missingPlan: true };
|
|
64
|
+
}
|
|
65
|
+
// Nothing was declared and nothing was instrumented — the agent is talking
|
|
66
|
+
// to the user, so leave the round open instead of inventing a gate.
|
|
67
|
+
if (facts.liveProbes === 0 && !facts.hasEvidencePlan) {
|
|
68
|
+
return { kind: "open", reason: "awaiting_reply" };
|
|
69
|
+
}
|
|
70
|
+
if (facts.nudges.tags < MAX_TAG_NUDGES) return { kind: "nudge", budget: "tags", context: GATE_NUDGE };
|
|
71
|
+
return { kind: "open", reason: "unclosed" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** User-facing explanation of why a round is open, plus what to do about it. */
|
|
75
|
+
export function describeOpenReason(reason: OpenReason, round: number): string {
|
|
76
|
+
if (reason === "probes_missing") {
|
|
77
|
+
return `Debug round ${round} declared runtime_probe but no probe reached the code, so there is nothing to reproduce yet. Reply to the agent to get it instrumented.`;
|
|
78
|
+
}
|
|
79
|
+
if (reason === "unclosed") {
|
|
80
|
+
return `Debug round ${round} stopped without a complete evidence plan and reproduction steps. Reply to the agent, or run /debug-proceed to close it with whatever evidence exists.`;
|
|
46
81
|
}
|
|
47
|
-
|
|
48
|
-
if (args.nudgesUsed < MAX_GATE_NUDGES) return { kind: "nudge", context: GATE_NUDGE };
|
|
49
|
-
return {
|
|
50
|
-
kind: "gate",
|
|
51
|
-
missingSteps: !args.hasReproductionSteps,
|
|
52
|
-
missingEvidencePlan: !args.hasEvidencePlan,
|
|
53
|
-
};
|
|
82
|
+
return `Debug round ${round} is still open — the agent is waiting on your reply, not on a reproduction.`;
|
|
54
83
|
}
|
package/src/machine.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { parseEvidencePlan } from "./evidence";
|
|
2
|
+
import { decideGate, describeOpenReason } 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 Probe,
|
|
16
|
+
type Round,
|
|
17
|
+
INACTIVE,
|
|
18
|
+
allRequests,
|
|
19
|
+
blankRound,
|
|
20
|
+
currentRound,
|
|
21
|
+
declaresRuntimeProbe,
|
|
22
|
+
evidenceSummary,
|
|
23
|
+
freshSession,
|
|
24
|
+
liveProbeIds,
|
|
25
|
+
pendingRequests,
|
|
26
|
+
} from "./state";
|
|
27
|
+
|
|
28
|
+
export const PROMPT_START = "debug-mode-start";
|
|
29
|
+
export const PROMPT_PROCEED = "debug-mode-proceed";
|
|
30
|
+
export const PROMPT_FIXED = "debug-mode-fixed";
|
|
31
|
+
|
|
32
|
+
/** Artifact metadata gathered by the shell; the reducer only files it. */
|
|
33
|
+
export type ArtifactCandidate = Omit<EvidenceArtifact, "id" | "addedAt" | "requestId">;
|
|
34
|
+
|
|
35
|
+
export type DebugEvent =
|
|
36
|
+
| { t: "start"; problem: string; debugDir: string; runId: string; logFile: string }
|
|
37
|
+
| { t: "turn_started" }
|
|
38
|
+
| { t: "assistant_message"; text: string }
|
|
39
|
+
| { t: "probes_found"; probes: Probe[] }
|
|
40
|
+
| { t: "ledger_synced"; probes: Probe[] }
|
|
41
|
+
| { t: "runs_observed"; runHistory: string[]; logCounts: Record<string, number> }
|
|
42
|
+
| { t: "turn_settled" }
|
|
43
|
+
| { t: "proceed"; runId: string; logCount: number; hypotheses: string; details?: string; now: number }
|
|
44
|
+
| { t: "mark_fixed" }
|
|
45
|
+
| { t: "attach_artifact"; candidate: ArtifactCandidate; requestId: string | null; now: number }
|
|
46
|
+
| { t: "abort" };
|
|
47
|
+
|
|
48
|
+
export type Effect =
|
|
49
|
+
| { kind: "notify"; level: "info" | "warning" | "error"; text: string }
|
|
50
|
+
/** Inject a prompt and start an agent turn. */
|
|
51
|
+
| { kind: "prompt"; customType: string; content: string; summary: string }
|
|
52
|
+
/** Keep the current turn going instead of settling it. */
|
|
53
|
+
| { kind: "continue"; context: string }
|
|
54
|
+
| { kind: "teardown"; outcome: "finished" | "aborted"; probesLeft: Probe[]; debugDir: string | null };
|
|
55
|
+
|
|
56
|
+
export interface Transition {
|
|
57
|
+
state: DebugState;
|
|
58
|
+
effects: Effect[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A narrowed active state. Kept as one type so no-ops can return it verbatim. */
|
|
62
|
+
type ActiveState = { active: true } & DebugSession;
|
|
63
|
+
|
|
64
|
+
function unchanged(state: DebugState): Transition {
|
|
65
|
+
return { state, effects: [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function withRound(session: ActiveState, round: Round): ActiveState {
|
|
69
|
+
return { ...session, rounds: [...session.rounds.slice(0, -1), round] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The whole state machine. Pure and total: every transition is a function of
|
|
74
|
+
* the previous state plus one event, and anything that touches the filesystem,
|
|
75
|
+
* the host UI or the model is returned as an effect for the shell to apply.
|
|
76
|
+
*/
|
|
77
|
+
export function reduce(state: DebugState, event: DebugEvent): Transition {
|
|
78
|
+
if (event.t === "start") {
|
|
79
|
+
if (state.active) return unchanged(state);
|
|
80
|
+
return {
|
|
81
|
+
state: { active: true, ...freshSession(event.problem, event.debugDir, event.runId) },
|
|
82
|
+
effects: [
|
|
83
|
+
{
|
|
84
|
+
kind: "prompt",
|
|
85
|
+
customType: PROMPT_START,
|
|
86
|
+
content: buildStartMessage(event.problem, event.logFile),
|
|
87
|
+
summary: "debug mode started — hypotheses and instrumentation",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!state.active) return unchanged(state);
|
|
93
|
+
const session: ActiveState = state;
|
|
94
|
+
|
|
95
|
+
switch (event.t) {
|
|
96
|
+
case "turn_started":
|
|
97
|
+
// A reply to an open round hands the turn back to the agent. The
|
|
98
|
+
// reproduction gate deliberately survives ordinary conversation.
|
|
99
|
+
if (session.stage !== "open" && !session.turnProduced) return unchanged(session);
|
|
100
|
+
return unchanged({
|
|
101
|
+
...session,
|
|
102
|
+
stage: session.stage === "open" ? "investigating" : session.stage,
|
|
103
|
+
turnProduced: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
case "assistant_message":
|
|
107
|
+
return unchanged(absorbAssistantText(session, event.text));
|
|
108
|
+
|
|
109
|
+
case "probes_found": {
|
|
110
|
+
const fresh = event.probes.filter(probe => !session.probes.some(known => known.id === probe.id));
|
|
111
|
+
if (fresh.length === 0) return unchanged(session);
|
|
112
|
+
const probes = [...session.probes, ...fresh];
|
|
113
|
+
if (session.stage !== "investigating") return unchanged({ ...session, probes });
|
|
114
|
+
const round = currentRound(session);
|
|
115
|
+
return unchanged(
|
|
116
|
+
withRound({ ...session, probes }, { ...round, probeIds: [...round.probeIds, ...fresh.map(p => p.id)] }),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case "ledger_synced": {
|
|
121
|
+
const same =
|
|
122
|
+
event.probes.length === session.probes.length &&
|
|
123
|
+
event.probes.every((probe, i) => probe.id === session.probes[i]?.id);
|
|
124
|
+
return unchanged(same ? session : { ...session, probes: event.probes });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case "runs_observed":
|
|
128
|
+
return unchanged({ ...session, runHistory: event.runHistory, logCounts: event.logCounts });
|
|
129
|
+
|
|
130
|
+
case "turn_settled":
|
|
131
|
+
return settle(session);
|
|
132
|
+
|
|
133
|
+
case "proceed":
|
|
134
|
+
return advance(session, event);
|
|
135
|
+
|
|
136
|
+
case "mark_fixed":
|
|
137
|
+
return startCleanup(session);
|
|
138
|
+
|
|
139
|
+
case "attach_artifact":
|
|
140
|
+
return attach(session, event);
|
|
141
|
+
|
|
142
|
+
case "abort":
|
|
143
|
+
return {
|
|
144
|
+
state: INACTIVE,
|
|
145
|
+
effects: [
|
|
146
|
+
{ kind: "teardown", outcome: "aborted", probesLeft: session.probes, debugDir: session.debugDir },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Record the round artefacts a settled-but-not-yet-judged turn produced. */
|
|
153
|
+
function absorbAssistantText(session: ActiveState, text: string): ActiveState {
|
|
154
|
+
const round = currentRound(session);
|
|
155
|
+
const steps = session.stage === "investigating" ? extractReproductionSteps(text) : [];
|
|
156
|
+
const plan = session.stage === "investigating" ? parseEvidencePlan(text) : { found: false, valid: false, requests: [] };
|
|
157
|
+
const next: Round = {
|
|
158
|
+
...round,
|
|
159
|
+
reproductionSteps: steps.length > 0 ? steps : round.reproductionSteps,
|
|
160
|
+
plan: plan.found ? (plan.valid ? plan.requests : null) : round.plan,
|
|
161
|
+
};
|
|
162
|
+
const roundChanged = next.reproductionSteps !== round.reproductionSteps || next.plan !== round.plan;
|
|
163
|
+
if (!roundChanged) return session.turnProduced ? session : { ...session, turnProduced: true };
|
|
164
|
+
return withRound({ ...session, turnProduced: true }, next);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function settle(session: ActiveState): Transition {
|
|
168
|
+
if (session.stage === "cleaning_up") return finishCleanup(session);
|
|
169
|
+
if (session.stage !== "investigating" || !session.turnProduced) return unchanged(session);
|
|
170
|
+
|
|
171
|
+
const round = currentRound(session);
|
|
172
|
+
const decision = decideGate({
|
|
173
|
+
hasReproductionSteps: round.reproductionSteps.length > 0,
|
|
174
|
+
hasEvidencePlan: round.plan !== null,
|
|
175
|
+
declaresRuntimeProbe: declaresRuntimeProbe(round),
|
|
176
|
+
liveProbes: liveProbeIds(session, round).length,
|
|
177
|
+
nudges: round.nudges,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (decision.kind === "nudge") {
|
|
181
|
+
const nudges =
|
|
182
|
+
decision.budget === "probes"
|
|
183
|
+
? { ...round.nudges, probes: round.nudges.probes + 1 }
|
|
184
|
+
: { ...round.nudges, tags: round.nudges.tags + 1 };
|
|
185
|
+
return {
|
|
186
|
+
state: withRound(session, { ...round, nudges }),
|
|
187
|
+
effects: [{ kind: "continue", context: decision.context }],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (decision.kind === "open") {
|
|
192
|
+
return {
|
|
193
|
+
state: withRound({ ...session, stage: "open" }, { ...round, openReason: decision.reason }),
|
|
194
|
+
effects: [
|
|
195
|
+
{
|
|
196
|
+
kind: "notify",
|
|
197
|
+
level: decision.reason === "awaiting_reply" ? "info" : "warning",
|
|
198
|
+
text: describeOpenReason(decision.reason, round.index),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const gated = withRound({ ...session, stage: "awaiting_evidence" }, { ...round, openReason: null });
|
|
205
|
+
return { state: gated, effects: [{ kind: "notify", ...gateNotice(gated, decision.missingPlan) }] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function gateNotice(session: ActiveState, missingPlan: boolean): { level: "info" | "warning"; text: string } {
|
|
209
|
+
const round = currentRound(session);
|
|
210
|
+
if (missingPlan) {
|
|
211
|
+
return {
|
|
212
|
+
level: "warning",
|
|
213
|
+
text: `Debug round ${round.index} paused, but it added no probes and declared no evidence plan — this round cannot produce runtime evidence. Use /debug-proceed to ask for instrumentation.`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const pending = pendingRequests(session, round);
|
|
217
|
+
if (pending.length > 0) {
|
|
218
|
+
return {
|
|
219
|
+
level: "info",
|
|
220
|
+
text: `Debug round ${round.index} paused. User evidence requested (${pending.length} pending: ${pending.map(r => r.id).join(", ")}) — attach via /debug-evidence <request-id> <path>, then press Proceed.`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { level: "info", text: `Debug round ${round.index} paused. Reproduce the bug, then press Proceed.` };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Cleanup may only finish once the ledger is actually empty. */
|
|
227
|
+
function finishCleanup(session: ActiveState): Transition {
|
|
228
|
+
if (!session.turnProduced) return unchanged(session);
|
|
229
|
+
if (session.probes.length > 0 && session.cleanupNudges < 1) {
|
|
230
|
+
return {
|
|
231
|
+
state: { ...session, cleanupNudges: session.cleanupNudges + 1 },
|
|
232
|
+
effects: [{ kind: "continue", context: `${CLEANUP_NUDGE}\nRemaining probes: ${JSON.stringify(session.probes)}` }],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
state: INACTIVE,
|
|
237
|
+
effects: [{ kind: "teardown", outcome: "finished", probesLeft: session.probes, debugDir: session.debugDir }],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function advance(session: ActiveState, event: Extract<DebugEvent, { t: "proceed" }>): Transition {
|
|
242
|
+
const closing = currentRound(session);
|
|
243
|
+
const details = event.details?.trim() ?? "";
|
|
244
|
+
let withObservation: ActiveState = session;
|
|
245
|
+
if (details.length > 0) {
|
|
246
|
+
const reportIds = (closing.plan ?? [])
|
|
247
|
+
.filter(request => request.method === "user_report")
|
|
248
|
+
.map(request => request.id);
|
|
249
|
+
const observation: EvidenceObservation = {
|
|
250
|
+
id: `observation-${event.now.toString(36)}`,
|
|
251
|
+
requestIds: reportIds,
|
|
252
|
+
text: details,
|
|
253
|
+
round: closing.index,
|
|
254
|
+
addedAt: event.now,
|
|
255
|
+
};
|
|
256
|
+
withObservation = { ...session, observations: [...session.observations, observation] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const summary = evidenceSummary(withObservation, closing);
|
|
260
|
+
const next: ActiveState = {
|
|
261
|
+
...withObservation,
|
|
262
|
+
stage: "investigating",
|
|
263
|
+
turnProduced: false,
|
|
264
|
+
rounds: [...withObservation.rounds, blankRound(closing.index + 1, event.runId)],
|
|
265
|
+
runHistory: [...withObservation.runHistory, event.runId],
|
|
266
|
+
logCounts: { ...withObservation.logCounts, [event.runId]: 0 },
|
|
267
|
+
};
|
|
268
|
+
const label = details.length > 0 ? "proceed with user details" : "proceed";
|
|
269
|
+
return {
|
|
270
|
+
state: next,
|
|
271
|
+
effects: [
|
|
272
|
+
{
|
|
273
|
+
kind: "prompt",
|
|
274
|
+
customType: PROMPT_PROCEED,
|
|
275
|
+
content: buildProceedMessage({
|
|
276
|
+
run: closing.runId ?? "(none)",
|
|
277
|
+
logCount: event.logCount,
|
|
278
|
+
userDetails: details.length > 0 ? details : undefined,
|
|
279
|
+
hypotheses: event.hypotheses,
|
|
280
|
+
evidenceSummary: summary,
|
|
281
|
+
}),
|
|
282
|
+
summary: `${label} — analyzing run ${closing.runId ?? "(none)"} (${event.logCount} entries)`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function startCleanup(session: ActiveState): Transition {
|
|
289
|
+
const next: ActiveState = { ...session, stage: "cleaning_up", turnProduced: false, cleanupNudges: 0 };
|
|
290
|
+
const evidenceJson = JSON.stringify({
|
|
291
|
+
requests: allRequests(session),
|
|
292
|
+
observations: session.observations,
|
|
293
|
+
artifacts: session.artifacts,
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
state: next,
|
|
297
|
+
effects: [
|
|
298
|
+
{
|
|
299
|
+
kind: "prompt",
|
|
300
|
+
customType: PROMPT_FIXED,
|
|
301
|
+
content: buildFixedMessage(JSON.stringify(session.probes), evidenceJson),
|
|
302
|
+
summary: `marked fixed — removing ${session.probes.length} probe(s)`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function attach(session: ActiveState, event: Extract<DebugEvent, { t: "attach_artifact" }>): Transition {
|
|
309
|
+
if (event.requestId !== null && !allRequests(session).some(request => request.id === event.requestId)) {
|
|
310
|
+
return {
|
|
311
|
+
state: session,
|
|
312
|
+
effects: [
|
|
313
|
+
{ kind: "notify", level: "error", text: `debug-mode: unknown evidence request id ${event.requestId}` },
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const existing = session.artifacts.find(artifact => artifact.path === event.candidate.path);
|
|
318
|
+
const artifact: EvidenceArtifact = existing
|
|
319
|
+
? { ...existing, ...event.candidate, requestId: event.requestId }
|
|
320
|
+
: { id: `artifact-${event.now.toString(36)}`, requestId: event.requestId, ...event.candidate, addedAt: event.now };
|
|
321
|
+
const artifacts = existing
|
|
322
|
+
? session.artifacts.map(a => (a.path === artifact.path ? artifact : a))
|
|
323
|
+
: [...session.artifacts, artifact];
|
|
324
|
+
return {
|
|
325
|
+
state: { ...session, artifacts },
|
|
326
|
+
effects: [
|
|
327
|
+
{
|
|
328
|
+
kind: "notify",
|
|
329
|
+
level: "info",
|
|
330
|
+
text: `debug-mode: attached ${artifact.id} → ${artifact.path} (${artifact.size} bytes)`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
package/src/methodology.ts
CHANGED
|
@@ -12,6 +12,18 @@ export const MINIMIZE_USER_INTERVENTION =
|
|
|
12
12
|
"NEVER ask the user to run a command you can run yourself. Batch all unavoidable user actions into the fewest reproductions/captures. " +
|
|
13
13
|
"Do not choose a lower-priority method merely because it is familiar.";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Closing-tag order repeated in the methodology, start prompt, and proceed prompt.
|
|
17
|
+
* Probe edits must land before the round may close; reproduction_steps are user-only.
|
|
18
|
+
*/
|
|
19
|
+
export const CLOSE_ROUND_RULES =
|
|
20
|
+
"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. " +
|
|
21
|
+
`Do not emit <${EVIDENCE_PLAN_TAG}> or <reproduction_steps> for a runtime_probe round until those probes are already on disk. ` +
|
|
22
|
+
"A runtime_probe plan without @omp-probe markers is an incomplete round: keep going and instrument first. " +
|
|
23
|
+
"<reproduction_steps> lists only actions the user performs now (reproduce, capture, restart). " +
|
|
24
|
+
"Never include future agent work such as installing probes, reading logs, or analyzing results. " +
|
|
25
|
+
"agent_inspection, user_report, and user_artifact rounds may close without adding probes.";
|
|
26
|
+
|
|
15
27
|
export const METHODOLOGY = `\
|
|
16
28
|
[DEBUG MODE METHODOLOGY — follow strictly]
|
|
17
29
|
This is OMP Debug Mode. Follow the steps in order. Do not skip them.
|
|
@@ -27,8 +39,9 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
|
|
|
27
39
|
|
|
28
40
|
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
41
|
|
|
30
|
-
3. For runtime_probe rounds, instrument code with probes that test
|
|
31
|
-
hypotheses in parallel.
|
|
42
|
+
3. For runtime_probe rounds, instrument code in THIS turn with probes that test
|
|
43
|
+
ALL remaining hypotheses in parallel. ${CLOSE_ROUND_RULES}
|
|
44
|
+
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
45
|
|
|
33
46
|
Probe rules:
|
|
34
47
|
- Wrap EACH probe in a collapsible region (\`// #region agent log\` /
|
|
@@ -47,7 +60,8 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
|
|
|
47
60
|
- The extension truncates the current log file at the start of each round.
|
|
48
61
|
Do not delete, rename, or overwrite that file yourself.
|
|
49
62
|
|
|
50
|
-
4. Close the round
|
|
63
|
+
4. Close the round only after step 3 is done for any runtime_probe plan. Emit
|
|
64
|
+
exactly one <${EVIDENCE_PLAN_TAG}> block containing a
|
|
51
65
|
non-empty JSON array covering EVERY hypothesis:
|
|
52
66
|
<${EVIDENCE_PLAN_TAG}>
|
|
53
67
|
[{"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"}]
|
|
@@ -58,8 +72,9 @@ This is OMP Debug Mode. Follow the steps in order. Do not skip them.
|
|
|
58
72
|
|
|
59
73
|
5. Ask the user to reproduce (or capture/report, per the plan). End your response
|
|
60
74
|
with a <reproduction_steps> numbered list (no header inside the tag) describing
|
|
61
|
-
the single combined reproduction/capture sequence
|
|
62
|
-
after the tag: "${PROCEED_REMINDER}"
|
|
75
|
+
the single combined reproduction/capture sequence the USER performs now, and
|
|
76
|
+
this exact sentence after the tag: "${PROCEED_REMINDER}"
|
|
77
|
+
${CLOSE_ROUND_RULES}
|
|
63
78
|
Never say "click". Never ask the user to reply "done". Remind them to restart
|
|
64
79
|
the app or service if the instrumented code would otherwise be stale.
|
|
65
80
|
Then STOP. The user reproduces out-of-band.
|
|
@@ -104,9 +119,11 @@ export function buildStartMessage(problem: string, logFile: string): string {
|
|
|
104
119
|
MINIMIZE_USER_INTERVENTION +
|
|
105
120
|
"\n\n" +
|
|
106
121
|
"Begin round 1: generate 3-5 precise hypotheses, decide an evidence method for each, and do NOT apply a product fix yet. " +
|
|
122
|
+
CLOSE_ROUND_RULES +
|
|
123
|
+
" " +
|
|
107
124
|
"Call list_debug_evidence whenever you need the request/observation/artifact ledger. " +
|
|
108
125
|
"Cite evidence as hypothesis ID plus log-line number or attached observation/artifact/report path. " +
|
|
109
|
-
`
|
|
126
|
+
`Then close with exactly one <${EVIDENCE_PLAN_TAG}> JSON block, then <reproduction_steps> and "${PROCEED_REMINDER}" then STOP.`
|
|
110
127
|
);
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -137,8 +154,11 @@ export function buildProceedMessage(args: {
|
|
|
137
154
|
"Call list_debug_evidence first, then read the previous run with get_debug_logs (previous=true). " +
|
|
138
155
|
"Evaluate each hypothesis CONFIRMED/REJECTED/INCONCLUSIVE citing hypothesis ID plus log-line number or attached observation/artifact/report path. " +
|
|
139
156
|
"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
|
-
|
|
157
|
+
"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. " +
|
|
158
|
+
"If a previous fix failed, first revert code changes from rejected hypotheses. " +
|
|
159
|
+
CLOSE_ROUND_RULES +
|
|
160
|
+
" " +
|
|
161
|
+
`Then end with <${EVIDENCE_PLAN_TAG}>, <reproduction_steps> and "${PROCEED_REMINDER}" then STOP.`
|
|
142
162
|
);
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -156,6 +176,12 @@ The user confirmed the fix. Only two things remain:
|
|
|
156
176
|
2. Summarize in 1-2 lines: the root cause and the fix that is staying.
|
|
157
177
|
Do not add probes, form new hypotheses, or ask for another reproduction.`;
|
|
158
178
|
|
|
179
|
+
/** Sent back into a cleanup turn that ended with probes still in the code. */
|
|
180
|
+
export const CLEANUP_NUDGE =
|
|
181
|
+
"Cleanup is not finished: the probe ledger below is still not empty. Remove each remaining `@omp-probe` marker " +
|
|
182
|
+
"and its `#region agent log` wrapper from the code, keep the proven fix, then call list_debug_probes to confirm " +
|
|
183
|
+
"the ledger is empty before summarizing.";
|
|
184
|
+
|
|
159
185
|
export function buildFixedMessage(probesJson: string, evidenceJson = "[]"): string {
|
|
160
186
|
return (
|
|
161
187
|
"User marked the problem FIXED.\n" +
|
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
|
-
/**
|
|
27
|
-
export function
|
|
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 (!
|
|
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
|
-
*
|
|
77
|
-
*
|
|
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
|
|
80
|
-
|
|
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 {
|