@osolmaz/pi-workflows 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +11 -7
  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 +29 -3
  6. package/dist/builtins/autoimplement.workflow.js +876 -231
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +3 -1
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +2 -0
  11. package/dist/builtins/index.js +1 -0
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/sanity-check-session.d.ts +17 -0
  14. package/dist/builtins/sanity-check-session.js +168 -0
  15. package/dist/builtins/sanity-check-session.js.map +1 -0
  16. package/dist/builtins/sanity-check.workflow.d.ts +83 -0
  17. package/dist/builtins/sanity-check.workflow.js +398 -0
  18. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  19. package/dist/controllers/sqlite.d.ts +90 -2
  20. package/dist/controllers/sqlite.js +380 -5
  21. package/dist/controllers/sqlite.js.map +1 -1
  22. package/dist/extension/decision-channels.js +7 -7
  23. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  24. package/dist/extension/deferred-turn-coordinator.js +143 -0
  25. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  26. package/dist/extension/deferred-turn.d.ts +44 -0
  27. package/dist/extension/deferred-turn.js +110 -0
  28. package/dist/extension/deferred-turn.js.map +1 -0
  29. package/dist/extension/index.js +286 -47
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/step-message.d.ts +1 -0
  32. package/dist/extension/step-message.js.map +1 -1
  33. package/dist/herdr/setup.js +4 -4
  34. package/dist/workflows/command-batch.d.ts +38 -0
  35. package/dist/workflows/command-batch.js +176 -0
  36. package/dist/workflows/command-batch.js.map +1 -0
  37. package/dist/workflows/engine.js +8 -4
  38. package/dist/workflows/engine.js.map +1 -1
  39. package/dist/workflows/index.d.ts +1 -0
  40. package/dist/workflows/index.js +1 -0
  41. package/dist/workflows/index.js.map +1 -1
  42. package/dist/workflows/schema.js +2 -1
  43. package/dist/workflows/schema.js.map +1 -1
  44. package/dist/workflows/shell.d.ts +4 -0
  45. package/dist/workflows/shell.js +6 -0
  46. package/dist/workflows/shell.js.map +1 -1
  47. package/dist/workflows/store.js +3 -1
  48. package/dist/workflows/store.js.map +1 -1
  49. package/dist/workflows/types.d.ts +4 -3
  50. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  51. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  52. package/docs/CONTROLLERS.md +2 -2
  53. package/docs/DEFERRED_TURNS.md +298 -0
  54. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  55. package/docs/HUMAN_DECISIONS.md +7 -7
  56. package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
  57. package/docs/MONITOR.md +2 -2
  58. package/docs/WORKFLOW_COMPOSITION.md +2 -2
  59. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  60. package/docs/WORKFLOW_UPDATES.md +28 -5
  61. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  62. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  63. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  64. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  65. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  66. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  67. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  68. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  69. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  70. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  71. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  72. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  73. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  74. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  75. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  76. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  77. package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
  78. package/docs/run-bundles.md +3 -1
  79. package/docs/workflows.md +62 -19
  80. package/examples/workflows/command-batch.workflow.ts +50 -0
  81. package/examples/workflows/sanity-check.workflow.ts +1 -0
  82. package/herdr-plugin.toml +3 -3
  83. package/package.json +1 -1
  84. package/schemas/decision-presentation-v1.schema.json +1 -1
  85. package/schemas/human-decision-accepted-v1.schema.json +1 -1
  86. package/schemas/human-decision-accepted-v2.schema.json +1 -1
  87. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  88. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  89. package/schemas/human-decision-continuation-v1.schema.json +1 -1
  90. package/schemas/human-decision-delivery-v1.schema.json +1 -1
  91. package/schemas/human-decision-delivery-v2.schema.json +1 -1
  92. package/schemas/human-decision-receipt-v1.schema.json +1 -1
  93. package/schemas/human-decision-receipt-v2.schema.json +1 -1
  94. package/schemas/human-decision-request-v1.schema.json +1 -1
  95. package/schemas/human-decision-request-v2.schema.json +1 -1
  96. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  97. package/schemas/human-decision-resolution-v2.schema.json +1 -1
  98. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  99. package/skills/autodoc/SKILL.md +33 -2
  100. package/skills/autoimplement/SKILL.md +66 -12
  101. package/skills/autoplan/SKILL.md +26 -2
  102. package/skills/monitor/SKILL.md +35 -21
  103. package/skills/pi-workflows/SKILL.md +19 -5
  104. package/src/builtins/autoimplement-command-batches.ts +364 -0
  105. package/src/builtins/autoimplement.workflow.ts +1065 -261
  106. package/src/builtins/catalog.ts +3 -1
  107. package/src/builtins/index.ts +13 -0
  108. package/src/builtins/sanity-check-session.ts +205 -0
  109. package/src/builtins/sanity-check.workflow.ts +624 -0
  110. package/src/controllers/sqlite.ts +580 -7
  111. package/src/extension/decision-channels.ts +7 -7
  112. package/src/extension/deferred-turn-coordinator.ts +171 -0
  113. package/src/extension/deferred-turn.ts +166 -0
  114. package/src/extension/index.ts +359 -49
  115. package/src/extension/step-message.ts +1 -0
  116. package/src/herdr/setup.ts +4 -4
  117. package/src/workflows/command-batch.ts +254 -0
  118. package/src/workflows/engine.ts +8 -5
  119. package/src/workflows/index.ts +15 -0
  120. package/src/workflows/schema.ts +2 -1
  121. package/src/workflows/shell.ts +10 -0
  122. package/src/workflows/store.ts +3 -1
  123. package/src/workflows/types.ts +4 -3
@@ -1,19 +1,32 @@
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";
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";
17
30
  import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
18
31
  import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
19
32
 
@@ -35,6 +48,7 @@ export type AutoimplementInput = {
35
48
  audience: string;
36
49
  maxReplans: number;
37
50
  };
51
+ concurrency?: Partial<AutoimplementConcurrency>;
38
52
  };
39
53
 
