@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,14 +1,43 @@
1
1
  import path from "node:path";
2
- import { agent, compute, defineWorkflow, includeWorkflow, includedResult, shell, } from "../workflows/definition.js";
2
+ import { runCommandBatch, } from "../workflows/command-batch.js";
3
+ import { action, agent, compute, defineWorkflow, includeWorkflow, includedResult, } from "../workflows/definition.js";
3
4
  import { digest } from "../workflows/human-decision.js";
4
5
  import autodocWorkflow, {} from "./autodoc.workflow.js";
5
- import autoplanWorkflow, {} from "./autoplan.workflow.js";
6
- import planApprovalWorkflow, {} from "./plan-approval.workflow.js";
7
- const FIVE_MINUTES_MS = 5 * 60_000;
8
- const TEN_MINUTES_MS = 10 * 60_000;
6
+ import { parseAutoimplementConcurrency, parseCiInspectionBatch, parsePublishedRepositories, parseVerificationCommandPlan, reviewerCommand, } from "./autoimplement-command-batches.js";
7
+ import { parsePlanApprovalPolicy } from "./plan-approval.workflow.js";
8
+ import planChangeWorkflow, {} from "./plan-change.workflow.js";
9
9
  const MAX_BLOCKER_CHALLENGES = 3;
10
10
  const MAX_CHALLENGE_ITEMS = 5;
11
11
  const MAX_CHALLENGE_TEXT = 500;
12
+ const MAX_TIMEOUT_FALLBACKS = 3;
13
+ const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
14
+ const TIMEOUT_FALLBACK_SOURCES = [
15
+ "implement",
16
+ "planVerification",
17
+ "verify",
18
+ "fix",
19
+ "publish",
20
+ "addressP2",
21
+ "verifyP2",
22
+ "inspectComments",
23
+ "inspectCi",
24
+ "opportunisticTest",
25
+ "finalizeDelivery",
26
+ ];
27
+ const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"];
28
+ const TIMEOUT_FALLBACK_ROUTES = {
29
+ implement: ["retry", "replan", "blocked"],
30
+ planVerification: ["retry", "verify", "replan", "blocked"],
31
+ verify: ["retry", "verify", "replan", "blocked"],
32
+ fix: ["retry", "replan", "blocked"],
33
+ publish: ["retry", "replan", "blocked"],
34
+ addressP2: ["retry", "replan", "blocked"],
35
+ verifyP2: ["retry", "replan", "blocked"],
36
+ inspectComments: ["retry", "review", "ci", "replan", "blocked"],
37
+ inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
38
+ opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
39
+ finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
40
+ };
12
41
  function requireRecord(value, label) {
13
42
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
43
  throw new Error(`${label} must be an object`);
@@ -21,6 +50,12 @@ function requireString(value, label) {
21
50
  }
22
51
  return value.trim();
23
52
  }
53
+ function requireStringArray(value, label) {
54
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
55
+ throw new Error(`${label} must be an array of strings`);
56
+ }
57
+ return [...value];
58
+ }
24
59
  function boundedChallengeItems(value, label) {
25
60
  if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
26
61
  throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
@@ -33,6 +68,148 @@ function boundedChallengeItems(value, label) {
33
68
  return text;
34
69
  });
35
70
  }
