pi-workflow-engine 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/pi-workflow-engine/index.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +114 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
- package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
- package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-result-renderer.ts +11 -8
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/workflows.ts +7 -0
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +57 -125
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/diagnose.ts +12 -73
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/perf-review.ts +41 -148
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/ping.ts +1 -0
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/refactor-scout.ts +43 -151
- package/.pi/settings.json +3 -0
- package/README.md +70 -23
- package/assets/preview-candidate-core.png +0 -0
- package/assets/preview-candidate-relic-emblem.png +0 -0
- package/assets/preview-candidate-robot-squad.png +0 -0
- package/assets/preview.png +0 -0
- package/package.json +32 -6
- package/src/agent-runner.ts +0 -122
- package/src/concurrency.ts +0 -55
- package/src/discovery.ts +0 -54
- package/src/index.ts +0 -141
- package/src/ui/workflow-widget.ts +0 -143
- /package/{src → .pi/extensions/pi-workflow-engine/src}/advisory-schema.ts +0 -0
|
@@ -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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
}
|
|
@@ -18,6 +18,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
18
18
|
phase: "Ping",
|
|
19
19
|
label: "ping",
|
|
20
20
|
tools: [],
|
|
21
|
+
thinkingLevel: "low",
|
|
21
22
|
schema: Type.Object({
|
|
22
23
|
headline: Type.String({ description: "One word" }),
|
|
23
24
|
summary: Type.String({ description: "One sentence" }),
|
|
@@ -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 /
|
|
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 /
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
}
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-workflow-engine
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Programmable multi-agent workflows for the [pi](https://pi.dev) coding agent.
|
|
6
6
|
|
|
7
7
|
This is not just a way to run a prompt. It is a way to turn an agentic procedure into code: scope the task, fork isolated subagents, fan out across review lenses, validate every handoff with schemas, verify candidate findings, and synthesize one ranked result.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ The built-in workflows are advisory: they inspect, fan out, verify, and report.
|
|
|
10
10
|
|
|
11
11
|
## Why workflows matter
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The best agentic coding work is rarely a single chat turn. It is a repeatable loop of context gathering, delegated investigation, structured handoffs, verification, and synthesis.
|
|
14
14
|
|
|
15
15
|
`pi-workflow-engine` brings that shape to pi with static TypeScript workflows:
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ Or from npm:
|
|
|
35
35
|
pi install npm:pi-workflow-engine
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
That's all. pi fetches the package and serves its core dependencies from its own bundle, so there is no clone, install, or build step. Restart pi, or run `/reload` in an open session, then confirm it is registered:
|
|
38
|
+
That's all. pi fetches the package and serves its core dependencies from its own bundle, so there is no clone, install, or build step. The package entrypoint is the canonical pi extension module at `.pi/extensions/pi-workflow-engine/index.ts`. Restart pi, or run `/reload` in an open session, then confirm it is registered:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
pi list
|
|
@@ -93,7 +93,7 @@ The bundled review workflow is deliberately shaped like a serious review process
|
|
|
93
93
|
4. **Verify**: send each survivor to an independent verifier that must confirm, mark plausible, or refute with evidence.
|
|
94
94
|
5. **Synthesize**: produce one ranked report with stats, verdicts, and concrete locations.
|
|
95
95
|
|
|
96
|
-
The review lenses live in
|
|
96
|
+
The review lenses live in `.pi/extensions/pi-workflow-engine/workflows/code-review.ts`. That is where your repo's real failure modes belong.
|
|
97
97
|
|
|
98
98
|
## Authoring workflows
|
|
99
99
|
|
|
@@ -151,36 +151,83 @@ git clone https://github.com/timbrinded/pi-workflow-engine
|
|
|
151
151
|
cd pi-workflow-engine
|
|
152
152
|
bun install
|
|
153
153
|
bun run typecheck
|
|
154
|
-
bun
|
|
154
|
+
bun run test
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
The test suite is no-LLM and uses Bun's built-in `bun test` runner, not a third-party test framework.
|
|
158
|
+
|
|
159
|
+
### Performance controls and benchmarks
|
|
160
|
+
|
|
161
|
+
The workflow engine is measurement-first: tune only after checking queue wait, local orchestration, discovery, and UI render costs on the machine where you run pi.
|
|
162
|
+
|
|
163
|
+
Runtime controls:
|
|
164
|
+
|
|
165
|
+
- `PI_WORKFLOW_PERF=1` enables the internal per-run performance recorder used by the orchestration instrumentation.
|
|
166
|
+
- `PI_WORKFLOW_CONCURRENCY=N` sets the per-run agent semaphore cap. The default remains `min(8, max(2, CPU count))`.
|
|
167
|
+
- `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` limits how many `parallel()` thunks are submitted at once; this is separate from the running-agent semaphore cap.
|
|
168
|
+
- `PI_WORKFLOW_LANE_ITEM_LIMIT=N` caps retained progress lane items per lane; snapshots report how many older items are hidden.
|
|
169
|
+
- Slash commands can override selected controls per run: `/workflow <name> --concurrency=N --parallel-limit=N ...`.
|
|
170
|
+
|
|
171
|
+
No-LLM benchmark scripts:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
bun run bench:concurrency -- --items 200 --concurrency 8 --json
|
|
175
|
+
bun run bench:discovery -- --iterations 3 --json
|
|
176
|
+
bun run bench:startup -- --json
|
|
177
|
+
bun run bench:ui -- --agents 1000 --lane-items 1000 --json
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Add `--out` to write machine-local JSON under `.artifacts/benchmarks/`. These timings are advisory, not portable thresholds. Optional LLM smoke timing is separate from the required gates:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pi -e . -p "/workflow ping"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Guardrails:
|
|
187
|
+
|
|
188
|
+
- Do not pool or reuse subagent sessions until `agent.create_session_ms` is proven material and isolation semantics are reviewed.
|
|
189
|
+
- Do not lazy-load guaranteed built-in workflow modules until pi/jiti bundled `typebox` identity is proven for that path.
|
|
190
|
+
- Do not raise the default concurrency without queue-wait versus run-time evidence.
|
|
191
|
+
|
|
192
|
+
Load your working copy through the package manifest without installing it. This is ephemeral and exercises the same `.pi/extensions/pi-workflow-engine/index.ts` entrypoint that installed packages use:
|
|
158
193
|
|
|
159
194
|
```bash
|
|
160
|
-
pi -e
|
|
195
|
+
pi -e . -p "/workflow ping"
|
|
161
196
|
```
|
|
162
197
|
|
|
163
|
-
|
|
198
|
+
This repo also includes `.pi/settings.json` for project-local auto-discovery. If you also have the global package installed, pi may report duplicate `/workflow` or `workflow` tool diagnostics; remove one source or force pi to ignore discovered extensions and load this working copy instead:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pi -ne -e .
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Add a built-in workflow by creating `.pi/extensions/pi-workflow-engine/workflows/<name>.ts`, importing it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`, and adding it to `BUILTIN_WORKFLOWS`. Statically imported workflows share pi's bundled `typebox`, which guarantees schema validation. Files in `.pi/extensions/pi-workflow-engine/workflows/` and `~/.pi/agent/workflows/` are also discovered dynamically at runtime on a best-effort basis.
|
|
205
|
+
|
|
206
|
+
Keep guaranteed built-ins statically imported. Optimize cold startup at the extension entrypoint or discovery boundary first; do not lazy-load built-in workflow modules unless a test proves pi/jiti preserves the same bundled `typebox` identity for dynamically imported built-ins.
|
|
164
207
|
|
|
165
|
-
Tune the built-in review workflow by editing the `ANGLES` array in
|
|
208
|
+
Tune the built-in review workflow by editing the `ANGLES` array in `.pi/extensions/pi-workflow-engine/workflows/code-review.ts`. Tune `model`, `thinkingLevel`, and `tools` per `agent()` call, and use `PI_WORKFLOW_CONCURRENCY` or `/workflow <name> --concurrency=N` for measured per-run concurrency experiments.
|
|
166
209
|
|
|
167
210
|
### Layout
|
|
168
211
|
|
|
169
212
|
```text
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
.pi/
|
|
214
|
+
settings.json project-local pi resource settings
|
|
215
|
+
extensions/pi-workflow-engine/
|
|
216
|
+
index.ts canonical extension entry; registers the command and tool
|
|
217
|
+
src/
|
|
218
|
+
types.ts WorkflowApi / WorkflowModule contracts
|
|
219
|
+
agent-runner.ts createAgentSession + terminating-tool schema bridge
|
|
220
|
+
concurrency.ts Semaphore, parallel(), pipeline()
|
|
221
|
+
engine.ts runWorkflow(); binds primitives to one run
|
|
222
|
+
progress.ts live phase/agent tree via ctx.ui.setWidget
|
|
223
|
+
discovery.ts static registry + best-effort drop-in loading
|
|
224
|
+
workflows.ts statically registered built-in workflows
|
|
225
|
+
workflows/
|
|
226
|
+
code-review.ts scope -> find -> verify -> synthesize
|
|
227
|
+
refactor-scout.ts advisory refactor opportunities
|
|
228
|
+
diagnose.ts advisory bug diagnosis
|
|
229
|
+
perf-review.ts advisory performance investigation
|
|
230
|
+
ping.ts minimal smoke workflow
|
|
184
231
|
```
|
|
185
232
|
|
|
186
233
|
## License
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|