pi-workflow-engine 0.3.0 → 0.5.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 (39) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +369 -0
  2. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
  3. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
  4. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
  5. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
  6. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +83 -0
  7. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -0
  8. package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
  9. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +396 -0
  10. package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
  11. package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
  12. package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
  13. package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
  14. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
  15. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
  16. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-result-renderer.ts +11 -8
  17. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
  18. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
  19. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +57 -0
  20. package/{src → .pi/extensions/pi-workflow-engine/src}/workflows.ts +8 -2
  21. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +57 -125
  22. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/diagnose.ts +12 -73
  23. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/perf-review.ts +41 -148
  24. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/refactor-scout.ts +43 -151
  25. package/.pi/settings.json +3 -0
  26. package/README.md +101 -27
  27. package/USAGE.md +183 -0
  28. package/assets/preview-candidate-core.png +0 -0
  29. package/assets/preview-candidate-relic-emblem.png +0 -0
  30. package/assets/preview-candidate-robot-squad.png +0 -0
  31. package/assets/preview.png +0 -0
  32. package/package.json +33 -6
  33. package/src/agent-runner.ts +0 -122
  34. package/src/concurrency.ts +0 -55
  35. package/src/discovery.ts +0 -54
  36. package/src/index.ts +0 -141
  37. package/src/ui/workflow-widget.ts +0 -143
  38. package/workflows/ping.ts +0 -28
  39. /package/{src → .pi/extensions/pi-workflow-engine/src}/advisory-schema.ts +0 -0
@@ -4,11 +4,17 @@ import {
4
4
  AdvisoryReportSchema,
5
5
  AdvisoryVerdictSchema,
6
6
  type AdvisoryCandidate,
7
- type AdvisoryFinding,
8
- type AdvisoryLocation,
9
7
  type AdvisoryReport,
10
8
  type AdvisoryVerdict,
11
9
  } from "../src/advisory-schema.ts";
10
+ import {
11
+ advisoryDedupKey as dedupKey,
12
+ backfillAdvisoryFindings,
13
+ formatEvidence,
14
+ formatLocation,
15
+ primaryLocation,
16
+ recordVerdictProgress,
17
+ } from "../src/workflow-advisory-utils.ts";
12
18
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
13
19
 
14
20
  export const meta: WorkflowMeta = {
@@ -156,18 +162,8 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
156
162
  },
157
163
  );
158
164
  if (!judged) return null;
