@osolmaz/pi-workflows 0.11.2 → 0.12.1

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 (177) hide show
  1. package/README.md +15 -10
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +509 -124
  6. package/dist/builtins/autoimplement.workflow.js +884 -327
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +5 -3
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +4 -1
  11. package/dist/builtins/index.js +3 -1
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/monitor.workflow.d.ts +2 -4
  14. package/dist/builtins/monitor.workflow.js +26 -128
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/builtins/pi-agent-group.d.ts +72 -0
  17. package/dist/builtins/pi-agent-group.js +1087 -0
  18. package/dist/builtins/pi-agent-group.js.map +1 -0
  19. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  20. package/dist/builtins/plan-approval.workflow.js +92 -14
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  22. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  23. package/dist/builtins/plan-change.workflow.js +256 -0
  24. package/dist/builtins/plan-change.workflow.js.map +1 -0
  25. package/dist/builtins/plan-presentation.js +2 -2
  26. package/dist/builtins/plan-presentation.js.map +1 -1
  27. package/dist/builtins/sanity-check.workflow.d.ts +85 -0
  28. package/dist/builtins/sanity-check.workflow.js +482 -0
  29. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  30. package/dist/controllers/sqlite.d.ts +90 -2
  31. package/dist/controllers/sqlite.js +380 -5
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.d.ts +2 -2
  34. package/dist/extension/decision-channels.js +29 -35
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  37. package/dist/extension/deferred-turn-coordinator.js +143 -0
  38. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  39. package/dist/extension/deferred-turn.d.ts +44 -0
  40. package/dist/extension/deferred-turn.js +110 -0
  41. package/dist/extension/deferred-turn.js.map +1 -0
  42. package/dist/extension/index.js +348 -80
  43. package/dist/extension/index.js.map +1 -1
  44. package/dist/extension/session-events.d.ts +2 -2
  45. package/dist/extension/step-message.d.ts +1 -0
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.js +23 -3
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/herdr/setup.js +4 -4
  50. package/dist/render/graph-render.js +1 -2
  51. package/dist/render/graph-render.js.map +1 -1
  52. package/dist/viewer/render.js +7 -6
  53. package/dist/viewer/render.js.map +1 -1
  54. package/dist/workflows/catalog.js +7 -2
  55. package/dist/workflows/catalog.js.map +1 -1
  56. package/dist/workflows/command-batch.d.ts +38 -0
  57. package/dist/workflows/command-batch.js +176 -0
  58. package/dist/workflows/command-batch.js.map +1 -0
  59. package/dist/workflows/composition.js +8 -0
  60. package/dist/workflows/composition.js.map +1 -1
  61. package/dist/workflows/decision-presentation.d.ts +1 -1
  62. package/dist/workflows/decision-presentation.js +51 -38
  63. package/dist/workflows/decision-presentation.js.map +1 -1
  64. package/dist/workflows/engine.d.ts +2 -2
  65. package/dist/workflows/engine.js +22 -17
  66. package/dist/workflows/engine.js.map +1 -1
  67. package/dist/workflows/errors.d.ts +13 -0
  68. package/dist/workflows/errors.js +15 -0
  69. package/dist/workflows/errors.js.map +1 -1
  70. package/dist/workflows/human-decision.d.ts +16 -4
  71. package/dist/workflows/human-decision.js +175 -72
  72. package/dist/workflows/human-decision.js.map +1 -1
  73. package/dist/workflows/index.d.ts +3 -2
  74. package/dist/workflows/index.js +2 -1
  75. package/dist/workflows/index.js.map +1 -1
  76. package/dist/workflows/progress.d.ts +1 -0
  77. package/dist/workflows/progress.js +15 -3
  78. package/dist/workflows/progress.js.map +1 -1
  79. package/dist/workflows/schema.js +12 -1
  80. package/dist/workflows/schema.js.map +1 -1
  81. package/dist/workflows/shell.d.ts +4 -0
  82. package/dist/workflows/shell.js +6 -0
  83. package/dist/workflows/shell.js.map +1 -1
  84. package/dist/workflows/store.js +8 -1
  85. package/dist/workflows/store.js.map +1 -1
  86. package/dist/workflows/types.d.ts +37 -48
  87. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  88. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  89. package/docs/CONTROLLERS.md +2 -2
  90. package/docs/DEFERRED_TURNS.md +298 -0
  91. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  92. package/docs/HUMAN_DECISIONS.md +30 -40
  93. package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
  94. package/docs/MONITOR.md +7 -13
  95. package/docs/WORKFLOW_COMPOSITION.md +10 -9
  96. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  97. package/docs/WORKFLOW_UPDATES.md +28 -5
  98. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  99. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  100. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  101. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  102. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  103. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  104. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  105. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  106. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  107. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  108. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  109. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  110. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  111. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  112. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  113. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  114. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  115. package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
  116. package/docs/run-bundles.md +8 -6
  117. package/docs/workflows.md +85 -22
  118. package/examples/workflows/approved-plan.workflow.ts +19 -46
  119. package/examples/workflows/command-batch.workflow.ts +50 -0
  120. package/examples/workflows/sanity-check.workflow.ts +1 -0
  121. package/herdr-plugin.toml +3 -3
  122. package/package.json +7 -7
  123. package/schemas/decision-presentation-v1.schema.json +1 -1
  124. package/schemas/human-decision-accepted-v1.schema.json +16 -4
  125. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  126. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  127. package/schemas/human-decision-continuation-v1.schema.json +11 -2
  128. package/schemas/human-decision-delivery-v1.schema.json +9 -1
  129. package/schemas/human-decision-receipt-v1.schema.json +9 -1
  130. package/schemas/human-decision-request-v1.schema.json +25 -5
  131. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  132. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  133. package/skills/autodoc/SKILL.md +33 -2
  134. package/skills/autoimplement/SKILL.md +93 -12
  135. package/skills/autoplan/SKILL.md +26 -2
  136. package/skills/monitor/SKILL.md +66 -24
  137. package/skills/pi-workflows/SKILL.md +21 -6
  138. package/skills/sanity-check/SKILL.md +44 -0
  139. package/src/builtins/autoimplement-command-batches.ts +364 -0
  140. package/src/builtins/autoimplement.workflow.ts +1075 -370
  141. package/src/builtins/catalog.ts +5 -3
  142. package/src/builtins/index.ts +24 -0
  143. package/src/builtins/monitor.workflow.ts +27 -150
  144. package/src/builtins/pi-agent-group.ts +1407 -0
  145. package/src/builtins/plan-approval.workflow.ts +157 -24
  146. package/src/builtins/plan-change.workflow.ts +321 -0
  147. package/src/builtins/plan-presentation.ts +2 -2
  148. package/src/builtins/sanity-check.workflow.ts +769 -0
  149. package/src/controllers/sqlite.ts +580 -7
  150. package/src/extension/decision-channels.ts +36 -66
  151. package/src/extension/deferred-turn-coordinator.ts +171 -0
  152. package/src/extension/deferred-turn.ts +166 -0
  153. package/src/extension/index.ts +438 -90
  154. package/src/extension/session-events.ts +2 -2
  155. package/src/extension/step-message.ts +1 -0
  156. package/src/extension/widget.ts +24 -5
  157. package/src/herdr/setup.ts +4 -4
  158. package/src/render/graph-render.ts +1 -2
  159. package/src/viewer/render.ts +7 -6
  160. package/src/workflows/catalog.ts +7 -2
  161. package/src/workflows/command-batch.ts +254 -0
  162. package/src/workflows/composition.ts +9 -0
  163. package/src/workflows/decision-presentation.ts +56 -43
  164. package/src/workflows/engine.ts +25 -20
  165. package/src/workflows/errors.ts +24 -0
  166. package/src/workflows/human-decision.ts +218 -101
  167. package/src/workflows/index.ts +20 -11
  168. package/src/workflows/progress.ts +18 -3
  169. package/src/workflows/schema.ts +19 -1
  170. package/src/workflows/shell.ts +10 -0
  171. package/src/workflows/store.ts +8 -1
  172. package/src/workflows/types.ts +43 -59
  173. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  174. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  175. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  176. package/schemas/human-decision-request-v2.schema.json +0 -69
  177. package/schemas/human-decision-resolution-v2.schema.json +0 -27
@@ -1,21 +1,34 @@
1
1
  import path from "node:path";
2
2
  import {
3
+ runCommandBatch,
4
+ type CommandBatchItem,
5
+ type CommandBatchResult,
6
+ } from "../workflows/command-batch.js";
7
+ import {
8
+ action,
3
9
  agent,
4
10
  compute,
5
11
  defineWorkflow,
6
12
  includeWorkflow,
7
13
  includedResult,
8
- shell,
9
14
  } from "../workflows/definition.js";
10
15
  import { digest } from "../workflows/human-decision.js";
11
- import type {
12
- ShellActionExecution,
13
- ShellActionResult,
14
- WorkflowNodeContext,
15
- } from "../workflows/types.js";
16
+ import type { WorkflowActionContext, WorkflowNodeContext } from "../workflows/types.js";
16
17
  import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
17
- import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
18
- import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
18
+ import {
19
+ parseAutoimplementConcurrency,
20
+ parseCiInspectionBatch,
21
+ parsePublishedRepositories,
22
+ parseVerificationCommandPlan,
23
+ reviewerCommand,
24
+ type AutoimplementConcurrency,
25
+ type CiInspectionBatch,
26
+ type PublishedRepositories,
27
+ type PublishedRepository,
28
+ type VerificationCommandPlan,
29
+ } from "./autoimplement-command-batches.js";
30
+ import { parsePlanApprovalPolicy, type PlanApprovalPolicy } from "./plan-approval.workflow.js";
31
+ import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
19
32
 
20
33
  export type AutoimplementInput = {
21
34
  task: string;
@@ -31,10 +44,8 @@ export type AutoimplementInput = {
31
44
  planDigest: string;
32
45
  documents: string[];
33
46
  };
34
- approval?: {
35
- audience: string;
36
- maxReplans: number;
37
- };
47
+ approval?: PlanApprovalPolicy;
48
+ concurrency?: Partial<AutoimplementConcurrency>;
38
49
  };
39
50
 
