@osolmaz/pi-workflows 0.9.0 → 0.10.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 (78) hide show
  1. package/README.md +50 -16
  2. package/dist/builtins/autodevise.workflow.d.ts +58 -0
  3. package/dist/builtins/autodevise.workflow.js +190 -0
  4. package/dist/builtins/autodevise.workflow.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +154 -0
  6. package/dist/builtins/autoimplement.workflow.js +729 -0
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -0
  8. package/dist/builtins/catalog.js +5 -1
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +3 -0
  11. package/dist/builtins/index.js +4 -0
  12. package/dist/builtins/index.js.map +1 -0
  13. package/dist/builtins/monitor.workflow.d.ts +25 -3
  14. package/dist/builtins/monitor.workflow.js +200 -13
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/extension/herdr-viewer.js +2 -6
  17. package/dist/extension/herdr-viewer.js.map +1 -1
  18. package/dist/render/graph-render.js +13 -2
  19. package/dist/render/graph-render.js.map +1 -1
  20. package/dist/workflows/catalog.d.ts +1 -0
  21. package/dist/workflows/catalog.js +6 -0
  22. package/dist/workflows/catalog.js.map +1 -1
  23. package/dist/workflows/composition.d.ts +45 -0
  24. package/dist/workflows/composition.js +471 -0
  25. package/dist/workflows/composition.js.map +1 -0
  26. package/dist/workflows/decision.d.ts +11 -5
  27. package/dist/workflows/decision.js.map +1 -1
  28. package/dist/workflows/definition.d.ts +22 -3
  29. package/dist/workflows/definition.js +46 -3
  30. package/dist/workflows/definition.js.map +1 -1
  31. package/dist/workflows/engine.js +115 -16
  32. package/dist/workflows/engine.js.map +1 -1
  33. package/dist/workflows/graph.js +8 -6
  34. package/dist/workflows/graph.js.map +1 -1
  35. package/dist/workflows/index.d.ts +3 -2
  36. package/dist/workflows/index.js +2 -1
  37. package/dist/workflows/index.js.map +1 -1
  38. package/dist/workflows/loader.d.ts +5 -4
  39. package/dist/workflows/loader.js +118 -18
  40. package/dist/workflows/loader.js.map +1 -1
  41. package/dist/workflows/schema.d.ts +3 -1
  42. package/dist/workflows/schema.js +49 -2
  43. package/dist/workflows/schema.js.map +1 -1
  44. package/dist/workflows/store.js +32 -2
  45. package/dist/workflows/store.js.map +1 -1
  46. package/dist/workflows/types.d.ts +77 -2
  47. package/docs/CONTROLLERS.md +1 -1
  48. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  49. package/docs/MONITOR.md +35 -18
  50. package/docs/WORKFLOW_COMPOSITION.md +326 -0
  51. package/docs/plans/2026-08-19-workflow-composition-plan.md +300 -0
  52. package/docs/run-bundles.md +24 -10
  53. package/docs/workflows.md +65 -12
  54. package/examples/workflows/autodevise.workflow.ts +1 -0
  55. package/examples/workflows/autoimplement.workflow.ts +1 -92
  56. package/herdr-plugin.toml +1 -1
  57. package/package.json +5 -1
  58. package/skills/monitor/SKILL.md +6 -1
  59. package/skills/pi-workflows/SKILL.md +3 -1
  60. package/src/builtins/autodevise.workflow.ts +231 -0
  61. package/src/builtins/autoimplement.workflow.ts +856 -0
  62. package/src/builtins/catalog.ts +5 -1
  63. package/src/builtins/index.ts +13 -0
  64. package/src/builtins/monitor.workflow.ts +242 -15
  65. package/src/extension/herdr-viewer.ts +1 -6
  66. package/src/render/graph-render.ts +14 -2
  67. package/src/workflows/catalog.ts +7 -0
  68. package/src/workflows/composition.ts +627 -0
  69. package/src/workflows/decision.ts +12 -5
  70. package/src/workflows/definition.ts +118 -8
  71. package/src/workflows/engine.ts +151 -18
  72. package/src/workflows/graph.ts +8 -6
  73. package/src/workflows/index.ts +20 -0
  74. package/src/workflows/loader.ts +186 -18
  75. package/src/workflows/schema.ts +62 -2
  76. package/src/workflows/store.ts +37 -2
  77. package/src/workflows/types.ts +109 -2
  78. package/examples/workflows/elegant-solution.workflow.ts +0 -95