40
54
  export type ExistingPlanDiscovery = {
@@ -46,19 +60,26 @@ export type ExistingPlanDiscovery = {
46
60
  evidence: unknown;
47
61
  };
48
62
 
49
- type StructuredCommand = {
50
- command: string;
51
- args: string[];
52
- cwd: string;
53
- timeoutMs: number;
54
- };
55
-
56
63
  type ReviewFinding = {
57
64
  severity: "P0" | "P1" | "P2" | "lower";
58
65
  kind: "design" | "implementation";
59
66
  summary: string;
60
67
  };
61
68
 
69
+ type RepositoryReviewAssessment = {
70
+ id: string;
71
+ repository: string;
72
+ baseBranch: string;
73
+ headRevision: string;
74
+ dependencyFingerprint?: string;
75
+ invocationSucceeded: boolean;
76
+ p0: ReviewFinding[];
77
+ p1: ReviewFinding[];
78
+ p2: ReviewFinding[];
79
+ lower: ReviewFinding[];
80
+ reason: string;
81
+ };
82
+
62
83
  type ReviewAssessment = {
63
84
  route: "critical" | "p2" | "clean" | "command_error";
64
85
  invocationSucceeded: boolean;
@@ -67,6 +88,7 @@ type ReviewAssessment = {
67
88
  p2: ReviewFinding[];
68
89
  lower: ReviewFinding[];
69
90
  reason: string;
91
+ repositories?: RepositoryReviewAssessment[];
70
92
  };
71
93
 
72
94
  export type AutoimplementCompleted = {
@@ -98,11 +120,49 @@ type BlockerChallenge = {
98
120
  evidence: string[];
99
121
  };
100
122
 
101
- const FIVE_MINUTES_MS = 5 * 60_000;
102
- const TEN_MINUTES_MS = 10 * 60_000;
103
123
  const MAX_BLOCKER_CHALLENGES = 3;
104
124
  const MAX_CHALLENGE_ITEMS = 5;
105
125
  const MAX_CHALLENGE_TEXT = 500;
126
+ const MAX_TIMEOUT_FALLBACKS = 3;
127
+ const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
128
+ const TIMEOUT_FALLBACK_SOURCES = [
129
+ "implement",
130
+ "planVerification",
131
+ "verify",
132
+ "fix",
133
+ "publish",
134
+ "addressP2",
135
+ "verifyP2",
136
+ "inspectComments",
137
+ "inspectCi",
138
+ "opportunisticTest",
139
+ "finalizeDelivery",
140
+ ] as const;
141
+
142
+ const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"] as const;
143
+
144
+ type TimeoutFallbackSource = (typeof TIMEOUT_FALLBACK_SOURCES)[number];
145
+ type TimeoutFallbackRoute = "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked";
146
+
147
+ const TIMEOUT_FALLBACK_ROUTES: Record<TimeoutFallbackSource, readonly TimeoutFallbackRoute[]> = {
148
+ implement: ["retry", "replan", "blocked"],
149
+ planVerification: ["retry", "verify", "replan", "blocked"],
150
+ verify: ["retry", "verify", "replan", "blocked"],
151
+ fix: ["retry", "replan", "blocked"],
152
+ publish: ["retry", "replan", "blocked"],
153
+ addressP2: ["retry", "replan", "blocked"],
154
+ verifyP2: ["retry", "replan", "blocked"],
155
+ inspectComments: ["retry", "review", "ci", "replan", "blocked"],
156
+ inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
157
+ opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
158
+ finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
159
+ };
160
+
161
+ type TimeoutFallbackResult = {
162
+ route: TimeoutFallbackRoute;
163
+ reason: string;
164
+ evidence: string[];
165
+ };
106
166
 
107
167
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
108
168
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -118,6 +178,13 @@ function requireString(value: unknown, label: string): string {
118
178
  return value.trim();
119
179
  }
120
180
 
181
+ function requireStringArray(value: unknown, label: string): string[] {
182
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
183
+ throw new Error(`${label} must be an array of strings`);
184
+ }
185
+ return [...value] as string[];
186
+ }
187
+
121
188
  function boundedChallengeItems(value: unknown, label: string): string[] {
122
189
  if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
123
190
  throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
@@ -131,6 +198,170 @@ function boundedChallengeItems(value: unknown, label: string): string[] {
131
198
  });
132
199
  }
133
200
 
201
+ function isTimeoutFallbackSource(nodeId: string): nodeId is TimeoutFallbackSource {
202
+ return (TIMEOUT_FALLBACK_SOURCES as readonly string[]).includes(nodeId);
203
+ }
204
+
205
+ function latestTimedOutStep(context: WorkflowNodeContext) {
206
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
207
+ const step = context.state.steps[index];
208
+ if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId)) return step;
209
+ }
210
+ throw new Error("No supported timed-out Autoimplement step is available");
211
+ }
212
+
213
+ function latestStepIndex(
214
+ context: WorkflowNodeContext,
215
+ predicate: (step: WorkflowNodeContext["state"]["steps"][number]) => boolean,
216
+ ): number {
217
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
218
+ const step = context.state.steps[index];
219
+ if (step && predicate(step)) return index;
220
+ }
221
+ return -1;
222
+ }
223
+
224
+ function latestWorkAttemptIndex(context: WorkflowNodeContext): number {
225
+ return latestStepIndex(context, (step) =>
226
+ (WORK_ATTEMPT_NODES as readonly string[]).includes(step.nodeId),
227
+ );
228
+ }
229
+
230
+ function hasCurrentAcceptedWork(context: WorkflowNodeContext): boolean {
231
+ const index = latestWorkAttemptIndex(context);
232
+ return index >= 0 && context.state.steps[index]?.outcome === "ok";
233
+ }
234
+
235
+ function hasCurrentPublication(context: WorkflowNodeContext): boolean {
236
+ const workIndex = latestWorkAttemptIndex(context);
237
+ const publicationIndex = latestStepIndex(context, (step) => {
238
+ if (step.outcome !== "ok") return false;
239
+ if (step.nodeId === "publish") return true;
240
+ if (step.nodeId !== "verifyP2") return false;
241
+ const output = step.output as { passed?: unknown } | null;
242
+ return output?.passed === true;
243
+ });
244
+ return publicationIndex > workIndex;
245
+ }
246
+
247
+ function parseTimeoutFallback(value: unknown, context: WorkflowNodeContext): TimeoutFallbackResult {
248
+ const result = requireRecord(value, "timeout fallback");
249
+ const routes: TimeoutFallbackRoute[] = [
250
+ "retry",
251
+ "verify",
252
+ "review",
253
+ "ci",
254
+ "deliver",
255
+ "replan",
256
+ "blocked",
257
+ ];
258
+ if (!routes.includes(result.route as TimeoutFallbackRoute)) {
259
+ throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
260
+ }
261
+ const route = result.route as TimeoutFallbackRoute;
262
+ const reason = requireString(result.reason, "timeout fallback reason");
263
+ const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map(
264
+ (item, index) => requireString(item, `timeout fallback evidence[${index}]`),
265
+ );
266
+ if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
267
+ throw new Error(
268
+ `timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`,
269
+ );
270
+ }
271
+
272
+ const timedOut = latestTimedOutStep(context);
273
+ const source = timedOut.nodeId as TimeoutFallbackSource;
274
+ if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
275
+ throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
276
+ }
277
+ if (route === "verify" && !hasCurrentAcceptedWork(context)) {
278
+ throw new Error("timeout fallback cannot route to verification without current accepted work");
279
+ }
280
+ if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
281
+ throw new Error(
282
+ "timeout fallback cannot move past publication without a current published head",
283
+ );
284
+ }
285
+ if (route === "ci") {
286
+ const comments = context.outputs.inspectComments as { route?: unknown } | undefined;
287
+ if (comments?.route !== "ci") {
288
+ throw new Error("timeout fallback cannot route to CI before comment inspection completed");
289
+ }
290
+ }
291
+ if (route === "deliver") {
292
+ const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi) as
293
+ | { route?: unknown }
294
+ | undefined;
295
+ const classification = context.outputs.classifyCi as { route?: unknown } | undefined;
296
+ if (ci?.route !== "green" && classification?.route !== "unrelated") {
297
+ throw new Error("timeout fallback cannot route to delivery before CI is ready");
298
+ }
299
+ }
300
+
301
+ return { route, reason, evidence };
302
+ }
303
+
304
+ function timeoutFallbackTarget(context: WorkflowNodeContext): { route: string } {
305
+ const fallback = context.outputs.timeoutFallback as TimeoutFallbackResult;
306
+ if (fallback.route !== "retry") {
307
+ const routes: Record<Exclude<TimeoutFallbackRoute, "retry">, string> = {
308
+ verify: "planVerification",
309
+ review: "selectReviewCommands",
310
+ ci: "inspectCi",
311
+ deliver: "finalizeDelivery",
312
+ replan: "redesign",
313
+ blocked: "blocked",
314
+ };
315
+ return { route: routes[fallback.route] };
316
+ }
317
+ return { route: latestTimedOutStep(context).nodeId };
318
+ }
319
+
320
+ function timeoutFallbackGuard(context: WorkflowNodeContext) {
321
+ const timeout = latestTimedOutStep(context);
322
+ const attempts = context.state.steps.filter(
323
+ (step) => step.nodeId === "timeoutFallback" && step.outcome === "ok",
324
+ ).length;
325
+ if (attempts >= MAX_TIMEOUT_FALLBACKS) {
326
+ const timeouts = context.state.steps
327
+ .filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
328
+ .map((step) => ({
329
+ nodeId: step.nodeId,
330
+ attemptId: step.attemptId,
331
+ error: step.error,
332
+ }));
333
+ return {
334
+ route: "blocked",
335
+ reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
336
+ evidence: {
337
+ attempts,
338
+ limit: MAX_TIMEOUT_FALLBACKS,
339
+ timeouts,
340
+ },
341
+ };
342
+ }
343
+ return {
344
+ route: "recover",
345
+ attempt: attempts + 1,
346
+ limit: MAX_TIMEOUT_FALLBACKS,
347
+ timeout: {
348
+ nodeId: timeout.nodeId,
349
+ attemptId: timeout.attemptId,
350
+ error: timeout.error,
351
+ },
352
+ };
353
+ }
354
+
355
+ function throwLatestSupportedFailure(context: WorkflowNodeContext): never {
356
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
357
+ const step = context.state.steps[index];
358
+ if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
359
+ throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
360
+ }
361
+ }
362
+ throw new Error("No supported failed Autoimplement step is available");
363
+ }
364
+
134
365
  function parseBlockerChallenge(value: unknown): BlockerChallenge {
135
366
  const result = requireRecord(value, "blocker challenge");
136
367
  if (result.route !== "continue" && result.route !== "blocked") {
@@ -233,6 +464,7 @@ function parseInput(value: unknown): AutoimplementInput {
233
464
  documents: [...raw.documents] as string[],
234
465
  };
235
466
  }
467
+ const concurrency = parseAutoimplementConcurrency(input.concurrency);
236
468
  let approval: AutoimplementInput["approval"];
237
469
  if (input.approval !== undefined) {
238
470
  const raw = requireRecord(input.approval, "autoimplement approval");
@@ -264,6 +496,7 @@ function parseInput(value: unknown): AutoimplementInput {
264
496
  ...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
265
497
  ...(documentation !== undefined ? { documentation } : {}),
266
498
  ...(approval !== undefined ? { approval } : {}),
499
+ concurrency,
267
500
  };
268
501
  }
269
502
 
@@ -312,70 +545,71 @@ function parseRoute<T extends string>(
312
545
  return { ...record, route: record.route as T };
313
546
  }
314
547
 
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 };
548
+ type ReviewCommandSelection = {
549
+ route: "run" | "reuse";
550
+ repositories: PublishedRepository[];
551
+ commands: CommandBatchItem[];
552
+ };
553
+
554
+ type BatchExecution = {
555
+ route: "assess" | "repair";
556
+ batch: CommandBatchResult;
557
+ };
558
+
559
+ function concurrency(context: WorkflowNodeContext): AutoimplementConcurrency {
560
+ return parseAutoimplementConcurrency((context.input as AutoimplementInput).concurrency);
345
561
  }