71
+ function isTimeoutFallbackSource(nodeId) {
72
+ return TIMEOUT_FALLBACK_SOURCES.includes(nodeId);
73
+ }
74
+ function latestTimedOutStep(context) {
75
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
76
+ const step = context.state.steps[index];
77
+ if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
78
+ return step;
79
+ }
80
+ throw new Error("No supported timed-out Autoimplement step is available");
81
+ }
82
+ function latestStepIndex(context, predicate) {
83
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
84
+ const step = context.state.steps[index];
85
+ if (step && predicate(step))
86
+ return index;
87
+ }
88
+ return -1;
89
+ }
90
+ function latestWorkAttemptIndex(context) {
91
+ return latestStepIndex(context, (step) => WORK_ATTEMPT_NODES.includes(step.nodeId));
92
+ }
93
+ function hasCurrentAcceptedWork(context) {
94
+ const index = latestWorkAttemptIndex(context);
95
+ return index >= 0 && context.state.steps[index]?.outcome === "ok";
96
+ }
97
+ function hasCurrentPublication(context) {
98
+ const workIndex = latestWorkAttemptIndex(context);
99
+ const publicationIndex = latestStepIndex(context, (step) => {
100
+ if (step.outcome !== "ok")
101
+ return false;
102
+ if (step.nodeId === "publish")
103
+ return true;
104
+ if (step.nodeId !== "verifyP2")
105
+ return false;
106
+ const output = step.output;
107
+ return output?.passed === true;
108
+ });
109
+ return publicationIndex > workIndex;
110
+ }
111
+ function parseTimeoutFallback(value, context) {
112
+ const result = requireRecord(value, "timeout fallback");
113
+ const routes = [
114
+ "retry",
115
+ "verify",
116
+ "review",
117
+ "ci",
118
+ "deliver",
119
+ "replan",
120
+ "blocked",
121
+ ];
122
+ if (!routes.includes(result.route)) {
123
+ throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
124
+ }
125
+ const route = result.route;
126
+ const reason = requireString(result.reason, "timeout fallback reason");
127
+ const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map((item, index) => requireString(item, `timeout fallback evidence[${index}]`));
128
+ if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
129
+ throw new Error(`timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`);
130
+ }
131
+ const timedOut = latestTimedOutStep(context);
132
+ const source = timedOut.nodeId;
133
+ if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
134
+ throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
135
+ }
136
+ if (route === "verify" && !hasCurrentAcceptedWork(context)) {
137
+ throw new Error("timeout fallback cannot route to verification without current accepted work");
138
+ }
139
+ if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
140
+ throw new Error("timeout fallback cannot move past publication without a current published head");
141
+ }
142
+ if (route === "ci") {
143
+ const comments = context.outputs.inspectComments;
144
+ if (comments?.route !== "ci") {
145
+ throw new Error("timeout fallback cannot route to CI before comment inspection completed");
146
+ }
147
+ }
148
+ if (route === "deliver") {
149
+ const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi);
150
+ const classification = context.outputs.classifyCi;
151
+ if (ci?.route !== "green" && classification?.route !== "unrelated") {
152
+ throw new Error("timeout fallback cannot route to delivery before CI is ready");
153
+ }
154
+ }
155
+ return { route, reason, evidence };
156
+ }
157
+ function timeoutFallbackTarget(context) {
158
+ const fallback = context.outputs.timeoutFallback;
159
+ if (fallback.route !== "retry") {
160
+ const routes = {
161
+ verify: "planVerification",
162
+ review: "selectReviewCommands",
163
+ ci: "inspectCi",
164
+ deliver: "finalizeDelivery",
165
+ replan: "redesign",
166
+ blocked: "blocked",
167
+ };
168
+ return { route: routes[fallback.route] };
169
+ }
170
+ return { route: latestTimedOutStep(context).nodeId };
171
+ }
172
+ function timeoutFallbackGuard(context) {
173
+ const timeout = latestTimedOutStep(context);
174
+ const attempts = context.state.steps.filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok").length;
175
+ if (attempts >= MAX_TIMEOUT_FALLBACKS) {
176
+ const timeouts = context.state.steps
177
+ .filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
178
+ .map((step) => ({
179
+ nodeId: step.nodeId,
180
+ attemptId: step.attemptId,
181
+ error: step.error,
182
+ }));
183
+ return {
184
+ route: "blocked",
185
+ reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
186
+ evidence: {
187
+ attempts,
188
+ limit: MAX_TIMEOUT_FALLBACKS,
189
+ timeouts,
190
+ },
191
+ };
192
+ }
193
+ return {
194
+ route: "recover",
195
+ attempt: attempts + 1,
196
+ limit: MAX_TIMEOUT_FALLBACKS,
197
+ timeout: {
198
+ nodeId: timeout.nodeId,
199
+ attemptId: timeout.attemptId,
200
+ error: timeout.error,
201
+ },
202
+ };
203
+ }
204
+ function throwLatestSupportedFailure(context) {
205
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
206
+ const step = context.state.steps[index];
207
+ if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
208
+ throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
209
+ }
210
+ }
211
+ throw new Error("No supported failed Autoimplement step is available");
212
+ }
36
213
  function parseBlockerChallenge(value) {
37
214
  const result = requireRecord(value, "blocker challenge");
38
215
  if (result.route !== "continue" && result.route !== "blocked") {
@@ -120,20 +297,8 @@ function parseInput(value) {
120
297
  documents: [...raw.documents],
121
298
  };
122
299
  }
123
- let approval;
124
- if (input.approval !== undefined) {
125
- const raw = requireRecord(input.approval, "autoimplement approval");
126
- const maxReplans = raw.maxReplans ?? 3;
127
- if (!Number.isInteger(maxReplans) ||
128
- maxReplans < 1 ||
129
- maxReplans > 20) {
130
- throw new Error("autoimplement approval maxReplans must be from 1 through 20");
131
- }
132
- approval = {
133
- audience: requireString(raw.audience, "autoimplement approval audience"),
134
- maxReplans: maxReplans,
135
- };
136
- }
300
+ const concurrency = parseAutoimplementConcurrency(input.concurrency);
301
+ const approval = parsePlanApprovalPolicy(input.approval);
137
302
  return {
138
303
  task: requireString(input.task, "autoimplement task"),
139
304
  ...(input.plan !== undefined ? { plan: input.plan } : {}),
@@ -148,7 +313,8 @@ function parseInput(value) {
148
313
  merge: input.merge === true,
149
314
  ...(documents !== undefined ? { documents: [...documents] } : {}),
150
315
  ...(documentation !== undefined ? { documentation } : {}),
151
- ...(approval !== undefined ? { approval } : {}),
316
+ approval,
317
+ concurrency,
152
318
  };
153
319
  }
154
320
  function parseExistingPlan(value) {
@@ -187,58 +353,50 @@ function parseRoute(value, routes, label) {
187
353
  }
188
354
  return { ...record, route: record.route };
189
355
  }
190
- function parseCommand(value, options) {
191
- const command = requireRecord(value, options.label);
192
- if (command.command !== options.command) {
193
- throw new Error(`${options.label} command must be ${options.command}`);
194
- }
195
- if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
196
- throw new Error(`${options.label} args must be an array of strings`);
197
- }
198
- const args = [...command.args];
199
- if (!options.validateArgs(args))
200
- throw new Error(`${options.label} args are not allowed`);
201
- const cwd = requireString(command.cwd, `${options.label} cwd`);
202
- if (!path.isAbsolute(cwd))
203
- throw new Error(`${options.label} cwd must be absolute`);
204
- const timeoutMs = command.timeoutMs;
205
- if (typeof timeoutMs !== "number" ||
206
- !Number.isInteger(timeoutMs) ||
207
- timeoutMs <= 0 ||
208
- timeoutMs > options.maxTimeoutMs) {
209
- throw new Error(`${options.label} timeoutMs must be at most ${options.maxTimeoutMs}`);
210
- }
211
- return { command: options.command, args, cwd, timeoutMs };
356
+ function concurrency(context) {
357
+ return parseAutoimplementConcurrency(context.input.concurrency);
212
358
  }
213
- function parseReviewerCommand(value) {
214
- return parseCommand(value, {
215
- command: "pi-reviewer",
216
- maxTimeoutMs: TEN_MINUTES_MS,
217
- label: "reviewer command",
218
- validateArgs: (args) => {
219
- const base = args.indexOf("--base");
220
- return base >= 0 && typeof args[base + 1] === "string" && args[base + 1].length > 0;
359
+ async function runAutoimplementBatch(context, kind, commands, maxConcurrency) {
360
+ return await runCommandBatch({ items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) }, {
361
+ signal: context.signal,
362
+ onItemSettled: async (result, completed, total) => {
363
+ if (context.signal.aborted)
364
+ return;
365
+ try {
366
+ await context.publishUpdate({
367
+ type: "command-batch.item",
368
+ key: `${kind}/${result.id}`,
369
+ data: {
370
+ schema: "pi-workflows.command-batch-item.v1",
371
+ batchKind: kind,
372
+ itemId: result.id,
373
+ outcome: result.outcome,
374
+ completed,
375
+ total,
376
+ },
377
+ });
378
+ }
379
+ catch (error) {
380
+ if (!context.signal.aborted)
381
+ throw error;
382
+ }
221
383
  },
222
384
  });
223
385
  }
224
- function parseCiCommand(value) {
225
- return parseCommand(value, {
226
- command: "gh",
227
- maxTimeoutMs: FIVE_MINUTES_MS,
228
- label: "CI tracking command",
229
- validateArgs: (args) => (args[0] === "pr" && args[1] === "checks" && args.includes("--watch")) ||
230
- (args[0] === "run" && args[1] === "watch"),
231
- });
386
+ function commandBatchTimeoutMs(commands, maxConcurrency) {
387
+ if (commands.length === 0)
388
+ return 10_000;
389
+ const concurrency = Math.min(maxConcurrency, commands.length);
390
+ const waves = Math.ceil(commands.length / concurrency);
391
+ const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
392
+ return waves * longestItem + 10_000;
232
393
  }
233
- function commandExecution(command) {
234
- return {
235
- command: command.command,
236
- args: command.args,
237
- cwd: command.cwd,
238
- timeoutMs: command.timeoutMs,
239
- allowNonZeroExit: true,
240
- maxOutputChars: 1_000_000,
241
- };
394
+ function reviewBatchNeedsRepair(result) {
395
+ return result.items.some((item) => item.outcome === "timedOut" ||
396
+ item.outcome === "cancelled" ||
397
+ (item.outcome === "failed" && item.exitCode === null) ||
398
+ item.stdoutTruncated ||
399
+ item.stderrTruncated);
242
400
  }
243
401
  function latestOutput(context, nodeIds) {
244
402
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -246,22 +404,11 @@ function latestOutput(context, nodeIds) {
246
404
  if (step && nodeIds.includes(step.nodeId))
247
405
  return step.output;
248
406
  }
249
- throw new Error(`No output found for ${nodeIds.join(" or ")}`);
250
- }
251
- function latestCiCommand(context) {
252
- for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
253
- const step = context.state.steps[index];
254
- if (!step)
255
- continue;
256
- if (step.nodeId === "repairCiCommand")
257
- return step.output;
258
- if (step.nodeId === "inspectCi") {
259
- const output = step.output;
260
- if (output.trackingCommand !== undefined)
261
- return output.trackingCommand;
262
- }
407
+ for (const nodeId of nodeIds) {
408
+ if (context.outputs[nodeId] !== undefined)
409
+ return context.outputs[nodeId];
263
410
  }
264
- throw new Error("No CI tracking command is available");
411
+ throw new Error(`No output found for ${nodeIds.join(" or ")}`);
265
412
  }
266
413
  function currentPlan(context) {
267
414
  const adopted = context.outputs.adoptPlan;
@@ -276,19 +423,6 @@ function currentPlan(context) {
276
423
  return discovered.plan;
277
424
  return context.input.plan;
278
425
  }
279
- function currentPlanDigest(context) {
280
- const adopted = context.outputs.adoptPlan;
281
- if (typeof adopted?.planDigest === "string")
282
- return adopted.planDigest;
283
- const documented = context.outputs.documentation;
284
- if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
285
- return documented.output.planDigest;
286
- }
287
- const plan = currentPlan(context);
288
- if (plan === undefined)
289
- throw new Error("autoimplement does not have a selected plan");
290
- return digest(plan);
291
- }
292
426
  function blockerChallenges(context) {
293
427
  return context.state.steps
294
428
  .filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
@@ -305,6 +439,7 @@ function latestBlockerClaim(context) {
305
439
  "assessTrackedCi",
306
440
  "classifyCi",
307
441
  "finalizeDelivery",
442
+ "timeoutFallback",
308
443
  ];
309
444
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
310
445
  const step = context.state.steps[index];
@@ -322,14 +457,6 @@ function recentWorkflowAttempts(context) {
322
457
  }));
323
458
  }
324
459
  function latestIssue(context) {
325
- const approval = context.outputs.approval;
326
- if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
327
- return {
328
- source: "human-replan",
329
- instructions: approval.output.instructions,
330
- priorPlanDigest: currentPlanDigest(context),
331
- };
332
- }
333
460
  const ids = [
334
461
  "challengeBlocker",
335
462
  "classifyImplementation",
@@ -337,6 +464,7 @@ function latestIssue(context) {
337
464
  "triageReview",
338
465
  "inspectComments",
339
466
  "classifyCi",
467
+ "timeoutFallback",
340
468
  "adoptPlan",
341
469
  ];
342
470
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -357,19 +485,277 @@ function parseFinding(value, severity) {
357
485
  summary: requireString(finding.summary, `${severity} finding summary`),
358
486
  };
359
487
  }
360
- function parseReviewAssessment(value) {
361
- const review = requireRecord(value, "review assessment");
362
- const parseList = (key, severity) => {
363
- const raw = review[key];
364
- if (!Array.isArray(raw))
365
- throw new Error(`review ${key} must be an array`);
366
- return raw.map((item) => parseFinding(item, severity));
488
+ function parseVerificationForContext(value, context) {
489
+ const plan = parseVerificationCommandPlan(value);
490
+ const implementation = latestOutput(context, ["implement"]);
491
+ const reported = Array.isArray(implementation.repositories)
492
+ ? implementation.repositories.filter((entry) => typeof entry === "string")
493
+ : [];
494
+ const request = context.input;
495
+ const roots = new Set((reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) => path.resolve(entry)));
496
+ for (const command of plan.commands) {
497
+ const cwd = path.resolve(command.cwd);
498
+ if (!roots.delete(cwd)) {
499
+ throw new Error(`verification command cwd was not reported by implementation: ${command.cwd}`);
500
+ }
501
+ }
502
+ if (roots.size > 0) {
503
+ throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
504
+ }
505
+ return plan;
506
+ }
507
+ function currentPublishedRepositories(context) {
508
+ return latestOutput(context, ["verifyP2", "publish"]);
509
+ }
510
+ function parseDeliveryRepository(value, index) {
511
+ const result = requireRecord(value, `delivery repositories[${index}]`);
512
+ const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
513
+ if (!path.isAbsolute(repository)) {
514
+ throw new Error(`delivery repositories[${index}].repository must be absolute`);
515
+ }
516
+ if (typeof result.merged !== "boolean") {
517
+ throw new Error(`delivery repositories[${index}].merged must be a boolean`);
518
+ }
519
+ return {
520
+ repository: path.resolve(repository),
521
+ pr: requireString(result.pr, `delivery repositories[${index}].pr`),
522
+ merged: result.merged,
523
+ reportComment: requireString(result.reportComment, `delivery repositories[${index}].reportComment`),
524
+ reason: requireString(result.reason, `delivery repositories[${index}].reason`),
367
525
  };
368
- const p0 = parseList("p0", "P0");
369
- const p1 = parseList("p1", "P1");
370
- const p2 = parseList("p2", "P2");
371
- const lower = parseList("lower", "lower");
372
- const invocationSucceeded = review.invocationSucceeded === true;
526
+ }
527
+ function parseDeliveryResult(value, context) {
528
+ const result = requireRecord(value, "delivery result");
529
+ if (result.status !== "completed" && result.status !== "blocked") {
530
+ throw new Error("delivery status must be completed or blocked");
531
+ }
532
+ const request = context.input;
533
+ if (request.merge !== true && result.merged === true) {
534
+ throw new Error("delivery cannot merge without explicit merge: true");
535
+ }
536
+ if (result.status === "blocked")
537
+ return result;
538
+ if (typeof result.merged !== "boolean") {
539
+ throw new Error("completed delivery merged must be a boolean");
540
+ }
541
+ const pr = requireString(result.pr, "completed delivery pr");
542
+ const reportComment = requireString(result.reportComment, "completed delivery reportComment");
543
+ const reason = requireString(result.reason, "completed delivery reason");
544
+ const published = currentPublishedRepositories(context).repositories;
545
+ let repositories;
546
+ if (result.repositories === undefined) {
547
+ if (published.length !== 1) {
548
+ throw new Error("completed delivery repositories must cover every published repository");
549
+ }
550
+ const only = published[0];
551
+ if (only === undefined)
552
+ throw new Error("completed delivery has no published repository");
553
+ repositories = [
554
+ {
555
+ repository: path.resolve(only.repository),
556
+ pr,
557
+ merged: result.merged,
558
+ reportComment,
559
+ reason,
560
+ },
561
+ ];
562
+ }
563
+ else {
564
+ if (!Array.isArray(result.repositories)) {
565
+ throw new Error("completed delivery repositories must be an array");
566
+ }
567
+ repositories = result.repositories.map(parseDeliveryRepository);
568
+ }
569
+ const actual = new Map();
570
+ for (const repository of repositories) {
571
+ if (actual.has(repository.repository)) {
572
+ throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
573
+ }
574
+ actual.set(repository.repository, repository);
575
+ }
576
+ const mergeExpected = request.merge === true;
577
+ for (const expected of published) {
578
+ const repository = actual.get(path.resolve(expected.repository));
579
+ if (repository === undefined || repository.pr !== expected.pr) {
580
+ throw new Error(`completed delivery does not match published repository and PR: ${expected.repository}`);
581
+ }
582
+ if (repository.merged !== mergeExpected) {
583
+ throw new Error(`completed delivery merge result does not match merge policy: ${expected.repository}`);
584
+ }
585
+ actual.delete(repository.repository);
586
+ }
587
+ if (actual.size > 0) {
588
+ throw new Error(`completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`);
589
+ }
590
+ const firstPublished = published[0];
591
+ const first = firstPublished === undefined
592
+ ? undefined
593
+ : repositories.find((repository) => repository.repository === path.resolve(firstPublished.repository));
594
+ if (first === undefined ||
595
+ first.pr !== pr ||
596
+ first.merged !== result.merged ||
597
+ first.reportComment !== reportComment) {
598
+ throw new Error("completed delivery top-level compatibility fields must match the first result");
599
+ }
600
+ return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
601
+ }
602
+ function parseP2Verification(value, context) {
603
+ const result = requireRecord(value, "P2 verification");
604
+ if (typeof result.passed !== "boolean") {
605
+ throw new Error("P2 verification passed must be a boolean");
606
+ }
607
+ if (result.pushed !== true) {
608
+ throw new Error("P2 verification pushed must be true");
609
+ }
610
+ const refreshed = parsePublishedRepositories(result);
611
+ const previous = latestOutput(context, ["publish"]);
612
+ const expected = new Map(previous.repositories.map((repository) => [repository.id, repository]));
613
+ for (const repository of refreshed.repositories) {
614
+ const prior = expected.get(repository.id);
615
+ if (prior === undefined ||
616
+ prior.repository !== repository.repository ||
617
+ prior.branch !== repository.branch ||
618
+ prior.baseBranch !== repository.baseBranch ||
619
+ prior.pr !== repository.pr ||
620
+ prior.dependencyFingerprint !== repository.dependencyFingerprint) {
621
+ throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
622
+ }
623
+ expected.delete(repository.id);
624
+ }
625
+ if (expected.size > 0) {
626
+ throw new Error(`P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`);
627
+ }
628
+ return { ...result, repositories: refreshed.repositories };
629
+ }
630
+ function parseCiInspectionForPublished(value, context) {
631
+ const inspected = parseCiInspectionBatch(value);
632
+ const published = currentPublishedRepositories(context);
633
+ const expected = new Map(published.repositories.map((repository) => [repository.id, repository]));
634
+ for (const target of inspected.targets) {
635
+ const repository = expected.get(target.id);
636
+ if (repository === undefined ||
637
+ repository.repository !== target.repository ||
638
+ repository.headRevision !== target.headRevision ||
639
+ repository.pr !== target.pr) {
640
+ throw new Error(`CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`);
641
+ }
642
+ expected.delete(target.id);
643
+ }
644
+ if (expected.size > 0) {
645
+ throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
646
+ }
647
+ return inspected;
648
+ }
649
+ function parseTrackedCiAssessment(value, context) {
650
+ const result = requireRecord(value, "tracked CI assessment");
651
+ const inspected = latestOutput(context, ["inspectCi"]);
652
+ const execution = latestOutput(context, ["trackCi"]);
653
+ const expectedIds = execution.batch.items.map((item) => item.id);
654
+ if (!Array.isArray(result.targets)) {
655
+ throw new Error("tracked CI assessment targets must be an array");
656
+ }
657
+ const seen = new Set();
658
+ const targets = result.targets.map((entry, index) => {
659
+ const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
660
+ const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
661
+ if (seen.has(id))
662
+ throw new Error(`tracked CI assessment target is duplicated: ${id}`);
663
+ seen.add(id);
664
+ if (target.route !== "green" &&
665
+ target.route !== "failed" &&
666
+ target.route !== "pending" &&
667
+ target.route !== "unavailable") {
668
+ throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
669
+ }
670
+ return {
671
+ id,
672
+ route: target.route,
673
+ reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
674
+ };
675
+ });
676
+ const missing = expectedIds.filter((id) => !seen.has(id));
677
+ const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
678
+ if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
679
+ throw new Error(`tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`);
680
+ }
681
+ const trackedRoutes = new Map(targets.map((target) => [target.id, target.route]));
682
+ const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
683
+ const route = routes.includes("failed")
684
+ ? "failed"
685
+ : routes.includes("pending")
686
+ ? "pending"
687
+ : routes.includes("unavailable")
688
+ ? "unavailable"
689
+ : "green";
690
+ if (result.route !== route) {
691
+ throw new Error(`tracked CI assessment route must be ${route}`);
692
+ }
693
+ return {
694
+ ...result,
695
+ route,
696
+ reason: requireString(result.reason, "tracked CI assessment reason"),
697
+ targets,
698
+ relatedFailures: requireStringArray(result.relatedFailures ?? [], "tracked CI assessment relatedFailures"),
699
+ unrelatedFailures: requireStringArray(result.unrelatedFailures ?? [], "tracked CI assessment unrelatedFailures"),
700
+ };
701
+ }
702
+ function selectReviewCommands(context) {
703
+ const published = latestOutput(context, ["publish"]);
704
+ const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
705
+ const repositories = published.repositories.filter((repository) => !reviewed.some((entry) => entry.id === repository.id &&
706
+ entry.headRevision === repository.headRevision &&
707
+ entry.dependencyFingerprint === repository.dependencyFingerprint &&
708
+ entry.invocationSucceeded));
709
+ return {
710
+ route: repositories.length === 0 ? "reuse" : "run",
711
+ repositories,
712
+ commands: repositories.map(reviewerCommand),
713
+ };
714
+ }
715
+ function parseReviewAssessment(value, context) {
716
+ const review = requireRecord(value, "review assessment");
717
+ if (!Array.isArray(review.repositories)) {
718
+ throw new Error("review repositories must be an array");
719
+ }
720
+ const selected = latestOutput(context, ["selectReviewCommands"]);
721
+ const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
722
+ const repositories = review.repositories.map((value, index) => {
723
+ const raw = requireRecord(value, `review repositories[${index}]`);
724
+ const id = requireString(raw.id, `review repositories[${index}].id`);
725
+ const published = expected.get(id);
726
+ if (published === undefined)
727
+ throw new Error(`review repository id was not in the batch: ${id}`);
728
+ expected.delete(id);
729
+ const parseList = (key, severity) => {
730
+ const list = raw[key];
731
+ if (!Array.isArray(list))
732
+ throw new Error(`review repositories[${index}].${key} must be an array`);
733
+ return list.map((item) => parseFinding(item, severity));
734
+ };
735
+ return {
736
+ id,
737
+ repository: published.repository,
738
+ baseBranch: published.baseBranch,
739
+ headRevision: published.headRevision,
740
+ ...(published.dependencyFingerprint !== undefined
741
+ ? { dependencyFingerprint: published.dependencyFingerprint }
742
+ : {}),
743
+ invocationSucceeded: raw.invocationSucceeded === true,
744
+ p0: parseList("p0", "P0"),
745
+ p1: parseList("p1", "P1"),
746
+ p2: parseList("p2", "P2"),
747
+ lower: parseList("lower", "lower"),
748
+ reason: requireString(raw.reason, `review repositories[${index}].reason`),
749
+ };
750
+ });
751
+ if (expected.size > 0) {
752
+ throw new Error(`review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`);
753
+ }
754
+ const p0 = repositories.flatMap((entry) => entry.p0);
755
+ const p1 = repositories.flatMap((entry) => entry.p1);
756
+ const p2 = repositories.flatMap((entry) => entry.p2);
757
+ const lower = repositories.flatMap((entry) => entry.lower);
758
+ const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
373
759
  const route = !invocationSucceeded
374
760
  ? "command_error"
375
761
  : p0.length + p1.length > 0
@@ -385,6 +771,7 @@ function parseReviewAssessment(value) {
385
771
  p2,
386
772
  lower,
387
773
  reason: requireString(review.reason, "review reason"),
774
+ repositories,
388
775
  };
389
776
  }
390
777
  function reviewRounds(context) {
@@ -392,8 +779,37 @@ function reviewRounds(context) {
392
779
  .filter((step) => step.nodeId === "assessReview" && step.outcome === "ok")
393
780
  .map((step) => step.output);
394
781
  }
782
+ function reviewRoundsForOutput(context) {
783
+ const rounds = reviewRounds(context);
784
+ const repositoryIds = new Set(rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)));
785
+ if (repositoryIds.size > 1)
786
+ return rounds;
787
+ return rounds.map(({ repositories: _repositories, ...round }) => round);
788
+ }
789
+ function ciForOutput(context) {
790
+ const result = latestOutput(context, ["assessTrackedCi", "inspectCi"]);
791
+ const targets = result.targets;
792
+ if (!Array.isArray(targets) || targets.length !== 1)
793
+ return result;
794
+ const { targets: _targets, ...aggregate } = result;
795
+ if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
796
+ const target = targets[0];
797
+ if (target !== null && typeof target === "object" && !Array.isArray(target)) {
798
+ const record = target;
799
+ return {
800
+ ...aggregate,
801
+ reason: aggregate.reason ?? record.reason,
802
+ relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
803
+ unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
804
+ };
805
+ }
806
+ }
807
+ return aggregate;
808
+ }
395
809
  function latestBlockedReason(context) {
396
810
  const candidates = [
811
+ "timeoutFallbackGuard",
812
+ "timeoutFallback",
397
813
  "challengeBlockerGuard",
398
814
  "challengeBlocker",
399
815
  "finalizeDelivery",
@@ -404,11 +820,10 @@ function latestBlockedReason(context) {
404
820
  "classifyImplementation",
405
821
  "classifyVerification",
406
822
  "triageReview",
407
- "replanGuard",
823
+ "redesign",
408
824
  "adoptPlan",
409
825
  "findPlan",
410
826
  "documentation",
411
- "approval",
412
827
  ];
413
828
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
414
829
  const step = context.state.steps[index];
@@ -450,36 +865,18 @@ export const autoimplementWorkflow = defineWorkflow({
450
865
  };
451
866
  },
452
867
  }),
453
- approval: includeWorkflow(planApprovalWorkflow, {
868
+ redesign: includeWorkflow(planChangeWorkflow, {
454
869
  input: (context) => {
455
870
  const request = context.input;
456
- if (request.approval === undefined) {
457
- throw new Error("autoimplement approval was entered without an approval policy");
458
- }
459
- const plan = currentPlan(context);
460
- if (plan === undefined)
461
- throw new Error("autoimplement approval is missing a plan");
462
- const revisions = context.state.steps.filter((step) => step.nodeId === "approval/approve").length;
463
871
  return {
464
872
  task: request.task,
465
- plan,
466
- planDigest: currentPlanDigest(context),
467
- audience: request.approval.audience,
468
- revision: revisions + 1,
469
- };
470
- },
471
- }),
472
- redesign: includeWorkflow({
473
- workflow: "autoplan",
474
- contract: autoplanWorkflow,
475
- input: (context) => {
476
- const request = context.input;
477
- return {
478
- problem: request.task,
479
873
  ...(request.scope !== undefined ? { scope: request.scope } : {}),
480
874
  ...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
875
+ ...(request.repository !== undefined ? { repository: request.repository } : {}),
876
+ documents: request.documents ?? request.documentation?.documents ?? [],
481
877
  ...(currentPlan(context) !== undefined ? { previousPlan: currentPlan(context) } : {}),
482
878
  newEvidence: latestIssue(context),
879
+ approval: parsePlanApprovalPolicy(request.approval),
483
880
  };
484
881
  },
485
882
  }),
@@ -541,41 +938,70 @@ export const autoimplementWorkflow = defineWorkflow({
541
938
  }),
542
939
  adoptPlan: compute({
543
940
  run: ({ outputs }) => {
544
- const result = includedResult(autoplanWorkflow, outputs.redesign);
941
+ const result = includedResult(planChangeWorkflow, outputs.redesign);
545
942
  if (result.exit !== "ready")
546
943
  throw new Error("redesign did not return a ready plan");
547
944
  return {
548
- route: result.output.changed ? "document" : "blocked",
549
945
  plan: result.output.plan,
550
946
  planDigest: result.output.planDigest,
551
- changed: result.output.changed,
552
- reason: result.output.changed
553
- ? "The plan changed in response to new evidence and must be documented."
554
- : "Redesign returned the same plan for the same unresolved evidence.",
947
+ documents: result.output.documents,
948
+ approval: result.output.approval,
949
+ reason: "The changed plan was documented and passed its approval policy.",
555
950
  };
556
951
  },
557
952
  }),
558
- maybeApproval: compute({
559
- run: ({ input }) => ({
560
- route: input.approval === undefined ? "implement" : "approve",
561
- }),
953
+ timeoutFallbackGuard: compute({
954
+ run: timeoutFallbackGuard,
562
955
  }),
563
- replanGuard: compute({
564
- run: (context) => {
956
+ timeoutFallback: agent({
957
+ timeoutMs: 30 * 60_000,
958
+ statusDetail: "choosing a safe timeout fallback",
959
+ prompt: (context) => {
565
960
  const request = context.input;
566
- const limit = request.approval?.maxReplans ?? 3;
567
- const replans = context.state.steps.filter((step) => step.nodeId === "approval/replan").length;
568
- return replans > limit
569
- ? {
570
- route: "blocked",
571
- reason: `Plan approval reached the ${limit}-replan safety limit.`,
572
- evidence: context.outputs.approval,
573
- }
574
- : { route: "redesign", replans, limit };
961
+ const guard = context.outputs.timeoutFallbackGuard;
962
+ const previousFallbacks = context.state.steps
963
+ .filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
964
+ .map((step) => step.output);
965
+ return [
966
+ "A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
967
+ "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.",
968
+ "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.",
969
+ "Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
970
+ "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.",
971
+ "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.",
972
+ "Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
973
+ `Task: ${request.task}`,
974
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
975
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
976
+ `Timeout: ${JSON.stringify(guard)}`,
977
+ `Accepted outputs: ${JSON.stringify(context.outputs)}`,
978
+ `Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
979
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
980
+ ].join("\n");
575
981
  },
982
+ expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
983
+ validate: parseTimeoutFallback,
984
+ }),
985
+ routeTimeoutFallback: compute({
986
+ run: timeoutFallbackTarget,
987
+ }),
988
+ propagateSupportedFailure: compute({
989
+ run: throwLatestSupportedFailure,
990
+ }),
991
+ routeVerifyP2Result: compute({
992
+ run: ({ outputs }) => outputs.verifyP2,
993
+ }),
994
+ routeInspectCommentsResult: compute({
995
+ run: ({ outputs }) => outputs.inspectComments,
996
+ }),
997
+ routeInspectCiResult: compute({
998
+ run: ({ outputs }) => outputs.inspectCi,
999
+ }),
1000
+ routeFinalizeDeliveryResult: compute({
1001
+ run: ({ outputs }) => outputs.finalizeDelivery,
576
1002
  }),
577
1003
  implement: agent({
578
- timeoutMs: 60 * 60_000,
1004
+ timeoutMs: 8 * 60 * 60_000,
579
1005
  statusDetail: "implementing",
580
1006
  prompt: (context) => {
581
1007
  const request = context.input;
@@ -584,12 +1010,14 @@ export const autoimplementWorkflow = defineWorkflow({
584
1010
  `Plan: ${JSON.stringify(currentPlan(context))}`,
585
1011
  `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
586
1012
  `Constraints: ${JSON.stringify(request.constraints ?? [])}`,
1013
+ "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.",
587
1014
  "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
588
1015
  "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
1016
+ "Report every changed repository as an absolute path so independent verification can be bounded safely.",
589
1017
  "Do not merge yet.",
590
1018
  ].join("\n");
591
1019
  },
592
- expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
1020
+ 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" }`,
593
1021
  validate: (value) => requireRecord(value, "implementation result"),
594
1022
  }),
595
1023
  classifyImplementation: agent({
@@ -650,14 +1078,39 @@ export const autoimplementWorkflow = defineWorkflow({
650
1078
  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"] }`,
651
1079
  validate: parseBlockerChallenge,
652
1080
  }),
653
- verify: agent({
654
- timeoutMs: 45 * 60_000,
655
- statusDetail: "verifying",
1081
+ planVerification: agent({
1082
+ timeoutMs: 15 * 60_000,
1083
+ statusDetail: "planning independent verification commands",
656
1084
  prompt: () => [
657
- "Verify the implementation thoroughly.",
658
- "Run required tests, formatting, lint, type checks, builds, and useful local smoke tests.",
659
- "Do not put optional mutation testing on the critical path.",
660
- "State exactly what ran, what passed, what failed, and what still needs remote verification.",
1085
+ "Select the required local verification commands for the implementation.",
1086
+ "Return one command per independent repository working directory.",
1087
+ "Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
1088
+ "Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
1089
+ "List checks that cannot run locally under untested.",
1090
+ ].join("\n"),
1091
+ expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
1092
+ validate: parseVerificationForContext,
1093
+ }),
1094
+ runVerification: action({
1095
+ timeoutMs: (context) => {
1096
+ const plan = latestOutput(context, ["planVerification"]);
1097
+ return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
1098
+ },
1099
+ statusDetail: "running independent verification commands",
1100
+ run: async (context) => {
1101
+ const plan = latestOutput(context, ["planVerification"]);
1102
+ return await runAutoimplementBatch(context, "verification", plan.commands, concurrency(context).verification);
1103
+ },
1104
+ }),
1105
+ verify: agent({
1106
+ timeoutMs: 20 * 60_000,
1107
+ statusDetail: "assessing verification",
1108
+ prompt: (context) => [
1109
+ "Assess the completed local verification commands.",
1110
+ "Set passed true only when every required command succeeded without truncated output.",
1111
+ "Report exact command outcomes, failures, and checks that still need remote verification.",
1112
+ `Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
1113
+ `Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
661
1114
  ].join("\n"),
662
1115
  expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
663
1116
  validate: (value) => requireRecord(value, "verification result"),
@@ -680,6 +1133,7 @@ export const autoimplementWorkflow = defineWorkflow({
680
1133
  statusDetail: "fixing",
681
1134
  prompt: (context) => [
682
1135
  "Fix the current implementation issue without expanding the approved design.",
1136
+ "Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
683
1137
  `Issue: ${JSON.stringify(latestIssue(context))}`,
684
1138
  `Current plan: ${JSON.stringify(currentPlan(context))}`,
685
1139
  "Stop after the fix so verification can run again.",
@@ -694,79 +1148,61 @@ export const autoimplementWorkflow = defineWorkflow({
694
1148
  const request = input;
695
1149
  return [
696
1150
  "Commit and push the verified implementation before review.",
1151
+ "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.",
697
1152
  "Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
698
1153
  "Inspect the complete public diff before every push or PR mutation.",
699
- `Requested base branch: ${request.baseBranch ?? "discover the repository default branch"}.`,
1154
+ "Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
1155
+ "Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
1156
+ `Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
700
1157
  "Do not merge yet.",
701
1158
  ].join("\n");
702
1159
  },
703
- expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
704
- validate: (value) => requireRecord(value, "publication result"),
1160
+ expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
1161
+ validate: parsePublishedRepositories,
705
1162
  }),
706
- authorReviewCommand: agent({
707
- statusDetail: "writing reviewer command",
708
- prompt: ({ outputs, input }) => {
709
- const published = outputs.publish;
710
- const request = input;
711
- return [
712
- "Write the exact Pi Reviewer command for the pushed branch.",
713
- "The executable must be pi-reviewer. Use its configured model and thinking settings.",
714
- "Use the repository base branch and an absolute repository working directory.",
715
- "Set timeoutMs to at most 600000.",
716
- `Published branch: ${JSON.stringify(published)}`,
717
- `Repository hint: ${request.repository ?? "current repository"}`,
718
- ].join("\n");
719
- },
720
- expectedOutput: `{ "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000 }`,
721
- validate: parseReviewerCommand,
1163
+ selectReviewCommands: compute({
1164
+ run: selectReviewCommands,
722
1165
  }),
723
- runReview: shell({
724
- statusDetail: "running Pi Reviewer",
725
- timeoutMs: TEN_MINUTES_MS + 10_000,
726
- exec: (context) => commandExecution(latestOutput(context, ["authorReviewCommand", "repairReviewCommand"])),
727
- }),
728
- repairReviewCommand: agent({
729
- statusDetail: "correcting reviewer command",
730
- prompt: (context) => {
731
- const failed = context.results.runReview;
732
- return [
733
- "The Pi Reviewer invocation failed. Diagnose the exact command, arguments, base branch, working directory, and error.",
734
- "Write a corrected pi-reviewer command. Do not substitute codex review or another reviewer.",
735
- "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.",
736
- `Failed result: ${JSON.stringify(failed)}`,
737
- ].join("\n");
1166
+ runReview: action({
1167
+ statusDetail: "running pi-reviewer commands",
1168
+ timeoutMs: (context) => {
1169
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1170
+ return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
738
1171
  },
739
- expectedOutput: `{ "route": "retry" | "blocked", "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000, "reason": "diagnosis" }`,
740
- validate: (value) => {
741
- const result = requireRecord(value, "reviewer command repair");
742
- if (result.route === "blocked") {
743
- return {
744
- route: "blocked",
745
- reason: requireString(result.reason, "reviewer command blocker"),
746
- };
747
- }
748
- if (result.route !== "retry")
749
- throw new Error("reviewer command repair route must be retry or blocked");
750
- return {
751
- route: "retry",
752
- ...parseReviewerCommand(result),
753
- reason: requireString(result.reason, "reviewer command repair reason"),
754
- };
1172
+ run: async (context) => {
1173
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1174
+ const batch = await runAutoimplementBatch(context, "review", selected.commands, concurrency(context).reviewer);
1175
+ return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
755
1176
  },
756
1177
  }),
1178
+ repairReviewCommand: agent({
1179
+ statusDetail: "repairing reviewer prerequisites",
1180
+ prompt: (context) => [
1181
+ "One or more pi-reviewer commands failed, timed out, or returned truncated output.",
1182
+ "Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
1183
+ "Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
1184
+ "Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
1185
+ `Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
1186
+ ].join("\n"),
1187
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
1188
+ validate: (value) => parseRoute(value, ["retry", "blocked"], "reviewer command repair"),
1189
+ }),
757
1190
  assessReview: agent({
758
1191
  statusDetail: "assessing reviewer findings",
759
1192
  prompt: (context) => {
760
- const result = latestOutput(context, ["runReview"]);
1193
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1194
+ const execution = latestOutput(context, ["runReview"]);
761
1195
  return [
762
- "Assess the completed Pi Reviewer invocation.",
763
- "Set invocationSucceeded false only when the reviewer did not produce a valid review.",
764
- "Record each finding under P0, P1, P2, or lower. Mark each finding as design or implementation.",
1196
+ "Assess each completed pi-reviewer result separately.",
1197
+ "Return one repository entry for every selected command, using the exact repository id.",
1198
+ "Set invocationSucceeded false when a complete valid review was not produced.",
1199
+ "Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
765
1200
  "Do not promote P2 findings to P1 merely to force another review round.",
766
- `Reviewer result: ${JSON.stringify(result)}`,
1201
+ `Selected repositories: ${JSON.stringify(selected.repositories)}`,
1202
+ `Reviewer results: ${JSON.stringify(execution.batch)}`,
767
1203
  ].join("\n");
768
1204
  },
769
- expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
1205
+ expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
770
1206
  validate: parseReviewAssessment,
771
1207
  }),
772
1208
  triageReview: compute({
@@ -785,7 +1221,8 @@ export const autoimplementWorkflow = defineWorkflow({
785
1221
  statusDetail: "addressing P2 findings",
786
1222
  prompt: ({ outputs }) => [
787
1223
  "Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
788
- "Do not rerun Pi Reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
1224
+ "Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
1225
+ "Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
789
1226
  `Review: ${JSON.stringify(outputs.assessReview)}`,
790
1227
  ].join("\n"),
791
1228
  expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
@@ -796,19 +1233,22 @@ export const autoimplementWorkflow = defineWorkflow({
796
1233
  statusDetail: "verifying P2 changes",
797
1234
  prompt: () => [
798
1235
  "Run focused verification for the P2 changes and push the verified result.",
799
- "Do not run Pi Reviewer again because the previous round had no P0 or P1 findings.",
1236
+ "Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
1237
+ "Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
1238
+ "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.",
800
1239
  "Report exact commands and outcomes.",
801
1240
  ].join("\n"),
802
- expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
803
- validate: (value) => requireRecord(value, "P2 verification"),
1241
+ 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" }] }`,
1242
+ validate: parseP2Verification,
804
1243
  }),
805
1244
  inspectComments: agent({
806
1245
  timeoutMs: 20 * 60_000,
807
1246
  statusDetail: "checking PR comments",
808
- prompt: () => [
809
- "Inspect current inline review comments and PR issue comments.",
810
- "Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
811
- "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.",
1247
+ prompt: (context) => [
1248
+ "Inspect current inline review comments and PR issue comments for every published pull request.",
1249
+ "Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
1250
+ "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.",
1251
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
812
1252
  ].join("\n"),
813
1253
  expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
814
1254
  validate: (value) => parseRoute(value, ["redesign", "fix", "ci", "blocked"], "PR comment assessment"),
@@ -816,60 +1256,61 @@ export const autoimplementWorkflow = defineWorkflow({
816
1256
  inspectCi: agent({
817
1257
  timeoutMs: 10 * 60_000,
818
1258
  statusDetail: "checking CI",
819
- prompt: () => [
820
- "Inspect CI once without waiting for completion.",
821
- "Choose green, failed, pending, or unavailable.",
822
- "When pending, provide an exact gh command that tracks this PR or run and set timeoutMs to at most 300000.",
823
- "Separate failures caused by this change from unrelated failures.",
1259
+ prompt: (context) => [
1260
+ "Inspect every published pull request once without waiting for completion.",
1261
+ "Return one target per repository and current PR head.",
1262
+ "Choose green, failed, pending, or unavailable for each target.",
1263
+ "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.",
1264
+ "Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
1265
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
824
1266
  ].join("\n"),
825
- 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) }`,
826
- validate: (value) => {
827
- const result = parseRoute(value, ["green", "failed", "pending", "unavailable"], "CI inspection");
828
- if (result.route === "pending")
829
- result.trackingCommand = parseCiCommand(result.trackingCommand);
830
- return result;
831
- },
1267
+ 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 } }] }`,
1268
+ validate: parseCiInspectionForPublished,
832
1269
  }),
833
- trackCi: shell({
834
- statusDetail: "tracking CI for at most five minutes",
835
- timeoutMs: FIVE_MINUTES_MS + 10_000,
836
- exec: (context) => commandExecution(latestCiCommand(context)),
1270
+ trackCi: action({
1271
+ statusDetail: "tracking pending CI commands",
1272
+ timeoutMs: (context) => {
1273
+ const inspected = latestOutput(context, ["inspectCi"]);
1274
+ const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
1275
+ return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
1276
+ },
1277
+ run: async (context) => {
1278
+ const inspected = latestOutput(context, ["inspectCi"]);
1279
+ const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
1280
+ const batch = await runAutoimplementBatch(context, "ciWatch", commands, concurrency(context).ciWatch);
1281
+ const needsRepair = batch.items.some((item) => (item.outcome === "failed" && item.exitCode === null) ||
1282
+ item.stdoutTruncated ||
1283
+ item.stderrTruncated);
1284
+ return { route: needsRepair ? "repair" : "assess", batch };
1285
+ },
837
1286
  }),
838
1287
  repairCiCommand: agent({
839
- statusDetail: "correcting CI tracking command",
1288
+ statusDetail: "repairing CI watch prerequisites",
840
1289
  prompt: (context) => [
841
- "The CI tracking command failed before it could provide a useful status.",
842
- "Write a corrected gh pr checks --watch or gh run watch command for the same PR or run.",
843
- "Use an absolute repository path and a timeout no longer than five minutes.",
844
- `Failure: ${JSON.stringify(context.results.trackCi)}`,
1290
+ "One or more supported CI watch commands failed or returned truncated output.",
1291
+ "Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
1292
+ "Do not change the PR identity or substitute another command form.",
1293
+ "Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
1294
+ `Failure: ${JSON.stringify(context.outputs.trackCi)}`,
845
1295
  ].join("\n"),
846
- expectedOutput: `{ "route": "retry" | "blocked", "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000, "reason": "diagnosis" }`,
847
- validate: (value) => {
848
- const result = requireRecord(value, "CI command repair");
849
- if (result.route === "blocked") {
850
- return { route: "blocked", reason: requireString(result.reason, "CI command blocker") };
851
- }
852
- if (result.route !== "retry")
853
- throw new Error("CI command repair route must be retry or blocked");
854
- return {
855
- route: "retry",
856
- ...parseCiCommand(result),
857
- reason: requireString(result.reason, "CI command repair reason"),
858
- };
859
- },
1296
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
1297
+ validate: (value) => parseRoute(value, ["retry", "blocked"], "CI command repair"),
860
1298
  }),
861
1299
  assessTrackedCi: agent({
862
1300
  statusDetail: "assessing tracked CI",
863
1301
  prompt: (context) => {
864
- const result = latestOutput(context, ["trackCi"]);
1302
+ const inspected = latestOutput(context, ["inspectCi"]);
1303
+ const execution = latestOutput(context, ["trackCi"]);
865
1304
  return [
866
- "Assess the CI tracking result without starting another wait.",
867
- "Choose green, failed, pending, or unavailable and separate related from unrelated failures.",
868
- `Tracking result: ${JSON.stringify(result)}`,
1305
+ "Assess every CI watch result without starting another wait.",
1306
+ "Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
1307
+ "A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
1308
+ `Initial inspection: ${JSON.stringify(inspected)}`,
1309
+ `Tracking results: ${JSON.stringify(execution.batch)}`,
869
1310
  ].join("\n");
870
1311
  },
871
- expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
872
- validate: (value) => parseRoute(value, ["green", "failed", "pending", "unavailable"], "tracked CI assessment"),
1312
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
1313
+ validate: parseTrackedCiAssessment,
873
1314
  }),
874
1315
  opportunisticTest: agent({
875
1316
  timeoutMs: 30 * 60_000,
@@ -895,30 +1336,23 @@ export const autoimplementWorkflow = defineWorkflow({
895
1336
  }),
896
1337
  finalizeDelivery: agent({
897
1338
  timeoutMs: 30 * 60_000,
898
- statusDetail: "finalizing PR",
899
- prompt: ({ input }) => {
900
- const request = input;
1339
+ statusDetail: "finalizing PRs",
1340
+ prompt: (context) => {
1341
+ const request = context.input;
901
1342
  return [
902
1343
  request.merge === false
903
- ? "Leave the verified PR ready without merging because input disabled merge."
904
- : "Merge the verified PR unless repository policy or explicit user instructions prohibit it.",
905
- "Use the repository's required merge method.",
906
- "Post a final PR report with the implementation summary and exact validation commands.",
1344
+ ? "Leave every verified PR ready without merging because input disabled merge."
1345
+ : "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
1346
+ "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.",
1347
+ "Use each repository's required merge method.",
1348
+ "Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
1349
+ "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.",
907
1350
  "Return blocked instead of claiming completion when a required merge or report action fails.",
1351
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
908
1352
  ].join("\n");
909
1353
  },
910
- expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "URL", "reportComment": "URL or summary", "reason": "result" }`,
911
- validate: (value, context) => {
912
- const result = requireRecord(value, "delivery result");
913
- if (result.status !== "completed" && result.status !== "blocked") {
914
- throw new Error("delivery status must be completed or blocked");
915
- }
916
- const request = context.input;
917
- if (request.merge !== true && result.merged === true) {
918
- throw new Error("delivery cannot merge without explicit merge: true");
919
- }
920
- return result;
921
- },
1354
+ 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" }] }`,
1355
+ validate: parseDeliveryResult,
922
1356
  }),
923
1357
  blocked: compute({
924
1358
  run: (context) => {
@@ -939,11 +1373,11 @@ export const autoimplementWorkflow = defineWorkflow({
939
1373
  status: "completed",
940
1374
  task: request.task,
941
1375
  plan: currentPlan(context),
942
- implementation: context.outputs.implement,
943
- verification: context.outputs.verifyP2 ?? context.outputs.verify,
944
- reviewRounds: reviewRounds(context),
945
- ci: context.outputs.assessTrackedCi ?? context.outputs.inspectCi,
946
- delivery: context.outputs.finalizeDelivery,
1376
+ implementation: latestOutput(context, ["implement"]),
1377
+ verification: latestOutput(context, ["verifyP2", "verify"]),
1378
+ reviewRounds: reviewRoundsForOutput(context),
1379
+ ci: ciForOutput(context),
1380
+ delivery: latestOutput(context, ["finalizeDelivery"]),
947
1381
  };
948
1382
  },
949
1383
  }),
@@ -953,7 +1387,7 @@ export const autoimplementWorkflow = defineWorkflow({
953
1387
  from: "prepare",
954
1388
  switch: {
955
1389
  on: "$.route",
956
- cases: { find: "findPlan", document: "documentation", ready: "maybeApproval" },
1390
+ cases: { find: "findPlan", document: "documentation", ready: "implement" },
957
1391
  },
958
1392
  },
959
1393
  {
@@ -964,35 +1398,59 @@ export const autoimplementWorkflow = defineWorkflow({
964
1398
  from: "routeFoundPlan",
965
1399
  switch: {
966
1400
  on: "$.route",
967
- cases: { ready: "maybeApproval", document: "documentation", blocked: "blocked" },
1401
+ cases: { ready: "implement", document: "documentation", blocked: "blocked" },
968
1402
  },
969
1403
  },
970
1404
  { from: "redesign.ready", to: "adoptPlan" },
971
1405
  { from: "redesign.blocked", to: "blocked" },
1406
+ { from: "adoptPlan", to: "implement" },
1407
+ { from: "documentation.ready", to: "implement" },
1408
+ { from: "documentation.blocked", to: "blocked" },
972
1409
  {
973
- from: "adoptPlan",
974
- switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
1410
+ from: "timeoutFallbackGuard",
1411
+ switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
975
1412
  },
976
- { from: "documentation.ready", to: "maybeApproval" },
977
- { from: "documentation.blocked", to: "blocked" },
1413
+ { from: "timeoutFallback", to: "routeTimeoutFallback" },
1414
+ { from: "propagateSupportedFailure", to: "blocked" },
978
1415
  {
979
- from: "maybeApproval",
980
- switch: { on: "$.route", cases: { approve: "approval", implement: "implement" } },
1416
+ from: "routeTimeoutFallback",
1417
+ switch: {
1418
+ on: "$.route",
1419
+ cases: {
1420
+ implement: "implement",
1421
+ planVerification: "planVerification",
1422
+ verify: "verify",
1423
+ fix: "fix",
1424
+ publish: "publish",
1425
+ addressP2: "addressP2",
1426
+ verifyP2: "verifyP2",
1427
+ inspectComments: "inspectComments",
1428
+ inspectCi: "inspectCi",
1429
+ opportunisticTest: "opportunisticTest",
1430
+ finalizeDelivery: "finalizeDelivery",
1431
+ selectReviewCommands: "selectReviewCommands",
1432
+ redesign: "redesign",
1433
+ blocked: "blocked",
1434
+ },
1435
+ },
981
1436
  },
982
- { from: "approval.continue", to: "implement" },
983
- { from: "approval.stop", to: "blocked" },
984
- { from: "approval.replan", to: "replanGuard" },
985
1437
  {
986
- from: "replanGuard",
987
- switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
1438
+ from: "implement",
1439
+ switch: {
1440
+ on: "$result.outcome",
1441
+ cases: {
1442
+ ok: "classifyImplementation",
1443
+ timed_out: "timeoutFallbackGuard",
1444
+ failed: "propagateSupportedFailure",
1445
+ },
1446
+ },
988
1447
  },
989
- { from: "implement", to: "classifyImplementation" },
990
1448
  {
991
1449
  from: "classifyImplementation",
992
1450
  switch: {
993
1451
  on: "$.route",
994
1452
  cases: {
995
- verify: "verify",
1453
+ verify: "planVerification",
996
1454
  redesign: "redesign",
997
1455
  fix: "fix",
998
1456
  blocked: "challengeBlockerGuard",
@@ -1007,7 +1465,29 @@ export const autoimplementWorkflow = defineWorkflow({
1007
1465
  from: "challengeBlocker",
1008
1466
  switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1009
1467
  },
1010
- { from: "verify", to: "classifyVerification" },
1468
+ {
1469
+ from: "planVerification",
1470
+ switch: {
1471
+ on: "$result.outcome",
1472
+ cases: {
1473
+ ok: "runVerification",
1474
+ timed_out: "timeoutFallbackGuard",
1475
+ failed: "propagateSupportedFailure",
1476
+ },
1477
+ },
1478
+ },
1479
+ { from: "runVerification", to: "verify" },
1480
+ {
1481
+ from: "verify",
1482
+ switch: {
1483
+ on: "$result.outcome",
1484
+ cases: {
1485
+ ok: "classifyVerification",
1486
+ timed_out: "timeoutFallbackGuard",
1487
+ failed: "propagateSupportedFailure",
1488
+ },
1489
+ },
1490
+ },
1011
1491
  {
1012
1492
  from: "classifyVerification",
1013
1493
  switch: {
@@ -1020,20 +1500,36 @@ export const autoimplementWorkflow = defineWorkflow({
1020
1500
  },
1021
1501
  },
1022
1502
  },
1023
- { from: "fix", to: "verify" },
1024
- { from: "publish", to: "authorReviewCommand" },
1025
- { from: "authorReviewCommand", to: "runReview" },
1026
1503
  {
1027
- from: "runReview",
1504
+ from: "fix",
1028
1505
  switch: {
1029
1506
  on: "$result.outcome",
1030
1507
  cases: {
1031
- ok: "assessReview",
1032
- failed: "repairReviewCommand",
1033
- timed_out: "repairReviewCommand",
1508
+ ok: "planVerification",
1509
+ timed_out: "timeoutFallbackGuard",
1510
+ failed: "propagateSupportedFailure",
1034
1511
  },
1035
1512
  },
1036
1513
  },
1514
+ {
1515
+ from: "publish",
1516
+ switch: {
1517
+ on: "$result.outcome",
1518
+ cases: {
1519
+ ok: "selectReviewCommands",
1520
+ timed_out: "timeoutFallbackGuard",
1521
+ failed: "propagateSupportedFailure",
1522
+ },
1523
+ },
1524
+ },
1525
+ {
1526
+ from: "selectReviewCommands",
1527
+ switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
1528
+ },
1529
+ {
1530
+ from: "runReview",
1531
+ switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
1532
+ },
1037
1533
  {
1038
1534
  from: "repairReviewCommand",
1039
1535
  switch: {
@@ -1057,13 +1553,45 @@ export const autoimplementWorkflow = defineWorkflow({
1057
1553
  from: "triageReview",
1058
1554
  switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
1059
1555
  },
1060
- { from: "addressP2", to: "verifyP2" },
1556
+ {
1557
+ from: "addressP2",
1558
+ switch: {
1559
+ on: "$result.outcome",
1560
+ cases: {
1561
+ ok: "verifyP2",
1562
+ timed_out: "timeoutFallbackGuard",
1563
+ failed: "propagateSupportedFailure",
1564
+ },
1565
+ },
1566
+ },
1061
1567
  {
1062
1568
  from: "verifyP2",
1569
+ switch: {
1570
+ on: "$result.outcome",
1571
+ cases: {
1572
+ ok: "routeVerifyP2Result",
1573
+ timed_out: "timeoutFallbackGuard",
1574
+ failed: "propagateSupportedFailure",
1575
+ },
1576
+ },
1577
+ },
1578
+ {
1579
+ from: "routeVerifyP2Result",
1063
1580
  switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
1064
1581
  },
1065
1582
  {
1066
1583
  from: "inspectComments",
1584
+ switch: {
1585
+ on: "$result.outcome",
1586
+ cases: {
1587
+ ok: "routeInspectCommentsResult",
1588
+ timed_out: "timeoutFallbackGuard",
1589
+ failed: "propagateSupportedFailure",
1590
+ },
1591
+ },
1592
+ },
1593
+ {
1594
+ from: "routeInspectCommentsResult",
1067
1595
  switch: {
1068
1596
  on: "$.route",
1069
1597
  cases: {
@@ -1076,6 +1604,17 @@ export const autoimplementWorkflow = defineWorkflow({
1076
1604
  },
1077
1605
  {
1078
1606
  from: "inspectCi",
1607
+ switch: {
1608
+ on: "$result.outcome",
1609
+ cases: {
1610
+ ok: "routeInspectCiResult",
1611
+ timed_out: "timeoutFallbackGuard",
1612
+ failed: "propagateSupportedFailure",
1613
+ },
1614
+ },
1615
+ },
1616
+ {
1617
+ from: "routeInspectCiResult",
1079
1618
  switch: {
1080
1619
  on: "$.route",
1081
1620
  cases: {
@@ -1088,10 +1627,7 @@ export const autoimplementWorkflow = defineWorkflow({
1088
1627
  },
1089
1628
  {
1090
1629
  from: "trackCi",
1091
- switch: {
1092
- on: "$result.outcome",
1093
- cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
1094
- },
1630
+ switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
1095
1631
  },
1096
1632
  {
1097
1633
  from: "repairCiCommand",
@@ -1112,7 +1648,17 @@ export const autoimplementWorkflow = defineWorkflow({
1112
1648
  },
1113
1649
  },
1114
1650
  },
1115
- { from: "opportunisticTest", to: "inspectCi" },
1651
+ {
1652
+ from: "opportunisticTest",
1653
+ switch: {
1654
+ on: "$result.outcome",
1655
+ cases: {
1656
+ ok: "inspectCi",
1657
+ timed_out: "timeoutFallbackGuard",
1658
+ failed: "propagateSupportedFailure",
1659
+ },
1660
+ },
1661
+ },
1116
1662
  {
1117
1663
  from: "classifyCi",
1118
1664
  switch: {
@@ -1127,6 +1673,17 @@ export const autoimplementWorkflow = defineWorkflow({
1127
1673
  },
1128
1674
  {
1129
1675
  from: "finalizeDelivery",
1676
+ switch: {
1677
+ on: "$result.outcome",
1678
+ cases: {
1679
+ ok: "routeFinalizeDeliveryResult",
1680
+ timed_out: "timeoutFallbackGuard",
1681
+ failed: "propagateSupportedFailure",
1682
+ },
1683
+ },
1684
+ },
1685
+ {
1686
+ from: "routeFinalizeDeliveryResult",
1130
1687
  switch: {
1131
1688
  on: "$.status",
1132
1689
  cases: { completed: "finalize", blocked: "challengeBlockerGuard" },