pi-workflow-engine 0.12.0 → 0.13.0
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/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +16 -2
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +12 -6
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +53 -3
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -25
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/types.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -2
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +131 -178
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +55 -128
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +45 -139
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +25 -67
- package/USAGE.md +59 -0
- package/package.json +1 -1
|
@@ -1,26 +1,21 @@
|
|
|
1
|
+
import { challengeFindings, parseChallengeArgs } from "../src/advisory-challenge.ts";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AdvisoryVerdict,
|
|
8
|
-
} from "../src/advisory-schema.ts";
|
|
9
|
-
import {
|
|
10
|
-
backfillAdvisoryFindings,
|
|
4
|
+
type AdvisoryVerified,
|
|
5
|
+
type AdvisoryLens,
|
|
6
|
+
synthesizeAdvisoryReport,
|
|
7
|
+
finishAdvisoryReport,
|
|
11
8
|
formatEvidence,
|
|
12
9
|
formatLocation,
|
|
13
10
|
normalizePath,
|
|
14
|
-
primaryLocation,
|
|
15
11
|
publishVerifiedKeptProgress,
|
|
16
|
-
|
|
12
|
+
runLensVerificationPipeline,
|
|
17
13
|
verdictConfidence,
|
|
18
14
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
19
15
|
DEFAULT_ADVISORY_TOOLS,
|
|
20
16
|
} from "../src/workflow-advisory-utils.ts";
|
|
21
|
-
import { compactResults } from "../src/concurrency.ts";
|
|
22
17
|
import { formatReviewDiffTarget, parseAllowedDiffCommand } from "../src/review-diff-target.ts";
|
|
23
|
-
import { buildCodeReviewScopeBlock
|
|
18
|
+
import { buildCodeReviewScopeBlock } from "../src/review/code-review-orchestration.ts";
|
|
24
19
|
import type { ReviewContext } from "../src/review/review-report.ts";
|
|
25
20
|
import { captureReviewMaterial, type ReviewMaterialCaptureResult } from "../src/review/review-snapshot.ts";
|
|
26
21
|
import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
|
|
@@ -28,7 +23,7 @@ import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.t
|
|
|
28
23
|
export const meta: WorkflowMeta = {
|
|
29
24
|
name: "code-review",
|
|
30
25
|
description: "Fan-out review of the branch's open PR (or branch vs main): scope → per-angle find → independent verify → synthesize.",
|
|
31
|
-
phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Synthesize" }],
|
|
26
|
+
phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
32
27
|
};
|
|
33
28
|
|
|
34
29
|
// ─── Schemas (the contracts that make orchestration plain code) ───
|
|
@@ -39,30 +34,15 @@ const ScopeSchema = Type.Object({
|
|
|
39
34
|
conventions: Type.Optional(Type.String({ description: "Relevant AGENTS.md / project conventions" })),
|
|
40
35
|
});
|
|
41
36
|
|
|
42
|
-
type Candidate = AdvisoryCandidate;
|
|
43
|
-
|
|
44
|
-
interface Angle {
|
|
45
|
-
label: string;
|
|
46
|
-
kind: "bug" | "cleanup";
|
|
47
|
-
text: string;
|
|
48
|
-
}
|
|
49
|
-
interface Verified extends Candidate {
|
|
50
|
-
verdict: AdvisoryVerdict["verdict"];
|
|
51
|
-
evidence: string[];
|
|
52
|
-
kind: "bug" | "cleanup";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
37
|
// The review lenses — this is the part you customise to your codebase's real failure modes.
|
|
56
|
-
const ANGLES:
|
|
57
|
-
{ label: "logic-bugs",
|
|
58
|
-
{ label: "error-paths",
|
|
59
|
-
{ label: "edge-cases",
|
|
60
|
-
{ label: "simplification",
|
|
61
|
-
{ label: "conventions",
|
|
38
|
+
const ANGLES: AdvisoryLens[] = [
|
|
39
|
+
{ label: "logic-bugs", category: "bug", text: "Off-by-one errors, wrong conditionals, incorrect return values, broken control flow." },
|
|
40
|
+
{ label: "error-paths", category: "bug", text: "Unhandled errors, swallowed exceptions, missing awaits, partial failure leaving inconsistent state." },
|
|
41
|
+
{ label: "edge-cases", category: "bug", text: "Empty/null inputs, boundary values, concurrency races, resource leaks." },
|
|
42
|
+
{ label: "simplification", category: "cleanup", text: "Dead code, needless complexity, duplicated logic, clearer equivalents." },
|
|
43
|
+
{ label: "conventions", category: "cleanup", text: "Violations of the project conventions noted in scope (naming, idioms, banned patterns)." },
|
|
62
44
|
];
|
|
63
45
|
|
|
64
|
-
const TOOLS = DEFAULT_ADVISORY_TOOLS;
|
|
65
|
-
const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
|
|
66
46
|
const PER_ANGLE = 6;
|
|
67
47
|
|
|
68
48
|
/** Parse a unified diff into the set of added/changed new-file line numbers per file. */
|
|
@@ -106,8 +86,9 @@ export interface CodeReviewDependencies {
|
|
|
106
86
|
}
|
|
107
87
|
|
|
108
88
|
export default async function run(api: WorkflowApi, dependencies: CodeReviewDependencies = {}): Promise<unknown> {
|
|
109
|
-
const { agent,
|
|
110
|
-
const
|
|
89
|
+
const { agent, phase, log, progress, args, cwd, signal } = api;
|
|
90
|
+
const challengeConfig = parseChallengeArgs(args);
|
|
91
|
+
const target = challengeConfig.args.trim();
|
|
111
92
|
let fileCount = 0;
|
|
112
93
|
let rawCandidateCount = 0;
|
|
113
94
|
let droppedCandidateCount = 0;
|
|
@@ -138,7 +119,7 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
|
|
|
138
119
|
"Then: list the changed files, summarize the change in one paragraph (mention the PR if one was found), " +
|
|
139
120
|
"and read any relevant AGENTS.md or project docs noting conventions a reviewer should know.\n" +
|
|
140
121
|
"Return diffCommand exactly as a reviewer should run it. Structured output only.",
|
|
141
|
-
{ phase: "Scope", label: "scope", tools:
|
|
122
|
+
{ phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
142
123
|
);
|
|
143
124
|
|
|
144
125
|
if (!scope) {
|
|
@@ -184,7 +165,7 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
|
|
|
184
165
|
: {}),
|
|
185
166
|
};
|
|
186
167
|
|
|
187
|
-
const scopeBlock = buildCodeReviewScopeBlock({
|
|
168
|
+
const scopeBlock = `Reviewed snapshot identity: ${JSON.stringify(reviewContext.snapshot ?? "unavailable")}\n` + buildCodeReviewScopeBlock({
|
|
188
169
|
diffCommand,
|
|
189
170
|
files: scope.files,
|
|
190
171
|
summary: scope.summary,
|
|
@@ -193,113 +174,59 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
|
|
|
193
174
|
target,
|
|
194
175
|
});
|
|
195
176
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
title: candidate.summary,
|
|
225
|
-
subtitle: formatLocation(candidate),
|
|
226
|
-
status: "pending",
|
|
227
|
-
details: candidate.impact,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
return { angle, candidates: bounded };
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// Dedup after all finders complete so verifier agents cannot consume the global cap before full candidate discovery.
|
|
235
|
-
const novel = dedupeCodeReviewCandidates(compactResults(perAngle));
|
|
236
|
-
|
|
237
|
-
phase("Verify");
|
|
238
|
-
const verdicts = await parallel(
|
|
239
|
-
novel.map(({ angle, candidate }) => async (): Promise<Verified | null> => {
|
|
240
|
-
const location = primaryLocation(candidate);
|
|
241
|
-
const judged = await agent(
|
|
242
|
-
`## Code-review verifier\n\n${scopeBlock}\n## Candidate\n` +
|
|
243
|
-
`Location: ${formatLocation(candidate)}\n` +
|
|
244
|
-
`Category: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n\n` +
|
|
245
|
-
"Run the diff command, read the relevant file(s), and return exactly one verdict (CONFIRMED / PLAUSIBLE / REFUTED) " +
|
|
246
|
-
"with evidence quoting the line(s). Default toward REFUTED if you cannot substantiate it. Structured output only.",
|
|
247
|
-
{
|
|
248
|
-
phase: "Verify",
|
|
249
|
-
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
250
|
-
tools: TOOLS,
|
|
251
|
-
toolHints: TOOL_HINTS,
|
|
252
|
-
profile: "small",
|
|
253
|
-
schema: AdvisoryVerdictSchema,
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
if (!judged) return null;
|
|
257
|
-
recordVerdictProgress(progress, candidate, judged);
|
|
258
|
-
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, kind: angle.kind };
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const verified = compactResults(verdicts);
|
|
177
|
+
const pipelineResult = await runLensVerificationPipeline({
|
|
178
|
+
api,
|
|
179
|
+
lenses: ANGLES,
|
|
180
|
+
perLens: PER_ANGLE,
|
|
181
|
+
boundCandidate: (candidate, lens) => {
|
|
182
|
+
const anchor = candidate.reviewAnchor ?? candidate.locations.find((location) => inDiff(changed, location.file, location.line));
|
|
183
|
+
return anchor && inDiff(changed, anchor.file, anchor.line)
|
|
184
|
+
? { ...candidate, category: lens.category, reviewAnchor: anchor } : undefined;
|
|
185
|
+
},
|
|
186
|
+
finderPrompt: (lens) =>
|
|
187
|
+
`## Code-review finder — ${lens.label}\n\n${scopeBlock}\n` +
|
|
188
|
+
`Review the change through ONLY this lens:\n${lens.text}\n` +
|
|
189
|
+
"Only flag issues on lines that are part of the diff above (run the diff command if it is not shown). " +
|
|
190
|
+
"Set reviewAnchor to the changed line causing the issue; locations and discoveryEvidence may include unchanged callers and other files. " +
|
|
191
|
+
`Surface up to ${PER_ANGLE} candidates. Use category exactly "${lens.category}". Each candidate must include a one-line summary, ` +
|
|
192
|
+
"locations with the changed file and a line that appears in the diff, and impact describing the concrete failure or maintenance scenario. " +
|
|
193
|
+
"Pass through anything with a nameable impact — a separate verifier judges them next. Structured output only.",
|
|
194
|
+
verifierPrompt: (candidate) =>
|
|
195
|
+
`## Code-review verifier\n\n${scopeBlock}\n## Candidate\n` +
|
|
196
|
+
`Location: ${formatLocation(candidate)}\n` +
|
|
197
|
+
`Category: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n\n` +
|
|
198
|
+
"Run the diff command, read the relevant file(s), and return exactly one verdict (CONFIRMED / PLAUSIBLE / NOT_SUBSTANTIATED / REFUTED) " +
|
|
199
|
+
"with evidence quoting the line(s). Use NOT_SUBSTANTIATED when evidence is insufficient; use REFUTED only for concrete disproof. Structured output only.",
|
|
200
|
+
});
|
|
201
|
+
rawCandidateCount += pipelineResult.rawCandidates;
|
|
202
|
+
droppedCandidateCount += pipelineResult.dropped;
|
|
203
|
+
const { coverage } = pipelineResult;
|
|
204
|
+
const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, coverage);
|
|
263
205
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
264
206
|
const stats = makeStats(verified.length, surviving.length);
|
|
265
207
|
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
266
208
|
|
|
267
209
|
if (surviving.length === 0) {
|
|
268
|
-
return { summary: "No findings survived verification.", findings: [], nextSteps: ["No code-review action is recommended from this workflow run."], stats, reviewContext };
|
|
210
|
+
return finishAdvisoryReport({ summary: "No findings survived verification.", findings: [], nextSteps: ["No code-review action is recommended from this workflow run."], stats, reviewContext }, coverage, verified);
|
|
269
211
|
}
|
|
270
212
|
|
|
271
213
|
// ─── Synthesize: rank, merge, report ───
|
|
272
|
-
|
|
273
|
-
const rank = (finding: Verified): number => (finding.kind === "cleanup" ? 2 : 0) + (finding.verdict === "PLAUSIBLE" ? 1 : 0);
|
|
214
|
+
const rank = (finding: AdvisoryVerified): number => (finding.category === "cleanup" ? 2 : 0) + (finding.verdict !== "CONFIRMED" ? 1 : 0);
|
|
274
215
|
const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
|
|
275
216
|
const block = ranked
|
|
276
217
|
.map(
|
|
277
218
|
(finding, index) =>
|
|
278
|
-
`### [${index}] ${formatLocation(finding)} (${finding.verdict}${finding.
|
|
279
|
-
`Category: ${finding.
|
|
219
|
+
`### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}${finding.category === "cleanup" ? ", cleanup" : ""})\n` +
|
|
220
|
+
`Category: ${finding.category}\nConfidence: ${verdictConfidence(finding.verdict)}\n` +
|
|
280
221
|
`${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}`,
|
|
281
222
|
)
|
|
282
223
|
.join("\n\n");
|
|
283
224
|
|
|
284
|
-
const
|
|
225
|
+
const resolved = await synthesizeAdvisoryReport(api,
|
|
285
226
|
`## Synthesis: final code-review report\n\n${ranked.length} findings survived independent verification.\n\n${block}\n\n` +
|
|
286
227
|
"Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final advisory report. " +
|
|
287
|
-
"Return summary,
|
|
288
|
-
|
|
289
|
-
{
|
|
290
|
-
phase: "Synthesize",
|
|
291
|
-
label: "synthesize",
|
|
292
|
-
tools: [],
|
|
293
|
-
profile: "medium",
|
|
294
|
-
resume: "read-only",
|
|
295
|
-
schema: AdvisoryReportSchema,
|
|
296
|
-
},
|
|
228
|
+
"Return summary, ID selections with severity (low/medium/high) and advisory recommendation, and nextSteps. Evidence and confidence are reconstructed from verified records. Structured output only.",
|
|
229
|
+
ranked, coverage,
|
|
297
230
|
);
|
|
298
|
-
|
|
299
|
-
if (!report) return { summary: "Synthesis produced no output.", findings: [], nextSteps: ["Re-run the workflow or inspect verifier evidence manually."], stats, reviewContext };
|
|
300
|
-
|
|
301
|
-
const findings = backfillAdvisoryFindings(report.findings, ranked, {
|
|
302
|
-
impact: "Impact not restated by synthesis.",
|
|
303
|
-
});
|
|
304
|
-
return { ...report, findings, stats, reviewContext };
|
|
231
|
+
return finishAdvisoryReport({ ...resolved, stats: { ...stats, kept: resolved.findings.length }, reviewContext }, coverage, verified);
|
|
305
232
|
}
|
|
@@ -1,30 +1,24 @@
|
|
|
1
|
+
import { challengeFindings, parseChallengeArgs } from "../src/advisory-challenge.ts";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AdvisoryVerdict,
|
|
8
|
-
} from "../src/advisory-schema.ts";
|
|
9
|
-
import {
|
|
10
|
-
advisoryDedupKey as dedupKey,
|
|
11
|
-
backfillAdvisoryFindings,
|
|
4
|
+
type AdvisoryVerified,
|
|
5
|
+
type AdvisoryLens,
|
|
6
|
+
synthesizeAdvisoryReport,
|
|
7
|
+
finishAdvisoryReport,
|
|
12
8
|
emptyAdvisoryReport,
|
|
13
9
|
formatEvidence,
|
|
14
10
|
formatLocation,
|
|
15
|
-
primaryLocation,
|
|
16
11
|
publishVerifiedKeptProgress,
|
|
17
|
-
|
|
12
|
+
runLensVerificationPipeline,
|
|
18
13
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
19
14
|
DEFAULT_ADVISORY_TOOLS,
|
|
20
15
|
} from "../src/workflow-advisory-utils.ts";
|
|
21
|
-
import { compactResults } from "../src/concurrency.ts";
|
|
22
16
|
import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
|
|
23
17
|
|
|
24
18
|
export const meta: WorkflowMeta = {
|
|
25
19
|
name: "diagnose",
|
|
26
20
|
description: "Advisory-only bug diagnosis: scope symptoms → competing hypotheses → independent verify → synthesize likely root causes.",
|
|
27
|
-
phases: [{ title: "Scope" }, { title: "Hypothesize" }, { title: "Verify" }, { title: "Synthesize" }],
|
|
21
|
+
phases: [{ title: "Scope" }, { title: "Hypothesize" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
28
22
|
};
|
|
29
23
|
|
|
30
24
|
const ScopeSchema = Type.Object({
|
|
@@ -35,25 +29,7 @@ const ScopeSchema = Type.Object({
|
|
|
35
29
|
constraints: Type.Optional(Type.String({ description: "Safety constraints, missing evidence, or commands intentionally not run." })),
|
|
36
30
|
});
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
label: string;
|
|
40
|
-
category: string;
|
|
41
|
-
text: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type Candidate = AdvisoryCandidate;
|
|
45
|
-
|
|
46
|
-
interface Hypothesis extends Candidate {
|
|
47
|
-
lens: HypothesisLens;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface Verified extends Hypothesis {
|
|
51
|
-
verdict: AdvisoryVerdict["verdict"];
|
|
52
|
-
evidence: string[];
|
|
53
|
-
confidence?: AdvisoryVerdict["confidence"];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const HYPOTHESIS_LENSES: HypothesisLens[] = [
|
|
32
|
+
const HYPOTHESIS_LENSES: AdvisoryLens[] = [
|
|
57
33
|
{ label: "recent-change", category: "regression", text: "A recent code change broke a previously working path or changed an implicit contract." },
|
|
58
34
|
{ label: "control-flow", category: "root-cause", text: "Incorrect branching, ordering, async flow, data flow, or state transition causes the symptom." },
|
|
59
35
|
{ label: "configuration", category: "configuration", text: "Configuration, environment, package scripts, or runtime assumptions differ from what the code expects." },
|
|
@@ -61,13 +37,12 @@ const HYPOTHESIS_LENSES: HypothesisLens[] = [
|
|
|
61
37
|
{ label: "test-fixture", category: "test-fixture", text: "The failure is caused by test setup, fixtures, mocks, generated files, or stale local state rather than product code." },
|
|
62
38
|
];
|
|
63
39
|
|
|
64
|
-
const TOOLS = DEFAULT_ADVISORY_TOOLS;
|
|
65
|
-
const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
|
|
66
40
|
const PER_LENS = 4;
|
|
67
41
|
|
|
68
42
|
export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
69
|
-
const { agent,
|
|
70
|
-
const
|
|
43
|
+
const { agent, phase, log, progress, args } = api;
|
|
44
|
+
const challengeConfig = parseChallengeArgs(args);
|
|
45
|
+
const symptom = challengeConfig.args.trim();
|
|
71
46
|
let fileCount = 0;
|
|
72
47
|
let rawCandidateCount = 0;
|
|
73
48
|
let droppedCandidateCount = 0;
|
|
@@ -90,15 +65,15 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
90
65
|
"Inspect relevant files, package/test configuration, and safe diagnostic commands. " +
|
|
91
66
|
"Safe commands are read-only commands such as status, grep, listing files, typecheck/test commands, or commands explicitly requested by the user. " +
|
|
92
67
|
"Do not run mutation, install, commit, network, or destructive commands. Return scoped files, observations, and constraints. Structured output only.",
|
|
93
|
-
{ phase: "Scope", label: "scope", tools:
|
|
68
|
+
{ phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
94
69
|
);
|
|
95
70
|
|
|
96
71
|
if (!scope) {
|
|
97
|
-
return emptyAdvisoryReport(
|
|
72
|
+
return finishAdvisoryReport(emptyAdvisoryReport(
|
|
98
73
|
"Diagnosis could not establish a scope.",
|
|
99
74
|
["Provide the failing command, error message, or regression description and rerun diagnose."],
|
|
100
75
|
makeStats(0, 0),
|
|
101
|
-
);
|
|
76
|
+
), []);
|
|
102
77
|
}
|
|
103
78
|
|
|
104
79
|
fileCount = scope.files.length;
|
|
@@ -113,88 +88,48 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
113
88
|
`## Observations\n${scope.observations.map((observation) => `- ${observation}`).join("\n") || "(none)"}\n\n` +
|
|
114
89
|
`## Constraints\n${scope.constraints ?? "(none noted)"}\n`;
|
|
115
90
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
title: candidate.summary,
|
|
135
|
-
subtitle: formatLocation(candidate),
|
|
136
|
-
status: "pending",
|
|
137
|
-
details: candidate.impact,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return candidates;
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const hypotheses = dedupe(compactResults(perLens).flat(), (dropped) => {
|
|
145
|
-
droppedCandidateCount += dropped;
|
|
146
|
-
progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
91
|
+
const pipelineResult = await runLensVerificationPipeline({
|
|
92
|
+
api,
|
|
93
|
+
lenses: HYPOTHESIS_LENSES,
|
|
94
|
+
perLens: PER_LENS,
|
|
95
|
+
finderPhase: "Hypothesize",
|
|
96
|
+
finderPrompt: (lens) =>
|
|
97
|
+
`## Diagnose hypothesis generator — ${lens.label}\n\n${scopeBlock}\n` +
|
|
98
|
+
"This workflow is advisory-only: diagnose and recommend validation/fix plans, but do not edit files.\n" +
|
|
99
|
+
`Consider ONLY this hypothesis lens:\n${lens.text}\n\n` +
|
|
100
|
+
`Surface up to ${PER_LENS} root-cause hypotheses. Use category exactly "${lens.category}". ` +
|
|
101
|
+
"Each hypothesis must include a one-line summary, locations, impact explaining how it produces the symptom, and an optional recommendation for the next validation step. Structured output only.",
|
|
102
|
+
verifierPrompt: (candidate) =>
|
|
103
|
+
`## Diagnose verifier\n\n${scopeBlock}\n## Hypothesis\n` +
|
|
104
|
+
`Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
|
|
105
|
+
`Recommended validation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
|
|
106
|
+
"Read relevant files and, when useful, run only safe read-only diagnostic commands from the scoped command list or commands explicitly requested by the user. " +
|
|
107
|
+
"Do not run mutation, install, commit, network, or destructive commands. Return CONFIRMED, PLAUSIBLE, NOT_SUBSTANTIATED, or REFUTED with evidence. " +
|
|
108
|
+
"Use NOT_SUBSTANTIATED when evidence is missing; REFUTED requires disproof. Structured output only.",
|
|
147
109
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const location = primaryLocation(hypothesis);
|
|
154
|
-
const judged = await agent(
|
|
155
|
-
`## Diagnose verifier\n\n${scopeBlock}\n## Hypothesis\n` +
|
|
156
|
-
`Location: ${formatLocation(hypothesis)}\nCategory: ${hypothesis.category}\nSummary: ${hypothesis.summary}\nImpact: ${hypothesis.impact}\n` +
|
|
157
|
-
`Recommended validation: ${hypothesis.recommendation ?? "(none supplied)"}\n\n` +
|
|
158
|
-
"Read relevant files and, when useful, run only safe read-only diagnostic commands from the scoped command list or commands explicitly requested by the user. " +
|
|
159
|
-
"Do not run mutation, install, commit, network, or destructive commands. Return CONFIRMED, PLAUSIBLE, or REFUTED with evidence. " +
|
|
160
|
-
"Default toward REFUTED if evidence does not connect the hypothesis to the symptom. Structured output only.",
|
|
161
|
-
{
|
|
162
|
-
phase: "Verify",
|
|
163
|
-
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
164
|
-
tools: TOOLS,
|
|
165
|
-
toolHints: TOOL_HINTS,
|
|
166
|
-
profile: "small",
|
|
167
|
-
schema: AdvisoryVerdictSchema,
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
if (!judged) return null;
|
|
171
|
-
recordVerdictProgress(progress, hypothesis, judged, () => {
|
|
172
|
-
refutedCandidateCount += 1;
|
|
173
|
-
});
|
|
174
|
-
return { ...hypothesis, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
|
|
175
|
-
}),
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
|
|
110
|
+
rawCandidateCount += pipelineResult.rawCandidates;
|
|
111
|
+
droppedCandidateCount += pipelineResult.dropped;
|
|
112
|
+
refutedCandidateCount += pipelineResult.refuted;
|
|
113
|
+
const { coverage } = pipelineResult;
|
|
114
|
+
const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, coverage);
|
|
179
115
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
180
116
|
const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
|
|
181
117
|
const stats = makeStats(verified.length, surviving.length);
|
|
182
118
|
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
183
119
|
|
|
184
120
|
if (surviving.length === 0) {
|
|
185
|
-
return emptyAdvisoryReport(
|
|
121
|
+
return finishAdvisoryReport(emptyAdvisoryReport(
|
|
186
122
|
"No root-cause hypothesis survived verification.",
|
|
187
123
|
["Capture the exact failing command and error output.", "Rerun diagnose with a narrower symptom or more evidence."],
|
|
188
124
|
stats,
|
|
189
|
-
);
|
|
125
|
+
), coverage, verified);
|
|
190
126
|
}
|
|
191
127
|
|
|
192
|
-
phase("Synthesize");
|
|
193
128
|
const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
|
|
194
129
|
const block = ranked
|
|
195
130
|
.map(
|
|
196
131
|
(finding, index) =>
|
|
197
|
-
`### [${index}] ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
|
|
132
|
+
`### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
|
|
198
133
|
`${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\nValidation/fix plan: ${finding.recommendation ?? "(none supplied)"}`,
|
|
199
134
|
)
|
|
200
135
|
.join("\n\n");
|
|
@@ -203,46 +138,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
203
138
|
.map((finding) => `- ${finding.summary} — REFUTED because ${formatEvidence(finding.evidence)}`)
|
|
204
139
|
.join("\n");
|
|
205
140
|
|
|
206
|
-
const
|
|
141
|
+
const resolved = await synthesizeAdvisoryReport(api,
|
|
207
142
|
`## Synthesis: final diagnosis report\n\n${ranked.length} hypotheses survived independent verification.\n\n${block}\n\n` +
|
|
208
143
|
`## Refuted hypotheses for context\n${refutedBlock || "(none recorded)"}\n\n` +
|
|
209
|
-
"
|
|
210
|
-
"Use categories such as root-cause, regression, configuration, dependency, or test-fixture. " +
|
|
144
|
+
"Select confirmed, plausible or explicitly unresolved root causes by ID. " +
|
|
211
145
|
"Recommendation must be a validation/fix plan, not a patch. nextSteps must be the minimum commands or code inspections needed to confirm the top diagnosis. Structured output only.",
|
|
212
|
-
|
|
213
|
-
phase: "Synthesize",
|
|
214
|
-
label: "synthesize",
|
|
215
|
-
tools: [],
|
|
216
|
-
profile: "medium",
|
|
217
|
-
resume: "read-only",
|
|
218
|
-
schema: AdvisoryReportSchema,
|
|
219
|
-
},
|
|
146
|
+
ranked, coverage,
|
|
220
147
|
);
|
|
221
|
-
|
|
222
|
-
if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun diagnose with a narrower symptom."], stats);
|
|
223
|
-
|
|
224
|
-
const findings = backfillAdvisoryFindings(report.findings, ranked, {
|
|
225
|
-
impact: "Impact not restated by synthesis.",
|
|
226
|
-
recommendation: "Validate this diagnosis with the smallest safe reproduction command.",
|
|
227
|
-
});
|
|
228
|
-
return { ...report, findings, stats };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function dedupe(candidates: Hypothesis[], onDropped: (dropped: number) => void): Hypothesis[] {
|
|
232
|
-
const seen = new Set<string>();
|
|
233
|
-
const novel: Hypothesis[] = [];
|
|
234
|
-
for (const candidate of candidates) {
|
|
235
|
-
const key = dedupKey(candidate);
|
|
236
|
-
if (seen.has(key)) continue;
|
|
237
|
-
seen.add(key);
|
|
238
|
-
novel.push(candidate);
|
|
239
|
-
}
|
|
240
|
-
const dropped = candidates.length - novel.length;
|
|
241
|
-
if (dropped > 0) onDropped(dropped);
|
|
242
|
-
return novel;
|
|
148
|
+
return finishAdvisoryReport({ ...resolved, stats: { ...stats, kept: resolved.findings.length } }, coverage, verified);
|
|
243
149
|
}
|
|
244
150
|
|
|
245
|
-
function rank(finding:
|
|
151
|
+
function rank(finding: AdvisoryVerified): number {
|
|
246
152
|
if (finding.verdict === "CONFIRMED") return 0;
|
|
247
153
|
return 1;
|
|
248
154
|
}
|