346
562
 
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;
563
+ async function runAutoimplementBatch(
564
+ context: WorkflowActionContext,
565
+ kind: "review" | "ciWatch" | "verification",
566
+ commands: CommandBatchItem[],
567
+ maxConcurrency: number,
568
+ ): Promise<CommandBatchResult> {
569
+ return await runCommandBatch(
570
+ { items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) },
571
+ {
572
+ signal: context.signal,
573
+ onItemSettled: async (result, completed, total) => {
574
+ if (context.signal.aborted) return;
575
+ try {
576
+ await context.publishUpdate({
577
+ type: "command-batch.item",
578
+ key: `${kind}/${result.id}`,
579
+ data: {
580
+ schema: "pi-workflows.command-batch-item.v1",
581
+ batchKind: kind,
582
+ itemId: result.id,
583
+ outcome: result.outcome,
584
+ completed,
585
+ total,
586
+ },
587
+ });
588
+ } catch (error) {
589
+ if (!context.signal.aborted) throw error;
590
+ }
591
+ },
355
592
  },
356
- });
593
+ );
357
594
  }
358
595
 
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
- });
596
+ function commandBatchTimeoutMs(commands: CommandBatchItem[], maxConcurrency: number): number {
597
+ if (commands.length === 0) return 10_000;
598
+ const concurrency = Math.min(maxConcurrency, commands.length);
599
+ const waves = Math.ceil(commands.length / concurrency);
600
+ const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
601
+ return waves * longestItem + 10_000;
368
602
  }
369
603
 
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
- };
604
+ function reviewBatchNeedsRepair(result: CommandBatchResult): boolean {
605
+ return result.items.some(
606
+ (item) =>
607
+ item.outcome === "timedOut" ||
608
+ item.outcome === "cancelled" ||
609
+ (item.outcome === "failed" && item.exitCode === null) ||
610
+ item.stdoutTruncated ||
611
+ item.stderrTruncated,
612
+ );
379
613
  }
380
614
 
381
615
  function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
@@ -383,20 +617,10 @@ function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
383
617
  const step = context.state.steps[index];
384
618
  if (step && nodeIds.includes(step.nodeId)) return step.output as T;
385
619
  }
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
- }
620
+ for (const nodeId of nodeIds) {
621
+ if (context.outputs[nodeId] !== undefined) return context.outputs[nodeId] as T;
398
622
  }
399
- throw new Error("No CI tracking command is available");
623
+ throw new Error(`No output found for ${nodeIds.join(" or ")}`);
400
624
  }
401
625
 
402
626
  function currentPlan(context: WorkflowNodeContext): unknown {
@@ -444,6 +668,7 @@ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
444
668
  "assessTrackedCi",
445
669
  "classifyCi",
446
670
  "finalizeDelivery",
671
+ "timeoutFallback",
447
672
  ];
448
673
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
449
674
  const step = context.state.steps[index];
@@ -480,6 +705,7 @@ function latestIssue(context: WorkflowNodeContext): unknown {
480
705
  "triageReview",
481
706
  "inspectComments",
482
707
  "classifyCi",
708
+ "timeoutFallback",
483
709
  "adoptPlan",
484
710
  ];
485
711
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -501,18 +727,356 @@ function parseFinding(value: unknown, severity: ReviewFinding["severity"]): Revi
501
727
  };
502
728
  }