159
- progress({ type: "counter_delta", key: `verdict.${judged.verdict.toLowerCase()}`, label: judged.verdict, delta: 1 });
160
- if (judged.verdict === "REFUTED") {
165
+ recordVerdictProgress(progress, hypothesis, judged, () => {
161
166
  refutedCandidateCount += 1;
162
- progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
163
- }
164
- progress({
165
- type: "lane_item",
166
- lane: verdictLane(judged.verdict),
167
- title: hypothesis.summary,
168
- subtitle: formatLocation(hypothesis),
169
- status: verdictStatus(judged.verdict),
170
- details: formatEvidence(judged.evidence),
171
167
  });
172
168
  return { ...hypothesis, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
173
169
  }),
@@ -216,14 +212,9 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
216
212
 
217
213
  if (!report) return emptyReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun diagnose with a narrower symptom."], stats);
218
214
 
219
- const findings = report.findings.map((finding) => {
220
- const source = ranked.find((candidate) => sameFinding(candidate, finding));
221
- return {
222
- ...finding,
223
- evidence: finding.evidence.length > 0 ? finding.evidence : (source?.evidence ?? []),
224
- impact: finding.impact || source?.impact || "Impact not restated by synthesis.",
225
- recommendation: finding.recommendation || source?.recommendation || "Validate this diagnosis with the smallest safe reproduction command.",
226
- };
215
+ const findings = backfillAdvisoryFindings(report.findings, ranked, {
216
+ impact: "Impact not restated by synthesis.",
217
+ recommendation: "Validate this diagnosis with the smallest safe reproduction command.",
227
218
  });
228
219
  return { ...report, findings, stats };
229
220
  }
@@ -246,60 +237,8 @@ function dedupe(candidates: Hypothesis[], onDropped: (dropped: number) => void):
246
237
  return novel;
247
238
  }
248
239
 
249
- function dedupKey(candidate: Candidate): string {
250
- const location = primaryLocation(candidate);
251
- const lineKey = location.line != null ? Math.round(location.line / 5) * 5 : "file";
252
- return `${candidate.category}:${normalizePath(location.file)}:${lineKey}:${candidate.summary.slice(0, 60).toLowerCase()}`;
253
- }
254
-
255
- function primaryLocation(candidate: Pick<Candidate, "locations">): AdvisoryLocation {
256
- return candidate.locations[0] ?? { file: "" };
257
- }
258
-
259
- function formatLocation(candidate: Pick<Candidate, "locations">): string {
260
- const location = primaryLocation(candidate);
261
- const line = location.line != null ? `:${location.line}` : "";
262
- const symbol = location.symbol ? ` (${location.symbol})` : "";
263
- return `${location.file}${line}${symbol}`;
264
- }
265
-
266
- function formatEvidence(evidence: string[]): string {
267
- return evidence.join("; ");
268
- }
269
-
270
- function normalizePath(path: string): string {
271
- return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
272
- }
273
-
274
240
  function rank(finding: Verified): number {
275
241
  if (finding.verdict === "CONFIRMED") return 0;
276
242
  return 1;
277
243
  }
278
244
 
279
- function verdictLane(verdict: Verified["verdict"]): string {
280
- switch (verdict) {
281
- case "CONFIRMED":
282
- return "Confirmed";
283
- case "PLAUSIBLE":
284
- return "Plausible";
285
- case "REFUTED":
286
- return "Refuted";
287
- }
288
- }
289
-
290
- function verdictStatus(verdict: Verified["verdict"]): "success" | "warning" | "error" {
291
- switch (verdict) {
292
- case "CONFIRMED":
293
- return "success";
294
- case "PLAUSIBLE":
295
- return "warning";
296
- case "REFUTED":
297
- return "error";
298
- }
299
- }
300
-
301
- function sameFinding(candidate: Verified, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
302
- const candidateLocation = primaryLocation(candidate);
303
- const findingLocation = primaryLocation(finding);
304
- return normalizePath(candidateLocation.file) === normalizePath(findingLocation.file) && candidateLocation.line === findingLocation.line;
305
- }
@@ -1,14 +1,16 @@
1
1
  import { Type } from "typebox";
2
2
  import {
3
- AdvisoryCandidatesSchema,
4
3
  AdvisoryReportSchema,
5
- AdvisoryVerdictSchema,
6
4
  type AdvisoryCandidate,
7
- type AdvisoryFinding,
8
- type AdvisoryLocation,
9
5
  type AdvisoryReport,
10
6
  type AdvisoryVerdict,
11
7
  } from "../src/advisory-schema.ts";
8
+ import {
9
+ backfillAdvisoryFindings,
10
+ formatEvidence,
11
+ formatLocation,
12
+ runLensVerificationPipeline,
13
+ } from "../src/workflow-advisory-utils.ts";
12
14
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
13
15
 
14
16
  export const meta: WorkflowMeta = {
@@ -98,91 +100,39 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
98
100
  `## Summary\n${scope.summary}\n\n## Known measurements\n${scope.knownMeasurements ?? "(none known)"}\n` +
99
101
  (args.trim() ? `\n## User instructions (verbatim)\n${args.trim()}\n` : "");
100
102
 
101
- const seen = new Set<string>();
102
-
103
103
  phase("Find");
104
- const perLens = await pipeline(
105
- PERF_LENSES,
106
- async (_prev, item) => {
107
- const lens = item as PerfLens;
108
- const found = await agent(
109
- `## Perf-review finder — ${lens.label}\n\n${scopeBlock}\n` +
110
- "This workflow is advisory-only: identify bottleneck hypotheses, measurement gaps, and safe optimization directions, but do not edit files.\n" +
111
- `Investigate ONLY this lens:\n${lens.text}\n\n` +
112
- `Surface up to ${PER_LENS} candidates. Use category exactly "${lens.category}". ` +
113
- "Each candidate must include a one-line summary, locations, impact stating the suspected performance consequence and workload where it matters, " +
114
- "and an optional recommendation. Prefer measurement recommendations when evidence is weak. Structured output only.",
115
- { phase: "Find", label: `find:${lens.label}`, tools: TOOLS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
116
- );
117
- const raw = (found?.candidates ?? []).slice(0, PER_LENS);
118
- rawCandidateCount += raw.length;
119
- progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
120
- for (const candidate of raw) {
121
- progress({
122
- type: "lane_item",
123
- lane: "Candidates",
124
- title: candidate.summary,
125
- subtitle: formatLocation(candidate),
126
- status: "pending",
127
- details: candidate.impact,
128
- });
129
- }
130
- return { lens, candidates: raw };
131
- },
132
- async (prev) => {
133
- const { lens, candidates } = prev as { lens: PerfLens; candidates: Candidate[] };
134
- const novel = candidates.filter((candidate) => {
135
- const key = dedupKey(candidate);
136
- if (seen.has(key)) return false;
137
- seen.add(key);
138
- return true;
139
- });
140
- const dropped = candidates.length - novel.length;
141
- if (dropped > 0) {
142
- droppedCandidateCount += dropped;
143
- progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
144
- log(`find:${lens.label}: dropped ${dropped} duplicate candidate(s)`);
145
- }
146
- const verdicts = await parallel(
147
- novel.map((candidate) => async (): Promise<Verified | null> => {
148
- const location = primaryLocation(candidate);
149
- const judged = await agent(
150
- `## Perf-review verifier\n\n${scopeBlock}\n## Candidate\n` +
151
- `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
152
- `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
153
- "Read relevant files and package/scripts. Run only safe read-only measurement or inspection commands when useful. " +
154
- "Return CONFIRMED, PLAUSIBLE, or REFUTED with evidence from code, scripts, config, or measurement output. " +
155
- "Default toward PLAUSIBLE or REFUTED when no measurement exists; do not overstate a bottleneck. Structured output only.",
156
- {
157
- phase: "Verify",
158
- label: `verify:${location.file.split("/").pop() ?? location.file}`,
159
- tools: TOOLS,
160
- thinkingLevel: "low",
161
- schema: AdvisoryVerdictSchema,
162
- },
163
- );
164
- if (!judged) return null;
165
- progress({ type: "counter_delta", key: `verdict.${judged.verdict.toLowerCase()}`, label: judged.verdict, delta: 1 });
166
- if (judged.verdict === "REFUTED") {
167
- refutedCandidateCount += 1;
168
- progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
169
- }
170
- progress({
171
- type: "lane_item",
172
- lane: verdictLane(judged.verdict),
173
- title: candidate.summary,
174
- subtitle: formatLocation(candidate),
175
- status: verdictStatus(judged.verdict),
176
- details: formatEvidence(judged.evidence),
177
- });
178
- return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence, lens };
179
- }),
180
- );
181
- return verdicts.filter((value): value is Verified => value !== null);
182
- },
183
- );
184
-
185
- const verified = (perLens as Verified[][]).flat();
104
+ const pipelineResult = await runLensVerificationPipeline({
105
+ api: { agent, parallel, pipeline, progress, log },
106
+ lenses: PERF_LENSES,
107
+ schedulingMode: "finder-barrier",
108
+ perLens: PER_LENS,
109
+ tools: TOOLS,
110
+ finderPrompt: (lens) =>
111
+ `## Perf-review finder — ${lens.label}\n\n${scopeBlock}\n` +
112
+ "This workflow is advisory-only: identify bottleneck hypotheses, measurement gaps, and safe optimization directions, but do not edit files.\n" +
113
+ `Investigate ONLY this lens:\n${lens.text}\n\n` +
114
+ `Surface up to ${PER_LENS} candidates. Use category exactly "${lens.category}". ` +
115
+ "Each candidate must include a one-line summary, locations, impact stating the suspected performance consequence and workload where it matters, " +
116
+ "and an optional recommendation. Prefer measurement recommendations when evidence is weak. Structured output only.",
117
+ verifierPrompt: (candidate) =>
118
+ `## Perf-review verifier\n\n${scopeBlock}\n## Candidate\n` +
119
+ `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
120
+ `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
121
+ "Read relevant files and package/scripts. Run only safe read-only measurement or inspection commands when useful. " +
122
+ "Return CONFIRMED, PLAUSIBLE, or REFUTED with evidence from code, scripts, config, or measurement output. " +
123
+ "Default toward PLAUSIBLE or REFUTED when no measurement exists; do not overstate a bottleneck. Structured output only.",
124
+ makeVerified: (candidate, lens, judged): Verified => ({
125
+ ...candidate,
126
+ verdict: judged.verdict,
127
+ evidence: judged.evidence,
128
+ confidence: judged.confidence,
129
+ lens,
130
+ }),
131
+ });
132
+ rawCandidateCount += pipelineResult.rawCandidates;
133
+ droppedCandidateCount += pipelineResult.dropped;
134
+ refutedCandidateCount += pipelineResult.refuted;
135
+ const verified = pipelineResult.verified;
186
136
  const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
187
137
  const stats = makeStats(verified.length, surviving.length);
188
138
  progress({ type: "counter", key: "verified", label: "verified", value: verified.length });
@@ -219,14 +169,9 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
219
169
 
220
170
  if (!report) return emptyReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun perf-review with a narrower target."], stats);
221
171
 
222
- const findings = report.findings.map((finding) => {
223
- const source = ranked.find((candidate) => sameFinding(candidate, finding));
224
- return {
225
- ...finding,
226
- evidence: finding.evidence.length > 0 ? finding.evidence : (source?.evidence ?? []),
227
- impact: finding.impact || source?.impact || "Performance impact not restated by synthesis.",
228
- recommendation: finding.recommendation || source?.recommendation || "Measure the target workload before changing code.",
229
- };
172
+ const findings = backfillAdvisoryFindings(report.findings, ranked, {
173
+ impact: "Performance impact not restated by synthesis.",
174
+ recommendation: "Measure the target workload before changing code.",
230
175
  });
231
176
  return { ...report, findings, stats };
232
177
  }
@@ -235,61 +180,9 @@ function emptyReport(summary: string, nextSteps: string[], stats: WorkflowRunSta
235
180
  return { summary, findings: [], nextSteps, stats };
236
181
  }
237
182
 
238
- function primaryLocation(candidate: Pick<Candidate, "locations">): AdvisoryLocation {
239
- return candidate.locations[0] ?? { file: "" };
240
- }
241
-
242
- function formatLocation(candidate: Pick<Candidate, "locations">): string {
243
- const location = primaryLocation(candidate);
244
- const line = location.line != null ? `:${location.line}` : "";
245
- const symbol = location.symbol ? ` (${location.symbol})` : "";
246
- return `${location.file}${line}${symbol}`;
247
- }
248
-
249
- function formatEvidence(evidence: string[]): string {
250
- return evidence.join("; ");
251
- }
252
-
253
- function dedupKey(candidate: Candidate): string {
254
- const location = primaryLocation(candidate);
255
- const lineKey = location.line != null ? Math.round(location.line / 5) * 5 : "file";
256
- return `${candidate.category}:${normalizePath(location.file)}:${lineKey}:${candidate.summary.slice(0, 60).toLowerCase()}`;
257
- }
258
-
259
- function normalizePath(path: string): string {
260
- return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
261
- }
262
-
263
183
  function rank(finding: Verified): number {
264
184
  const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
265
185
  const measurementPenalty = finding.category === "measurement" ? 1 : 0;
266
186
  return verdictRank + measurementPenalty;
267
187
  }
268
188
 
269
- function verdictLane(verdict: Verified["verdict"]): string {
270
- switch (verdict) {
271
- case "CONFIRMED":
272
- return "Confirmed";
273
- case "PLAUSIBLE":
274
- return "Plausible";
275
- case "REFUTED":
276
- return "Refuted";
277
- }
278
- }
279
-
280
- function verdictStatus(verdict: Verified["verdict"]): "success" | "warning" | "error" {
281
- switch (verdict) {
282
- case "CONFIRMED":
283
- return "success";
284
- case "PLAUSIBLE":
285
- return "warning";
286
- case "REFUTED":
287
- return "error";
288
- }
289
- }
290
-
291
- function sameFinding(candidate: Verified, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
292
- const candidateLocation = primaryLocation(candidate);
293
- const findingLocation = primaryLocation(finding);
294
- return normalizePath(candidateLocation.file) === normalizePath(findingLocation.file) && candidateLocation.line === findingLocation.line;
295
- }
@@ -1,14 +1,16 @@
1
1
  import { Type } from "typebox";
2
2
  import {
3
- AdvisoryCandidatesSchema,
4
3
  AdvisoryReportSchema,
5
- AdvisoryVerdictSchema,
6
4
  type AdvisoryCandidate,
7
- type AdvisoryFinding,
8
- type AdvisoryLocation,
9
5
  type AdvisoryReport,
10
6
  type AdvisoryVerdict,
11
7
  } from "../src/advisory-schema.ts";
8
+ import {
9
+ backfillAdvisoryFindings,
10
+ formatEvidence,
11
+ formatLocation,
12
+ runLensVerificationPipeline,
13
+ } from "../src/workflow-advisory-utils.ts";
12
14
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
13
15
 
14
16
  export const meta: WorkflowMeta = {
@@ -21,7 +23,7 @@ const ScopeSchema = Type.Object({
21
23
  target: Type.String({ description: "Verbatim target path, module, or focus area being scouted." }),
22
24
  files: Type.Array(Type.String(), { description: "Repository-relative files in scope." }),
23
25
  summary: Type.String({ description: "One-paragraph summary of the scoped code." }),
24
- conventions: Type.Optional(Type.String({ description: "Relevant project conventions from AGENTS.md / CLAUDE.md / docs." })),
26
+ conventions: Type.Optional(Type.String({ description: "Relevant project conventions from AGENTS.md / docs." })),
25
27
  });
26
28
 
27
29
  interface RefactorLens {
@@ -71,7 +73,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
71
73
  const scope = await agent(
72
74
  "Establish the scope for an advisory-only refactor scout. Do not edit files.\n" +
73
75
  `Target / focus (verbatim): ${target}\n\n` +
74
- "Inspect repository structure, the target path or module, and relevant AGENTS.md / CLAUDE.md conventions. " +
76
+ "Inspect repository structure, the target path or module, and relevant AGENTS.md / project docs conventions. " +
75
77
  "Return the concrete files that should be considered, a short summary, and any conventions that affect refactor advice. " +
76
78
  `This workflow will fan out across ${REFACTOR_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
77
79
  { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
@@ -95,91 +97,39 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
95
97
  `## Summary\n${scope.summary}\n\n## Conventions\n${scope.conventions ?? "(none noted)"}\n` +
96
98
  (args.trim() ? `\n## User instructions (verbatim)\n${args.trim()}\n` : "");
97
99
 
98
- const seen = new Set<string>();
99
-
100
100
  phase("Find");
101
- const perLens = await pipeline(
102
- REFACTOR_LENSES,
103
- async (_prev, item) => {
104
- const lens = item as RefactorLens;
105
- const found = await agent(
106
- `## Refactor-scout finder — ${lens.label}\n\n${scopeBlock}\n` +
107
- "This workflow is advisory-only: do not edit files and do not propose broad rewrites.\n" +
108
- `Scout through ONLY this lens:\n${lens.text}\n\n` +
109
- `Surface up to ${PER_LENS} candidates. Use category exactly "${lens.category}". ` +
110
- "Each candidate must include a one-line summary, locations, impact on maintainability or future correctness, and an optional safe first recommendation. " +
111
- "Only include opportunities where a small, reviewable first step is plausible. Structured output only.",
112
- { phase: "Find", label: `find:${lens.label}`, tools: TOOLS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
113
- );
114
- const raw = (found?.candidates ?? []).slice(0, PER_LENS);
115
- rawCandidateCount += raw.length;
116
- progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
117
- for (const candidate of raw) {
118
- progress({
119
- type: "lane_item",
120
- lane: "Candidates",
121
- title: candidate.summary,
122
- subtitle: formatLocation(candidate),
123
- status: "pending",
124
- details: candidate.impact,
125
- });
126
- }
127
- return { lens, candidates: raw };
128
- },
129
- async (prev) => {
130
- const { lens, candidates } = prev as { lens: RefactorLens; candidates: Candidate[] };
131
- const novel = candidates.filter((candidate) => {
132
- const key = dedupKey(candidate);
133
- if (seen.has(key)) return false;
134
- seen.add(key);
135
- return true;
136
- });
137
- const dropped = candidates.length - novel.length;
138
- if (dropped > 0) {
139
- droppedCandidateCount += dropped;
140
- progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
141
- log(`find:${lens.label}: dropped ${dropped} duplicate candidate(s)`);
142
- }
143
- const verdicts = await parallel(
144
- novel.map((candidate) => async (): Promise<Verified | null> => {
145
- const location = primaryLocation(candidate);
146
- const judged = await agent(
147
- `## Refactor-scout verifier\n\n${scopeBlock}\n## Candidate\n` +
148
- `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
149
- `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
150
- "Read the relevant files and return CONFIRMED, PLAUSIBLE, or REFUTED. " +
151
- "Default toward REFUTED if the opportunity is generic, too broad, not evidenced by code, or lacks a safe first step. " +
152
- "Evidence must quote or cite code. Structured output only.",
153
- {
154
- phase: "Verify",
155
- label: `verify:${location.file.split("/").pop() ?? location.file}`,
156
- tools: TOOLS,
157
- thinkingLevel: "low",
158
- schema: AdvisoryVerdictSchema,
159
- },
160
- );
161
- if (!judged) return null;
162
- progress({ type: "counter_delta", key: `verdict.${judged.verdict.toLowerCase()}`, label: judged.verdict, delta: 1 });
163
- if (judged.verdict === "REFUTED") {
164
- refutedCandidateCount += 1;
165
- progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
166
- }
167
- progress({
168
- type: "lane_item",
169
- lane: verdictLane(judged.verdict),
170
- title: candidate.summary,
171
- subtitle: formatLocation(candidate),
172
- status: verdictStatus(judged.verdict),
173
- details: formatEvidence(judged.evidence),
174
- });
175
- return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence, lens };
176
- }),
177
- );
178
- return verdicts.filter((value): value is Verified => value !== null);
179
- },
180
- );
181
-
182
- const verified = (perLens as Verified[][]).flat();
101
+ const pipelineResult = await runLensVerificationPipeline({
102
+ api: { agent, parallel, pipeline, progress, log },
103
+ lenses: REFACTOR_LENSES,
104
+ schedulingMode: "finder-barrier",
105
+ perLens: PER_LENS,
106
+ tools: TOOLS,
107
+ finderPrompt: (lens) =>
108
+ `## Refactor-scout finder — ${lens.label}\n\n${scopeBlock}\n` +
109
+ "This workflow is advisory-only: do not edit files and do not propose broad rewrites.\n" +
110
+ `Scout through ONLY this lens:\n${lens.text}\n\n` +
111
+ `Surface up to ${PER_LENS} candidates. Use category exactly "${lens.category}". ` +
112
+ "Each candidate must include a one-line summary, locations, impact on maintainability or future correctness, and an optional safe first recommendation. " +
113
+ "Only include opportunities where a small, reviewable first step is plausible. Structured output only.",
114
+ verifierPrompt: (candidate) =>
115
+ `## Refactor-scout verifier\n\n${scopeBlock}\n## Candidate\n` +
116
+ `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
117
+ `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
118
+ "Read the relevant files and return CONFIRMED, PLAUSIBLE, or REFUTED. " +
119
+ "Default toward REFUTED if the opportunity is generic, too broad, not evidenced by code, or lacks a safe first step. " +
120
+ "Evidence must quote or cite code. Structured output only.",
121
+ makeVerified: (candidate, lens, judged): Verified => ({
122
+ ...candidate,
123
+ verdict: judged.verdict,
124
+ evidence: judged.evidence,
125
+ confidence: judged.confidence,
126
+ lens,
127
+ }),
128
+ });
129
+ rawCandidateCount += pipelineResult.rawCandidates;
130
+ droppedCandidateCount += pipelineResult.dropped;
131
+ refutedCandidateCount += pipelineResult.refuted;
132
+ const verified = pipelineResult.verified;
183
133
  const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
184
134
  const stats = makeStats(verified.length, surviving.length);
185
135
  progress({ type: "counter", key: "verified", label: "verified", value: verified.length });
@@ -213,14 +163,9 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
213
163
 
214
164
  if (!report) return emptyReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun the workflow with a narrower target."], stats);
215
165
 
216
- const findings = report.findings.map((finding) => {
217
- const source = ranked.find((candidate) => sameFinding(candidate, finding));
218
- return {
219
- ...finding,
220
- evidence: finding.evidence.length > 0 ? finding.evidence : (source?.evidence ?? []),
221
- impact: finding.impact || source?.impact || "Impact not restated by synthesis.",
222
- recommendation: finding.recommendation || source?.recommendation || "Choose a small, behavior-preserving refactor first step.",
223
- };
166
+ const findings = backfillAdvisoryFindings(report.findings, ranked, {
167
+ impact: "Impact not restated by synthesis.",
168
+ recommendation: "Choose a small, behavior-preserving refactor first step.",
224
169
  });
225
170
  return { ...report, findings, stats };
226
171
  }
@@ -229,61 +174,8 @@ function emptyReport(summary: string, nextSteps: string[], stats: WorkflowRunSta
229
174
  return { summary, findings: [], nextSteps, stats };
230
175
  }
231
176
 
232
- function primaryLocation(candidate: Pick<Candidate, "locations">): AdvisoryLocation {
233
- return candidate.locations[0] ?? { file: "" };
234
- }
235
-
236
- function formatLocation(candidate: Pick<Candidate, "locations">): string {
237
- const location = primaryLocation(candidate);
238
- const line = location.line != null ? `:${location.line}` : "";
239
- const symbol = location.symbol ? ` (${location.symbol})` : "";
240
- return `${location.file}${line}${symbol}`;
241
- }
242
-
243
- function formatEvidence(evidence: string[]): string {
244
- return evidence.join("; ");
245
- }
246
-
247
- function dedupKey(candidate: Candidate): string {
248
- const location = primaryLocation(candidate);
249
- const lineKey = location.line != null ? Math.round(location.line / 5) * 5 : "file";
250
- return `${candidate.category}:${normalizePath(location.file)}:${lineKey}:${candidate.summary.slice(0, 60).toLowerCase()}`;
251
- }
252
-
253
- function normalizePath(path: string): string {
254
- return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
255
- }
256
-
257
177
  function rank(finding: Verified): number {
258
178
  const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
259
179
  const categoryRank = finding.category === "dead-code" || finding.category === "conventions" ? 2 : 0;
260
180
  return verdictRank + categoryRank;
261
181
  }
262
-
263
- function verdictLane(verdict: Verified["verdict"]): string {
264
- switch (verdict) {
265
- case "CONFIRMED":
266
- return "Confirmed";
267
- case "PLAUSIBLE":
268
- return "Plausible";
269
- case "REFUTED":
270
- return "Refuted";
271
- }
272
- }
273
-
274
- function verdictStatus(verdict: Verified["verdict"]): "success" | "warning" | "error" {
275
- switch (verdict) {
276
- case "CONFIRMED":
277
- return "success";
278
- case "PLAUSIBLE":
279
- return "warning";
280
- case "REFUTED":
281
- return "error";
282
- }
283
- }
284
-
285
- function sameFinding(candidate: Verified, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
286
- const candidateLocation = primaryLocation(candidate);
287
- const findingLocation = primaryLocation(finding);
288
- return normalizePath(candidateLocation.file) === normalizePath(findingLocation.file) && candidateLocation.line === findingLocation.line;
289
- }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extensions": ["extensions/pi-workflow-engine/index.ts"]
3
+ }