pi-workflow-engine 0.12.0 → 1.0.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.
Files changed (92) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
  6. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
  7. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  17. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
  18. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  19. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  20. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
  21. package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
  22. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  23. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  24. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  25. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  31. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  32. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  33. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  34. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  35. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  36. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  37. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  38. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
  39. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
  40. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  41. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  52. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  53. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  54. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
  55. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  56. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  57. package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
  58. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  59. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  61. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  62. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  68. package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  86. package/USAGE.md +90 -17
  87. package/package.json +1 -2
  88. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  89. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  90. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
  91. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  92. package/.pi/settings.json +0 -3
@@ -1,34 +1,29 @@
1
+ import { challengeFindings, parseChallengeArgs } from "../src/advisory-challenge.ts";
1
2
  import { Type } from "typebox";
2
3
  import {
3
- AdvisoryCandidatesSchema,
4
- AdvisoryReportSchema,
5
- AdvisoryVerdictSchema,
6
- type AdvisoryCandidate,
7
- type AdvisoryVerdict,
8
- } from "../src/advisory-schema.ts";
9
- import {
10
- backfillAdvisoryFindings,
4
+ type AdvisoryVerified,
5
+ type AdvisoryLens,
6
+ concludeLensReview,
7
+ emptyAdvisoryReport,
8
+ finishAdvisoryReport,
11
9
  formatEvidence,
12
10
  formatLocation,
13
- normalizePath,
14
- primaryLocation,
15
- publishVerifiedKeptProgress,
16
- recordVerdictProgress,
11
+ runLensVerificationPipeline,
12
+ EMPTY_LENS_REVIEW_STATS,
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, dedupeCodeReviewCandidates } from "../src/review/code-review-orchestration.ts";
18
+ import { changedLines, diffAnchor } from "../src/review/review-diff-lines.ts";
24
19
  import type { ReviewContext } from "../src/review/review-report.ts";