503
729
 
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));
730
+ function parseVerificationForContext(
731
+ value: unknown,
732
+ context: WorkflowNodeContext,
733
+ ): VerificationCommandPlan {
734
+ const plan = parseVerificationCommandPlan(value);
735
+ const implementation = latestOutput<Record<string, unknown>>(context, ["implement"]);
736
+ const reported = Array.isArray(implementation.repositories)
737
+ ? implementation.repositories.filter((entry): entry is string => typeof entry === "string")
738
+ : [];
739
+ const request = context.input as AutoimplementInput;
740
+ const roots = new Set(
741
+ (reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) =>
742
+ path.resolve(entry),
743
+ ),
744
+ );
745
+ for (const command of plan.commands) {
746
+ const cwd = path.resolve(command.cwd);
747
+ if (!roots.delete(cwd)) {
748
+ throw new Error(
749
+ `verification command cwd was not reported by implementation: ${command.cwd}`,
750
+ );
751
+ }
752
+ }
753
+ if (roots.size > 0) {
754
+ throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
755
+ }
756
+ return plan;
757
+ }
758
+
759
+ function currentPublishedRepositories(context: WorkflowNodeContext): PublishedRepositories {
760
+ return latestOutput<PublishedRepositories>(context, ["verifyP2", "publish"]);
761
+ }
762
+
763
+ type DeliveryRepositoryResult = {
764
+ repository: string;
765
+ pr: string;
766
+ merged: boolean;
767
+ reportComment: string;
768
+ reason: string;
769
+ };
770
+
771
+ function parseDeliveryRepository(value: unknown, index: number): DeliveryRepositoryResult {
772
+ const result = requireRecord(value, `delivery repositories[${index}]`);
773
+ const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
774
+ if (!path.isAbsolute(repository)) {
775
+ throw new Error(`delivery repositories[${index}].repository must be absolute`);
776
+ }
777
+ if (typeof result.merged !== "boolean") {
778
+ throw new Error(`delivery repositories[${index}].merged must be a boolean`);
779
+ }
780
+ return {
781
+ repository: path.resolve(repository),
782
+ pr: requireString(result.pr, `delivery repositories[${index}].pr`),
783
+ merged: result.merged,
784
+ reportComment: requireString(
785
+ result.reportComment,
786
+ `delivery repositories[${index}].reportComment`,
787
+ ),
788
+ reason: requireString(result.reason, `delivery repositories[${index}].reason`),
789
+ };
790
+ }
791
+
792
+ function parseDeliveryResult(
793
+ value: unknown,
794
+ context: WorkflowNodeContext,
795
+ ): Record<string, unknown> {
796
+ const result = requireRecord(value, "delivery result");
797
+ if (result.status !== "completed" && result.status !== "blocked") {
798
+ throw new Error("delivery status must be completed or blocked");
799
+ }
800
+ const request = context.input as AutoimplementInput;
801
+ if (request.merge !== true && result.merged === true) {
802
+ throw new Error("delivery cannot merge without explicit merge: true");
803
+ }
804
+ if (result.status === "blocked") return result;
805
+ if (typeof result.merged !== "boolean") {
806
+ throw new Error("completed delivery merged must be a boolean");
807
+ }
808
+ const pr = requireString(result.pr, "completed delivery pr");
809
+ const reportComment = requireString(result.reportComment, "completed delivery reportComment");
810
+ const reason = requireString(result.reason, "completed delivery reason");
811
+ const published = currentPublishedRepositories(context).repositories;
812
+ let repositories: DeliveryRepositoryResult[];
813
+ if (result.repositories === undefined) {
814
+ if (published.length !== 1) {
815
+ throw new Error("completed delivery repositories must cover every published repository");
816
+ }
817
+ const only = published[0];
818
+ if (only === undefined) throw new Error("completed delivery has no published repository");
819
+ repositories = [
820
+ {
821
+ repository: path.resolve(only.repository),
822
+ pr,
823
+ merged: result.merged,
824
+ reportComment,
825
+ reason,
826
+ },
827
+ ];
828
+ } else {
829
+ if (!Array.isArray(result.repositories)) {
830
+ throw new Error("completed delivery repositories must be an array");
831
+ }
832
+ repositories = result.repositories.map(parseDeliveryRepository);
833
+ }
834
+ const actual = new Map<string, DeliveryRepositoryResult>();
835
+ for (const repository of repositories) {
836
+ if (actual.has(repository.repository)) {
837
+ throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
838
+ }
839
+ actual.set(repository.repository, repository);
840
+ }
841
+ const mergeExpected = request.merge === true;
842
+ for (const expected of published) {
843
+ const repository = actual.get(path.resolve(expected.repository));
844
+ if (repository === undefined || repository.pr !== expected.pr) {
845
+ throw new Error(
846
+ `completed delivery does not match published repository and PR: ${expected.repository}`,
847
+ );
848
+ }
849
+ if (repository.merged !== mergeExpected) {
850
+ throw new Error(
851
+ `completed delivery merge result does not match merge policy: ${expected.repository}`,
852
+ );
853
+ }
854
+ actual.delete(repository.repository);
855
+ }
856
+ if (actual.size > 0) {
857
+ throw new Error(
858
+ `completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`,
859
+ );
860
+ }
861
+ const firstPublished = published[0];
862
+ const first =
863
+ firstPublished === undefined
864
+ ? undefined
865
+ : repositories.find(
866
+ (repository) => repository.repository === path.resolve(firstPublished.repository),
867
+ );
868
+ if (
869
+ first === undefined ||
870
+ first.pr !== pr ||
871
+ first.merged !== result.merged ||
872
+ first.reportComment !== reportComment
873
+ ) {
874
+ throw new Error(
875
+ "completed delivery top-level compatibility fields must match the first result",
876
+ );
877
+ }
878
+ return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
879
+ }
880
+
881
+ function parseP2Verification(
882
+ value: unknown,
883
+ context: WorkflowNodeContext,
884
+ ): Record<string, unknown> {
885
+ const result = requireRecord(value, "P2 verification");
886
+ if (typeof result.passed !== "boolean") {
887
+ throw new Error("P2 verification passed must be a boolean");
888
+ }
889
+ if (result.pushed !== true) {
890
+ throw new Error("P2 verification pushed must be true");
891
+ }
892
+ const refreshed = parsePublishedRepositories(result);
893
+ const previous = latestOutput<PublishedRepositories>(context, ["publish"]);
894
+ const expected = new Map(
895
+ previous.repositories.map((repository) => [repository.id, repository] as const),
896
+ );
897
+ for (const repository of refreshed.repositories) {
898
+ const prior = expected.get(repository.id);
899
+ if (
900
+ prior === undefined ||
901
+ prior.repository !== repository.repository ||
902
+ prior.branch !== repository.branch ||
903
+ prior.baseBranch !== repository.baseBranch ||
904
+ prior.pr !== repository.pr ||
905
+ prior.dependencyFingerprint !== repository.dependencyFingerprint
906
+ ) {
907
+ throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
908
+ }
909
+ expected.delete(repository.id);
910
+ }
911
+ if (expected.size > 0) {
912
+ throw new Error(
913
+ `P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`,
914
+ );
915
+ }
916
+ return { ...result, repositories: refreshed.repositories };
917
+ }
918
+
919
+ function parseCiInspectionForPublished(
920
+ value: unknown,
921
+ context: WorkflowNodeContext,
922
+ ): CiInspectionBatch {
923
+ const inspected = parseCiInspectionBatch(value);
924
+ const published = currentPublishedRepositories(context);
925
+ const expected = new Map(
926
+ published.repositories.map((repository) => [repository.id, repository] as const),
927
+ );
928
+ for (const target of inspected.targets) {
929
+ const repository = expected.get(target.id);
930
+ if (
931
+ repository === undefined ||
932
+ repository.repository !== target.repository ||
933
+ repository.headRevision !== target.headRevision ||
934
+ repository.pr !== target.pr
935
+ ) {
936
+ throw new Error(
937
+ `CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`,
938
+ );
939
+ }
940
+ expected.delete(target.id);
941
+ }
942
+ if (expected.size > 0) {
943
+ throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
944
+ }
945
+ return inspected;
946
+ }
947
+
948
+ function parseTrackedCiAssessment(
949
+ value: unknown,
950
+ context: WorkflowNodeContext,
951
+ ): Record<string, unknown> & { route: CiInspectionBatch["route"] } {
952
+ const result = requireRecord(value, "tracked CI assessment");
953
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
954
+ const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
955
+ const expectedIds = execution.batch.items.map((item) => item.id);
956
+ if (!Array.isArray(result.targets)) {
957
+ throw new Error("tracked CI assessment targets must be an array");
958
+ }
959
+ const seen = new Set<string>();
960
+ const targets = result.targets.map((entry, index) => {
961
+ const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
962
+ const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
963
+ if (seen.has(id)) throw new Error(`tracked CI assessment target is duplicated: ${id}`);
964
+ seen.add(id);
965
+ if (
966
+ target.route !== "green" &&
967
+ target.route !== "failed" &&
968
+ target.route !== "pending" &&
969
+ target.route !== "unavailable"
970
+ ) {
971
+ throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
972
+ }
973
+ return {
974
+ id,
975
+ route: target.route,
976
+ reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
977
+ };
978
+ });
979
+ const missing = expectedIds.filter((id) => !seen.has(id));
980
+ const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
981
+ if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
982
+ throw new Error(
983
+ `tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`,
984
+ );
985
+ }
986
+ const trackedRoutes = new Map(targets.map((target) => [target.id, target.route] as const));
987
+ const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
988
+ const route = routes.includes("failed")
989
+ ? "failed"
990
+ : routes.includes("pending")
991
+ ? "pending"
992
+ : routes.includes("unavailable")
993
+ ? "unavailable"
994
+ : "green";
995
+ if (result.route !== route) {
996
+ throw new Error(`tracked CI assessment route must be ${route}`);
997
+ }
998
+ return {
999
+ ...result,
1000
+ route,
1001
+ reason: requireString(result.reason, "tracked CI assessment reason"),
1002
+ targets,
1003
+ relatedFailures: requireStringArray(
1004
+ result.relatedFailures ?? [],
1005
+ "tracked CI assessment relatedFailures",
1006
+ ),
1007
+ unrelatedFailures: requireStringArray(
1008
+ result.unrelatedFailures ?? [],
1009
+ "tracked CI assessment unrelatedFailures",
1010
+ ),
510
1011
  };
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;
1012
+ }
1013
+
1014
+ function selectReviewCommands(context: WorkflowNodeContext): ReviewCommandSelection {
1015
+ const published = latestOutput<PublishedRepositories>(context, ["publish"]);
1016
+ const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
1017
+ const repositories = published.repositories.filter(
1018
+ (repository) =>
1019
+ !reviewed.some(
1020
+ (entry) =>
1021
+ entry.id === repository.id &&
1022
+ entry.headRevision === repository.headRevision &&
1023
+ entry.dependencyFingerprint === repository.dependencyFingerprint &&
1024
+ entry.invocationSucceeded,
1025
+ ),
1026
+ );
1027
+ return {
1028
+ route: repositories.length === 0 ? "reuse" : "run",
1029
+ repositories,
1030
+ commands: repositories.map(reviewerCommand),
1031
+ };
1032
+ }
1033
+
1034
+ function parseReviewAssessment(value: unknown, context: WorkflowNodeContext): ReviewAssessment {
1035
+ const review = requireRecord(value, "review assessment");
1036
+ if (!Array.isArray(review.repositories)) {
1037
+ throw new Error("review repositories must be an array");
1038
+ }
1039
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1040
+ const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
1041
+ const repositories = review.repositories.map((value, index) => {
1042
+ const raw = requireRecord(value, `review repositories[${index}]`);
1043
+ const id = requireString(raw.id, `review repositories[${index}].id`);
1044
+ const published = expected.get(id);
1045
+ if (published === undefined)
1046
+ throw new Error(`review repository id was not in the batch: ${id}`);
1047
+ expected.delete(id);
1048
+ const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
1049
+ const list = raw[key];
1050
+ if (!Array.isArray(list))
1051
+ throw new Error(`review repositories[${index}].${key} must be an array`);
1052
+ return list.map((item) => parseFinding(item, severity));
1053
+ };
1054
+ return {
1055
+ id,
1056
+ repository: published.repository,
1057
+ baseBranch: published.baseBranch,
1058
+ headRevision: published.headRevision,
1059
+ ...(published.dependencyFingerprint !== undefined
1060
+ ? { dependencyFingerprint: published.dependencyFingerprint }
1061
+ : {}),
1062
+ invocationSucceeded: raw.invocationSucceeded === true,
1063
+ p0: parseList("p0", "P0"),
1064
+ p1: parseList("p1", "P1"),
1065
+ p2: parseList("p2", "P2"),
1066
+ lower: parseList("lower", "lower"),
1067
+ reason: requireString(raw.reason, `review repositories[${index}].reason`),
1068
+ } satisfies RepositoryReviewAssessment;
1069
+ });
1070
+ if (expected.size > 0) {
1071
+ throw new Error(
1072
+ `review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`,
1073
+ );
1074
+ }
1075
+ const p0 = repositories.flatMap((entry) => entry.p0);
1076
+ const p1 = repositories.flatMap((entry) => entry.p1);
1077
+ const p2 = repositories.flatMap((entry) => entry.p2);
1078
+ const lower = repositories.flatMap((entry) => entry.lower);
1079
+ const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
516
1080
  const route = !invocationSucceeded
