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,30 +1,22 @@
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
- advisoryDedupKey as dedupKey,
11
- backfillAdvisoryFindings,
12
- emptyAdvisoryReport,
4
+ type AdvisoryVerified,
5
+ type AdvisoryLens,
6
+ concludeLensReview,
13
7
  formatEvidence,
14
8
  formatLocation,
15
- primaryLocation,
16
- publishVerifiedKeptProgress,
17
- recordVerdictProgress,
9
+ formatRankedFinding,
10
+ runLensVerificationPipeline,
18
11
  DEFAULT_ADVISORY_TOOL_HINTS,
19
12
  DEFAULT_ADVISORY_TOOLS,
20
13
  } from "../src/workflow-advisory-utils.ts";
21
- import { compactResults } from "../src/concurrency.ts";
22
- import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
14
+ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
23
15
 
24
16
  export const meta: WorkflowMeta = {
25
17
  name: "diagnose",
26
18
  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" }],
19
+ phases: [{ title: "Scope" }, { title: "Hypothesize" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
28
20
  };
29
21
 
30
22
  const ScopeSchema = Type.Object({
@@ -35,25 +27,7 @@ const ScopeSchema = Type.Object({
35
27
  constraints: Type.Optional(Type.String({ description: "Safety constraints, missing evidence, or commands intentionally not run." })),
36
28
  });
37
29
 
38
- interface HypothesisLens {
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[] = [
30
+ const HYPOTHESIS_LENSES: AdvisoryLens[] = [
57
31
  { label: "recent-change", category: "regression", text: "A recent code change broke a previously working path or changed an implicit contract." },
58
32
  { label: "control-flow", category: "root-cause", text: "Incorrect branching, ordering, async flow, data flow, or state transition causes the symptom." },
59
33
  { label: "configuration", category: "configuration", text: "Configuration, environment, package scripts, or runtime assumptions differ from what the code expects." },
@@ -61,25 +35,12 @@ const HYPOTHESIS_LENSES: HypothesisLens[] = [
61
35
  { 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
36
  ];
63
37
 
64
- const TOOLS = DEFAULT_ADVISORY_TOOLS;
65
- const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
66
38
  const PER_LENS = 4;
67
39
 
68
40
  export default async function run(api: WorkflowApi): Promise<unknown> {
69
- const { agent, parallel, phase, log, progress, args } = api;
70
- const symptom = args.trim();
71
- let fileCount = 0;
72
- let rawCandidateCount = 0;
73
- let droppedCandidateCount = 0;
74
- let refutedCandidateCount = 0;
75
- const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
76
- files: fileCount,
77
- candidates: rawCandidateCount,
78
- verified,
79
- kept,
80
- dropped: droppedCandidateCount,
81
- refuted: refutedCandidateCount,
82
- });
41
+ const { agent, phase, log, progress, args } = api;
42
+ const challengeConfig = parseChallengeArgs(args);
43
+ const symptom = challengeConfig.args;
83
44
 
84
45
  phase("Scope");
85
46
  const scope = await agent(
@@ -90,19 +51,10 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
90
51
  "Inspect relevant files, package/test configuration, and safe diagnostic commands. " +
91
52
  "Safe commands are read-only commands such as status, grep, listing files, typecheck/test commands, or commands explicitly requested by the user. " +
92
53
  "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: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
54
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
94
55
  );
95
56
 
96
- if (!scope) {
97
- return emptyAdvisoryReport(
98
- "Diagnosis could not establish a scope.",
99
- ["Provide the failing command, error message, or regression description and rerun diagnose."],
100
- makeStats(0, 0),
101
- );
102
- }
103
-
104
- fileCount = scope.files.length;
105
- progress({ type: "counter", key: "files", label: "files", value: fileCount });
57
+ progress({ type: "counter", key: "files", label: "files", value: scope.files.length });
106
58
  progress({ type: "summary", key: "symptom", value: scope.symptom });
107
59
  progress({ type: "summary", key: "files", value: scope.files.join(", ") || "(none)" });
108
60
  log(`${scope.files.length} files scoped for diagnosis`);
@@ -113,136 +65,48 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
113
65
  `## Observations\n${scope.observations.map((observation) => `- ${observation}`).join("\n") || "(none)"}\n\n` +
114
66
  `## Constraints\n${scope.constraints ?? "(none noted)"}\n`;
115
67
 
116
- phase("Hypothesize");
117
- const perLens = await parallel(
118
- HYPOTHESIS_LENSES.map((lens) => async (): Promise<Hypothesis[]> => {
119
- const found = await agent(
120
- `## Diagnose hypothesis generator — ${lens.label}\n\n${scopeBlock}\n` +
121
- "This workflow is advisory-only: diagnose and recommend validation/fix plans, but do not edit files.\n" +
122
- `Consider ONLY this hypothesis lens:\n${lens.text}\n\n` +
123
- `Surface up to ${PER_LENS} root-cause hypotheses. Use category exactly "${lens.category}". ` +
124
- "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.",
125
- { phase: "Hypothesize", label: `hypothesize:${lens.label}`, tools: TOOLS, toolHints: TOOL_HINTS, profile: "small", schema: AdvisoryCandidatesSchema },
126
- );
127
- const candidates = (found?.candidates ?? []).slice(0, PER_LENS).map((candidate) => ({ ...candidate, lens }));
128
- rawCandidateCount += candidates.length;
129
- progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: candidates.length });
130
- for (const candidate of candidates) {
131
- progress({
132
- type: "lane_item",
133
- lane: "Hypotheses",
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 });
68
+ const pipelineResult = await runLensVerificationPipeline({
69
+ api,
70
+ lenses: HYPOTHESIS_LENSES,
71
+ perLens: PER_LENS,
72
+ finderPhase: "Hypothesize",
73
+ finderPrompt: (lens) =>
74
+ `## Diagnose hypothesis generator — ${lens.label}\n\n${scopeBlock}\n` +
75
+ "This workflow is advisory-only: diagnose and recommend validation/fix plans, but do not edit files.\n" +
76
+ `Consider ONLY this hypothesis lens:\n${lens.text}\n\n` +
77
+ `Surface up to ${PER_LENS} root-cause hypotheses. Use category exactly "${lens.category}". ` +
78
+ "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.",
79
+ verifierPrompt: (candidate) =>
80
+ `## Diagnose verifier\n\n${scopeBlock}\n## Hypothesis\n` +
81
+ `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
82
+ `Recommended validation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
83
+ "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. " +
84
+ "Do not run mutation, install, commit, network, or destructive commands. Return CONFIRMED, PLAUSIBLE, NOT_SUBSTANTIATED, or REFUTED with evidence. " +
85
+ "Use NOT_SUBSTANTIATED when evidence is missing; REFUTED requires disproof. Structured output only.",
147
86
  });
148
-
149
- phase("Verify");
150
- const verified = compactResults(
151
- await parallel(
152
- hypotheses.map((hypothesis) => async (): Promise<Verified | null> => {
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
-
179
- const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
180
- const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
181
- const stats = makeStats(verified.length, surviving.length);
182
- publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
183
-
184
- if (surviving.length === 0) {
185
- return emptyAdvisoryReport(
186
- "No root-cause hypothesis survived verification.",
187
- ["Capture the exact failing command and error output.", "Rerun diagnose with a narrower symptom or more evidence."],
188
- stats,
189
- );
190
- }
191
-
192
- phase("Synthesize");
193
- const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
194
- const block = ranked
195
- .map(
196
- (finding, index) =>
197
- `### [${index}] ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
198
- `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\nValidation/fix plan: ${finding.recommendation ?? "(none supplied)"}`,
199
- )
200
- .join("\n\n");
201
- const refutedBlock = refuted
202
- .slice(0, 8)
203
- .map((finding) => `- ${finding.summary} — REFUTED because ${formatEvidence(finding.evidence)}`)
204
- .join("\n");
205
-
206
- const report = await agent(
207
- `## Synthesis: final diagnosis report\n\n${ranked.length} hypotheses survived independent verification.\n\n${block}\n\n` +
208
- `## Refuted hypotheses for context\n${refutedBlock || "(none recorded)"}\n\n` +
209
- "Produce the shared advisory report shape. Only include confirmed or plausible root causes in findings. " +
210
- "Use categories such as root-cause, regression, configuration, dependency, or test-fixture. " +
211
- "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,
87
+ const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, pipelineResult.coverage);
88
+ return concludeLensReview(api, pipelineResult, verified, {
89
+ files: scope.files.length,
90
+ rank,
91
+ formatFinding: (finding, index) => formatRankedFinding(finding, index, "Validation/fix plan"),
92
+ empty: {
93
+ summary: "No root-cause hypothesis survived verification.",
94
+ nextSteps: ["Capture the exact failing command and error output.", "Rerun diagnose with a narrower symptom or more evidence."],
95
+ },
96
+ synthesisPrompt: (block, ranked, refuted) => {
97
+ const refutedBlock = refuted
98
+ .slice(0, 8)
99
+ .map((finding) => `- ${finding.summary} — REFUTED because ${formatEvidence(finding.evidence)}`)
100
+ .join("\n");
101
+ return `## Synthesis: final diagnosis report\n\n${ranked.length} hypotheses survived independent verification.\n\n${block}\n\n` +
102
+ `## Refuted hypotheses for context\n${refutedBlock || "(none recorded)"}\n\n` +
103
+ "Select confirmed, plausible or explicitly unresolved root causes by ID. " +
104
+ "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.";
219
105
  },
220
- );
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
106
  });
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;
243
107
  }
244
108
 
245
- function rank(finding: Verified): number {
109
+ function rank(finding: AdvisoryVerified): number {
246
110
  if (finding.verdict === "CONFIRMED") return 0;
247
111
  return 1;
248
112
  }
@@ -1,25 +1,23 @@
1
+ import { challengeFindings, parseChallengeArgs } from "../src/advisory-challenge.ts";
1
2
  import { Type } from "typebox";
2
3
  import {
3
- AdvisoryReportSchema,
4
- type AdvisoryCandidate,
5
- type AdvisoryVerdict,
6
- } from "../src/advisory-schema.ts";
7
- import {
8
- backfillAdvisoryFindings,
4
+ type AdvisoryVerified,
5
+ type AdvisoryLens,
6
+ concludeLensReview,
7
+ finishAdvisoryReport,
9
8
  emptyAdvisoryReport,
10
- formatEvidence,
9
+ EMPTY_LENS_REVIEW_STATS,
11
10
  formatLocation,
12
- publishVerifiedKeptProgress,
13
11
  runLensVerificationPipeline,
14
12
  DEFAULT_ADVISORY_TOOL_HINTS,
15
13
  DEFAULT_ADVISORY_TOOLS,
16
14
  } from "../src/workflow-advisory-utils.ts";
17
- import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
15
+ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
18
16
 
19
17
  export const meta: WorkflowMeta = {
20
18
  name: "perf-review",
21
19
  description: "Advisory-only performance review: scope slow path → per-lens bottleneck hypotheses → verify evidence → synthesize measurements and safe optimizations.",
22
- phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Synthesize" }],
20
+ phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
23
21
  };
24
22
 
25
23
  const ScopeSchema = Type.Object({
@@ -30,22 +28,7 @@ const ScopeSchema = Type.Object({
30
28
  knownMeasurements: Type.Optional(Type.String({ description: "Existing measurements, timings, or explicit lack of measurements." })),
31
29
  });
32
30
 
33
- interface PerfLens {
34
- label: string;
35
- category: string;
36
- text: string;
37
- }
38
-
39
- type Candidate = AdvisoryCandidate;
40
-
41
- interface Verified extends Candidate {
42
- verdict: AdvisoryVerdict["verdict"];
43
- evidence: string[];
44
- confidence?: AdvisoryVerdict["confidence"];
45
- lens: PerfLens;
46
- }
47
-
48
- const PERF_LENSES: PerfLens[] = [
31
+ const PERF_LENSES: AdvisoryLens[] = [
49
32
  { label: "algorithmic", category: "algorithmic", text: "Complexity, repeated scans, avoidable nested loops, or data-structure choices that grow poorly with input size." },
50
33
  { label: "io", category: "io", text: "Filesystem, subprocess, network, or other I/O costs on hot paths or startup paths." },
51
34
  { label: "concurrency", category: "concurrency", text: "Unnecessary serialization, missing batching, excessive fan-out, contention, or concurrency limits." },
@@ -54,25 +37,12 @@ const PERF_LENSES: PerfLens[] = [
54
37
  { label: "measurement", category: "measurement", text: "Missing, misleading, noisy, or insufficient benchmark/measurement design." },
55
38
  ];
56
39
 
57
- const TOOLS = DEFAULT_ADVISORY_TOOLS;
58
- const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
59
40
  const PER_LENS = 4;
60
41
 
61
42
  export default async function run(api: WorkflowApi): Promise<unknown> {
62
- const { agent, parallel, pipeline, phase, log, progress, args } = api;
63
- const target = args.trim() || "repository performance";
64
- let fileCount = 0;
65
- let rawCandidateCount = 0;
66
- let droppedCandidateCount = 0;
67
- let refutedCandidateCount = 0;
68
- const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
69
- files: fileCount,
70
- candidates: rawCandidateCount,
71
- verified,
72
- kept,
73
- dropped: droppedCandidateCount,
74
- refuted: refutedCandidateCount,
75
- });
43
+ const { agent, phase, log, progress, args } = api;
44
+ const challengeConfig = parseChallengeArgs(args);
45
+ const target = challengeConfig.args || "repository performance";
76
46
 
77
47
  phase("Scope");
78
48
  const scope = await agent(
@@ -81,19 +51,18 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
81
51
  "Inspect repository structure, scripts, likely hot-path files, and any existing benchmark or measurement commands. " +
82
52
  "Prefer identifying what to measure before claiming bottlenecks. Return files, commands, summary, and known measurements or the lack of them. " +
83
53
  `This workflow will fan out across ${PERF_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
84
- { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
54
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
85
55
  );
86
56
 
87
- if (!scope || scope.files.length === 0) {
88
- return emptyAdvisoryReport(
57
+ if (scope.files.length === 0) {
58
+ return finishAdvisoryReport(emptyAdvisoryReport(
89
59
  "No performance-relevant files were identified.",
90
60
  ["Provide a slow command, workload, file path, or user-visible latency concern to review."],
91
- makeStats(0, 0),
92
- );
61
+ EMPTY_LENS_REVIEW_STATS,
62
+ ), []);
93
63
  }
94
64
 
95
- fileCount = scope.files.length;
96
- progress({ type: "counter", key: "files", label: "files", value: fileCount });
65
+ progress({ type: "counter", key: "files", label: "files", value: scope.files.length });
97
66
  progress({ type: "summary", key: "target", value: scope.target });
98
67
  progress({ type: "summary", key: "files", value: scope.files.join(", ") });
99
68
  log(`${scope.files.length} files scoped for performance review`);
@@ -102,16 +71,12 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
102
71
  `## Target\n${scope.target}\n\n## Files\n${scope.files.map((file) => `- ${file}`).join("\n")}\n\n` +
103
72
  `## Measurement commands\n${scope.commands.map((command) => `- ${command}`).join("\n") || "(none identified)"}\n\n` +
104
73
  `## Summary\n${scope.summary}\n\n## Known measurements\n${scope.knownMeasurements ?? "(none known)"}\n` +
105
- (args.trim() ? `\n## User instructions (verbatim)\n${args.trim()}\n` : "");
74
+ (challengeConfig.args ? `\n## User instructions (verbatim)\n${challengeConfig.args}\n` : "");
106
75
 
107
- phase("Find");
108
76
  const pipelineResult = await runLensVerificationPipeline({
109
- api: { agent, parallel, pipeline, progress, log },
77
+ api,
110
78
  lenses: PERF_LENSES,
111
- schedulingMode: "finder-barrier",
112
79
  perLens: PER_LENS,
113
- tools: TOOLS,
114
- toolHints: TOOL_HINTS,
115
80
  finderPrompt: (lens) =>
116
81
  `## Perf-review finder — ${lens.label}\n\n${scopeBlock}\n` +
117
82
  "This workflow is advisory-only: identify bottleneck hypotheses, measurement gaps, and safe optimization directions, but do not edit files.\n" +
@@ -124,67 +89,26 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
124
89
  `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
125
90
  `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
126
91
  "Read relevant files and package/scripts. Run only safe read-only measurement or inspection commands when useful. " +
127
- "Return CONFIRMED, PLAUSIBLE, or REFUTED with evidence from code, scripts, config, or measurement output. " +
92
+ "Return CONFIRMED, PLAUSIBLE, NOT_SUBSTANTIATED, or REFUTED with evidence from code, scripts, config, or measurement output. " +
128
93
  "Default toward PLAUSIBLE or REFUTED when no measurement exists; do not overstate a bottleneck. Structured output only.",
129
- makeVerified: (candidate, lens, judged): Verified => ({
130
- ...candidate,
131
- verdict: judged.verdict,
132
- evidence: judged.evidence,
133
- confidence: judged.confidence,
134
- lens,
135
- }),
136
94
  });
137
- rawCandidateCount += pipelineResult.rawCandidates;
138
- droppedCandidateCount += pipelineResult.dropped;
139
- refutedCandidateCount += pipelineResult.refuted;
140
- const verified = pipelineResult.verified;
141
- const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
142
- const stats = makeStats(verified.length, surviving.length);
143
- publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
144
-
145
- if (surviving.length === 0) {
146
- return emptyAdvisoryReport(
147
- "No performance finding survived verification.",
148
- ["Add or run a focused measurement for the target workload before optimizing.", "Rerun perf-review with benchmark output or a narrower slow path."],
149
- stats,
150
- );
151
- }
152
-
153
- phase("Synthesize");
154
- const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
155
- const block = ranked
156
- .map(
157
- (finding, index) =>
158
- `### [${index}] ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
159
- `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\nRecommendation: ${finding.recommendation ?? "(none supplied)"}`,
160
- )
161
- .join("\n\n");
162
-
163
- const report = await agent(
164
- `## Synthesis: final perf-review report\n\n${ranked.length} candidates survived independent verification.\n\n${block}\n\n` +
165
- "Produce the shared advisory report shape. Categories should be algorithmic, io, concurrency, startup, allocation, or measurement when applicable. " +
95
+ const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, pipelineResult.coverage);
96
+ return concludeLensReview(api, pipelineResult, verified, {
97
+ files: scope.files.length,
98
+ rank,
99
+ empty: {
100
+ summary: "No performance finding survived verification.",
101
+ nextSteps: ["Add or run a focused measurement for the target workload before optimizing.", "Rerun perf-review with benchmark output or a narrower slow path."],
102
+ },
103
+ synthesisPrompt: (block, ranked) =>
104
+ `## Synthesis: final perf-review report\n\n${ranked.length} candidates survived independent verification.\n\n${block}\n\n` +
105
+ "Select findings by ID. " +
166
106
  "Severity is expected performance impact for the target workload. Prefer measurement recommendations before optimization recommendations when evidence is weak. " +
167
107
  "Recommendations must be safe advisory next actions, not patches. Include risky optimizations to avoid in recommendations or nextSteps when relevant. Structured output only.",
168
- {
169
- phase: "Synthesize",
170
- label: "synthesize",
171
- tools: [],
172
- profile: "medium",
173
- resume: "read-only",
174
- schema: AdvisoryReportSchema,
175
- },
176
- );
177
-
178
- if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun perf-review with a narrower target."], stats);
179
-
180
- const findings = backfillAdvisoryFindings(report.findings, ranked, {
181
- impact: "Performance impact not restated by synthesis.",
182
- recommendation: "Measure the target workload before changing code.",
183
108
  });
184
- return { ...report, findings, stats };
185
109
  }
186
110
 
187
- function rank(finding: Verified): number {
111
+ function rank(finding: AdvisoryVerified): number {
188
112
  const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
189
113
  const measurementPenalty = finding.category === "measurement" ? 1 : 0;
190
114
  return verdictRank + measurementPenalty;