25
- import { captureReviewMaterial, type ReviewMaterialCaptureResult } from "../src/review/review-snapshot.ts";
26
- import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
20
+ import { captureReviewMaterial } from "../src/review/review-snapshot.ts";
21
+ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
27
22
 
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,85 +34,50 @@ 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: Angle[] = [
57
- { label: "logic-bugs", kind: "bug", text: "Off-by-one errors, wrong conditionals, incorrect return values, broken control flow." },
58
- { label: "error-paths", kind: "bug", text: "Unhandled errors, swallowed exceptions, missing awaits, partial failure leaving inconsistent state." },
59
- { label: "edge-cases", kind: "bug", text: "Empty/null inputs, boundary values, concurrency races, resource leaks." },
60
- { label: "simplification", kind: "cleanup", text: "Dead code, needless complexity, duplicated logic, clearer equivalents." },
61
- { label: "conventions", kind: "cleanup", text: "Violations of the project conventions noted in scope (naming, idioms, banned patterns)." },
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
- /** Parse a unified diff into the set of added/changed new-file line numbers per file. */
69
- export function changedLines(diff: string): Map<string, Set<number>> {
70
- const byFile = new Map<string, Set<number>>();
71
- let file: string | null = null;
72
- let newLine = 0;
73
- for (const raw of diff.split("\n")) {
74
- if (raw.startsWith("+++ ")) {
75
- const path = raw.slice(4).trim();
76
- file = path === "/dev/null" ? null : normalizePath(path);
77
- if (file && !byFile.has(file)) byFile.set(file, new Set());
78
- } else if (raw.startsWith("@@")) {
79
- const match = /\+(\d+)/.exec(raw);
80
- newLine = match ? Number(match[1]) : 0;
81
- } else if (file === null || raw.startsWith("---") || raw.startsWith("\\")) {
82
- // file header, deletion, or "No newline" marker — record nothing
83
- } else if (raw.startsWith("+")) {
84
- byFile.get(file)!.add(newLine++);
85
- } else if (!raw.startsWith("-")) {
86
- newLine++; // context line advances the new-file counter; deletions do not
87
- }
88
- }
89
- return byFile;
90
- }
91
-
92
- /** Is a finding inside the diff? File-level findings count if the file changed; ±1 line of fuzz. */
93
- export function inDiff(changed: Map<string, Set<number>>, file: string, line?: number): boolean {
94
- const set = changed.get(normalizePath(file));
95
- if (!set) return false;
96
- if (line == null) return true;
97
- return set.has(line) || set.has(line - 1) || set.has(line + 1);
48
+ const DIFF_EMBED_CAP = 60_000;
49
+
50
+ export function buildCodeReviewScopeBlock(input: {
51
+ readonly diffCommand: string;
52
+ readonly files: readonly string[];
53
+ readonly summary: string;
54
+ readonly conventions?: string;
55
+ readonly diffText: string;
56
+ readonly target: string;
57
+ }): string {
58
+ const diffBlock = input.diffText
59
+ ? `\n## Diff (review is bounded to these changed lines)\n\`\`\`diff\n${
60
+ input.diffText.length > DIFF_EMBED_CAP
61
+ ? `${input.diffText.slice(0, DIFF_EMBED_CAP)}\n... (truncated — run \`${input.diffCommand}\` for the full diff)`
62
+ : input.diffText
63
+ }\n\`\`\`\n`
64
+ : "";
65
+ return (
66
+ `## Diff command\n${input.diffCommand}\n\n## Changed files\n${input.files.map((file) => `- ${file}`).join("\n")}\n\n` +
67
+ `## Summary\n${input.summary}\n\n## Conventions\n${input.conventions ?? "(none noted)"}\n` +
68
+ diffBlock +
69
+ (input.target ? `\n## User instructions (verbatim)\n${input.target}\n` : "")
70
+ );
98
71
  }
99
72
 
100
73
  export interface CodeReviewDependencies {
101
- readonly captureReviewMaterial?: (
102
- target: Parameters<typeof captureReviewMaterial>[0],
103
- cwd: string,
104
- signal?: AbortSignal,
105
- ) => Promise<ReviewMaterialCaptureResult>;
74
+ readonly captureReviewMaterial?: typeof captureReviewMaterial;
106
75
  }
107
76
 
108
77
  export default async function run(api: WorkflowApi, dependencies: CodeReviewDependencies = {}): Promise<unknown> {
109
- const { agent, parallel, phase, log, progress, args, cwd, signal } = api;
110
- const target = args.trim();
111
- let fileCount = 0;
112
- let rawCandidateCount = 0;
113
- let droppedCandidateCount = 0;
114
- const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
115
- files: fileCount,
116
- candidates: rawCandidateCount,
117
- verified,
118
- kept,
119
- dropped: droppedCandidateCount,
120
- });
78
+ const { agent, phase, log, progress, args, cwd, signal } = api;
79
+ const challengeConfig = parseChallengeArgs(args);
80
+ const target = challengeConfig.args;
121
81
 
122
82
  // ─── Phase 0: Scope ───
123
83
  phase("Scope");
@@ -138,14 +98,9 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
138
98
  "Then: list the changed files, summarize the change in one paragraph (mention the PR if one was found), " +
139
99
  "and read any relevant AGENTS.md or project docs noting conventions a reviewer should know.\n" +
140
100
  "Return diffCommand exactly as a reviewer should run it. Structured output only.",
141
- { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
101
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
142
102
  );
143
103
 
144
- if (!scope) {
145
- return { summary: "No changes found to review.", findings: [], nextSteps: ["Provide a PR, ref range, or changed files to review."], stats: makeStats(0, 0) };
146
- }
147
-
148
- fileCount = scope.files.length;
149
104
  progress({ type: "summary", key: "files", value: scope.files.join(", ") || "(none)" });
150
105
  const diffTarget = parseAllowedDiffCommand(scope.diffCommand);
151
106
  if ("error" in diffTarget) {
@@ -153,11 +108,11 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
153
108
  }
154
109
  const diffCommand = formatReviewDiffTarget(diffTarget);
155
110
  progress({ type: "summary", key: "diffCommand", value: diffCommand });
156
- progress({ type: "counter", key: "files", label: "files", value: fileCount });
111
+ progress({ type: "counter", key: "files", label: "files", value: scope.files.length });
112
+ const noChanges = (summary: string, nextSteps = ["Provide a PR, ref range, or changed files to review."]) =>
113
+ finishAdvisoryReport(emptyAdvisoryReport(summary, nextSteps, { ...EMPTY_LENS_REVIEW_STATS, files: scope.files.length }), []);
157
114
 
158
- if (scope.files.length === 0) {
159
- return { summary: "No changes found to review.", findings: [], nextSteps: ["Provide a PR, ref range, or changed files to review."], stats: makeStats(0, 0) };
160
- }
115
+ if (scope.files.length === 0) return noChanges("No changes found to review.");
161
116
 
162
117
  log(`${scope.files.length} changed files`);
163
118
 
@@ -169,6 +124,14 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
169
124
  const diffText = reviewMaterial.diff;
170
125
  const changed = changedLines(diffText);
171
126
  progress({ type: "summary", key: "diffBytes", value: Buffer.byteLength(diffText) });
127
+ // Findings anchor only on files with a new-side text hunk; without one, skip the finder fan-out.
128
+ if (changed.size === 0) {
129
+ if (!diffText.trim()) return noChanges(`No changes found to review: \`${diffCommand}\` has no added or modified files.`);
130
+ return noChanges(
131
+ `Nothing to review line by line: \`${diffCommand}\` only deletes or renames files, changes file modes, or touches binary files; there are no added lines to anchor findings on.`,
132
+ ["Inspect the deleted, renamed, mode-changed or binary files directly; code-review anchors findings on added lines only."],
133
+ );
134
+ }
172
135
  if (reviewMaterial.snapshot.status === "unavailable") {
173
136
  log(`review snapshot unavailable (${reviewMaterial.snapshot.reason}) — patch previews will be unavailable`);
174
137
  }
@@ -184,7 +147,7 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
184
147
  : {}),