517
1081
  ? "command_error"
518
1082
  : p0.length + p1.length > 0
@@ -528,6 +1092,7 @@ function parseReviewAssessment(value: unknown): ReviewAssessment {
528
1092
  p2,
529
1093
  lower,
530
1094
  reason: requireString(review.reason, "review reason"),
1095
+ repositories,
531
1096
  };
532
1097
  }
533
1098
 
@@ -537,8 +1102,39 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
537
1102
  .map((step) => step.output as ReviewAssessment);
538
1103
  }
539
1104
 
1105
+ function reviewRoundsForOutput(context: WorkflowNodeContext): ReviewAssessment[] {
1106
+ const rounds = reviewRounds(context);
1107
+ const repositoryIds = new Set(
1108
+ rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)),
1109
+ );
1110
+ if (repositoryIds.size > 1) return rounds;
1111
+ return rounds.map(({ repositories: _repositories, ...round }) => round);
1112
+ }
1113
+
1114
+ function ciForOutput(context: WorkflowNodeContext): unknown {
1115
+ const result = latestOutput<Record<string, unknown>>(context, ["assessTrackedCi", "inspectCi"]);
1116
+ const targets = result.targets;
1117
+ if (!Array.isArray(targets) || targets.length !== 1) return result;
1118
+ const { targets: _targets, ...aggregate } = result;
1119
+ if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
1120
+ const target = targets[0];
1121
+ if (target !== null && typeof target === "object" && !Array.isArray(target)) {
1122
+ const record = target as Record<string, unknown>;
1123
+ return {
1124
+ ...aggregate,
1125
+ reason: aggregate.reason ?? record.reason,
1126
+ relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
1127
+ unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
1128
+ };
1129
+ }
1130
+ }
1131
+ return aggregate;
1132
+ }
1133
+
540
1134
  function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
541
1135
  const candidates = [
1136
+ "timeoutFallbackGuard",
1137
+ "timeoutFallback",
542
1138
  "challengeBlockerGuard",
543
1139
  "challengeBlocker",
544
1140
  "finalizeDelivery",
@@ -723,8 +1319,58 @@ export const autoimplementWorkflow = defineWorkflow({
723
1319
  : { route: "redesign", replans, limit };
724
1320
  },
725
1321
  }),