@@ -0,0 +1,856 @@
1
+ import path from "node:path";
2
+ import {
3
+ agent,
4
+ compute,
5
+ defineWorkflow,
6
+ includeWorkflow,
7
+ includedResult,
8
+ shell,
9
+ } from "../workflows/definition.js";
10
+ import type {
11
+ ShellActionExecution,
12
+ ShellActionResult,
13
+ WorkflowNodeContext,
14
+ } from "../workflows/types.js";
15
+ import autodeviseWorkflow, { type AutodeviseInput } from "./autodevise.workflow.js";
16
+
17
+ export type AutoimplementInput = {
18
+ task: string;
19
+ plan?: unknown;
20
+ scope?: string;
21
+ constraints?: string[];
22
+ repository?: string;
23
+ baseBranch?: string;
24
+ merge?: boolean;
25
+ };
26
+
27
+ type StructuredCommand = {
28
+ command: string;
29
+ args: string[];
30
+ cwd: string;
31
+ timeoutMs: number;
32
+ };
33
+
34
+ type ReviewFinding = {
35
+ severity: "P0" | "P1" | "P2" | "lower";
36
+ kind: "design" | "implementation";
37
+ summary: string;
38
+ };
39
+
40
+ type ReviewAssessment = {
41
+ route: "critical" | "p2" | "clean" | "command_error";
42
+ invocationSucceeded: boolean;
43
+ p0: ReviewFinding[];
44
+ p1: ReviewFinding[];
45
+ p2: ReviewFinding[];
46
+ lower: ReviewFinding[];
47
+ reason: string;
48
+ };
49
+
50
+ export type AutoimplementCompleted = {
51
+ status: "completed";
52
+ task: string;
53
+ plan: unknown;
54
+ implementation: unknown;
55
+ verification: unknown;
56
+ reviewRounds: ReviewAssessment[];
57
+ ci: unknown;
58
+ delivery: unknown;
59
+ };
60
+
61
+ export type AutoimplementBlocked = {
62
+ status: "blocked";
63
+ task: string;
64
+ reason: string;
65
+ evidence: unknown;
66
+ };
67
+
68
+ const FIVE_MINUTES_MS = 5 * 60_000;
69
+ const TEN_MINUTES_MS = 10 * 60_000;
70
+
71
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
72
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
73
+ throw new Error(`${label} must be an object`);
74
+ }
75
+ return value as Record<string, unknown>;
76
+ }
77
+
78
+ function requireString(value: unknown, label: string): string {
79
+ if (typeof value !== "string" || value.trim().length === 0) {
80
+ throw new Error(`${label} must be a non-empty string`);
81
+ }
82
+ return value.trim();
83
+ }
84
+
85
+ function parseInput(value: unknown): AutoimplementInput {
86
+ const input = requireRecord(value, "autoimplement input");
87
+ const constraints = input.constraints;
88
+ if (
89
+ constraints !== undefined &&
90
+ (!Array.isArray(constraints) || constraints.some((item) => typeof item !== "string"))
91
+ ) {
92
+ throw new Error("autoimplement constraints must be an array of strings");
93
+ }
94
+ if (input.merge !== undefined && typeof input.merge !== "boolean") {
95
+ throw new Error("autoimplement merge must be a boolean");
96
+ }
97
+ return {
98
+ task: requireString(input.task, "autoimplement task"),
99
+ ...(input.plan !== undefined ? { plan: input.plan } : {}),
100
+ ...(input.scope !== undefined ? { scope: requireString(input.scope, "scope") } : {}),
101
+ ...(constraints !== undefined ? { constraints: [...constraints] as string[] } : {}),
102
+ ...(input.repository !== undefined
103
+ ? { repository: requireString(input.repository, "repository") }
104
+ : {}),
105
+ ...(input.baseBranch !== undefined
106
+ ? { baseBranch: requireString(input.baseBranch, "baseBranch") }
107
+ : {}),
108
+ merge: input.merge === true,
109
+ };
110
+ }
111
+
112
+ function parseRoute<T extends string>(
113
+ value: unknown,
114
+ routes: readonly T[],
115
+ label: string,
116
+ ): Record<string, unknown> & { route: T } {
117
+ const record = requireRecord(value, label);
118
+ if (!routes.includes(record.route as T)) {
119
+ throw new Error(`${label} route must be one of ${routes.join(", ")}`);
120
+ }
121
+ return { ...record, route: record.route as T };
122
+ }
123
+
124
+ function parseCommand(
125
+ value: unknown,
126
+ options: {
127
+ command: string;
128
+ maxTimeoutMs: number;
129
+ validateArgs: (args: string[]) => boolean;
130
+ label: string;
131
+ },
132
+ ): StructuredCommand {
133
+ const command = requireRecord(value, options.label);
134
+ if (command.command !== options.command) {
135
+ throw new Error(`${options.label} command must be ${options.command}`);
136
+ }
137
+ if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
138
+ throw new Error(`${options.label} args must be an array of strings`);
139
+ }
140
+ const args = [...command.args] as string[];
141
+ if (!options.validateArgs(args)) throw new Error(`${options.label} args are not allowed`);
142
+ const cwd = requireString(command.cwd, `${options.label} cwd`);
143
+ if (!path.isAbsolute(cwd)) throw new Error(`${options.label} cwd must be absolute`);
144
+ const timeoutMs = command.timeoutMs;
145
+ if (
146
+ typeof timeoutMs !== "number" ||
147
+ !Number.isInteger(timeoutMs) ||
148
+ timeoutMs <= 0 ||
149
+ timeoutMs > options.maxTimeoutMs
150
+ ) {
151
+ throw new Error(`${options.label} timeoutMs must be at most ${options.maxTimeoutMs}`);
152
+ }
153
+ return { command: options.command, args, cwd, timeoutMs };
154
+ }
155
+
156
+ function parseReviewerCommand(value: unknown): StructuredCommand {
157
+ return parseCommand(value, {
158
+ command: "pi-reviewer",
159
+ maxTimeoutMs: TEN_MINUTES_MS,
160
+ label: "reviewer command",
161
+ validateArgs: (args) => {
162
+ const base = args.indexOf("--base");
163
+ return base >= 0 && typeof args[base + 1] === "string" && args[base + 1]!.length > 0;
164
+ },
165
+ });
166
+ }
167
+
168
+ function parseCiCommand(value: unknown): StructuredCommand {
169
+ return parseCommand(value, {
170
+ command: "gh",
171
+ maxTimeoutMs: FIVE_MINUTES_MS,
172
+ label: "CI tracking command",
173
+ validateArgs: (args) =>
174
+ (args[0] === "pr" && args[1] === "checks" && args.includes("--watch")) ||
175
+ (args[0] === "run" && args[1] === "watch"),
176
+ });
177
+ }
178
+
179
+ function commandExecution(command: StructuredCommand): ShellActionExecution {
180
+ return {
181
+ command: command.command,
182
+ args: command.args,
183
+ cwd: command.cwd,
184
+ timeoutMs: command.timeoutMs,
185
+ allowNonZeroExit: true,
186
+ maxOutputChars: 1_000_000,
187
+ };
188
+ }
189
+
190
+ function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
191
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
192
+ const step = context.state.steps[index];
193
+ if (step && nodeIds.includes(step.nodeId)) return step.output as T;
194
+ }
195
+ throw new Error(`No output found for ${nodeIds.join(" or ")}`);
196
+ }
197
+
198
+ function latestCiCommand(context: WorkflowNodeContext): StructuredCommand {
199
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
200
+ const step = context.state.steps[index];
201
+ if (!step) continue;
202
+ if (step.nodeId === "repairCiCommand") return step.output as StructuredCommand;
203
+ if (step.nodeId === "inspectCi") {
204
+ const output = step.output as { trackingCommand?: StructuredCommand };
205
+ if (output.trackingCommand !== undefined) return output.trackingCommand;
206
+ }
207
+ }
208
+ throw new Error("No CI tracking command is available");
209
+ }
210
+
211
+ function currentPlan(context: WorkflowNodeContext): unknown {
212
+ const adopted = context.outputs.adoptPlan as { plan?: unknown } | undefined;
213
+ return adopted?.plan ?? (context.input as AutoimplementInput).plan;
214
+ }
215
+
216
+ function latestIssue(context: WorkflowNodeContext): unknown {
217
+ const ids = [
218
+ "classifyImplementation",
219
+ "classifyVerification",
220
+ "triageReview",
221
+ "inspectComments",
222
+ "classifyCi",
223
+ "adoptPlan",
224
+ ];
225
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
226
+ const step = context.state.steps[index];
227
+ if (step && ids.includes(step.nodeId)) return step.output;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ function parseFinding(value: unknown, severity: ReviewFinding["severity"]): ReviewFinding {
233
+ const finding = requireRecord(value, `${severity} finding`);
234
+ if (finding.kind !== "design" && finding.kind !== "implementation") {
235
+ throw new Error(`${severity} finding kind must be design or implementation`);
236
+ }
237
+ return {
238
+ severity,
239
+ kind: finding.kind,
240
+ summary: requireString(finding.summary, `${severity} finding summary`),
241
+ };
242
+ }
243
+
244
+ function parseReviewAssessment(value: unknown): ReviewAssessment {
245
+ const review = requireRecord(value, "review assessment");
246
+ const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
247
+ const raw = review[key];
248
+ if (!Array.isArray(raw)) throw new Error(`review ${key} must be an array`);
249
+ return raw.map((item) => parseFinding(item, severity));
250
+ };
251
+ const p0 = parseList("p0", "P0");
252
+ const p1 = parseList("p1", "P1");
253
+ const p2 = parseList("p2", "P2");
254
+ const lower = parseList("lower", "lower");
255
+ const invocationSucceeded = review.invocationSucceeded === true;
256
+ const route = !invocationSucceeded
257
+ ? "command_error"
258
+ : p0.length + p1.length > 0
259
+ ? "critical"
260
+ : p2.length > 0
261
+ ? "p2"
262
+ : "clean";
263
+ return {
264
+ route,
265
+ invocationSucceeded,
266
+ p0,
267
+ p1,
268
+ p2,
269
+ lower,
270
+ reason: requireString(review.reason, "review reason"),
271
+ };
272
+ }
273
+
274
+ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
275
+ return context.state.steps
276
+ .filter((step) => step.nodeId === "assessReview" && step.outcome === "ok")
277
+ .map((step) => step.output as ReviewAssessment);
278
+ }
279
+
280
+ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
281
+ const candidates = [
282
+ "finalizeDelivery",
283
+ "inspectCi",
284
+ "assessTrackedCi",
285
+ "classifyCi",
286
+ "inspectComments",
287
+ "classifyImplementation",
288
+ "classifyVerification",
289
+ "triageReview",
290
+ "adoptPlan",
291
+ ];
292
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
293
+ const step = context.state.steps[index];
294
+ if (!step || !candidates.includes(step.nodeId)) continue;
295
+ const output = step.output as Record<string, unknown>;
296
+ const reason = output.reason ?? output.blocker ?? output.summary;
297
+ if (typeof reason === "string" && reason.length > 0) return { reason, evidence: step.output };
298
+ }
299
+ return {
300
+ reason: "Autoimplementation could not continue within the authorized scope.",
301
+ evidence: null,
302
+ };
303
+ }
304
+
305
+ export const autoimplementWorkflow = defineWorkflow({
306
+ source: import.meta.url,
307
+ contractId: "pi-workflows.autoimplement.v1",
308
+ name: "autoimplement",
309
+ input: parseInput,
310
+ title: ({ input }) => `autoimplement: ${input.task.slice(0, 60)}`,
311
+ presentationPrompt:
312
+ "Summarize what was implemented, the review rounds by severity, the CI result, the PR or merge result, and any remaining limitation. Include exact validation commands.",
313
+ startAt: "prepare",
314
+ maxSteps: 160,
315
+ includes: {
316
+ redesign: includeWorkflow({
317
+ workflow: "autodevise",
318
+ contract: autodeviseWorkflow,
319
+ input: (context): AutodeviseInput => {
320
+ const request = context.input as AutoimplementInput;
321
+ return {
322
+ problem: request.task,
323
+ ...(request.scope !== undefined ? { scope: request.scope } : {}),
324
+ ...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
325
+ ...(currentPlan(context) !== undefined ? { previousPlan: currentPlan(context) } : {}),
326
+ newEvidence: latestIssue(context),
327
+ };
328
+ },
329
+ }),
330
+ },
331
+ exits: {
332
+ completed: {
333
+ from: "finalize",
334
+ validate: (value: unknown): AutoimplementCompleted => value as AutoimplementCompleted,
335
+ },
336
+ blocked: {
337
+ from: "blocked",
338
+ validate: (value: unknown): AutoimplementBlocked => value as AutoimplementBlocked,
339
+ },
340
+ },
341
+ nodes: {
342
+ prepare: compute({
343
+ run: ({ input }) => ({
344
+ route: (input as AutoimplementInput).plan === undefined ? "redesign" : "implement",
345
+ }),
346
+ }),
347
+ adoptPlan: compute({
348
+ run: ({ outputs }) => {
349
+ const result = includedResult(autodeviseWorkflow, outputs.redesign);
350
+ if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
351
+ return {
352
+ route: result.output.changed ? "implement" : "blocked",
353
+ plan: result.output.plan,
354
+ planDigest: result.output.planDigest,
355
+ changed: result.output.changed,
356
+ reason: result.output.changed
357
+ ? "The plan changed in response to new evidence."
358
+ : "Redesign returned the same plan for the same unresolved evidence.",
359
+ };
360
+ },
361
+ }),
362
+ implement: agent({
363
+ timeoutMs: 60 * 60_000,
364
+ statusDetail: "implementing",
365
+ prompt: (context) => {
366
+ const request = context.input as AutoimplementInput;
367
+ return [
368
+ `Implement this task end-to-end: ${request.task}`,
369
+ `Plan: ${JSON.stringify(currentPlan(context))}`,
370
+ "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
371
+ "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
372
+ "Do not merge yet.",
373
+ ].join("\n");
374
+ },
375
+ expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
376
+ validate: (value) => requireRecord(value, "implementation result"),
377
+ }),
378
+ classifyImplementation: agent({
379
+ statusDetail: "assessing implementation",
380
+ prompt: ({ outputs }) =>
381
+ [
382
+ "Assess the implementation result.",
383
+ "Choose verify when implementation is ready for tests.",
384
+ "Choose redesign when new evidence invalidates the plan.",
385
+ "Choose fix for a local implementation issue that does not change the plan.",
386
+ "Choose blocked only for a material issue outside the authorized scope.",
387
+ `Implementation: ${JSON.stringify(outputs.implement)}`,
388
+ ].join("\n"),
389
+ expectedOutput: `{ "route": "verify" | "redesign" | "fix" | "blocked", "summary": "reason", "evidence": "evidence" }`,
390
+ validate: (value) =>
391
+ parseRoute(
392
+ value,
393
+ ["verify", "redesign", "fix", "blocked"] as const,
394
+ "implementation assessment",
395
+ ),
396
+ }),
397
+ verify: agent({
398
+ timeoutMs: 45 * 60_000,
399
+ statusDetail: "verifying",
400
+ prompt: () =>
401
+ [
402
+ "Verify the implementation thoroughly.",
403
+ "Run required tests, formatting, lint, type checks, builds, and useful local smoke tests.",
404
+ "Do not put optional mutation testing on the critical path.",
405
+ "State exactly what ran, what passed, what failed, and what still needs remote verification.",
406
+ ].join("\n"),
407
+ expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
408
+ validate: (value) => requireRecord(value, "verification result"),
409
+ }),
410
+ classifyVerification: agent({
411
+ statusDetail: "classifying verification",
412
+ prompt: ({ outputs }) =>
413
+ [
414
+ "Classify the verification result.",
415
+ "Choose publish when required local checks passed.",
416
+ "Choose redesign for evidence that invalidates the plan.",
417
+ "Choose fix for a local implementation or test issue.",
418
+ "Choose blocked only when the work cannot continue in scope.",
419
+ `Verification: ${JSON.stringify(outputs.verify)}`,
420
+ ].join("\n"),
421
+ expectedOutput: `{ "route": "publish" | "redesign" | "fix" | "blocked", "summary": "reason", "evidence": "evidence" }`,
422
+ validate: (value) =>
423
+ parseRoute(
424
+ value,
425
+ ["publish", "redesign", "fix", "blocked"] as const,
426
+ "verification assessment",
427
+ ),
428
+ }),
429
+ fix: agent({
430
+ timeoutMs: 45 * 60_000,
431
+ statusDetail: "fixing",
432
+ prompt: (context) =>
433
+ [
434
+ "Fix the current implementation issue without expanding the approved design.",
435
+ `Issue: ${JSON.stringify(latestIssue(context))}`,
436
+ `Current plan: ${JSON.stringify(currentPlan(context))}`,
437
+ "Stop after the fix so verification can run again.",
438
+ ].join("\n"),
439
+ expectedOutput: `{ "fixed": "what changed", "files": ["changed file"] }`,
440
+ validate: (value) => requireRecord(value, "fix result"),
441
+ }),
442
+ publish: agent({
443
+ timeoutMs: 30 * 60_000,
444
+ statusDetail: "committing and pushing",
445
+ prompt: ({ input }) => {
446
+ const request = input as AutoimplementInput;
447
+ return [
448
+ "Commit and push the verified implementation before review.",
449
+ "Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
450
+ "Inspect the complete public diff before every push or PR mutation.",
451
+ `Requested base branch: ${request.baseBranch ?? "discover the repository default branch"}.`,
452
+ "Do not merge yet.",
453
+ ].join("\n");
454
+ },
455
+ expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
456
+ validate: (value) => requireRecord(value, "publication result"),
457
+ }),
458
+ authorReviewCommand: agent({
459
+ statusDetail: "writing reviewer command",
460
+ prompt: ({ outputs, input }) => {
461
+ const published = outputs.publish as Record<string, unknown>;
462
+ const request = input as AutoimplementInput;
463
+ return [
464
+ "Write the exact Pi Reviewer command for the pushed branch.",
465
+ "The executable must be pi-reviewer. Use its configured model and thinking settings.",
466
+ "Use the repository base branch and an absolute repository working directory.",
467
+ "Set timeoutMs to at most 600000.",
468
+ `Published branch: ${JSON.stringify(published)}`,
469
+ `Repository hint: ${request.repository ?? "current repository"}`,
470
+ ].join("\n");
471
+ },
472
+ expectedOutput: `{ "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000 }`,
473
+ validate: parseReviewerCommand,
474
+ }),
475
+ runReview: shell({
476
+ statusDetail: "running Pi Reviewer",
477
+ timeoutMs: TEN_MINUTES_MS + 10_000,
478
+ exec: (context) =>
479
+ commandExecution(
480
+ latestOutput<StructuredCommand>(context, ["authorReviewCommand", "repairReviewCommand"]),
481
+ ),
482
+ }),
483
+ repairReviewCommand: agent({
484
+ statusDetail: "correcting reviewer command",
485
+ prompt: (context) => {
486
+ const failed = context.results.runReview;
487
+ return [
488
+ "The Pi Reviewer invocation failed. Diagnose the exact command, arguments, base branch, working directory, and error.",
489
+ "Write a corrected pi-reviewer command. Do not substitute codex review or another reviewer.",
490
+ "If Pi Reviewer or its configuration is missing, report that through the same command shape only when another valid invocation exists; otherwise the next assessment must block.",
491
+ `Failed result: ${JSON.stringify(failed)}`,
492
+ ].join("\n");
493
+ },
494
+ expectedOutput: `{ "route": "retry" | "blocked", "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000, "reason": "diagnosis" }`,
495
+ validate: (value) => {
496
+ const result = requireRecord(value, "reviewer command repair");
497
+ if (result.route === "blocked") {
498
+ return {
499
+ route: "blocked",
500
+ reason: requireString(result.reason, "reviewer command blocker"),
501
+ };
502
+ }
503
+ if (result.route !== "retry")
504
+ throw new Error("reviewer command repair route must be retry or blocked");
505
+ return {
506
+ route: "retry",
507
+ ...parseReviewerCommand(result),
508
+ reason: requireString(result.reason, "reviewer command repair reason"),
509
+ };
510
+ },
511
+ }),
512
+ assessReview: agent({
513
+ statusDetail: "assessing reviewer findings",
514
+ prompt: (context) => {
515
+ const result = latestOutput<ShellActionResult>(context, ["runReview"]);
516
+ return [
517
+ "Assess the completed Pi Reviewer invocation.",
518
+ "Set invocationSucceeded false only when the reviewer did not produce a valid review.",
519
+ "Record each finding under P0, P1, P2, or lower. Mark each finding as design or implementation.",
520
+ "Do not promote P2 findings to P1 merely to force another review round.",
521
+ `Reviewer result: ${JSON.stringify(result)}`,
522
+ ].join("\n");
523
+ },
524
+ expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
525
+ validate: parseReviewAssessment,
526
+ }),
527
+ triageReview: compute({
528
+ run: ({ outputs }) => {
529
+ const review = outputs.assessReview as ReviewAssessment;
530
+ const critical = [...review.p0, ...review.p1];
531
+ return {
532
+ route: critical.some((finding) => finding.kind === "design") ? "redesign" : "fix",
533
+ summary: `${critical.length} P0/P1 finding(s) require changes`,
534
+ evidence: critical,
535
+ };
536
+ },
537
+ }),
538
+ addressP2: agent({
539
+ timeoutMs: 30 * 60_000,
540
+ statusDetail: "addressing P2 findings",
541
+ prompt: ({ outputs }) =>
542
+ [
543
+ "Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
544
+ "Do not rerun Pi Reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
545
+ `Review: ${JSON.stringify(outputs.assessReview)}`,
546
+ ].join("\n"),
547
+ expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
548
+ validate: (value) => requireRecord(value, "P2 result"),
549
+ }),
550
+ verifyP2: agent({
551
+ timeoutMs: 30 * 60_000,
552
+ statusDetail: "verifying P2 changes",
553
+ prompt: () =>
554
+ [
555
+ "Run focused verification for the P2 changes and push the verified result.",
556
+ "Do not run Pi Reviewer again because the previous round had no P0 or P1 findings.",
557
+ "Report exact commands and outcomes.",
558
+ ].join("\n"),
559
+ expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
560
+ validate: (value) => requireRecord(value, "P2 verification"),
561
+ }),
562
+ inspectComments: agent({
563
+ timeoutMs: 20 * 60_000,
564
+ statusDetail: "checking PR comments",
565
+ prompt: () =>
566
+ [
567
+ "Inspect current inline review comments and PR issue comments.",
568
+ "Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
569
+ "Choose redesign for a valid design issue, fix for a local code issue, ci when no actionable comment remains, or blocked for an external blocker.",
570
+ ].join("\n"),
571
+ expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
572
+ validate: (value) =>
573
+ parseRoute(value, ["redesign", "fix", "ci", "blocked"] as const, "PR comment assessment"),
574
+ }),
575
+ inspectCi: agent({
576
+ timeoutMs: 10 * 60_000,
577
+ statusDetail: "checking CI",
578
+ prompt: () =>
579
+ [
580
+ "Inspect CI once without waiting for completion.",
581
+ "Choose green, failed, pending, or unavailable.",
582
+ "When pending, provide an exact gh command that tracks this PR or run and set timeoutMs to at most 300000.",
583
+ "Separate failures caused by this change from unrelated failures.",
584
+ ].join("\n"),
585
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"], "trackingCommand": { "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000 } (required when pending) }`,
586
+ validate: (value) => {
587
+ const result = parseRoute(
588
+ value,
589
+ ["green", "failed", "pending", "unavailable"] as const,
590
+ "CI inspection",
591
+ );
592
+ if (result.route === "pending")
593
+ result.trackingCommand = parseCiCommand(result.trackingCommand);
594
+ return result;
595
+ },
596
+ }),
597
+ trackCi: shell({
598
+ statusDetail: "tracking CI for at most five minutes",
599
+ timeoutMs: FIVE_MINUTES_MS + 10_000,
600
+ exec: (context) => commandExecution(latestCiCommand(context)),
601
+ }),
602
+ repairCiCommand: agent({
603
+ statusDetail: "correcting CI tracking command",
604
+ prompt: (context) =>
605
+ [
606
+ "The CI tracking command failed before it could provide a useful status.",
607
+ "Write a corrected gh pr checks --watch or gh run watch command for the same PR or run.",
608
+ "Use an absolute repository path and a timeout no longer than five minutes.",
609
+ `Failure: ${JSON.stringify(context.results.trackCi)}`,
610
+ ].join("\n"),
611
+ expectedOutput: `{ "route": "retry" | "blocked", "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000, "reason": "diagnosis" }`,
612
+ validate: (value) => {
613
+ const result = requireRecord(value, "CI command repair");
614
+ if (result.route === "blocked") {
615
+ return { route: "blocked", reason: requireString(result.reason, "CI command blocker") };
616
+ }
617
+ if (result.route !== "retry")
618
+ throw new Error("CI command repair route must be retry or blocked");
619
+ return {
620
+ route: "retry",
621
+ ...parseCiCommand(result),
622
+ reason: requireString(result.reason, "CI command repair reason"),
623
+ };
624
+ },
625
+ }),
626
+ assessTrackedCi: agent({
627
+ statusDetail: "assessing tracked CI",
628
+ prompt: (context) => {
629
+ const result = latestOutput<ShellActionResult>(context, ["trackCi"]);
630
+ return [
631
+ "Assess the CI tracking result without starting another wait.",
632
+ "Choose green, failed, pending, or unavailable and separate related from unrelated failures.",
633
+ `Tracking result: ${JSON.stringify(result)}`,
634
+ ].join("\n");
635
+ },
636
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
637
+ validate: (value) =>
638
+ parseRoute(
639
+ value,
640
+ ["green", "failed", "pending", "unavailable"] as const,
641
+ "tracked CI assessment",
642
+ ),
643
+ }),
644
+ opportunisticTest: agent({
645
+ timeoutMs: 30 * 60_000,
646
+ statusDetail: "using CI wait for more testing",
647
+ prompt: () =>
648
+ [
649
+ "CI has remained pending for about five minutes.",
650
+ "Do not spend this model turn waiting for CI.",
651
+ "Run additional useful local tests, smoke tests, or targeted checks that were not covered earlier.",
652
+ "If no further useful test exists, say so plainly. Then stop so the workflow can inspect CI again.",
653
+ ].join("\n"),
654
+ expectedOutput: `{ "performed": [{ "command": "exact command", "outcome": "result" }], "furtherUsefulTests": true | false, "summary": "what was learned" }`,
655
+ validate: (value) => requireRecord(value, "opportunistic test result"),
656
+ }),
657
+ classifyCi: agent({
658
+ statusDetail: "classifying CI failures",
659
+ prompt: (context) =>
660
+ [
661
+ "Classify the current CI failure.",
662
+ "Choose redesign when it invalidates the plan, fix for a related local issue, unrelated when the failures are demonstrably outside this change, or blocked when required CI cannot be verified.",
663
+ `CI: ${JSON.stringify(latestOutput(context, ["inspectCi", "assessTrackedCi"]))}`,
664
+ ].join("\n"),
665
+ expectedOutput: `{ "route": "redesign" | "fix" | "unrelated" | "blocked", "reason": "classification", "evidence": ["failure"] }`,
666
+ validate: (value) =>
667
+ parseRoute(
668
+ value,
669
+ ["redesign", "fix", "unrelated", "blocked"] as const,
670
+ "CI classification",
671
+ ),
672
+ }),
673
+ finalizeDelivery: agent({
674
+ timeoutMs: 30 * 60_000,
675
+ statusDetail: "finalizing PR",
676
+ prompt: ({ input }) => {
677
+ const request = input as AutoimplementInput;
678
+ return [
679
+ request.merge === false
680
+ ? "Leave the verified PR ready without merging because input disabled merge."
681
+ : "Merge the verified PR unless repository policy or explicit user instructions prohibit it.",
682
+ "Use the repository's required merge method.",
683
+ "Post a final PR report with the implementation summary and exact validation commands.",
684
+ "Return blocked instead of claiming completion when a required merge or report action fails.",
685
+ ].join("\n");
686
+ },
687
+ expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "URL", "reportComment": "URL or summary", "reason": "result" }`,
688
+ validate: (value, context) => {
689
+ const result = requireRecord(value, "delivery result");
690
+ if (result.status !== "completed" && result.status !== "blocked") {
691
+ throw new Error("delivery status must be completed or blocked");
692
+ }
693
+ const request = context.input as AutoimplementInput;
694
+ if (request.merge !== true && result.merged === true) {
695
+ throw new Error("delivery cannot merge without explicit merge: true");
696
+ }
697
+ return result;
698
+ },
699
+ }),
700
+ blocked: compute({
701
+ run: (context) => {
702
+ const request = context.input as AutoimplementInput;
703
+ const blocked = latestBlockedReason(context);
704
+ return {
705
+ status: "blocked",
706
+ task: request.task,
707
+ reason: blocked.reason,
708
+ evidence: blocked.evidence,
709
+ } satisfies AutoimplementBlocked;
710
+ },
711
+ }),
712
+ finalize: compute({
713
+ run: (context) => {
714
+ const request = context.input as AutoimplementInput;
715
+ return {
716
+ status: "completed",
717
+ task: request.task,
718
+ plan: currentPlan(context),
719
+ implementation: context.outputs.implement,
720
+ verification: context.outputs.verifyP2 ?? context.outputs.verify,
721
+ reviewRounds: reviewRounds(context),
722
+ ci: context.outputs.assessTrackedCi ?? context.outputs.inspectCi,
723
+ delivery: context.outputs.finalizeDelivery,
724
+ } satisfies AutoimplementCompleted;
725
+ },
726
+ }),
727
+ },
728
+ edges: [
729
+ {
730
+ from: "prepare",
731
+ switch: { on: "$.route", cases: { redesign: "redesign", implement: "implement" } },
732
+ },
733
+ { from: "redesign.ready", to: "adoptPlan" },
734
+ { from: "redesign.blocked", to: "blocked" },
735
+ {
736
+ from: "adoptPlan",
737
+ switch: { on: "$.route", cases: { implement: "implement", blocked: "blocked" } },
738
+ },
739
+ { from: "implement", to: "classifyImplementation" },
740
+ {
741
+ from: "classifyImplementation",
742
+ switch: {
743
+ on: "$.route",
744
+ cases: { verify: "verify", redesign: "redesign", fix: "fix", blocked: "blocked" },
745
+ },
746
+ },
747
+ { from: "verify", to: "classifyVerification" },
748
+ {
749
+ from: "classifyVerification",
750
+ switch: {
751
+ on: "$.route",
752
+ cases: { publish: "publish", redesign: "redesign", fix: "fix", blocked: "blocked" },
753
+ },
754
+ },
755
+ { from: "fix", to: "verify" },
756
+ { from: "publish", to: "authorReviewCommand" },
757
+ { from: "authorReviewCommand", to: "runReview" },
758
+ {
759
+ from: "runReview",
760
+ switch: {
761
+ on: "$result.outcome",
762
+ cases: {
763
+ ok: "assessReview",
764
+ failed: "repairReviewCommand",
765
+ timed_out: "repairReviewCommand",
766
+ },
767
+ },
768
+ },
769
+ {
770
+ from: "repairReviewCommand",
771
+ switch: { on: "$.route", cases: { retry: "runReview", blocked: "blocked" } },
772
+ },
773
+ {
774
+ from: "assessReview",
775
+ switch: {
776
+ on: "$.route",
777
+ cases: {
778
+ command_error: "repairReviewCommand",
779
+ critical: "triageReview",
780
+ p2: "addressP2",
781
+ clean: "inspectComments",
782
+ },
783
+ },
784
+ },
785
+ {
786
+ from: "triageReview",
787
+ switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
788
+ },
789
+ { from: "addressP2", to: "verifyP2" },
790
+ {
791
+ from: "verifyP2",
792
+ switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
793
+ },
794
+ {
795
+ from: "inspectComments",
796
+ switch: {
797
+ on: "$.route",
798
+ cases: { redesign: "redesign", fix: "fix", ci: "inspectCi", blocked: "blocked" },
799
+ },
800
+ },
801
+ {
802
+ from: "inspectCi",
803
+ switch: {
804
+ on: "$.route",
805
+ cases: {
806
+ green: "finalizeDelivery",
807
+ failed: "classifyCi",
808
+ pending: "trackCi",
809
+ unavailable: "blocked",
810
+ },
811
+ },
812
+ },
813
+ {
814
+ from: "trackCi",
815
+ switch: {
816
+ on: "$result.outcome",
817
+ cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
818
+ },
819
+ },
820
+ {
821
+ from: "repairCiCommand",
822
+ switch: { on: "$.route", cases: { retry: "trackCi", blocked: "blocked" } },
823
+ },
824
+ {
825
+ from: "assessTrackedCi",
826
+ switch: {
827
+ on: "$.route",
828
+ cases: {
829
+ green: "finalizeDelivery",
830
+ failed: "classifyCi",
831
+ pending: "opportunisticTest",
832
+ unavailable: "blocked",
833
+ },
834
+ },
835
+ },
836
+ { from: "opportunisticTest", to: "inspectCi" },
837
+ {
838
+ from: "classifyCi",
839
+ switch: {
840
+ on: "$.route",
841
+ cases: {
842
+ redesign: "redesign",
843
+ fix: "fix",
844
+ unrelated: "finalizeDelivery",
845
+ blocked: "blocked",
846
+ },
847
+ },
848
+ },
849
+ {
850
+ from: "finalizeDelivery",
851
+ switch: { on: "$.status", cases: { completed: "finalize", blocked: "blocked" } },
852
+ },
853
+ ],
854
+ });
855
+
856
+ export default autoimplementWorkflow;