185
148
  };
186
149
 
187
- const scopeBlock = buildCodeReviewScopeBlock({
150
+ const scopeBlock = `Reviewed snapshot identity: ${JSON.stringify(reviewContext.snapshot ?? "unavailable")}\n` + buildCodeReviewScopeBlock({
188
151
  diffCommand,
189
152
  files: scope.files,
190
153
  summary: scope.summary,
@@ -193,113 +156,48 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
193
156
  target,
194
157
  });
195
158
 
196
- // ─── Find barrier → dedup → Verify ───
197
- phase("Find");
198
- const perAngle = await parallel(
199
- ANGLES.map((angle) => async () => {
200
- const found = await agent(
201
- `## Code-review finder — ${angle.label}\n\n${scopeBlock}\n` +
202
- `Review the change through ONLY this lens:\n${angle.text}\n` +
203
- "Only flag issues on lines that are part of the diff above (run the diff command if it is not shown). " +
204
- "You may read surrounding files for context, but never report issues in unchanged code. " +
205
- `Surface up to ${PER_ANGLE} candidates. Use category exactly "${angle.kind}". Each candidate must include a one-line summary, ` +
206
- "locations with the changed file and a line that appears in the diff, and impact describing the concrete failure or maintenance scenario. " +
207
- "Pass through anything with a nameable impact — a separate verifier judges them next. Structured output only.",
208
- { phase: "Find", label: `find:${angle.label}`, tools: TOOLS, toolHints: TOOL_HINTS, profile: "small", schema: AdvisoryCandidatesSchema },
209
- );
210
- const raw = (found?.candidates ?? []).slice(0, PER_ANGLE);
211
- rawCandidateCount += raw.length;
212
- progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
213
- const bounded = raw.filter((candidate) => inDiff(changed, primaryLocation(candidate).file, primaryLocation(candidate).line));
214
- const dropped = raw.length - bounded.length;
215
- if (dropped > 0) {
216
- droppedCandidateCount += dropped;
217
- progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
218
- log(`find:${angle.label}: dropped ${dropped} out-of-diff candidate(s)`);
219
- }
220
- for (const candidate of bounded) {
221
- progress({
222
- type: "lane_item",
223
- lane: "Candidates",
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);
263
- const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
264
- const stats = makeStats(verified.length, surviving.length);
265
- publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
266
-
267
- 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 };
269
- }
270
-
271
- // ─── Synthesize: rank, merge, report ───
272
- phase("Synthesize");
273
- const rank = (finding: Verified): number => (finding.kind === "cleanup" ? 2 : 0) + (finding.verdict === "PLAUSIBLE" ? 1 : 0);
274
- const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
275
- const block = ranked
276
- .map(
277
- (finding, index) =>
278
- `### [${index}] ${formatLocation(finding)} (${finding.verdict}${finding.kind === "cleanup" ? ", cleanup" : ""})\n` +
279
- `Category: ${finding.kind}\nConfidence: ${verdictConfidence(finding.verdict)}\n` +
280
- `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}`,
281
- )
282
- .join("\n\n");
283
-
284
- const report = await agent(
285
- `## Synthesis: final code-review report\n\n${ranked.length} findings survived independent verification.\n\n${block}\n\n` +
286
- "Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final advisory report. " +
287
- "Return summary, findings, and nextSteps. For each finding: category must be bug or cleanup; severity is impact level (low/medium/high), not category; " +
288
- "confidence must be high for CONFIRMED and medium for PLAUSIBLE; copy locations and evidence arrays from the verified finding; impact is the concrete failure or maintenance scenario; recommendation is an advisory fix direction, not an edit. Structured output only.",
289
- {
290
- phase: "Synthesize",
291
- label: "synthesize",
292
- tools: [],
293
- profile: "medium",
294
- resume: "read-only",
295
- schema: AdvisoryReportSchema,
159
+ const pipelineResult = await runLensVerificationPipeline({
160
+ api,
161
+ lenses: ANGLES,
162
+ perLens: PER_ANGLE,
163
+ boundCandidate: (candidate, lens) => {
164
+ const reviewAnchor = candidate.reviewAnchor
165
+ ? diffAnchor(changed, candidate.reviewAnchor)
166
+ : candidate.locations.map((location) => diffAnchor(changed, location)).find((anchor) => anchor !== undefined);
167
+ return reviewAnchor ? { ...candidate, category: lens.category, reviewAnchor } : undefined;
296
168
  },
297
- );
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.",
169
+ finderPrompt: (lens) =>
170
+ `## Code-review finder — ${lens.label}\n\n${scopeBlock}\n` +
171
+ `Review the change through ONLY this lens:\n${lens.text}\n` +
172
+ "Only flag issues on lines that are part of the diff above (run the diff command if it is not shown). " +
173
+ "Set reviewAnchor to the changed line causing the issue; locations and discoveryEvidence may include unchanged callers and other files. " +
174
+ `Surface up to ${PER_ANGLE} candidates. Use category exactly "${lens.category}". Each candidate must include a one-line summary, ` +
175
+ "locations with the changed file and a line that appears in the diff, and impact describing the concrete failure or maintenance scenario. " +
176
+ "Pass through anything with a nameable impact — a separate verifier judges them next. Structured output only.",
177
+ verifierPrompt: (candidate) =>
178
+ `## Code-review verifier\n\n${scopeBlock}\n## Candidate\n` +
179
+ `Location: ${formatLocation(candidate)}\n` +
180
+ `Category: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n\n` +
181
+ "Run the diff command, read the relevant file(s), and return exactly one verdict (CONFIRMED / PLAUSIBLE / NOT_SUBSTANTIATED / REFUTED) " +
182
+ "with evidence quoting the line(s). Use NOT_SUBSTANTIATED when evidence is insufficient; use REFUTED only for concrete disproof. Structured output only.",
303
183
  });
304
- return { ...report, findings, stats, reviewContext };
184
+ const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, pipelineResult.coverage);
185
+ const report = await concludeLensReview(api, pipelineResult, verified, {
186
+ files: scope.files.length,
187
+ rank,
188
+ formatFinding: (finding, index) =>
189
+ `### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}${finding.category === "cleanup" ? ", cleanup" : ""})\n` +
190
+ `Category: ${finding.category}\nConfidence: ${verdictConfidence(finding.verdict)}\n` +
191
+ `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}`,
192
+ empty: { summary: "No findings survived verification.", nextSteps: ["No code-review action is recommended from this workflow run."] },
193
+ synthesisPrompt: (block, ranked) =>
194
+ `## Synthesis: final code-review report\n\n${ranked.length} findings survived independent verification.\n\n${block}\n\n` +
195
+ "Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final advisory report. " +
196
+ "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.",
197
+ });
198
+ return { ...report, reviewContext };
199
+ }
200
+
201
+ function rank(finding: AdvisoryVerified): number {
202
+ return (finding.category === "cleanup" ? 2 : 0) + (finding.verdict !== "CONFIRMED" ? 1 : 0);
305
203
  }