1322
+ timeoutFallbackGuard: compute({
1323
+ run: timeoutFallbackGuard,
1324
+ }),
1325
+ timeoutFallback: agent({
1326
+ timeoutMs: 30 * 60_000,
1327
+ statusDetail: "choosing a safe timeout fallback",
1328
+ prompt: (context) => {
1329
+ const request = context.input as AutoimplementInput;
1330
+ const guard = context.outputs.timeoutFallbackGuard;
1331
+ const previousFallbacks = context.state.steps
1332
+ .filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
1333
+ .map((step) => step.output);
1334
+ return [
1335
+ "A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
1336
+ "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.",
1337
+ "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.",
1338
+ "Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
1339
+ "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.",
1340
+ "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.",
1341
+ "Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
1342
+ `Task: ${request.task}`,
1343
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
1344
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
1345
+ `Timeout: ${JSON.stringify(guard)}`,
1346
+ `Accepted outputs: ${JSON.stringify(context.outputs)}`,
1347
+ `Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
1348
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
1349
+ ].join("\n");
1350
+ },
1351
+ expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
1352
+ validate: parseTimeoutFallback,
1353
+ }),
1354
+ routeTimeoutFallback: compute({
1355
+ run: timeoutFallbackTarget,
1356
+ }),
1357
+ propagateSupportedFailure: compute({
1358
+ run: throwLatestSupportedFailure,
1359
+ }),
1360
+ routeVerifyP2Result: compute({
1361
+ run: ({ outputs }) => outputs.verifyP2,
1362
+ }),
1363
+ routeInspectCommentsResult: compute({
1364
+ run: ({ outputs }) => outputs.inspectComments,
1365
+ }),
1366
+ routeInspectCiResult: compute({
1367
+ run: ({ outputs }) => outputs.inspectCi,
1368
+ }),
1369
+ routeFinalizeDeliveryResult: compute({
1370
+ run: ({ outputs }) => outputs.finalizeDelivery,
1371
+ }),
726
1372
  implement: agent({
727
- timeoutMs: 60 * 60_000,
1373
+ timeoutMs: 8 * 60 * 60_000,
728
1374
  statusDetail: "implementing",
729
1375
  prompt: (context) => {
730
1376
  const request = context.input as AutoimplementInput;
@@ -733,12 +1379,14 @@ export const autoimplementWorkflow = defineWorkflow({
733
1379
  `Plan: ${JSON.stringify(currentPlan(context))}`,
734
1380
  `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
735
1381
  `Constraints: ${JSON.stringify(request.constraints ?? [])}`,
1382
+ "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
1383
  "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
737
1384
  "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
1385
+ "Report every changed repository as an absolute path so independent verification can be bounded safely.",
738
1386
  "Do not merge yet.",
739
1387
  ].join("\n");
740
1388
  },
741
- expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
1389
+ 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
1390
  validate: (value) => requireRecord(value, "implementation result"),
743
1391
  }),
