pi-workflow-engine 0.1.1 → 0.2.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/package.json +2 -1
- package/src/agent-runner.ts +8 -4
- package/src/engine.ts +18 -2
- package/src/index.ts +79 -14
- package/src/progress.ts +258 -23
- package/src/types.ts +29 -0
- package/src/ui/workflow-format.ts +83 -0
- package/src/ui/workflow-inspector.ts +248 -0
- package/src/ui/workflow-result-renderer.ts +149 -0
- package/src/ui/workflow-widget.ts +143 -0
- package/workflows/code-review.ts +103 -10
package/workflows/code-review.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
3
|
+
import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
|
|
4
4
|
|
|
5
5
|
export const meta: WorkflowMeta = {
|
|
6
6
|
name: "code-review",
|
|
@@ -41,6 +41,8 @@ const ReportSchema = Type.Object({
|
|
|
41
41
|
severity: Type.Union([Type.Literal("bug"), Type.Literal("cleanup")]),
|
|
42
42
|
verdict: Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE")]),
|
|
43
43
|
summary: Type.String(),
|
|
44
|
+
evidence: Type.Optional(Type.String()),
|
|
45
|
+
failure_scenario: Type.Optional(Type.String()),
|
|
44
46
|
}),
|
|
45
47
|
),
|
|
46
48
|
});
|
|
@@ -113,8 +115,18 @@ export function inDiff(changed: Map<string, Set<number>>, file: string, line?: n
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
116
|
-
const { agent, parallel, pipeline, phase, log, args, cwd } = api;
|
|
118
|
+
const { agent, parallel, pipeline, phase, log, progress, args, cwd } = api;
|
|
117
119
|
const target = args.trim();
|
|
120
|
+
let fileCount = 0;
|
|
121
|
+
let rawCandidateCount = 0;
|
|
122
|
+
let droppedCandidateCount = 0;
|
|
123
|
+
const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
|
|
124
|
+
files: fileCount,
|
|
125
|
+
candidates: rawCandidateCount,
|
|
126
|
+
verified,
|
|
127
|
+
kept,
|
|
128
|
+
dropped: droppedCandidateCount,
|
|
129
|
+
});
|
|
118
130
|
|
|
119
131
|
// ─── Phase 0: Scope ───
|
|
120
132
|
phase("Scope");
|
|
@@ -137,8 +149,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
137
149
|
{ phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
|
|
138
150
|
);
|
|
139
151
|
|
|
140
|
-
if (!scope
|
|
141
|
-
return { summary: "No changes found to review.", findings: [], stats:
|
|
152
|
+
if (!scope) {
|
|
153
|
+
return { summary: "No changes found to review.", findings: [], stats: makeStats(0, 0) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fileCount = scope.files.length;
|
|
157
|
+
progress({ type: "summary", key: "files", value: scope.files.join(", ") || "(none)" });
|
|
158
|
+
progress({ type: "summary", key: "diffCommand", value: scope.diffCommand });
|
|
159
|
+
progress({ type: "counter", key: "files", label: "files", value: fileCount });
|
|
160
|
+
|
|
161
|
+
if (scope.files.length === 0) {
|
|
162
|
+
return { summary: "No changes found to review.", findings: [], stats: makeStats(0, 0) };
|
|
142
163
|
}
|
|
143
164
|
log(`${scope.files.length} changed files`);
|
|
144
165
|
|
|
@@ -192,10 +213,27 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
192
213
|
{ phase: "Find", label: `find:${angle.label}`, tools: TOOLS, thinkingLevel: "low", schema: CandidatesSchema },
|
|
193
214
|
);
|
|
194
215
|
const raw = (found?.candidates ?? []).slice(0, PER_ANGLE);
|
|
216
|
+
rawCandidateCount += raw.length;
|
|
217
|
+
progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
|
|
195
218
|
const gate = changed;
|
|
196
219
|
const bounded = gate ? raw.filter((candidate) => inDiff(gate, candidate.file, candidate.line)) : raw;
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
const dropped = raw.length - bounded.length;
|
|
221
|
+
if (dropped > 0) {
|
|
222
|
+
droppedCandidateCount += dropped;
|
|
223
|
+
progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
224
|
+
}
|
|
225
|
+
if (gate && dropped > 0) {
|
|
226
|
+
log(`find:${angle.label}: dropped ${dropped} out-of-diff candidate(s)`);
|
|
227
|
+
}
|
|
228
|
+
for (const candidate of bounded) {
|
|
229
|
+
progress({
|
|
230
|
+
type: "lane_item",
|
|
231
|
+
lane: "Candidates",
|
|
232
|
+
title: candidate.summary,
|
|
233
|
+
subtitle: formatLocation(candidate),
|
|
234
|
+
status: "pending",
|
|
235
|
+
details: candidate.failure_scenario,
|
|
236
|
+
});
|
|
199
237
|
}
|
|
200
238
|
return { angle, candidates: bounded };
|
|
201
239
|
},
|
|
@@ -218,7 +256,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
218
256
|
"with evidence quoting the line(s). Default toward REFUTED if you cannot substantiate it. Structured output only.",
|
|
219
257
|
{ phase: "Verify", label: `verify:${candidate.file.split("/").pop() ?? candidate.file}`, tools: TOOLS, thinkingLevel: "low", schema: VerdictSchema },
|
|
220
258
|
);
|
|
221
|
-
|
|
259
|
+
if (!judged) return null;
|
|
260
|
+
progress({ type: "counter_delta", key: `verdict.${judged.verdict.toLowerCase()}`, label: judged.verdict, delta: 1 });
|
|
261
|
+
progress({
|
|
262
|
+
type: "lane_item",
|
|
263
|
+
lane: verdictLane(judged.verdict),
|
|
264
|
+
title: candidate.summary,
|
|
265
|
+
subtitle: formatLocation(candidate),
|
|
266
|
+
status: verdictStatus(judged.verdict),
|
|
267
|
+
details: judged.evidence,
|
|
268
|
+
});
|
|
269
|
+
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, kind: angle.kind };
|
|
222
270
|
}),
|
|
223
271
|
);
|
|
224
272
|
return verdicts.filter((value): value is Verified => value !== null);
|
|
@@ -227,10 +275,15 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
227
275
|
|
|
228
276
|
const verified = (perAngle as Verified[][]).flat();
|
|
229
277
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
278
|
+
const stats = makeStats(verified.length, surviving.length);
|
|
279
|
+
progress({ type: "counter", key: "verified", label: "verified", value: verified.length });
|
|
280
|
+
progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
|
|
281
|
+
progress({ type: "summary", key: "verified", value: verified.length });
|
|
282
|
+
progress({ type: "summary", key: "kept", value: surviving.length });
|
|
230
283
|
log(`${verified.length} verified → ${surviving.length} kept`);
|
|
231
284
|
|
|
232
285
|
if (surviving.length === 0) {
|
|
233
|
-
return { summary: "No findings survived verification.", findings: [], stats
|
|
286
|
+
return { summary: "No findings survived verification.", findings: [], stats };
|
|
234
287
|
}
|
|
235
288
|
|
|
236
289
|
// ─── Synthesize: rank, merge, report ───
|
|
@@ -247,9 +300,49 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
247
300
|
|
|
248
301
|
const report = await agent(
|
|
249
302
|
`## Synthesis: final code-review report\n\n${ranked.length} findings survived independent verification.\n\n${block}\n\n` +
|
|
250
|
-
"Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final report. Structured output only.",
|
|
303
|
+
"Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final report. Include evidence and failure_scenario for each finding when available. Structured output only.",
|
|
251
304
|
{ phase: "Synthesize", label: "synthesize", thinkingLevel: "medium", schema: ReportSchema },
|
|
252
305
|
);
|
|
253
306
|
|
|
254
|
-
|
|
307
|
+
if (!report) return { summary: "Synthesis produced no output.", findings: [], stats };
|
|
308
|
+
|
|
309
|
+
const findings = report.findings.map((finding) => {
|
|
310
|
+
const source = ranked.find((candidate) => sameFinding(candidate, finding));
|
|
311
|
+
return {
|
|
312
|
+
...finding,
|
|
313
|
+
evidence: finding.evidence ?? source?.evidence,
|
|
314
|
+
failure_scenario: finding.failure_scenario ?? source?.failure_scenario,
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
return { ...report, findings, stats };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatLocation(candidate: Pick<Candidate, "file" | "line">): string {
|
|
321
|
+
return `${candidate.file}${candidate.line != null ? `:${candidate.line}` : ""}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function verdictLane(verdict: Verified["verdict"]): string {
|
|
325
|
+
switch (verdict) {
|
|
326
|
+
case "CONFIRMED":
|
|
327
|
+
return "Confirmed";
|
|
328
|
+
case "PLAUSIBLE":
|
|
329
|
+
return "Plausible";
|
|
330
|
+
case "REFUTED":
|
|
331
|
+
return "Refuted";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function verdictStatus(verdict: Verified["verdict"]): "success" | "warning" | "error" {
|
|
336
|
+
switch (verdict) {
|
|
337
|
+
case "CONFIRMED":
|
|
338
|
+
return "success";
|
|
339
|
+
case "PLAUSIBLE":
|
|
340
|
+
return "warning";
|
|
341
|
+
case "REFUTED":
|
|
342
|
+
return "error";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function sameFinding(candidate: Verified, finding: { file: string; line?: number; summary: string }): boolean {
|
|
347
|
+
return normalizePath(candidate.file) === normalizePath(finding.file) && candidate.line === finding.line;
|
|
255
348
|
}
|