40
51
  export type ExistingPlanDiscovery = {
@@ -46,19 +57,26 @@ export type ExistingPlanDiscovery = {
46
57
  evidence: unknown;
47
58
  };
48
59
 
49
- type StructuredCommand = {
50
- command: string;
51
- args: string[];
52
- cwd: string;
53
- timeoutMs: number;
54
- };
55
-
56
60
  type ReviewFinding = {
57
61
  severity: "P0" | "P1" | "P2" | "lower";
58
62
  kind: "design" | "implementation";
59
63
  summary: string;
60
64
  };
61
65
 
66
+ type RepositoryReviewAssessment = {
67
+ id: string;
68
+ repository: string;
69
+ baseBranch: string;
70
+ headRevision: string;
71
+ dependencyFingerprint?: string;
72
+ invocationSucceeded: boolean;
73
+ p0: ReviewFinding[];
74
+ p1: ReviewFinding[];
75
+ p2: ReviewFinding[];
76
+ lower: ReviewFinding[];
77
+ reason: string;
78
+ };
79
+
62
80
  type ReviewAssessment = {
63
81
  route: "critical" | "p2" | "clean" | "command_error";
64
82
  invocationSucceeded: boolean;
@@ -67,6 +85,7 @@ type ReviewAssessment = {
67
85
  p2: ReviewFinding[];
68
86
  lower: ReviewFinding[];
69
87
  reason: string;
88
+ repositories?: RepositoryReviewAssessment[];
70
89
  };
71
90
 
72
91
  export type AutoimplementCompleted = {
@@ -98,11 +117,49 @@ type BlockerChallenge = {
98
117
  evidence: string[];
99
118
  };
100
119
 
101
- const FIVE_MINUTES_MS = 5 * 60_000;
102
- const TEN_MINUTES_MS = 10 * 60_000;
103
120
  const MAX_BLOCKER_CHALLENGES = 3;
104
121
  const MAX_CHALLENGE_ITEMS = 5;
105
122
  const MAX_CHALLENGE_TEXT = 500;
123
+ const MAX_TIMEOUT_FALLBACKS = 3;
124
+ const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
125
+ const TIMEOUT_FALLBACK_SOURCES = [
126
+ "implement",
127
+ "planVerification",
128
+ "verify",
129
+ "fix",
130
+ "publish",
131
+ "addressP2",
132
+ "verifyP2",
133
+ "inspectComments",
134
+ "inspectCi",
135
+ "opportunisticTest",
136
+ "finalizeDelivery",
137
+ ] as const;
138
+
139
+ const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"] as const;
140
+
141
+ type TimeoutFallbackSource = (typeof TIMEOUT_FALLBACK_SOURCES)[number];
142
+ type TimeoutFallbackRoute = "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked";
143
+
144
+ const TIMEOUT_FALLBACK_ROUTES: Record<TimeoutFallbackSource, readonly TimeoutFallbackRoute[]> = {
145
+ implement: ["retry", "replan", "blocked"],
146
+ planVerification: ["retry", "verify", "replan", "blocked"],
147
+ verify: ["retry", "verify", "replan", "blocked"],
148
+ fix: ["retry", "replan", "blocked"],
149
+ publish: ["retry", "replan", "blocked"],
150
+ addressP2: ["retry", "replan", "blocked"],
151
+ verifyP2: ["retry", "replan", "blocked"],
152
+ inspectComments: ["retry", "review", "ci", "replan", "blocked"],
153
+ inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
154
+ opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
155
+ finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
156
+ };
157
+
158
+ type TimeoutFallbackResult = {
159
+ route: TimeoutFallbackRoute;
160
+ reason: string;
161
+ evidence: string[];
162
+ };
106
163
 
107
164
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
108
165
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -118,6 +175,13 @@ function requireString(value: unknown, label: string): string {
118
175
  return value.trim();
119
176
  }
120
177
 
178
+ function requireStringArray(value: unknown, label: string): string[] {
179
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
180
+ throw new Error(`${label} must be an array of strings`);
181
+ }
182
+ return [...value] as string[];
183
+ }
184
+
121
185
  function boundedChallengeItems(value: unknown, label: string): string[] {
122
186
  if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
123
187
  throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
@@ -131,6 +195,170 @@ function boundedChallengeItems(value: unknown, label: string): string[] {
131
195
  });
132
196
  }
133
197
 
198
+ function isTimeoutFallbackSource(nodeId: string): nodeId is TimeoutFallbackSource {
199
+ return (TIMEOUT_FALLBACK_SOURCES as readonly string[]).includes(nodeId);
200
+ }
201
+
202
+ function latestTimedOutStep(context: WorkflowNodeContext) {
203
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
204
+ const step = context.state.steps[index];
205
+ if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId)) return step;
206
+ }
207
+ throw new Error("No supported timed-out Autoimplement step is available");
208
+ }
209
+
210
+ function latestStepIndex(
211
+ context: WorkflowNodeContext,
212
+ predicate: (step: WorkflowNodeContext["state"]["steps"][number]) => boolean,
213
+ ): number {
214
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
215
+ const step = context.state.steps[index];
216
+ if (step && predicate(step)) return index;
217
+ }
218
+ return -1;
219
+ }
220
+
221
+ function latestWorkAttemptIndex(context: WorkflowNodeContext): number {
222
+ return latestStepIndex(context, (step) =>
223
+ (WORK_ATTEMPT_NODES as readonly string[]).includes(step.nodeId),
224
+ );
225
+ }
226
+
227
+ function hasCurrentAcceptedWork(context: WorkflowNodeContext): boolean {
228
+ const index = latestWorkAttemptIndex(context);
229
+ return index >= 0 && context.state.steps[index]?.outcome === "ok";
230
+ }
231
+
232
+ function hasCurrentPublication(context: WorkflowNodeContext): boolean {
233
+ const workIndex = latestWorkAttemptIndex(context);
234
+ const publicationIndex = latestStepIndex(context, (step) => {
235
+ if (step.outcome !== "ok") return false;
236
+ if (step.nodeId === "publish") return true;
237
+ if (step.nodeId !== "verifyP2") return false;
238
+ const output = step.output as { passed?: unknown } | null;
239
+ return output?.passed === true;
240
+ });
241
+ return publicationIndex > workIndex;
242
+ }
243
+
244
+ function parseTimeoutFallback(value: unknown, context: WorkflowNodeContext): TimeoutFallbackResult {
245
+ const result = requireRecord(value, "timeout fallback");
246
+ const routes: TimeoutFallbackRoute[] = [
247
+ "retry",
248
+ "verify",
249
+ "review",
250
+ "ci",
251
+ "deliver",
252
+ "replan",
253
+ "blocked",
254
+ ];
255
+ if (!routes.includes(result.route as TimeoutFallbackRoute)) {
256
+ throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
257
+ }
258
+ const route = result.route as TimeoutFallbackRoute;
259
+ const reason = requireString(result.reason, "timeout fallback reason");
260
+ const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map(
261
+ (item, index) => requireString(item, `timeout fallback evidence[${index}]`),
262
+ );
263
+ if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
264
+ throw new Error(
265
+ `timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`,
266
+ );
267
+ }
268
+
269
+ const timedOut = latestTimedOutStep(context);
270
+ const source = timedOut.nodeId as TimeoutFallbackSource;
271
+ if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
272
+ throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
273
+ }
274
+ if (route === "verify" && !hasCurrentAcceptedWork(context)) {
275
+ throw new Error("timeout fallback cannot route to verification without current accepted work");
276
+ }
277
+ if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
278
+ throw new Error(
279
+ "timeout fallback cannot move past publication without a current published head",
280
+ );
281
+ }
282
+ if (route === "ci") {
283
+ const comments = context.outputs.inspectComments as { route?: unknown } | undefined;
284
+ if (comments?.route !== "ci") {
285
+ throw new Error("timeout fallback cannot route to CI before comment inspection completed");
286
+ }
287
+ }
288
+ if (route === "deliver") {
289
+ const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi) as
290
+ | { route?: unknown }
291
+ | undefined;
292
+ const classification = context.outputs.classifyCi as { route?: unknown } | undefined;
293
+ if (ci?.route !== "green" && classification?.route !== "unrelated") {
294
+ throw new Error("timeout fallback cannot route to delivery before CI is ready");
295
+ }
296
+ }
297
+
298
+ return { route, reason, evidence };
299
+ }
300
+
301
+ function timeoutFallbackTarget(context: WorkflowNodeContext): { route: string } {
302
+ const fallback = context.outputs.timeoutFallback as TimeoutFallbackResult;
303
+ if (fallback.route !== "retry") {
304
+ const routes: Record<Exclude<TimeoutFallbackRoute, "retry">, string> = {
305
+ verify: "planVerification",
306
+ review: "selectReviewCommands",
307
+ ci: "inspectCi",
308
+ deliver: "finalizeDelivery",
309
+ replan: "redesign",
310
+ blocked: "blocked",
311
+ };
312
+ return { route: routes[fallback.route] };
313
+ }
314
+ return { route: latestTimedOutStep(context).nodeId };
315
+ }
316
+
317
+ function timeoutFallbackGuard(context: WorkflowNodeContext) {
318
+ const timeout = latestTimedOutStep(context);
319
+ const attempts = context.state.steps.filter(
320
+ (step) => step.nodeId === "timeoutFallback" && step.outcome === "ok",
321
+ ).length;
322
+ if (attempts >= MAX_TIMEOUT_FALLBACKS) {
323
+ const timeouts = context.state.steps
324
+ .filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
325
+ .map((step) => ({
326
+ nodeId: step.nodeId,
327
+ attemptId: step.attemptId,
328
+ error: step.error,
329
+ }));
330
+ return {
331
+ route: "blocked",
332
+ reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
333
+ evidence: {
334
+ attempts,
335
+ limit: MAX_TIMEOUT_FALLBACKS,
336
+ timeouts,
337
+ },
338
+ };
339
+ }
340
+ return {
341
+ route: "recover",
342
+ attempt: attempts + 1,
343
+ limit: MAX_TIMEOUT_FALLBACKS,
344
+ timeout: {
345
+ nodeId: timeout.nodeId,
346
+ attemptId: timeout.attemptId,
347
+ error: timeout.error,
348
+ },
349
+ };
350
+ }
351
+
352
+ function throwLatestSupportedFailure(context: WorkflowNodeContext): never {
353
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
354
+ const step = context.state.steps[index];
355
+ if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
356
+ throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
357
+ }
358
+ }
359
+ throw new Error("No supported failed Autoimplement step is available");
360
+ }
361
+
134
362
  function parseBlockerChallenge(value: unknown): BlockerChallenge {
135
363
  const result = requireRecord(value, "blocker challenge");
136
364
  if (result.route !== "continue" && result.route !== "blocked") {
@@ -233,22 +461,8 @@ function parseInput(value: unknown): AutoimplementInput {
233
461
  documents: [...raw.documents] as string[],
234
462
  };
235
463
  }
236
- let approval: AutoimplementInput["approval"];
237
- if (input.approval !== undefined) {
238
- const raw = requireRecord(input.approval, "autoimplement approval");
239
- const maxReplans = raw.maxReplans ?? 3;
240
- if (
241
- !Number.isInteger(maxReplans) ||
242
- (maxReplans as number) < 1 ||
243
- (maxReplans as number) > 20
244
- ) {
245
- throw new Error("autoimplement approval maxReplans must be from 1 through 20");
246
- }
247
- approval = {
248
- audience: requireString(raw.audience, "autoimplement approval audience"),
249
- maxReplans: maxReplans as number,
250
- };
251
- }
464
+ const concurrency = parseAutoimplementConcurrency(input.concurrency);
465
+ const approval = parsePlanApprovalPolicy(input.approval);
252
466
  return {
253
467
  task: requireString(input.task, "autoimplement task"),
254
468
  ...(input.plan !== undefined ? { plan: input.plan } : {}),
@@ -263,7 +477,8 @@ function parseInput(value: unknown): AutoimplementInput {
263
477
  merge: input.merge === true,
264
478
  ...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
265
479
  ...(documentation !== undefined ? { documentation } : {}),
266
- ...(approval !== undefined ? { approval } : {}),
480
+ approval,
481
+ concurrency,
267
482
  };
268
483
  }
269
484
 
@@ -312,70 +527,71 @@ function parseRoute<T extends string>(
312
527
  return { ...record, route: record.route as T };
313
528
  }
314
529
 
315
- function parseCommand(
316
- value: unknown,
317
- options: {
318
- command: string;
319
- maxTimeoutMs: number;
320
- validateArgs: (args: string[]) => boolean;
321
- label: string;
322
- },
323
- ): StructuredCommand {
324
- const command = requireRecord(value, options.label);
325
- if (command.command !== options.command) {
326
- throw new Error(`${options.label} command must be ${options.command}`);
327
- }
328
- if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
329
- throw new Error(`${options.label} args must be an array of strings`);
330
- }
331
- const args = [...command.args] as string[];
332
- if (!options.validateArgs(args)) throw new Error(`${options.label} args are not allowed`);
333
- const cwd = requireString(command.cwd, `${options.label} cwd`);
334
- if (!path.isAbsolute(cwd)) throw new Error(`${options.label} cwd must be absolute`);
335
- const timeoutMs = command.timeoutMs;
336
- if (
337
- typeof timeoutMs !== "number" ||
338
- !Number.isInteger(timeoutMs) ||
339
- timeoutMs <= 0 ||
340
- timeoutMs > options.maxTimeoutMs
341
- ) {
342
- throw new Error(`${options.label} timeoutMs must be at most ${options.maxTimeoutMs}`);
343
- }
344
- return { command: options.command, args, cwd, timeoutMs };
530
+ type ReviewCommandSelection = {
531
+ route: "run" | "reuse";
532
+ repositories: PublishedRepository[];
533
+ commands: CommandBatchItem[];
534
+ };
535
+
536
+ type BatchExecution = {
537
+ route: "assess" | "repair";
538
+ batch: CommandBatchResult;
539
+ };
540
+
541
+ function concurrency(context: WorkflowNodeContext): AutoimplementConcurrency {
542
+ return parseAutoimplementConcurrency((context.input as AutoimplementInput).concurrency);
345
543
  }
346
544
 
347
- function parseReviewerCommand(value: unknown): StructuredCommand {
348
- return parseCommand(value, {
349
- command: "pi-reviewer",
350
- maxTimeoutMs: TEN_MINUTES_MS,
351
- label: "reviewer command",
352
- validateArgs: (args) => {
353
- const base = args.indexOf("--base");
354
- return base >= 0 && typeof args[base + 1] === "string" && args[base + 1]!.length > 0;
545
+ async function runAutoimplementBatch(
546
+ context: WorkflowActionContext,
547
+ kind: "review" | "ciWatch" | "verification",
548
+ commands: CommandBatchItem[],
549
+ maxConcurrency: number,
550
+ ): Promise<CommandBatchResult> {
551
+ return await runCommandBatch(
552
+ { items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) },
553
+ {
554
+ signal: context.signal,
555
+ onItemSettled: async (result, completed, total) => {
556
+ if (context.signal.aborted) return;
557
+ try {
558
+ await context.publishUpdate({
559
+ type: "command-batch.item",
560
+ key: `${kind}/${result.id}`,
561
+ data: {
562
+ schema: "pi-workflows.command-batch-item.v1",
563
+ batchKind: kind,
564
+ itemId: result.id,
565
+ outcome: result.outcome,
566
+ completed,
567
+ total,
568
+ },
569
+ });
570
+ } catch (error) {
571
+ if (!context.signal.aborted) throw error;
572
+ }
573
+ },
355
574
  },
356
- });
575
+ );
357
576
  }
358
577
 
359
- function parseCiCommand(value: unknown): StructuredCommand {
360
- return parseCommand(value, {
361
- command: "gh",
362
- maxTimeoutMs: FIVE_MINUTES_MS,
363
- label: "CI tracking command",
364
- validateArgs: (args) =>
365
- (args[0] === "pr" && args[1] === "checks" && args.includes("--watch")) ||
366
- (args[0] === "run" && args[1] === "watch"),
367
- });
578
+ function commandBatchTimeoutMs(commands: CommandBatchItem[], maxConcurrency: number): number {
579
+ if (commands.length === 0) return 10_000;
580
+ const concurrency = Math.min(maxConcurrency, commands.length);
581
+ const waves = Math.ceil(commands.length / concurrency);
582
+ const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
583
+ return waves * longestItem + 10_000;
368
584
  }
369
585
 
370
- function commandExecution(command: StructuredCommand): ShellActionExecution {
371
- return {
372
- command: command.command,
373
- args: command.args,
374
- cwd: command.cwd,
375
- timeoutMs: command.timeoutMs,
376
- allowNonZeroExit: true,
377
- maxOutputChars: 1_000_000,
378
- };
586
+ function reviewBatchNeedsRepair(result: CommandBatchResult): boolean {
587
+ return result.items.some(
588
+ (item) =>
589
+ item.outcome === "timedOut" ||
590
+ item.outcome === "cancelled" ||
591
+ (item.outcome === "failed" && item.exitCode === null) ||
592
+ item.stdoutTruncated ||
593
+ item.stderrTruncated,
594
+ );
379
595
  }
380
596
 
381
597
  function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
@@ -383,20 +599,10 @@ function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
383
599
  const step = context.state.steps[index];
384
600
  if (step && nodeIds.includes(step.nodeId)) return step.output as T;
385
601
  }
386
- throw new Error(`No output found for ${nodeIds.join(" or ")}`);
387
- }
388
-
389
- function latestCiCommand(context: WorkflowNodeContext): StructuredCommand {
390
- for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
391
- const step = context.state.steps[index];
392
- if (!step) continue;
393
- if (step.nodeId === "repairCiCommand") return step.output as StructuredCommand;
394
- if (step.nodeId === "inspectCi") {
395
- const output = step.output as { trackingCommand?: StructuredCommand };
396
- if (output.trackingCommand !== undefined) return output.trackingCommand;
397
- }
602
+ for (const nodeId of nodeIds) {
603
+ if (context.outputs[nodeId] !== undefined) return context.outputs[nodeId] as T;
398
604
  }
399
- throw new Error("No CI tracking command is available");
605
+ throw new Error(`No output found for ${nodeIds.join(" or ")}`);
400
606
  }
401
607
 
402
608
  function currentPlan(context: WorkflowNodeContext): unknown {
@@ -413,20 +619,6 @@ function currentPlan(context: WorkflowNodeContext): unknown {
413
619
  return (context.input as AutoimplementInput).plan;
414
620
  }
415
621
 
416
- function currentPlanDigest(context: WorkflowNodeContext): string {
417
- const adopted = context.outputs.adoptPlan as { planDigest?: unknown } | undefined;
418
- if (typeof adopted?.planDigest === "string") return adopted.planDigest;
419
- const documented = context.outputs.documentation as
420
- | { exit?: string; output?: { planDigest?: unknown } }
421
- | undefined;
422
- if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
423
- return documented.output.planDigest;
424
- }
425
- const plan = currentPlan(context);
426
- if (plan === undefined) throw new Error("autoimplement does not have a selected plan");
427
- return digest(plan);
428
- }
429
-
430
622
  function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
431
623
  return context.state.steps
432
624
  .filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
@@ -444,6 +636,7 @@ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
444
636
  "assessTrackedCi",
445
637
  "classifyCi",
446
638
  "finalizeDelivery",
639
+ "timeoutFallback",
447
640
  ];
448
641
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
449
642
  const step = context.state.steps[index];
@@ -463,16 +656,6 @@ function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
463
656
  }