744
1392
  classifyImplementation: agent({
@@ -805,15 +1453,46 @@ export const autoimplementWorkflow = defineWorkflow({
805
1453
  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
1454
  validate: parseBlockerChallenge,
807
1455
  }),
808
- verify: agent({
809
- timeoutMs: 45 * 60_000,
810
- statusDetail: "verifying",
1456
+ planVerification: agent({
1457
+ timeoutMs: 15 * 60_000,
1458
+ statusDetail: "planning independent verification commands",
811
1459
  prompt: () =>
812
1460
  [
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.",
1461
+ "Select the required local verification commands for the implementation.",
1462
+ "Return one command per independent repository working directory.",
1463
+ "Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
1464
+ "Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
1465
+ "List checks that cannot run locally under untested.",
1466
+ ].join("\n"),
1467
+ expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
1468
+ validate: parseVerificationForContext,
1469
+ }),
1470
+ runVerification: action({
1471
+ timeoutMs: (context) => {
1472
+ const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
1473
+ return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
1474
+ },
1475
+ statusDetail: "running independent verification commands",
1476
+ run: async (context) => {
1477
+ const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
1478
+ return await runAutoimplementBatch(
1479
+ context,
1480
+ "verification",
1481
+ plan.commands,
1482
+ concurrency(context).verification,
1483
+ );
1484
+ },
1485
+ }),
1486
+ verify: agent({
1487
+ timeoutMs: 20 * 60_000,
1488
+ statusDetail: "assessing verification",
1489
+ prompt: (context) =>
1490
+ [
1491
+ "Assess the completed local verification commands.",
1492
+ "Set passed true only when every required command succeeded without truncated output.",
1493
+ "Report exact command outcomes, failures, and checks that still need remote verification.",
1494
+ `Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
1495
+ `Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
817
1496
  ].join("\n"),
818
1497
  expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
819
1498
  validate: (value) => requireRecord(value, "verification result"),
@@ -843,6 +1522,7 @@ export const autoimplementWorkflow = defineWorkflow({
843
1522
  prompt: (context) =>
844
1523
  [
845
1524
  "Fix the current implementation issue without expanding the approved design.",
1525
+ "Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
846
1526
  `Issue: ${JSON.stringify(latestIssue(context))}`,
847
1527
  `Current plan: ${JSON.stringify(currentPlan(context))}`,
848
1528
  "Stop after the fix so verification can run again.",
@@ -857,82 +1537,68 @@ export const autoimplementWorkflow = defineWorkflow({
857
1537
  const request = input as AutoimplementInput;
858
1538
  return [
859
1539
  "Commit and push the verified implementation before review.",
1540
+ "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
1541
  "Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
861
1542
  "Inspect the complete public diff before every push or PR mutation.",
862
- `Requested base branch: ${request.baseBranch ?? "discover the repository default branch"}.`,
1543
+ "Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
1544
+ "Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
1545
+ `Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
863
1546
  "Do not merge yet.",
864
1547
  ].join("\n");
865
1548
  },
866
- expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
867
- validate: (value) => requireRecord(value, "publication result"),
868
- }),
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,
1549
+ expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
1550
+ validate: parsePublishedRepositories,
885
1551
  }),
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
- ),
1552
+ selectReviewCommands: compute({
1553
+ run: selectReviewCommands,
893
1554
  }),
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");
1555
+ runReview: action({
1556
+ statusDetail: "running pi-reviewer commands",
1557
+ timeoutMs: (context) => {
1558
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1559
+ return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
904
1560
  },
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
- };
1561
+ run: async (context): Promise<BatchExecution> => {
1562
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1563
+ const batch = await runAutoimplementBatch(
1564
+ context,
1565
+ "review",
1566
+ selected.commands,
1567
+ concurrency(context).reviewer,
1568
+ );
1569
+ return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
921
1570
  },
922
1571
  }),
1572
+ repairReviewCommand: agent({
1573
+ statusDetail: "repairing reviewer prerequisites",
1574
+ prompt: (context) =>
1575
+ [
1576
+ "One or more pi-reviewer commands failed, timed out, or returned truncated output.",
1577
+ "Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
1578
+ "Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
1579
+ "Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
1580
+ `Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
1581
+ ].join("\n"),
1582
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
1583
+ validate: (value) =>
1584
+ parseRoute(value, ["retry", "blocked"] as const, "reviewer command repair"),
1585
+ }),
923
1586
  assessReview: agent({
924
1587
  statusDetail: "assessing reviewer findings",
925
1588
  prompt: (context) => {
926
- const result = latestOutput<ShellActionResult>(context, ["runReview"]);
1589
+ const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
1590
+ const execution = latestOutput<BatchExecution>(context, ["runReview"]);
927
1591
  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.",
1592
+ "Assess each completed pi-reviewer result separately.",
1593
+ "Return one repository entry for every selected command, using the exact repository id.",
1594
+ "Set invocationSucceeded false when a complete valid review was not produced.",
1595
+ "Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
931
1596
  "Do not promote P2 findings to P1 merely to force another review round.",
932
- `Reviewer result: ${JSON.stringify(result)}`,
1597
+ `Selected repositories: ${JSON.stringify(selected.repositories)}`,
1598
+ `Reviewer results: ${JSON.stringify(execution.batch)}`,
933
1599
  ].join("\n");
934
1600
  },
935
- expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
1601
+ expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
936
1602
  validate: parseReviewAssessment,
937
1603
  }),
938
1604
  triageReview: compute({
@@ -952,7 +1618,8 @@ export const autoimplementWorkflow = defineWorkflow({
952
1618
  prompt: ({ outputs }) =>
953
1619
  [
954
1620
  "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.",
1621
+ "Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
1622
+ "Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
956
1623
  `Review: ${JSON.stringify(outputs.assessReview)}`,
957
1624
  ].join("\n"),
958
1625
  expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
@@ -964,20 +1631,23 @@ export const autoimplementWorkflow = defineWorkflow({
964
1631
  prompt: () =>
965
1632
  [
966
1633
  "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.",
1634
+ "Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
1635
+ "Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
1636
+ "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
1637
  "Report exact commands and outcomes.",
969
1638
  ].join("\n"),
970
- expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
971
- validate: (value) => requireRecord(value, "P2 verification"),
1639
+ 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" }] }`,
1640
+ validate: parseP2Verification,
972
1641
  }),
973
1642
  inspectComments: agent({
974
1643
  timeoutMs: 20 * 60_000,
975
1644
  statusDetail: "checking PR comments",
976
- prompt: () =>
1645
+ prompt: (context) =>
977
1646
  [
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.",
1647
+ "Inspect current inline review comments and PR issue comments for every published pull request.",
1648
+ "Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
1649
+ "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.",
1650
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
981
1651
  ].join("\n"),
982
1652
  expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
983
1653
  validate: (value) =>
@@ -986,71 +1656,75 @@ export const autoimplementWorkflow = defineWorkflow({
986
1656
  inspectCi: agent({
987
1657
  timeoutMs: 10 * 60_000,
988
1658
  statusDetail: "checking CI",
989
- prompt: () =>
1659
+ prompt: (context) =>
990
1660
  [
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.",
1661
+ "Inspect every published pull request once without waiting for completion.",
1662
+ "Return one target per repository and current PR head.",
1663
+ "Choose green, failed, pending, or unavailable for each target.",
1664
+ "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.",
1665
+ "Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
1666
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
995
1667
  ].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",
1668
+ 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 } }] }`,
1669
+ validate: parseCiInspectionForPublished,
1670
+ }),
1671
+ trackCi: action({
1672
+ statusDetail: "tracking pending CI commands",
1673
+ timeoutMs: (context) => {
1674
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1675
+ const commands = inspected.targets.flatMap((target) =>
1676
+ target.trackingCommand === undefined ? [] : [target.trackingCommand],
1002
1677
  );
1003
- if (result.route === "pending")
1004
- result.trackingCommand = parseCiCommand(result.trackingCommand);
1005
- return result;
1678
+ return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
1679
+ },
1680
+ run: async (context): Promise<BatchExecution> => {
1681
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1682
+ const commands = inspected.targets.flatMap((target) =>
1683
+ target.trackingCommand === undefined ? [] : [target.trackingCommand],
1684
+ );
1685
+ const batch = await runAutoimplementBatch(
1686
+ context,
1687
+ "ciWatch",
1688
+ commands,
1689
+ concurrency(context).ciWatch,
1690
+ );
1691
+ const needsRepair = batch.items.some(
1692
+ (item) =>
1693
+ (item.outcome === "failed" && item.exitCode === null) ||
1694
+ item.stdoutTruncated ||
1695
+ item.stderrTruncated,
1696
+ );
1697
+ return { route: needsRepair ? "repair" : "assess", batch };
1006
1698
  },
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
1699
  }),
1013
1700
  repairCiCommand: agent({
1014
- statusDetail: "correcting CI tracking command",
1701
+ statusDetail: "repairing CI watch prerequisites",
1015
1702
  prompt: (context) =>
1016
1703
  [
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)}`,
1704
+ "One or more supported CI watch commands failed or returned truncated output.",
1705
+ "Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
1706
+ "Do not change the PR identity or substitute another command form.",
1707
+ "Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
1708
+ `Failure: ${JSON.stringify(context.outputs.trackCi)}`,
1021
1709
  ].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
- },
1710
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
1711
+ validate: (value) => parseRoute(value, ["retry", "blocked"] as const, "CI command repair"),
1036
1712
  }),
1037
1713
  assessTrackedCi: agent({
1038
1714
  statusDetail: "assessing tracked CI",
1039
1715
  prompt: (context) => {
1040
- const result = latestOutput<ShellActionResult>(context, ["trackCi"]);
1716
+ const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
1717
+ const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
1041
1718
  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)}`,
1719
+ "Assess every CI watch result without starting another wait.",
1720
+ "Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
1721
+ "A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
1722
+ `Initial inspection: ${JSON.stringify(inspected)}`,
1723
+ `Tracking results: ${JSON.stringify(execution.batch)}`,
1045
1724
  ].join("\n");
1046
1725
  },
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
- ),
1726
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
1727
+ validate: parseTrackedCiAssessment,
1054
1728
  }),
1055
1729
  opportunisticTest: agent({
1056
1730
  timeoutMs: 30 * 60_000,
@@ -1083,30 +1757,23 @@ export const autoimplementWorkflow = defineWorkflow({
1083
1757
  }),
1084
1758
  finalizeDelivery: agent({
1085
1759
  timeoutMs: 30 * 60_000,
1086
- statusDetail: "finalizing PR",
1087
- prompt: ({ input }) => {
1088
- const request = input as AutoimplementInput;
1760
+ statusDetail: "finalizing PRs",
1761
+ prompt: (context) => {
1762
+ const request = context.input as AutoimplementInput;
1089
1763
  return [
1090
1764
  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.",
1765
+ ? "Leave every verified PR ready without merging because input disabled merge."
1766
+ : "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
1767
+ "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.",
1768
+ "Use each repository's required merge method.",
1769
+ "Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
1770
+ "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
1771
  "Return blocked instead of claiming completion when a required merge or report action fails.",
1772
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
1096
1773
  ].join("\n");
1097
1774
  },
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
- },
1775
+ 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" }] }`,
1776
+ validate: parseDeliveryResult,
1110
1777
  }),
1111
1778
  blocked: compute({
1112
1779
  run: (context) => {
@@ -1127,11 +1794,11 @@ export const autoimplementWorkflow = defineWorkflow({
1127
1794
  status: "completed",
1128
1795
  task: request.task,
1129
1796
  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,
1797
+ implementation: latestOutput(context, ["implement"]),
1798
+ verification: latestOutput(context, ["verifyP2", "verify"]),
1799
+ reviewRounds: reviewRoundsForOutput(context),
1800
+ ci: ciForOutput(context),
1801
+ delivery: latestOutput(context, ["finalizeDelivery"]),
1135
1802
  } satisfies AutoimplementCompleted;
1136
1803
  },
1137
1804
  }),
@@ -1174,13 +1841,51 @@ export const autoimplementWorkflow = defineWorkflow({
1174
1841
  from: "replanGuard",
1175
1842
  switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
1176
1843
  },
1177
- { from: "implement", to: "classifyImplementation" },
1178
1844
  {
1179
- from: "classifyImplementation",
1845
+ from: "timeoutFallbackGuard",
1846
+ switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
1847
+ },
1848
+ { from: "timeoutFallback", to: "routeTimeoutFallback" },
1849
+ { from: "propagateSupportedFailure", to: "blocked" },
1850
+ {
1851
+ from: "routeTimeoutFallback",
1180
1852
  switch: {
1181
1853
  on: "$.route",
1182
1854
  cases: {
1855
+ implement: "implement",
1856
+ planVerification: "planVerification",
1183
1857
  verify: "verify",
1858
+ fix: "fix",
1859
+ publish: "publish",
1860
+ addressP2: "addressP2",
1861
+ verifyP2: "verifyP2",
1862
+ inspectComments: "inspectComments",
1863
+ inspectCi: "inspectCi",
1864
+ opportunisticTest: "opportunisticTest",
1865
+ finalizeDelivery: "finalizeDelivery",
1866
+ selectReviewCommands: "selectReviewCommands",
1867
+ redesign: "redesign",
1868
+ blocked: "blocked",
1869
+ },
1870
+ },
1871
+ },
1872
+ {
1873
+ from: "implement",
1874
+ switch: {
1875
+ on: "$result.outcome",
1876
+ cases: {
1877
+ ok: "classifyImplementation",
1878
+ timed_out: "timeoutFallbackGuard",
1879
+ failed: "propagateSupportedFailure",
1880
+ },
1881
+ },
1882
+ },
1883
+ {
1884
+ from: "classifyImplementation",
1885
+ switch: {
1886
+ on: "$.route",
1887
+ cases: {
1888
+ verify: "planVerification",
1184
1889
  redesign: "redesign",
1185
1890
  fix: "fix",
1186
1891
  blocked: "challengeBlockerGuard",
@@ -1195,7 +1900,29 @@ export const autoimplementWorkflow = defineWorkflow({
1195
1900
  from: "challengeBlocker",
1196
1901
  switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1197
1902
  },
1198
- { from: "verify", to: "classifyVerification" },
1903
+ {
1904
+ from: "planVerification",
1905
+ switch: {
1906
+ on: "$result.outcome",
1907
+ cases: {
1908
+ ok: "runVerification",
1909
+ timed_out: "timeoutFallbackGuard",
1910
+ failed: "propagateSupportedFailure",
1911
+ },
1912
+ },
1913
+ },
1914
+ { from: "runVerification", to: "verify" },
1915
+ {
1916
+ from: "verify",
1917
+ switch: {
1918
+ on: "$result.outcome",
1919
+ cases: {
1920
+ ok: "classifyVerification",
1921
+ timed_out: "timeoutFallbackGuard",
1922
+ failed: "propagateSupportedFailure",
1923
+ },
1924
+ },
1925
+ },
1199
1926
  {
1200
1927
  from: "classifyVerification",
1201
1928
  switch: {
@@ -1208,20 +1935,36 @@ export const autoimplementWorkflow = defineWorkflow({
1208
1935
  },
1209
1936
  },
1210
1937
  },
1211
- { from: "fix", to: "verify" },
1212
- { from: "publish", to: "authorReviewCommand" },
1213
- { from: "authorReviewCommand", to: "runReview" },
1214
1938
  {
1215
- from: "runReview",
1939
+ from: "fix",
1940
+ switch: {
1941
+ on: "$result.outcome",
1942
+ cases: {
1943
+ ok: "planVerification",
1944
+ timed_out: "timeoutFallbackGuard",
1945
+ failed: "propagateSupportedFailure",
1946
+ },
1947
+ },
1948
+ },
1949
+ {
1950
+ from: "publish",
1216
1951
  switch: {
1217
1952
  on: "$result.outcome",
1218
1953
  cases: {
1219
- ok: "assessReview",
1220
- failed: "repairReviewCommand",
1221
- timed_out: "repairReviewCommand",
1954
+ ok: "selectReviewCommands",
1955
+ timed_out: "timeoutFallbackGuard",
1956
+ failed: "propagateSupportedFailure",
1222
1957
  },
1223
1958
  },
1224
1959
  },
1960
+ {
1961
+ from: "selectReviewCommands",
1962
+ switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
1963
+ },
1964
+ {
1965
+ from: "runReview",
1966
+ switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
1967
+ },
1225
1968
  {
1226
1969
  from: "repairReviewCommand",
1227
1970
  switch: {
@@ -1245,13 +1988,45 @@ export const autoimplementWorkflow = defineWorkflow({
1245
1988
  from: "triageReview",
1246
1989
  switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
1247
1990
  },
1248
- { from: "addressP2", to: "verifyP2" },
1991
+ {
1992
+ from: "addressP2",
1993
+ switch: {
1994
+ on: "$result.outcome",
1995
+ cases: {
1996
+ ok: "verifyP2",
1997
+ timed_out: "timeoutFallbackGuard",
1998
+ failed: "propagateSupportedFailure",
1999
+ },
2000
+ },
2001
+ },
1249
2002
  {
1250
2003
  from: "verifyP2",
2004
+ switch: {
2005
+ on: "$result.outcome",
2006
+ cases: {
2007
+ ok: "routeVerifyP2Result",
2008
+ timed_out: "timeoutFallbackGuard",
2009
+ failed: "propagateSupportedFailure",
2010
+ },
2011
+ },
2012
+ },
2013
+ {
2014
+ from: "routeVerifyP2Result",
1251
2015
  switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
1252
2016
  },
1253
2017
  {
1254
2018
  from: "inspectComments",
2019
+ switch: {
2020
+ on: "$result.outcome",
2021
+ cases: {
2022
+ ok: "routeInspectCommentsResult",
2023
+ timed_out: "timeoutFallbackGuard",
2024
+ failed: "propagateSupportedFailure",
2025
+ },
2026
+ },
2027
+ },
2028
+ {
2029
+ from: "routeInspectCommentsResult",
1255
2030
  switch: {
1256
2031
  on: "$.route",
1257
2032
  cases: {
@@ -1264,6 +2039,17 @@ export const autoimplementWorkflow = defineWorkflow({
1264
2039
  },
1265
2040
  {
1266
2041
  from: "inspectCi",
2042
+ switch: {
2043
+ on: "$result.outcome",
2044
+ cases: {
2045
+ ok: "routeInspectCiResult",
2046
+ timed_out: "timeoutFallbackGuard",
2047
+ failed: "propagateSupportedFailure",
2048
+ },
2049
+ },
2050
+ },
2051
+ {
2052
+ from: "routeInspectCiResult",
1267
2053
  switch: {
1268
2054
  on: "$.route",
1269
2055
  cases: {
@@ -1276,10 +2062,7 @@ export const autoimplementWorkflow = defineWorkflow({
1276
2062
  },
1277
2063
  {
1278
2064
  from: "trackCi",
1279
- switch: {
1280
- on: "$result.outcome",
1281
- cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
1282
- },
2065
+ switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
1283
2066
  },
1284
2067
  {
1285
2068
  from: "repairCiCommand",
@@ -1300,7 +2083,17 @@ export const autoimplementWorkflow = defineWorkflow({
1300
2083
  },
1301
2084
  },
1302
2085
  },
1303
- { from: "opportunisticTest", to: "inspectCi" },
2086
+ {
2087
+ from: "opportunisticTest",
2088
+ switch: {
2089
+ on: "$result.outcome",
2090
+ cases: {
2091
+ ok: "inspectCi",
2092
+ timed_out: "timeoutFallbackGuard",
2093
+ failed: "propagateSupportedFailure",
2094
+ },
2095
+ },
2096
+ },
1304
2097
  {
1305
2098
  from: "classifyCi",
1306
2099
  switch: {
@@ -1315,6 +2108,17 @@ export const autoimplementWorkflow = defineWorkflow({
1315
2108
  },
1316
2109
  {
1317
2110
  from: "finalizeDelivery",
2111
+ switch: {
2112
+ on: "$result.outcome",
2113
+ cases: {
2114
+ ok: "routeFinalizeDeliveryResult",
2115
+ timed_out: "timeoutFallbackGuard",
2116
+ failed: "propagateSupportedFailure",
2117
+ },
2118
+ },
2119
+ },
2120
+ {
2121
+ from: "routeFinalizeDeliveryResult",
1318
2122
  switch: {
1319
2123
  on: "$.status",
1320
2124
  cases: { completed: "finalize", blocked: "challengeBlockerGuard" },