464
657
 
465
658
  function latestIssue(context: WorkflowNodeContext): unknown {
466
- const approval = context.outputs.approval as
467
- | { exit?: string; output?: { instructions?: unknown } }
468
- | undefined;
469
- if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
470
- return {
471
- source: "human-replan",
472
- instructions: approval.output.instructions,
473
- priorPlanDigest: currentPlanDigest(context),
474
- };
475
- }
476
659
  const ids = [
477
660
  "challengeBlocker",
478
661
  "classifyImplementation",
@@ -480,6 +663,7 @@ function latestIssue(context: WorkflowNodeContext): unknown {
480
663
  "triageReview",
481
664
  "inspectComments",
482
665
  "classifyCi",
666
+ "timeoutFallback",
483
667
  "adoptPlan",
484
668
  ];
485
669
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -501,18 +685,356 @@ function parseFinding(value: unknown, severity: ReviewFinding["severity"]): Revi
501
685
  };
502
686
  }
503
687
 
504
- function parseReviewAssessment(value: unknown): ReviewAssessment {
505
- const review = requireRecord(value, "review assessment");
506
- const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
507
- const raw = review[key];
508
- if (!Array.isArray(raw)) throw new Error(`review ${key} must be an array`);
509
- return raw.map((item) => parseFinding(item, severity));
688
+ function parseVerificationForContext(
689
+ value: unknown,
690
+ context: WorkflowNodeContext,
691
+ ): VerificationCommandPlan {
692
+ const plan = parseVerificationCommandPlan(value);
693
+ const implementation = latestOutput<Record<string, unknown>>(context, ["implement"]);
694
+ const reported = Array.isArray(implementation.repositories)
695
+ ? implementation.repositories.filter((entry): entry is string => typeof entry === "string")
696
+ : [];
697
+ const request = context.input as AutoimplementInput;
698
+ const roots = new Set(
699
+ (reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) =>
700
+ path.resolve(entry),
701
+ ),
702
+ );
703
+ for (const command of plan.commands) {
704
+ const cwd = path.resolve(command.cwd);
705
+ if (!roots.delete(cwd)) {
706
+ throw new Error(
707
+ `verification command cwd was not reported by implementation: ${command.cwd}`,
708
+ );
709
+ }
710
+ }
711
+ if (roots.size > 0) {
712
+ throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
713
+ }
714
+ return plan;
715
+ }
716
+
717
+ function currentPublishedRepositories(context: WorkflowNodeContext): PublishedRepositories {
718
+ return latestOutput<PublishedRepositories>(context, ["verifyP2", "publish"]);
719
+ }
720
+
721
+ type DeliveryRepositoryResult = {
722
+ repository: string;
723
+ pr: string;
724
+ merged: boolean;
725
+ reportComment: string;
726
+ reason: string;
727
+ };
728
+
729
+ function parseDeliveryRepository(value: unknown, index: number): DeliveryRepositoryResult {
730
+ const result = requireRecord(value, `delivery repositories[${index}]`);
731
+ const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
732
+ if (!path.isAbsolute(repository)) {
733
+ throw new Error(`delivery repositories[${index}].repository must be absolute`);
734
+ }
735
+ if (typeof result.merged !== "boolean") {
736
+ throw new Error(`delivery repositories[${index}].merged must be a boolean`);
737
+ }
738
+ return {
739
+ repository: path.resolve(repository),
740
+ pr: requireString(result.pr, `delivery repositories[${index}].pr`),
741
+ merged: result.merged,
742
+ reportComment: requireString(
743
+ result.reportComment,
744
+ `delivery repositories[${index}].reportComment`,
745
+ ),
746
+ reason: requireString(result.reason, `delivery repositories[${index}].reason`),
510
747
  };
511
- const p0 = parseList("p0", "P0");
512
- const p1 = parseList("p1", "P1");
513
- const p2 = parseList("p2", "P2");
514
- const lower = parseList("lower", "lower");
515
- const invocationSucceeded = review.invocationSucceeded === true;
748
+ }
749
+
750
+ function parseDeliveryResult(
751
+ value: unknown,
752
+ context: WorkflowNodeContext,
753
+ ): Record<string, unknown> {
754
+ const result = requireRecord(value, "delivery result");
755
+ if (result.status !== "completed" && result.status !== "blocked") {
756
+ throw new Error("delivery status must be completed or blocked");
757
+ }
758
+ const request = context.input as AutoimplementInput;
759
+ if (request.merge !== true && result.merged === true) {
760
+ throw new Error("delivery cannot merge without explicit merge: true");
761
+ }
762
+ if (result.status === "blocked") return result;
763
+ if (typeof result.merged !== "boolean") {
764
+ throw new Error("completed delivery merged must be a boolean");
765
+ }
766
+ const pr = requireString(result.pr, "completed delivery pr");
767
+ const reportComment = requireString(result.reportComment, "completed delivery reportComment");
768
+ const reason = requireString(result.reason, "completed delivery reason");
769
+ const published = currentPublishedRepositories(context).repositories;
770
+ let repositories: DeliveryRepositoryResult[];
771
+ if (result.repositories === undefined) {
772
+ if (published.length !== 1) {
773
+ throw new Error("completed delivery repositories must cover every published repository");
774
+ }
775
+ const only = published[0];
776
+ if (only === undefined) throw new Error("completed delivery has no published repository");
777
+ repositories = [
778
+ {
779
+ repository: path.resolve(only.repository),
780
+ pr,
781
+ merged: result.merged,
782
+ reportComment,
783
+ reason,
784
+ },
785
+ ];
786
+ } else {
787
+ if (!Array.isArray(result.repositories)) {
788
+ throw new Error("completed delivery repositories must be an array");
789
+ }
790
+ repositories = result.repositories.map(parseDeliveryRepository);
791
+ }
792
+ const actual = new Map<string, DeliveryRepositoryResult>();
793
+ for (const repository of repositories) {
794
+ if (actual.has(repository.repository)) {
795
+ throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
796
+ }
797
+ actual.set(repository.repository, repository);
798
+ }
799
+ const mergeExpected = request.merge === true;
800
+ for (const expected of published) {
801
+ const repository = actual.get(path.resolve(expected.repository));
802
+ if (repository === undefined || repository.pr !== expected.pr) {
803
+ throw new Error(
804
+ `completed delivery does not match published repository and PR: ${expected.repository}`,
805
+ );
806
+ }
807
+ if (repository.merged !== mergeExpected) {
808
+ throw new Error(
809
+ `completed delivery merge result does not match merge policy: ${expected.repository}`,
810
+ );
811
+ }
812
+ actual.delete(repository.repository);
813
+ }
814
+ if (actual.size > 0) {
815
+ throw new Error(
816
+ `completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`,
817
+ );
818
+ }
819
+ const firstPublished = published[0];
820
+ const first =
821
+ firstPublished === undefined
822
+ ? undefined
823
+ : repositories.find(
824
+ (repository) => repository.repository === path.resolve(firstPublished.repository),
825
+ );
826
+ if (
827
+ first === undefined ||
828
+ first.pr !== pr ||
829
+ first.merged !== result.merged ||
830
+ first.reportComment !== reportComment
831
+ ) {
832
+ throw new Error(
833
+ "completed delivery top-level compatibility fields must match the first result",
834
+ );
835
+ }
836
+ return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
837
+ }
838
+
839
+ function parseP2Verification(
840
+ value: unknown,
841
+ context: WorkflowNodeContext,
842
+ ): Record<string, unknown> {
843
+ const result = requireRecord(value, "P2 verification");
844
+ if (typeof result.passed !== "boolean") {
845
+ throw new Error("P2 verification passed must be a boolean");
846
+ }
847
+ if (result.pushed !== true) {
848
+ throw new Error("P2 verification pushed must be true");
849
+ }
850
+ const refreshed = parsePublishedRepositories(result);
851
+ const previous = latestOutput<PublishedRepositories>(context, ["publish"]);
852
+ const expected = new Map(
853
+ previous.repositories.map((repository) => [repository.id, repository] as const),
854
+ );
855
+ for (const repository of refreshed.repositories) {
856
+ const prior = expected.get(repository.id);
857
+ if (
858
+ prior === undefined ||
859
+ prior.repository !== repository.repository ||
860
+ prior.branch !== repository.branch ||
861
+ prior.baseBranch !== repository.baseBranch ||
862
+ prior.pr !== repository.pr ||
863
+ prior.dependencyFingerprint !== repository.dependencyFingerprint
864
+ ) {
865
+ throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
866
+ }
867
+ expected.delete(repository.id);
868
+ }
869
+ if (expected.size > 0) {
870
+ throw new Error(
871
+ `P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`,
872
+ );
873
+ }
874
+ return { ...result, repositories: refreshed.repositories };
875
+ }
876
+
877
+ function parseCiInspectionForPublished(
878
+ value: unknown,
879
+ context: WorkflowNodeContext,
880
+ ): CiInspectionBatch {
881
+ const inspected = parseCiInspectionBatch(value);
882
+ const published = currentPublishedRepositories(context);
883
+ const expected = new Map(
884
+ published.repositories.map((repository) => [repository.id, repository] as const),
885
+ );
886
+ for (const target of inspected.targets) {
887
+ const repository = expected.get(target.id);
888
+ if (
889
+ repository === undefined ||
890
+ repository.repository !== target.repository ||
891
+ repository.headRevision !== target.headRevision ||
892
+ repository.pr !== target.pr
893
+ ) {
894
+ throw new Error(
895
+ `CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`,
896
+ );
897
+ }
898
+ expected.delete(target.id);
899
+ }
900
+ if (expected.size > 0) {
901
+ throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
902
+ }
903
+ return inspected;
904
+ }
905
+
906
+ function parseTrackedCiAssessment(
907
+ value: unknown,
908
+ context: WorkflowNodeContext,
909
+ ): Record<string, unknown> & { route: CiInspectionBatch["route"] } {
910
+ const result = requireRecord(value, "tracked CI assessment");
911
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
912
+ const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
913
+ const expectedIds = execution.batch.items.map((item) => item.id);
914
+ if (!Array.isArray(result.targets)) {
915
+ throw new Error("tracked CI assessment targets must be an array");
916
+ }
917
+ const seen = new Set<string>();
918
+ const targets = result.targets.map((entry, index) => {
919
+ const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
920
+ const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
921
+ if (seen.has(id)) throw new Error(`tracked CI assessment target is duplicated: ${id}`);
922
+ seen.add(id);
923
+ if (
924
+ target.route !== "green" &&
925
+ target.route !== "failed" &&
926
+ target.route !== "pending" &&
927
+ target.route !== "unavailable"
928
+ ) {
929
+ throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
930
+ }
931
+ return {
932
+ id,
933
+ route: target.route,
934
+ reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
935
+ };
936
+ });
937
+ const missing = expectedIds.filter((id) => !seen.has(id));
938
+ const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
939
+ if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
940
+ throw new Error(
941
+ `tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`,
942
+ );
943
+ }
944
+ const trackedRoutes = new Map(targets.map((target) => [target.id, target.route] as const));
945
+ const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
946
+ const route = routes.includes("failed")
947
+ ? "failed"
948
+ : routes.includes("pending")
949
+ ? "pending"
950
+ : routes.includes("unavailable")
951
+ ? "unavailable"
952
+ : "green";
953
+ if (result.route !== route) {
954
+ throw new Error(`tracked CI assessment route must be ${route}`);
955
+ }
956
+ return {
957
+ ...result,
958
+ route,
959
+ reason: requireString(result.reason, "tracked CI assessment reason"),
960
+ targets,
961
+ relatedFailures: requireStringArray(
962
+ result.relatedFailures ?? [],
963
+ "tracked CI assessment relatedFailures",
964
+ ),
965
+ unrelatedFailures: requireStringArray(
966
+ result.unrelatedFailures ?? [],
967
+ "tracked CI assessment unrelatedFailures",
968
+ ),
969
+ };
970
+ }
971
+
972
+ function selectReviewCommands(context: WorkflowNodeContext): ReviewCommandSelection {
973
+ const published = latestOutput<PublishedRepositories>(context, ["publish"]);
974
+ const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
975
+ const repositories = published.repositories.filter(
976
+ (repository) =>
977
+ !reviewed.some(
978
+ (entry) =>
979
+ entry.id === repository.id &&
980
+ entry.headRevision === repository.headRevision &&
981
+ entry.dependencyFingerprint === repository.dependencyFingerprint &&
982
+ entry.invocationSucceeded,
983
+ ),
984
+ );
985
+ return {
986
+ route: repositories.length === 0 ? "reuse" : "run",
987
+ repositories,
988
+ commands: repositories.map(reviewerCommand),
989
+ };
990
+ }
991
+
992
+ function parseReviewAssessment(value: unknown, context: WorkflowNodeContext): ReviewAssessment {
993
+ const review = requireRecord(value, "review assessment");
994
+ if (!Array.isArray(review.repositories)) {
995
+ throw new Error("review repositories must be an array");
996
+ }
997
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
998
+ const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
999
+ const repositories = review.repositories.map((value, index) => {
1000
+ const raw = requireRecord(value, `review repositories[${index}]`);
1001
+ const id = requireString(raw.id, `review repositories[${index}].id`);
1002
+ const published = expected.get(id);
1003
+ if (published === undefined)
1004
+ throw new Error(`review repository id was not in the batch: ${id}`);
1005
+ expected.delete(id);
1006
+ const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
1007
+ const list = raw[key];
1008
+ if (!Array.isArray(list))
1009
+ throw new Error(`review repositories[${index}].${key} must be an array`);
1010
+ return list.map((item) => parseFinding(item, severity));
1011
+ };
1012
+ return {
1013
+ id,
1014
+ repository: published.repository,
1015
+ baseBranch: published.baseBranch,
1016
+ headRevision: published.headRevision,
1017
+ ...(published.dependencyFingerprint !== undefined
1018
+ ? { dependencyFingerprint: published.dependencyFingerprint }
1019
+ : {}),
1020
+ invocationSucceeded: raw.invocationSucceeded === true,
1021
+ p0: parseList("p0", "P0"),
1022
+ p1: parseList("p1", "P1"),
1023
+ p2: parseList("p2", "P2"),
1024
+ lower: parseList("lower", "lower"),
1025
+ reason: requireString(raw.reason, `review repositories[${index}].reason`),
1026
+ } satisfies RepositoryReviewAssessment;
1027
+ });
1028
+ if (expected.size > 0) {
1029
+ throw new Error(
1030
+ `review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`,
1031
+ );
1032
+ }
1033
+ const p0 = repositories.flatMap((entry) => entry.p0);
1034
+ const p1 = repositories.flatMap((entry) => entry.p1);
1035
+ const p2 = repositories.flatMap((entry) => entry.p2);
1036
+ const lower = repositories.flatMap((entry) => entry.lower);
1037
+ const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
516
1038
  const route = !invocationSucceeded
517
1039
  ? "command_error"
518
1040
  : p0.length + p1.length > 0
@@ -528,6 +1050,7 @@ function parseReviewAssessment(value: unknown): ReviewAssessment {
528
1050
  p2,
529
1051
  lower,
530
1052
  reason: requireString(review.reason, "review reason"),
1053
+ repositories,
531
1054
  };
532
1055
  }
533
1056
 
@@ -537,8 +1060,39 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
537
1060
  .map((step) => step.output as ReviewAssessment);
538
1061
  }
539
1062
 
1063
+ function reviewRoundsForOutput(context: WorkflowNodeContext): ReviewAssessment[] {
1064
+ const rounds = reviewRounds(context);
1065
+ const repositoryIds = new Set(
1066
+ rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)),
1067
+ );
1068
+ if (repositoryIds.size > 1) return rounds;
1069
+ return rounds.map(({ repositories: _repositories, ...round }) => round);
1070
+ }
1071
+
1072
+ function ciForOutput(context: WorkflowNodeContext): unknown {
1073
+ const result = latestOutput<Record<string, unknown>>(context, ["assessTrackedCi", "inspectCi"]);
1074
+ const targets = result.targets;
1075
+ if (!Array.isArray(targets) || targets.length !== 1) return result;
1076
+ const { targets: _targets, ...aggregate } = result;
1077
+ if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
1078
+ const target = targets[0];
1079
+ if (target !== null && typeof target === "object" && !Array.isArray(target)) {
1080
+ const record = target as Record<string, unknown>;
1081
+ return {
1082
+ ...aggregate,
1083
+ reason: aggregate.reason ?? record.reason,
1084
+ relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
1085
+ unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
1086
+ };
1087
+ }
1088
+ }
1089
+ return aggregate;
1090
+ }
1091
+
540
1092
  function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
541
1093
  const candidates = [
1094
+ "timeoutFallbackGuard",
1095
+ "timeoutFallback",
542
1096
  "challengeBlockerGuard",
543
1097
  "challengeBlocker",
544
1098
  "finalizeDelivery",
@@ -549,11 +1103,10 @@ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; ev
549
1103
  "classifyImplementation",
550
1104
  "classifyVerification",
551
1105
  "triageReview",
552
- "replanGuard",
1106
+ "redesign",
553
1107
  "adoptPlan",
554
1108
  "findPlan",
555
1109
  "documentation",
556
- "approval",
557
1110
  ];
558
1111
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
559
1112
  const step = context.state.steps[index];
@@ -595,37 +1148,18 @@ export const autoimplementWorkflow = defineWorkflow({
595
1148
  };
596
1149
  },
597
1150
  }),
598
- approval: includeWorkflow(planApprovalWorkflow, {
599
- input: (context): PlanApprovalInput => {
1151
+ redesign: includeWorkflow(planChangeWorkflow, {
1152
+ input: (context): NormalizedPlanChangeInput => {
600
1153
  const request = context.input as AutoimplementInput;
601
- if (request.approval === undefined) {
602
- throw new Error("autoimplement approval was entered without an approval policy");
603
- }
604
- const plan = currentPlan(context);
605
- if (plan === undefined) throw new Error("autoimplement approval is missing a plan");
606
- const revisions = context.state.steps.filter(
607
- (step) => step.nodeId === "approval/approve",
608
- ).length;
609
1154
  return {
610
1155
  task: request.task,
611
- plan,
612
- planDigest: currentPlanDigest(context),
613
- audience: request.approval.audience,
614
- revision: revisions + 1,
615
- };
616
- },
617
- }),
618
- redesign: includeWorkflow({
619
- workflow: "autoplan",
620
- contract: autoplanWorkflow,
621
- input: (context): AutoplanInput => {
622
- const request = context.input as AutoimplementInput;
623
- return {
624
- problem: request.task,
625
1156
  ...(request.scope !== undefined ? { scope: request.scope } : {}),
626
1157
  ...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
1158
+ ...(request.repository !== undefined ? { repository: request.repository } : {}),
1159
+ documents: request.documents ?? request.documentation?.documents ?? [],
627
1160
  ...(currentPlan(context) !== undefined ? { previousPlan: currentPlan(context) } : {}),
628
1161
  newEvidence: latestIssue(context),
1162
+ approval: parsePlanApprovalPolicy(request.approval),
629
1163
  };
630
1164
  },
631
1165
  }),
@@ -689,42 +1223,69 @@ export const autoimplementWorkflow = defineWorkflow({
689
1223
  }),
690
1224
  adoptPlan: compute({
691
1225
  run: ({ outputs }) => {
692
- const result = includedResult(autoplanWorkflow, outputs.redesign);
1226
+ const result = includedResult(planChangeWorkflow, outputs.redesign);
693
1227
  if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
694
1228
  return {
695
- route: result.output.changed ? "document" : "blocked",
696
1229
  plan: result.output.plan,
697
1230
  planDigest: result.output.planDigest,
698
- changed: result.output.changed,
699
- reason: result.output.changed
700
- ? "The plan changed in response to new evidence and must be documented."
701
- : "Redesign returned the same plan for the same unresolved evidence.",
1231
+ documents: result.output.documents,
1232
+ approval: result.output.approval,
1233
+ reason: "The changed plan was documented and passed its approval policy.",
702
1234
  };
703
1235
  },
704
1236
  }),
705
- maybeApproval: compute({
706
- run: ({ input }) => ({
707
- route: (input as AutoimplementInput).approval === undefined ? "implement" : "approve",
708
- }),
1237
+ timeoutFallbackGuard: compute({
1238
+ run: timeoutFallbackGuard,
709
1239
  }),
710
- replanGuard: compute({
711
- run: (context) => {
1240
+ timeoutFallback: agent({
1241
+ timeoutMs: 30 * 60_000,
1242
+ statusDetail: "choosing a safe timeout fallback",
1243
+ prompt: (context) => {
712
1244
  const request = context.input as AutoimplementInput;
713
- const limit = request.approval?.maxReplans ?? 3;
714
- const replans = context.state.steps.filter(
715
- (step) => step.nodeId === "approval/replan",
716
- ).length;
717
- return replans > limit
718
- ? {
719
- route: "blocked",
720
- reason: `Plan approval reached the ${limit}-replan safety limit.`,
721
- evidence: context.outputs.approval,
722
- }
723
- : { route: "redesign", replans, limit };
1245
+ const guard = context.outputs.timeoutFallbackGuard;
1246
+ const previousFallbacks = context.state.steps
1247
+ .filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
1248
+ .map((step) => step.output);
1249
+ return [
1250
+ "A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
1251
+ "This is a read-only fallback step. Inspect state, but do not edit files, run mutating commands, commit, push, open or update a pull request, post comments, merge, deploy, or release.",
1252
+ "Inspect the current repository worktree, branch, diff, and commits. Inspect the remote branch, pull request, review, CI, merge, and final report when they exist and affect the next route.",
1253
+ "Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
1254
+ "Before any forward route, confirm that its accepted output belongs to the current work attempt and that observed local and remote heads match the accepted publication. Otherwise retry, replan, or block.",
1255
+ "Choose retry only when the timed-out stage must run again. Choose verify when accepted implementation output exists and verification is next. Choose review when accepted publication output exists. Choose ci only after comment inspection routed to CI. Choose deliver only after CI is green or classified unrelated. Choose replan when evidence invalidates the approved plan. Choose blocked only when no safe route exists.",
1256
+ "Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
1257
+ `Task: ${request.task}`,
1258
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
1259
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
1260
+ `Timeout: ${JSON.stringify(guard)}`,
1261
+ `Accepted outputs: ${JSON.stringify(context.outputs)}`,
1262
+ `Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
1263
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
1264
+ ].join("\n");
724
1265
  },
1266
+ expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
1267
+ validate: parseTimeoutFallback,
1268
+ }),
1269
+ routeTimeoutFallback: compute({
1270
+ run: timeoutFallbackTarget,
1271
+ }),
1272
+ propagateSupportedFailure: compute({
1273
+ run: throwLatestSupportedFailure,
1274
+ }),
1275
+ routeVerifyP2Result: compute({
1276
+ run: ({ outputs }) => outputs.verifyP2,
1277
+ }),
1278
+ routeInspectCommentsResult: compute({
1279
+ run: ({ outputs }) => outputs.inspectComments,
1280
+ }),
1281
+ routeInspectCiResult: compute({
1282
+ run: ({ outputs }) => outputs.inspectCi,
1283
+ }),
1284
+ routeFinalizeDeliveryResult: compute({
1285
+ run: ({ outputs }) => outputs.finalizeDelivery,
725
1286
  }),
726
1287
  implement: agent({
727
- timeoutMs: 60 * 60_000,
1288
+ timeoutMs: 8 * 60 * 60_000,
728
1289
  statusDetail: "implementing",
729
1290
  prompt: (context) => {
730
1291
  const request = context.input as AutoimplementInput;
@@ -733,12 +1294,14 @@ export const autoimplementWorkflow = defineWorkflow({
733
1294
  `Plan: ${JSON.stringify(currentPlan(context))}`,
734
1295
  `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
735
1296
  `Constraints: ${JSON.stringify(request.constraints ?? [])}`,
1297
+ "Before changing files, inspect the current worktree, diff, commits, branch, remote state, and matching pull request. Continue existing work and do not repeat completed effects.",
736
1298
  "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
737
1299
  "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
1300
+ "Report every changed repository as an absolute path so independent verification can be bounded safely.",
738
1301
  "Do not merge yet.",
739
1302
  ].join("\n");
740
1303
  },
741
- expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
1304
+ expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "repositories": ["absolute repository path changed"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
742
1305
  validate: (value) => requireRecord(value, "implementation result"),
743
1306
  }),
744
1307
  classifyImplementation: agent({
@@ -805,15 +1368,46 @@ export const autoimplementWorkflow = defineWorkflow({
805
1368
  expectedOutput: `{ "route": "continue" | "blocked", "blockingNow": true | false, "outsideAuthority": true | false, "canProceed": true | false, "reason": "concise reason", "nextAction": "practical action or empty when blocked", "alternativesChecked": ["checked alternative"], "evidence": ["concrete evidence"] }`,
806
1369
  validate: parseBlockerChallenge,
807
1370
  }),
808
- verify: agent({
809
- timeoutMs: 45 * 60_000,
810
- statusDetail: "verifying",
1371
+ planVerification: agent({
1372
+ timeoutMs: 15 * 60_000,
1373
+ statusDetail: "planning independent verification commands",
811
1374
  prompt: () =>
812
1375
  [
813
- "Verify the implementation thoroughly.",
814
- "Run required tests, formatting, lint, type checks, builds, and useful local smoke tests.",
815
- "Do not put optional mutation testing on the critical path.",
816
- "State exactly what ran, what passed, what failed, and what still needs remote verification.",
1376
+ "Select the required local verification commands for the implementation.",
1377
+ "Return one command per independent repository working directory.",
1378
+ "Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
1379
+ "Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
1380
+ "List checks that cannot run locally under untested.",
1381
+ ].join("\n"),
1382
+ expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
1383
+ validate: parseVerificationForContext,
1384
+ }),
1385
+ runVerification: action({
1386
+ timeoutMs: (context) => {
1387
+ const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
1388
+ return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
1389
+ },
1390
+ statusDetail: "running independent verification commands",
1391
+ run: async (context) => {
1392
+ const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
1393
+ return await runAutoimplementBatch(
1394
+ context,
1395
+ "verification",
1396
+ plan.commands,
1397
+ concurrency(context).verification,
1398
+ );
1399
+ },
1400
+ }),
1401
+ verify: agent({
1402
+ timeoutMs: 20 * 60_000,
1403
+ statusDetail: "assessing verification",
1404
+ prompt: (context) =>
1405
+ [
1406
+ "Assess the completed local verification commands.",
1407
+ "Set passed true only when every required command succeeded without truncated output.",
1408
+ "Report exact command outcomes, failures, and checks that still need remote verification.",
1409
+ `Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
1410
+ `Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
817
1411
  ].join("\n"),
818
1412
  expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
819
1413
  validate: (value) => requireRecord(value, "verification result"),
@@ -843,6 +1437,7 @@ export const autoimplementWorkflow = defineWorkflow({
843
1437
  prompt: (context) =>
844
1438
  [
845
1439
  "Fix the current implementation issue without expanding the approved design.",
1440
+ "Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
846
1441
  `Issue: ${JSON.stringify(latestIssue(context))}`,
847
1442
  `Current plan: ${JSON.stringify(currentPlan(context))}`,
848
1443
  "Stop after the fix so verification can run again.",
@@ -857,82 +1452,68 @@ export const autoimplementWorkflow = defineWorkflow({
857
1452
  const request = input as AutoimplementInput;
858
1453
  return [
859
1454
  "Commit and push the verified implementation before review.",
1455
+ "Inspect the branch, local and remote heads, and matching pull requests first. Do not push an already-pushed head or create a second pull request for the same branch and base.",
860
1456
  "Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
861
1457
  "Inspect the complete public diff before every push or PR mutation.",
862
- `Requested base branch: ${request.baseBranch ?? "discover the repository default branch"}.`,
1458
+ "Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
1459
+ "Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
1460
+ `Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
863
1461
  "Do not merge yet.",
864
1462
  ].join("\n");
865
1463
  },
866
- expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
867
- validate: (value) => requireRecord(value, "publication result"),
1464
+ expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
1465
+ validate: parsePublishedRepositories,
868
1466
  }),
869
- authorReviewCommand: agent({
870
- statusDetail: "writing reviewer command",
871
- prompt: ({ outputs, input }) => {
872
- const published = outputs.publish as Record<string, unknown>;
873
- const request = input as AutoimplementInput;
874
- return [
875
- "Write the exact Pi Reviewer command for the pushed branch.",
876
- "The executable must be pi-reviewer. Use its configured model and thinking settings.",
877
- "Use the repository base branch and an absolute repository working directory.",
878
- "Set timeoutMs to at most 600000.",
879
- `Published branch: ${JSON.stringify(published)}`,
880
- `Repository hint: ${request.repository ?? "current repository"}`,
881
- ].join("\n");
882
- },
883
- expectedOutput: `{ "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000 }`,
884
- validate: parseReviewerCommand,
1467
+ selectReviewCommands: compute({
1468
+ run: selectReviewCommands,
885
1469
  }),
886
- runReview: shell({
887
- statusDetail: "running Pi Reviewer",
888
- timeoutMs: TEN_MINUTES_MS + 10_000,
889
- exec: (context) =>
890
- commandExecution(
891
- latestOutput<StructuredCommand>(context, ["authorReviewCommand", "repairReviewCommand"]),
892
- ),
893
- }),
894
- repairReviewCommand: agent({
895
- statusDetail: "correcting reviewer command",
896
- prompt: (context) => {
897
- const failed = context.results.runReview;
898
- return [
899
- "The Pi Reviewer invocation failed. Diagnose the exact command, arguments, base branch, working directory, and error.",
900
- "Write a corrected pi-reviewer command. Do not substitute codex review or another reviewer.",
901
- "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.",
902
- `Failed result: ${JSON.stringify(failed)}`,
903
- ].join("\n");
1470
+ runReview: action({
1471
+ statusDetail: "running pi-reviewer commands",
1472
+ timeoutMs: (context) => {
1473
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1474
+ return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
904
1475
  },
905
- expectedOutput: `{ "route": "retry" | "blocked", "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000, "reason": "diagnosis" }`,
906
- validate: (value) => {
907
- const result = requireRecord(value, "reviewer command repair");
908
- if (result.route === "blocked") {
909
- return {
910
- route: "blocked",
911
- reason: requireString(result.reason, "reviewer command blocker"),
912
- };
913
- }
914
- if (result.route !== "retry")
915
- throw new Error("reviewer command repair route must be retry or blocked");
916
- return {
917
- route: "retry",
918
- ...parseReviewerCommand(result),
919
- reason: requireString(result.reason, "reviewer command repair reason"),
920
- };
1476
+ run: async (context): Promise<BatchExecution> => {
1477
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1478
+ const batch = await runAutoimplementBatch(
1479
+ context,
1480
+ "review",
1481
+ selected.commands,
1482
+ concurrency(context).reviewer,
1483
+ );
1484
+ return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
921
1485
  },
922
1486
  }),
1487
+ repairReviewCommand: agent({
1488
+ statusDetail: "repairing reviewer prerequisites",
1489
+ prompt: (context) =>
1490
+ [
1491
+ "One or more pi-reviewer commands failed, timed out, or returned truncated output.",
1492
+ "Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
1493
+ "Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
1494
+ "Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
1495
+ `Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
1496
+ ].join("\n"),
1497
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
1498
+ validate: (value) =>
1499
+ parseRoute(value, ["retry", "blocked"] as const, "reviewer command repair"),
1500
+ }),
923
1501
  assessReview: agent({
924
1502
  statusDetail: "assessing reviewer findings",
925
1503
  prompt: (context) => {
926
- const result = latestOutput<ShellActionResult>(context, ["runReview"]);
1504
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1505
+ const execution = latestOutput<BatchExecution>(context, ["runReview"]);
927
1506
  return [
928
- "Assess the completed Pi Reviewer invocation.",
929
- "Set invocationSucceeded false only when the reviewer did not produce a valid review.",
930
- "Record each finding under P0, P1, P2, or lower. Mark each finding as design or implementation.",
1507
+ "Assess each completed pi-reviewer result separately.",
1508
+ "Return one repository entry for every selected command, using the exact repository id.",
1509
+ "Set invocationSucceeded false when a complete valid review was not produced.",
1510
+ "Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
931
1511
  "Do not promote P2 findings to P1 merely to force another review round.",
932
- `Reviewer result: ${JSON.stringify(result)}`,
1512
+ `Selected repositories: ${JSON.stringify(selected.repositories)}`,
1513
+ `Reviewer results: ${JSON.stringify(execution.batch)}`,
933
1514
  ].join("\n");
934
1515
  },
935
- expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
1516
+ expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
936
1517
  validate: parseReviewAssessment,
937
1518
  }),
938
1519
  triageReview: compute({
@@ -952,7 +1533,8 @@ export const autoimplementWorkflow = defineWorkflow({
952
1533
  prompt: ({ outputs }) =>
953
1534
  [
954
1535
  "Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
955
- "Do not rerun Pi Reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
1536
+ "Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
1537
+ "Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
956
1538
  `Review: ${JSON.stringify(outputs.assessReview)}`,
957
1539
  ].join("\n"),
958
1540
  expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
@@ -964,20 +1546,23 @@ export const autoimplementWorkflow = defineWorkflow({
964
1546
  prompt: () =>
965
1547
  [
966
1548
  "Run focused verification for the P2 changes and push the verified result.",
967
- "Do not run Pi Reviewer again because the previous round had no P0 or P1 findings.",
1549
+ "Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
1550
+ "Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
1551
+ "Re-observe every published PR after the push and return its current repository, branch, base branch, head revision, PR URL, pushed status, and unchanged dependency fingerprint.",
968
1552
  "Report exact commands and outcomes.",
969
1553
  ].join("\n"),
970
- expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
971
- validate: (value) => requireRecord(value, "P2 verification"),
1554
+ expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true, "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "current pushed revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional fingerprint" }] }`,
1555
+ validate: parseP2Verification,
972
1556
  }),
973
1557
  inspectComments: agent({
974
1558
  timeoutMs: 20 * 60_000,
975
1559
  statusDetail: "checking PR comments",
976
- prompt: () =>
1560
+ prompt: (context) =>
977
1561
  [
978
- "Inspect current inline review comments and PR issue comments.",
979
- "Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
980
- "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.",
1562
+ "Inspect current inline review comments and PR issue comments for every published pull request.",
1563
+ "Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
1564
+ "Choose redesign for a valid design issue, fix for a local code issue, ci when no actionable comment remains on any PR, or blocked for an external blocker.",
1565
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
981
1566
  ].join("\n"),
982
1567
  expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
983
1568
  validate: (value) =>
@@ -986,71 +1571,75 @@ export const autoimplementWorkflow = defineWorkflow({
986
1571
  inspectCi: agent({
987
1572
  timeoutMs: 10 * 60_000,
988
1573
  statusDetail: "checking CI",
989
- prompt: () =>
1574
+ prompt: (context) =>
990
1575
  [
991
- "Inspect CI once without waiting for completion.",
992
- "Choose green, failed, pending, or unavailable.",
993
- "When pending, provide an exact gh command that tracks this PR or run and set timeoutMs to at most 300000.",
994
- "Separate failures caused by this change from unrelated failures.",
1576
+ "Inspect every published pull request once without waiting for completion.",
1577
+ "Return one target per repository and current PR head.",
1578
+ "Choose green, failed, pending, or unavailable for each target.",
1579
+ "When pending, provide an exact supported gh pr checks --watch or gh run watch command with the repository id, absolute repository cwd, timeoutMs at most 300000, and maxOutputChars at most 1000000. The workflow binds it to the target PR before execution.",
1580
+ "Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
1581
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
995
1582
  ].join("\n"),
996
- 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) }`,
997
- validate: (value) => {
998
- const result = parseRoute(
999
- value,
1000
- ["green", "failed", "pending", "unavailable"] as const,
1001
- "CI inspection",
1583
+ expectedOutput: `{ "targets": [{ "repository": "/absolute/repository", "headRevision": "revision", "pr": "URL", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"], "trackingCommand": { "id": "repository-id", "command": "gh", "args": ["pr", "checks", "PR URL", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000, "maxOutputChars": 1000000 } }] }`,
1584
+ validate: parseCiInspectionForPublished,
1585
+ }),
1586
+ trackCi: action({
1587
+ statusDetail: "tracking pending CI commands",
1588
+ timeoutMs: (context) => {
1589
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1590
+ const commands = inspected.targets.flatMap((target) =>
1591
+ target.trackingCommand === undefined ? [] : [target.trackingCommand],
1002
1592
  );
1003
- if (result.route === "pending")
1004
- result.trackingCommand = parseCiCommand(result.trackingCommand);
1005
- return result;
1593
+ return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
1594
+ },
1595
+ run: async (context): Promise<BatchExecution> => {
1596
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1597
+ const commands = inspected.targets.flatMap((target) =>
1598
+ target.trackingCommand === undefined ? [] : [target.trackingCommand],
1599
+ );
1600
+ const batch = await runAutoimplementBatch(
1601
+ context,
1602
+ "ciWatch",
1603
+ commands,
1604
+ concurrency(context).ciWatch,
1605
+ );
1606
+ const needsRepair = batch.items.some(
1607
+ (item) =>
1608
+ (item.outcome === "failed" && item.exitCode === null) ||
1609
+ item.stdoutTruncated ||
1610
+ item.stderrTruncated,
1611
+ );
1612
+ return { route: needsRepair ? "repair" : "assess", batch };
1006
1613
  },
1007
- }),
1008
- trackCi: shell({
1009
- statusDetail: "tracking CI for at most five minutes",
1010
- timeoutMs: FIVE_MINUTES_MS + 10_000,
1011
- exec: (context) => commandExecution(latestCiCommand(context)),
1012
1614
  }),
1013
1615
  repairCiCommand: agent({
1014
- statusDetail: "correcting CI tracking command",
1616
+ statusDetail: "repairing CI watch prerequisites",
1015
1617
  prompt: (context) =>
1016
1618
  [
1017
- "The CI tracking command failed before it could provide a useful status.",
1018
- "Write a corrected gh pr checks --watch or gh run watch command for the same PR or run.",
1019
- "Use an absolute repository path and a timeout no longer than five minutes.",
1020
- `Failure: ${JSON.stringify(context.results.trackCi)}`,
1619
+ "One or more supported CI watch commands failed or returned truncated output.",
1620
+ "Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
1621
+ "Do not change the PR identity or substitute another command form.",
1622
+ "Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
1623
+ `Failure: ${JSON.stringify(context.outputs.trackCi)}`,
1021
1624
  ].join("\n"),
1022
- expectedOutput: `{ "route": "retry" | "blocked", "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000, "reason": "diagnosis" }`,
1023
- validate: (value) => {
1024
- const result = requireRecord(value, "CI command repair");
1025
- if (result.route === "blocked") {
1026
- return { route: "blocked", reason: requireString(result.reason, "CI command blocker") };
1027
- }
1028
- if (result.route !== "retry")
1029
- throw new Error("CI command repair route must be retry or blocked");
1030
- return {
1031
- route: "retry",
1032
- ...parseCiCommand(result),
1033
- reason: requireString(result.reason, "CI command repair reason"),
1034
- };
1035
- },
1625
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
1626
+ validate: (value) => parseRoute(value, ["retry", "blocked"] as const, "CI command repair"),
1036
1627
  }),
1037
1628
  assessTrackedCi: agent({
1038
1629
  statusDetail: "assessing tracked CI",
1039
1630
  prompt: (context) => {
1040
- const result = latestOutput<ShellActionResult>(context, ["trackCi"]);
1631
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1632
+ const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
1041
1633
  return [
1042
- "Assess the CI tracking result without starting another wait.",
1043
- "Choose green, failed, pending, or unavailable and separate related from unrelated failures.",
1044
- `Tracking result: ${JSON.stringify(result)}`,
1634
+ "Assess every CI watch result without starting another wait.",
1635
+ "Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
1636
+ "A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
1637
+ `Initial inspection: ${JSON.stringify(inspected)}`,
1638
+ `Tracking results: ${JSON.stringify(execution.batch)}`,
1045
1639
  ].join("\n");
1046
1640
  },
1047
- expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
1048
- validate: (value) =>
1049
- parseRoute(
1050
- value,
1051
- ["green", "failed", "pending", "unavailable"] as const,
1052
- "tracked CI assessment",
1053
- ),
1641
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
1642
+ validate: parseTrackedCiAssessment,
1054
1643
  }),
1055
1644
  opportunisticTest: agent({
1056
1645
  timeoutMs: 30 * 60_000,
@@ -1083,30 +1672,23 @@ export const autoimplementWorkflow = defineWorkflow({
1083
1672
  }),
1084
1673
  finalizeDelivery: agent({
1085
1674
  timeoutMs: 30 * 60_000,
1086
- statusDetail: "finalizing PR",
1087
- prompt: ({ input }) => {
1088
- const request = input as AutoimplementInput;
1675
+ statusDetail: "finalizing PRs",
1676
+ prompt: (context) => {
1677
+ const request = context.input as AutoimplementInput;
1089
1678
  return [
1090
1679
  request.merge === false
1091
- ? "Leave the verified PR ready without merging because input disabled merge."
1092
- : "Merge the verified PR unless repository policy or explicit user instructions prohibit it.",
1093
- "Use the repository's required merge method.",
1094
- "Post a final PR report with the implementation summary and exact validation commands.",
1680
+ ? "Leave every verified PR ready without merging because input disabled merge."
1681
+ : "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
1682
+ "Before each mutation, inspect the current PR head, merge state, and existing final report. Do not merge an already merged expected head or post a duplicate report.",
1683
+ "Use each repository's required merge method.",
1684
+ "Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
1685
+ "Keep the existing top-level merged, pr, reportComment, and reason fields. For several PRs, use the first PR for the top-level compatibility fields and include every result under repositories.",
1095
1686
  "Return blocked instead of claiming completion when a required merge or report action fails.",
1687
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
1096
1688
  ].join("\n");
1097
1689
  },
1098
- expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "URL", "reportComment": "URL or summary", "reason": "result" }`,
1099
- validate: (value, context) => {
1100
- const result = requireRecord(value, "delivery result");
1101
- if (result.status !== "completed" && result.status !== "blocked") {
1102
- throw new Error("delivery status must be completed or blocked");
1103
- }
1104
- const request = context.input as AutoimplementInput;
1105
- if (request.merge !== true && result.merged === true) {
1106
- throw new Error("delivery cannot merge without explicit merge: true");
1107
- }
1108
- return result;
1109
- },
1690
+ expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "first PR URL", "reportComment": "first report URL or summary", "reason": "aggregate result", "repositories": [{ "repository": "/absolute/repository", "pr": "URL", "merged": true | false, "reportComment": "URL or summary", "reason": "result" }] }`,
1691
+ validate: parseDeliveryResult,
1110
1692
  }),
1111
1693
  blocked: compute({
1112
1694
  run: (context) => {
@@ -1127,11 +1709,11 @@ export const autoimplementWorkflow = defineWorkflow({
1127
1709
  status: "completed",
1128
1710
  task: request.task,
1129
1711
  plan: currentPlan(context),
1130
- implementation: context.outputs.implement,
1131
- verification: context.outputs.verifyP2 ?? context.outputs.verify,
1132
- reviewRounds: reviewRounds(context),
1133
- ci: context.outputs.assessTrackedCi ?? context.outputs.inspectCi,
1134
- delivery: context.outputs.finalizeDelivery,
1712
+ implementation: latestOutput(context, ["implement"]),
1713
+ verification: latestOutput(context, ["verifyP2", "verify"]),
1714
+ reviewRounds: reviewRoundsForOutput(context),
1715
+ ci: ciForOutput(context),
1716
+ delivery: latestOutput(context, ["finalizeDelivery"]),
1135
1717
  } satisfies AutoimplementCompleted;
1136
1718
  },
1137
1719
  }),
@@ -1141,7 +1723,7 @@ export const autoimplementWorkflow = defineWorkflow({
1141
1723
  from: "prepare",
1142
1724
  switch: {
1143
1725
  on: "$.route",
1144
- cases: { find: "findPlan", document: "documentation", ready: "maybeApproval" },
1726
+ cases: { find: "findPlan", document: "documentation", ready: "implement" },
1145
1727
  },
1146
1728
  },
1147
1729
  {
@@ -1152,35 +1734,59 @@ export const autoimplementWorkflow = defineWorkflow({
1152
1734
  from: "routeFoundPlan",
1153
1735
  switch: {
1154
1736
  on: "$.route",
1155
- cases: { ready: "maybeApproval", document: "documentation", blocked: "blocked" },
1737
+ cases: { ready: "implement", document: "documentation", blocked: "blocked" },
1156
1738
  },
1157
1739
  },
1158
1740
  { from: "redesign.ready", to: "adoptPlan" },
1159
1741
  { from: "redesign.blocked", to: "blocked" },
1742
+ { from: "adoptPlan", to: "implement" },
1743
+ { from: "documentation.ready", to: "implement" },
1744
+ { from: "documentation.blocked", to: "blocked" },
1160
1745
  {
1161
- from: "adoptPlan",
1162
- switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
1746
+ from: "timeoutFallbackGuard",
1747
+ switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
1163
1748
  },
1164
- { from: "documentation.ready", to: "maybeApproval" },
1165
- { from: "documentation.blocked", to: "blocked" },
1749
+ { from: "timeoutFallback", to: "routeTimeoutFallback" },
1750
+ { from: "propagateSupportedFailure", to: "blocked" },
1166
1751
  {
1167
- from: "maybeApproval",
1168
- switch: { on: "$.route", cases: { approve: "approval", implement: "implement" } },
1752
+ from: "routeTimeoutFallback",
1753
+ switch: {
1754
+ on: "$.route",
1755
+ cases: {
1756
+ implement: "implement",
1757
+ planVerification: "planVerification",
1758
+ verify: "verify",
1759
+ fix: "fix",
1760
+ publish: "publish",
1761
+ addressP2: "addressP2",
1762
+ verifyP2: "verifyP2",
1763
+ inspectComments: "inspectComments",
1764
+ inspectCi: "inspectCi",
1765
+ opportunisticTest: "opportunisticTest",
1766
+ finalizeDelivery: "finalizeDelivery",
1767
+ selectReviewCommands: "selectReviewCommands",
1768
+ redesign: "redesign",
1769
+ blocked: "blocked",
1770
+ },
1771
+ },
1169
1772
  },
1170
- { from: "approval.continue", to: "implement" },
1171
- { from: "approval.stop", to: "blocked" },
1172
- { from: "approval.replan", to: "replanGuard" },
1173
1773
  {
1174
- from: "replanGuard",
1175
- switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
1774
+ from: "implement",
1775
+ switch: {
1776
+ on: "$result.outcome",
1777
+ cases: {
1778
+ ok: "classifyImplementation",
1779
+ timed_out: "timeoutFallbackGuard",
1780
+ failed: "propagateSupportedFailure",
1781
+ },
1782
+ },
1176
1783
  },
1177
- { from: "implement", to: "classifyImplementation" },
1178
1784
  {
1179
1785
  from: "classifyImplementation",
1180
1786
  switch: {
1181
1787
  on: "$.route",
1182
1788
  cases: {
1183
- verify: "verify",
1789
+ verify: "planVerification",
1184
1790
  redesign: "redesign",
1185
1791
  fix: "fix",
1186
1792
  blocked: "challengeBlockerGuard",
@@ -1195,7 +1801,29 @@ export const autoimplementWorkflow = defineWorkflow({
1195
1801
  from: "challengeBlocker",
1196
1802
  switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1197
1803
  },
1198
- { from: "verify", to: "classifyVerification" },
1804
+ {
1805
+ from: "planVerification",
1806
+ switch: {
1807
+ on: "$result.outcome",
1808
+ cases: {
1809
+ ok: "runVerification",
1810
+ timed_out: "timeoutFallbackGuard",
1811
+ failed: "propagateSupportedFailure",
1812
+ },
1813
+ },
1814
+ },
1815
+ { from: "runVerification", to: "verify" },
1816
+ {
1817
+ from: "verify",
1818
+ switch: {
1819
+ on: "$result.outcome",
1820
+ cases: {
1821
+ ok: "classifyVerification",
1822
+ timed_out: "timeoutFallbackGuard",
1823
+ failed: "propagateSupportedFailure",
1824
+ },
1825
+ },
1826
+ },
1199
1827
  {
1200
1828
  from: "classifyVerification",
1201
1829
  switch: {
@@ -1208,20 +1836,36 @@ export const autoimplementWorkflow = defineWorkflow({
1208
1836
  },
1209
1837
  },
1210
1838
  },
1211
- { from: "fix", to: "verify" },
1212
- { from: "publish", to: "authorReviewCommand" },
1213
- { from: "authorReviewCommand", to: "runReview" },
1214
1839
  {
1215
- from: "runReview",
1840
+ from: "fix",
1841
+ switch: {
1842
+ on: "$result.outcome",
1843
+ cases: {
1844
+ ok: "planVerification",
1845
+ timed_out: "timeoutFallbackGuard",
1846
+ failed: "propagateSupportedFailure",
1847
+ },
1848
+ },
1849
+ },
1850
+ {
1851
+ from: "publish",
1216
1852
  switch: {
1217
1853
  on: "$result.outcome",
1218
1854
  cases: {
1219
- ok: "assessReview",
1220
- failed: "repairReviewCommand",
1221
- timed_out: "repairReviewCommand",
1855
+ ok: "selectReviewCommands",
1856
+ timed_out: "timeoutFallbackGuard",
1857
+ failed: "propagateSupportedFailure",
1222
1858
  },
1223
1859
  },
1224
1860
  },
1861
+ {
1862
+ from: "selectReviewCommands",
1863
+ switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
1864
+ },
1865
+ {
1866
+ from: "runReview",
1867
+ switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
1868
+ },
1225
1869
  {
1226
1870
  from: "repairReviewCommand",
1227
1871
  switch: {
@@ -1245,13 +1889,45 @@ export const autoimplementWorkflow = defineWorkflow({
1245
1889
  from: "triageReview",
1246
1890
  switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
1247
1891
  },
1248
- { from: "addressP2", to: "verifyP2" },
1892
+ {
1893
+ from: "addressP2",
1894
+ switch: {
1895
+ on: "$result.outcome",
1896
+ cases: {
1897
+ ok: "verifyP2",
1898
+ timed_out: "timeoutFallbackGuard",
1899
+ failed: "propagateSupportedFailure",
1900
+ },
1901
+ },
1902
+ },
1249
1903
  {
1250
1904
  from: "verifyP2",
1905
+ switch: {
1906
+ on: "$result.outcome",
1907
+ cases: {
1908
+ ok: "routeVerifyP2Result",
1909
+ timed_out: "timeoutFallbackGuard",
1910
+ failed: "propagateSupportedFailure",
1911
+ },
1912
+ },
1913
+ },
1914
+ {
1915
+ from: "routeVerifyP2Result",
1251
1916
  switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
1252
1917
  },
1253
1918
  {
1254
1919
  from: "inspectComments",
1920
+ switch: {
1921
+ on: "$result.outcome",
1922
+ cases: {
1923
+ ok: "routeInspectCommentsResult",
1924
+ timed_out: "timeoutFallbackGuard",
1925
+ failed: "propagateSupportedFailure",
1926
+ },
1927
+ },
1928
+ },
1929
+ {
1930
+ from: "routeInspectCommentsResult",
1255
1931
  switch: {
1256
1932
  on: "$.route",
1257
1933
  cases: {
@@ -1264,6 +1940,17 @@ export const autoimplementWorkflow = defineWorkflow({
1264
1940
  },
1265
1941
  {
1266
1942
  from: "inspectCi",
1943
+ switch: {
1944
+ on: "$result.outcome",
1945
+ cases: {
1946
+ ok: "routeInspectCiResult",
1947
+ timed_out: "timeoutFallbackGuard",
1948
+ failed: "propagateSupportedFailure",
1949
+ },
1950
+ },
1951
+ },
1952
+ {
1953
+ from: "routeInspectCiResult",
1267
1954
  switch: {
1268
1955
  on: "$.route",
1269
1956
  cases: {
@@ -1276,10 +1963,7 @@ export const autoimplementWorkflow = defineWorkflow({
1276
1963
  },
1277
1964
  {
1278
1965
  from: "trackCi",
1279
- switch: {
1280
- on: "$result.outcome",
1281
- cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
1282
- },
1966
+ switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
1283
1967
  },
1284
1968
  {
1285
1969
  from: "repairCiCommand",
@@ -1300,7 +1984,17 @@ export const autoimplementWorkflow = defineWorkflow({
1300
1984
  },
1301
1985
  },
1302
1986
  },
1303
- { from: "opportunisticTest", to: "inspectCi" },
1987
+ {
1988
+ from: "opportunisticTest",
1989
+ switch: {
1990
+ on: "$result.outcome",
1991
+ cases: {
1992
+ ok: "inspectCi",
1993
+ timed_out: "timeoutFallbackGuard",
1994
+ failed: "propagateSupportedFailure",
1995
+ },
1996
+ },
1997
+ },
1304
1998
  {
1305
1999
  from: "classifyCi",
1306
2000
  switch: {
@@ -1315,6 +2009,17 @@ export const autoimplementWorkflow = defineWorkflow({
1315
2009
  },
1316
2010
  {
1317
2011
  from: "finalizeDelivery",
2012
+ switch: {
2013
+ on: "$result.outcome",
2014
+ cases: {
2015
+ ok: "routeFinalizeDeliveryResult",
2016
+ timed_out: "timeoutFallbackGuard",
2017
+ failed: "propagateSupportedFailure",
2018
+ },
2019
+ },
2020
+ },
2021
+ {
2022
+ from: "routeFinalizeDeliveryResult",
1318
2023
  switch: {
1319
2024
  on: "$.status",
1320
2025
  cases: { completed: "finalize", blocked: "challengeBlockerGuard" },