@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,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";
6
+ import { parseAutoimplementConcurrency, parseCiInspectionBatch, parsePublishedRepositories, parseVerificationCommandPlan, reviewerCommand, } from "./autoimplement-command-batches.js";
5
7
  import autoplanWorkflow, {} from "./autoplan.workflow.js";
6
8
  import planApprovalWorkflow, {} from "./plan-approval.workflow.js";
7
- const FIVE_MINUTES_MS = 5 * 60_000;
8
- const TEN_MINUTES_MS = 10 * 60_000;
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,6 +297,7 @@ function parseInput(value) {
120
297
  documents: [...raw.documents],
121
298
  };
122
299
  }
300
+ const concurrency = parseAutoimplementConcurrency(input.concurrency);
123
301
  let approval;
124
302
  if (input.approval !== undefined) {
125
303
  const raw = requireRecord(input.approval, "autoimplement approval");
@@ -149,6 +327,7 @@ function parseInput(value) {
149
327
  ...(documents !== undefined ? { documents: [...documents] } : {}),
150
328
  ...(documentation !== undefined ? { documentation } : {}),
151
329
  ...(approval !== undefined ? { approval } : {}),
330
+ concurrency,
152
331
  };
153
332
  }
154
333
  function parseExistingPlan(value) {
@@ -187,58 +366,50 @@ function parseRoute(value, routes, label) {
187
366
  }
188
367
  return { ...record, route: record.route };
189
368
  }
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 };
369
+ function concurrency(context) {
370
+ return parseAutoimplementConcurrency(context.input.concurrency);
212
371
  }
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;
372
+ async function runAutoimplementBatch(context, kind, commands, maxConcurrency) {
373
+ return await runCommandBatch({ items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) }, {
374
+ signal: context.signal,
375
+ onItemSettled: async (result, completed, total) => {
376
+ if (context.signal.aborted)
377
+ return;
378
+ try {
379
+ await context.publishUpdate({
380
+ type: "command-batch.item",
381
+ key: `${kind}/${result.id}`,
382
+ data: {
383
+ schema: "pi-workflows.command-batch-item.v1",
384
+ batchKind: kind,
385
+ itemId: result.id,
386
+ outcome: result.outcome,
387
+ completed,
388
+ total,
389
+ },
390
+ });
391
+ }
392
+ catch (error) {
393
+ if (!context.signal.aborted)
394
+ throw error;
395
+ }
221
396
  },
222
397
  });
223
398
  }
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
- });
399
+ function commandBatchTimeoutMs(commands, maxConcurrency) {
400
+ if (commands.length === 0)
401
+ return 10_000;
402
+ const concurrency = Math.min(maxConcurrency, commands.length);
403
+ const waves = Math.ceil(commands.length / concurrency);
404
+ const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
405
+ return waves * longestItem + 10_000;
232
406
  }
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
- };
407
+ function reviewBatchNeedsRepair(result) {
408
+ return result.items.some((item) => item.outcome === "timedOut" ||
409
+ item.outcome === "cancelled" ||
410
+ (item.outcome === "failed" && item.exitCode === null) ||
411
+ item.stdoutTruncated ||
412
+ item.stderrTruncated);
242
413
  }
243
414
  function latestOutput(context, nodeIds) {
244
415
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -246,22 +417,11 @@ function latestOutput(context, nodeIds) {
246
417
  if (step && nodeIds.includes(step.nodeId))
247
418
  return step.output;
248
419
  }
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
- }
420
+ for (const nodeId of nodeIds) {
421
+ if (context.outputs[nodeId] !== undefined)
422
+ return context.outputs[nodeId];
263
423
  }
264
- throw new Error("No CI tracking command is available");
424
+ throw new Error(`No output found for ${nodeIds.join(" or ")}`);
265
425
  }
266
426
  function currentPlan(context) {
267
427
  const adopted = context.outputs.adoptPlan;
@@ -305,6 +465,7 @@ function latestBlockerClaim(context) {
305
465
  "assessTrackedCi",
306
466
  "classifyCi",
307
467
  "finalizeDelivery",
468
+ "timeoutFallback",
308
469
  ];
309
470
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
310
471
  const step = context.state.steps[index];
@@ -337,6 +498,7 @@ function latestIssue(context) {
337
498
  "triageReview",
338
499
  "inspectComments",
339
500
  "classifyCi",
501
+ "timeoutFallback",
340
502
  "adoptPlan",
341
503
  ];
342
504
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
@@ -357,19 +519,277 @@ function parseFinding(value, severity) {
357
519
  summary: requireString(finding.summary, `${severity} finding summary`),
358
520
  };
359
521
  }
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));
522
+ function parseVerificationForContext(value, context) {
523
+ const plan = parseVerificationCommandPlan(value);
524
+ const implementation = latestOutput(context, ["implement"]);
525
+ const reported = Array.isArray(implementation.repositories)
526
+ ? implementation.repositories.filter((entry) => typeof entry === "string")
527
+ : [];
528
+ const request = context.input;
529
+ const roots = new Set((reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) => path.resolve(entry)));
530
+ for (const command of plan.commands) {
531
+ const cwd = path.resolve(command.cwd);
532
+ if (!roots.delete(cwd)) {
533
+ throw new Error(`verification command cwd was not reported by implementation: ${command.cwd}`);
534
+ }
535
+ }
536
+ if (roots.size > 0) {
537
+ throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
538
+ }
539
+ return plan;
540
+ }
541
+ function currentPublishedRepositories(context) {
542
+ return latestOutput(context, ["verifyP2", "publish"]);
543
+ }
544
+ function parseDeliveryRepository(value, index) {
545
+ const result = requireRecord(value, `delivery repositories[${index}]`);
546
+ const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
547
+ if (!path.isAbsolute(repository)) {
548
+ throw new Error(`delivery repositories[${index}].repository must be absolute`);
549
+ }
550
+ if (typeof result.merged !== "boolean") {
551
+ throw new Error(`delivery repositories[${index}].merged must be a boolean`);
552
+ }
553
+ return {
554
+ repository: path.resolve(repository),
555
+ pr: requireString(result.pr, `delivery repositories[${index}].pr`),
556
+ merged: result.merged,
557
+ reportComment: requireString(result.reportComment, `delivery repositories[${index}].reportComment`),
558
+ reason: requireString(result.reason, `delivery repositories[${index}].reason`),
367
559
  };
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;
560
+ }
561
+ function parseDeliveryResult(value, context) {
562
+ const result = requireRecord(value, "delivery result");
563
+ if (result.status !== "completed" && result.status !== "blocked") {
564
+ throw new Error("delivery status must be completed or blocked");
565
+ }
566
+ const request = context.input;
567
+ if (request.merge !== true && result.merged === true) {
568
+ throw new Error("delivery cannot merge without explicit merge: true");
569
+ }
570
+ if (result.status === "blocked")
571
+ return result;
572
+ if (typeof result.merged !== "boolean") {
573
+ throw new Error("completed delivery merged must be a boolean");
574
+ }
575
+ const pr = requireString(result.pr, "completed delivery pr");
576
+ const reportComment = requireString(result.reportComment, "completed delivery reportComment");
577
+ const reason = requireString(result.reason, "completed delivery reason");
578
+ const published = currentPublishedRepositories(context).repositories;
579
+ let repositories;
580
+ if (result.repositories === undefined) {
581
+ if (published.length !== 1) {
582
+ throw new Error("completed delivery repositories must cover every published repository");
583
+ }
584
+ const only = published[0];
585
+ if (only === undefined)
586
+ throw new Error("completed delivery has no published repository");
587
+ repositories = [
588
+ {
589
+ repository: path.resolve(only.repository),
590
+ pr,
591
+ merged: result.merged,
592
+ reportComment,
593
+ reason,
594
+ },
595
+ ];
596
+ }
597
+ else {
598
+ if (!Array.isArray(result.repositories)) {
599
+ throw new Error("completed delivery repositories must be an array");
600
+ }
601
+ repositories = result.repositories.map(parseDeliveryRepository);
602
+ }
603
+ const actual = new Map();
604
+ for (const repository of repositories) {
605
+ if (actual.has(repository.repository)) {
606
+ throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
607
+ }
608
+ actual.set(repository.repository, repository);
609
+ }
610
+ const mergeExpected = request.merge === true;
611
+ for (const expected of published) {
612
+ const repository = actual.get(path.resolve(expected.repository));
613
+ if (repository === undefined || repository.pr !== expected.pr) {
614
+ throw new Error(`completed delivery does not match published repository and PR: ${expected.repository}`);
615
+ }
616
+ if (repository.merged !== mergeExpected) {
617
+ throw new Error(`completed delivery merge result does not match merge policy: ${expected.repository}`);
618
+ }
619
+ actual.delete(repository.repository);
620
+ }
621
+ if (actual.size > 0) {
622
+ throw new Error(`completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`);
623
+ }
624
+ const firstPublished = published[0];
625
+ const first = firstPublished === undefined
626
+ ? undefined
627
+ : repositories.find((repository) => repository.repository === path.resolve(firstPublished.repository));
628
+ if (first === undefined ||
629
+ first.pr !== pr ||
630
+ first.merged !== result.merged ||
631
+ first.reportComment !== reportComment) {
632
+ throw new Error("completed delivery top-level compatibility fields must match the first result");
633
+ }
634
+ return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
635
+ }
636
+ function parseP2Verification(value, context) {
637
+ const result = requireRecord(value, "P2 verification");
638
+ if (typeof result.passed !== "boolean") {
639
+ throw new Error("P2 verification passed must be a boolean");
640
+ }
641
+ if (result.pushed !== true) {
642
+ throw new Error("P2 verification pushed must be true");
643
+ }
644
+ const refreshed = parsePublishedRepositories(result);
645
+ const previous = latestOutput(context, ["publish"]);
646
+ const expected = new Map(previous.repositories.map((repository) => [repository.id, repository]));
647
+ for (const repository of refreshed.repositories) {
648
+ const prior = expected.get(repository.id);
649
+ if (prior === undefined ||
650
+ prior.repository !== repository.repository ||
651
+ prior.branch !== repository.branch ||
652
+ prior.baseBranch !== repository.baseBranch ||
653
+ prior.pr !== repository.pr ||
654
+ prior.dependencyFingerprint !== repository.dependencyFingerprint) {
655
+ throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
656
+ }
657
+ expected.delete(repository.id);
658
+ }
659
+ if (expected.size > 0) {
660
+ throw new Error(`P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`);
661
+ }
662
+ return { ...result, repositories: refreshed.repositories };
663
+ }
664
+ function parseCiInspectionForPublished(value, context) {
665
+ const inspected = parseCiInspectionBatch(value);
666
+ const published = currentPublishedRepositories(context);
667
+ const expected = new Map(published.repositories.map((repository) => [repository.id, repository]));
668
+ for (const target of inspected.targets) {
669
+ const repository = expected.get(target.id);
670
+ if (repository === undefined ||
671
+ repository.repository !== target.repository ||
672
+ repository.headRevision !== target.headRevision ||
673
+ repository.pr !== target.pr) {
674
+ throw new Error(`CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`);
675
+ }
676
+ expected.delete(target.id);
677
+ }
678
+ if (expected.size > 0) {
679
+ throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
680
+ }
681
+ return inspected;
682
+ }
683
+ function parseTrackedCiAssessment(value, context) {
684
+ const result = requireRecord(value, "tracked CI assessment");
685
+ const inspected = latestOutput(context, ["inspectCi"]);
686
+ const execution = latestOutput(context, ["trackCi"]);
687
+ const expectedIds = execution.batch.items.map((item) => item.id);
688
+ if (!Array.isArray(result.targets)) {
689
+ throw new Error("tracked CI assessment targets must be an array");
690
+ }
691
+ const seen = new Set();
692
+ const targets = result.targets.map((entry, index) => {
693
+ const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
694
+ const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
695
+ if (seen.has(id))
696
+ throw new Error(`tracked CI assessment target is duplicated: ${id}`);
697
+ seen.add(id);
698
+ if (target.route !== "green" &&
699
+ target.route !== "failed" &&
700
+ target.route !== "pending" &&
701
+ target.route !== "unavailable") {
702
+ throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
703
+ }
704
+ return {
705
+ id,
706
+ route: target.route,
707
+ reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
708
+ };
709
+ });
710
+ const missing = expectedIds.filter((id) => !seen.has(id));
711
+ const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
712
+ if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
713
+ throw new Error(`tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`);
714
+ }
715
+ const trackedRoutes = new Map(targets.map((target) => [target.id, target.route]));
716
+ const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
717
+ const route = routes.includes("failed")
718
+ ? "failed"
719
+ : routes.includes("pending")
720
+ ? "pending"
721
+ : routes.includes("unavailable")
722
+ ? "unavailable"
723
+ : "green";
724
+ if (result.route !== route) {
725
+ throw new Error(`tracked CI assessment route must be ${route}`);
726
+ }
727
+ return {
728
+ ...result,
729
+ route,
730
+ reason: requireString(result.reason, "tracked CI assessment reason"),
731
+ targets,
732
+ relatedFailures: requireStringArray(result.relatedFailures ?? [], "tracked CI assessment relatedFailures"),
733
+ unrelatedFailures: requireStringArray(result.unrelatedFailures ?? [], "tracked CI assessment unrelatedFailures"),
734
+ };
735
+ }
736
+ function selectReviewCommands(context) {
737
+ const published = latestOutput(context, ["publish"]);
738
+ const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
739
+ const repositories = published.repositories.filter((repository) => !reviewed.some((entry) => entry.id === repository.id &&
740
+ entry.headRevision === repository.headRevision &&
741
+ entry.dependencyFingerprint === repository.dependencyFingerprint &&
742
+ entry.invocationSucceeded));
743
+ return {
744
+ route: repositories.length === 0 ? "reuse" : "run",
745
+ repositories,
746
+ commands: repositories.map(reviewerCommand),
747
+ };
748
+ }
749
+ function parseReviewAssessment(value, context) {
750
+ const review = requireRecord(value, "review assessment");
751
+ if (!Array.isArray(review.repositories)) {
752
+ throw new Error("review repositories must be an array");
753
+ }
754
+ const selected = latestOutput(context, ["selectReviewCommands"]);
755
+ const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
756
+ const repositories = review.repositories.map((value, index) => {
757
+ const raw = requireRecord(value, `review repositories[${index}]`);
758
+ const id = requireString(raw.id, `review repositories[${index}].id`);
759
+ const published = expected.get(id);
760
+ if (published === undefined)
761
+ throw new Error(`review repository id was not in the batch: ${id}`);
762
+ expected.delete(id);
763
+ const parseList = (key, severity) => {
764
+ const list = raw[key];
765
+ if (!Array.isArray(list))
766
+ throw new Error(`review repositories[${index}].${key} must be an array`);
767
+ return list.map((item) => parseFinding(item, severity));
768
+ };
769
+ return {
770
+ id,
771
+ repository: published.repository,
772
+ baseBranch: published.baseBranch,
773
+ headRevision: published.headRevision,
774
+ ...(published.dependencyFingerprint !== undefined
775
+ ? { dependencyFingerprint: published.dependencyFingerprint }
776
+ : {}),
777
+ invocationSucceeded: raw.invocationSucceeded === true,
778
+ p0: parseList("p0", "P0"),
779
+ p1: parseList("p1", "P1"),
780
+ p2: parseList("p2", "P2"),
781
+ lower: parseList("lower", "lower"),
782
+ reason: requireString(raw.reason, `review repositories[${index}].reason`),
783
+ };
784
+ });
785
+ if (expected.size > 0) {
786
+ throw new Error(`review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`);
787
+ }
788
+ const p0 = repositories.flatMap((entry) => entry.p0);
789
+ const p1 = repositories.flatMap((entry) => entry.p1);
790
+ const p2 = repositories.flatMap((entry) => entry.p2);
791
+ const lower = repositories.flatMap((entry) => entry.lower);
792
+ const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
373
793
  const route = !invocationSucceeded
374
794
  ? "command_error"
375
795
  : p0.length + p1.length > 0
@@ -385,6 +805,7 @@ function parseReviewAssessment(value) {
385
805
  p2,
386
806
  lower,
387
807
  reason: requireString(review.reason, "review reason"),
808
+ repositories,
388
809
  };
389
810
  }
390
811
  function reviewRounds(context) {
@@ -392,8 +813,37 @@ function reviewRounds(context) {
392
813
  .filter((step) => step.nodeId === "assessReview" && step.outcome === "ok")
393
814
  .map((step) => step.output);
394
815
  }
816
+ function reviewRoundsForOutput(context) {
817
+ const rounds = reviewRounds(context);
818
+ const repositoryIds = new Set(rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)));
819
+ if (repositoryIds.size > 1)
820
+ return rounds;
821
+ return rounds.map(({ repositories: _repositories, ...round }) => round);
822
+ }
823
+ function ciForOutput(context) {
824
+ const result = latestOutput(context, ["assessTrackedCi", "inspectCi"]);
825
+ const targets = result.targets;
826
+ if (!Array.isArray(targets) || targets.length !== 1)
827
+ return result;
828
+ const { targets: _targets, ...aggregate } = result;
829
+ if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
830
+ const target = targets[0];
831
+ if (target !== null && typeof target === "object" && !Array.isArray(target)) {
832
+ const record = target;
833
+ return {
834
+ ...aggregate,
835
+ reason: aggregate.reason ?? record.reason,
836
+ relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
837
+ unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
838
+ };
839
+ }
840
+ }
841
+ return aggregate;
842
+ }
395
843
  function latestBlockedReason(context) {
396
844
  const candidates = [
845
+ "timeoutFallbackGuard",
846
+ "timeoutFallback",
397
847
  "challengeBlockerGuard",
398
848
  "challengeBlocker",
399
849
  "finalizeDelivery",
@@ -574,8 +1024,58 @@ export const autoimplementWorkflow = defineWorkflow({
574
1024
  : { route: "redesign", replans, limit };
575
1025
  },
576
1026
  }),
1027
+ timeoutFallbackGuard: compute({
1028
+ run: timeoutFallbackGuard,
1029
+ }),
1030
+ timeoutFallback: agent({
1031
+ timeoutMs: 30 * 60_000,
1032
+ statusDetail: "choosing a safe timeout fallback",
1033
+ prompt: (context) => {
1034
+ const request = context.input;
1035
+ const guard = context.outputs.timeoutFallbackGuard;
1036
+ const previousFallbacks = context.state.steps
1037
+ .filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
1038
+ .map((step) => step.output);
1039
+ return [
1040
+ "A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
1041
+ "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.",
1042
+ "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.",
1043
+ "Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
1044
+ "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.",
1045
+ "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.",
1046
+ "Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
1047
+ `Task: ${request.task}`,
1048
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
1049
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
1050
+ `Timeout: ${JSON.stringify(guard)}`,
1051
+ `Accepted outputs: ${JSON.stringify(context.outputs)}`,
1052
+ `Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
1053
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
1054
+ ].join("\n");
1055
+ },
1056
+ expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
1057
+ validate: parseTimeoutFallback,
1058
+ }),
1059
+ routeTimeoutFallback: compute({
1060
+ run: timeoutFallbackTarget,
1061
+ }),
1062
+ propagateSupportedFailure: compute({
1063
+ run: throwLatestSupportedFailure,
1064
+ }),
1065
+ routeVerifyP2Result: compute({
1066
+ run: ({ outputs }) => outputs.verifyP2,
1067
+ }),
1068
+ routeInspectCommentsResult: compute({
1069
+ run: ({ outputs }) => outputs.inspectComments,
1070
+ }),
1071
+ routeInspectCiResult: compute({
1072
+ run: ({ outputs }) => outputs.inspectCi,
1073
+ }),
1074
+ routeFinalizeDeliveryResult: compute({
1075
+ run: ({ outputs }) => outputs.finalizeDelivery,
1076
+ }),
577
1077
  implement: agent({
578
- timeoutMs: 60 * 60_000,
1078
+ timeoutMs: 8 * 60 * 60_000,
579
1079
  statusDetail: "implementing",
580
1080
  prompt: (context) => {
581
1081
  const request = context.input;
@@ -584,12 +1084,14 @@ export const autoimplementWorkflow = defineWorkflow({
584
1084
  `Plan: ${JSON.stringify(currentPlan(context))}`,
585
1085
  `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
586
1086
  `Constraints: ${JSON.stringify(request.constraints ?? [])}`,
1087
+ "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
1088
  "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
588
1089
  "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
1090
+ "Report every changed repository as an absolute path so independent verification can be bounded safely.",
589
1091
  "Do not merge yet.",
590
1092
  ].join("\n");
591
1093
  },
592
- expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
1094
+ 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
1095
  validate: (value) => requireRecord(value, "implementation result"),
594
1096
  }),
595
1097
  classifyImplementation: agent({
@@ -650,14 +1152,39 @@ export const autoimplementWorkflow = defineWorkflow({
650
1152
  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
1153
  validate: parseBlockerChallenge,
652
1154
  }),
653
- verify: agent({
654
- timeoutMs: 45 * 60_000,
655
- statusDetail: "verifying",
1155
+ planVerification: agent({
1156
+ timeoutMs: 15 * 60_000,
1157
+ statusDetail: "planning independent verification commands",
656
1158
  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.",
1159
+ "Select the required local verification commands for the implementation.",
1160
+ "Return one command per independent repository working directory.",
1161
+ "Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
1162
+ "Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
1163
+ "List checks that cannot run locally under untested.",
1164
+ ].join("\n"),
1165
+ expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
1166
+ validate: parseVerificationForContext,
1167
+ }),
1168
+ runVerification: action({
1169
+ timeoutMs: (context) => {
1170
+ const plan = latestOutput(context, ["planVerification"]);
1171
+ return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
1172
+ },
1173
+ statusDetail: "running independent verification commands",
1174
+ run: async (context) => {
1175
+ const plan = latestOutput(context, ["planVerification"]);
1176
+ return await runAutoimplementBatch(context, "verification", plan.commands, concurrency(context).verification);
1177
+ },
1178
+ }),
1179
+ verify: agent({
1180
+ timeoutMs: 20 * 60_000,
1181
+ statusDetail: "assessing verification",
1182
+ prompt: (context) => [
1183
+ "Assess the completed local verification commands.",
1184
+ "Set passed true only when every required command succeeded without truncated output.",
1185
+ "Report exact command outcomes, failures, and checks that still need remote verification.",
1186
+ `Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
1187
+ `Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
661
1188
  ].join("\n"),
662
1189
  expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
663
1190
  validate: (value) => requireRecord(value, "verification result"),
@@ -680,6 +1207,7 @@ export const autoimplementWorkflow = defineWorkflow({
680
1207
  statusDetail: "fixing",
681
1208
  prompt: (context) => [
682
1209
  "Fix the current implementation issue without expanding the approved design.",
1210
+ "Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
683
1211
  `Issue: ${JSON.stringify(latestIssue(context))}`,
684
1212
  `Current plan: ${JSON.stringify(currentPlan(context))}`,
685
1213
  "Stop after the fix so verification can run again.",
@@ -694,79 +1222,61 @@ export const autoimplementWorkflow = defineWorkflow({
694
1222
  const request = input;
695
1223
  return [
696
1224
  "Commit and push the verified implementation before review.",
1225
+ "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
1226
  "Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
698
1227
  "Inspect the complete public diff before every push or PR mutation.",
699
- `Requested base branch: ${request.baseBranch ?? "discover the repository default branch"}.`,
1228
+ "Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
1229
+ "Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
1230
+ `Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
700
1231
  "Do not merge yet.",
701
1232
  ].join("\n");
702
1233
  },
703
- expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
704
- validate: (value) => requireRecord(value, "publication result"),
1234
+ expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
1235
+ validate: parsePublishedRepositories,
705
1236
  }),
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,
722
- }),
723
- runReview: shell({
724
- statusDetail: "running Pi Reviewer",
725
- timeoutMs: TEN_MINUTES_MS + 10_000,
726
- exec: (context) => commandExecution(latestOutput(context, ["authorReviewCommand", "repairReviewCommand"])),
1237
+ selectReviewCommands: compute({
1238
+ run: selectReviewCommands,
727
1239
  }),
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");
1240
+ runReview: action({
1241
+ statusDetail: "running pi-reviewer commands",
1242
+ timeoutMs: (context) => {
1243
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1244
+ return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
738
1245
  },
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
- };
1246
+ run: async (context) => {
1247
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1248
+ const batch = await runAutoimplementBatch(context, "review", selected.commands, concurrency(context).reviewer);
1249
+ return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
755
1250
  },
756
1251
  }),
1252
+ repairReviewCommand: agent({
1253
+ statusDetail: "repairing reviewer prerequisites",
1254
+ prompt: (context) => [
1255
+ "One or more pi-reviewer commands failed, timed out, or returned truncated output.",
1256
+ "Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
1257
+ "Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
1258
+ "Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
1259
+ `Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
1260
+ ].join("\n"),
1261
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
1262
+ validate: (value) => parseRoute(value, ["retry", "blocked"], "reviewer command repair"),
1263
+ }),
757
1264
  assessReview: agent({
758
1265
  statusDetail: "assessing reviewer findings",
759
1266
  prompt: (context) => {
760
- const result = latestOutput(context, ["runReview"]);
1267
+ const selected = latestOutput(context, ["selectReviewCommands"]);
1268
+ const execution = latestOutput(context, ["runReview"]);
761
1269
  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.",
1270
+ "Assess each completed pi-reviewer result separately.",
1271
+ "Return one repository entry for every selected command, using the exact repository id.",
1272
+ "Set invocationSucceeded false when a complete valid review was not produced.",
1273
+ "Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
765
1274
  "Do not promote P2 findings to P1 merely to force another review round.",
766
- `Reviewer result: ${JSON.stringify(result)}`,
1275
+ `Selected repositories: ${JSON.stringify(selected.repositories)}`,
1276
+ `Reviewer results: ${JSON.stringify(execution.batch)}`,
767
1277
  ].join("\n");
768
1278
  },
769
- expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
1279
+ expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
770
1280
  validate: parseReviewAssessment,
771
1281
  }),
772
1282
  triageReview: compute({
@@ -785,7 +1295,8 @@ export const autoimplementWorkflow = defineWorkflow({
785
1295
  statusDetail: "addressing P2 findings",
786
1296
  prompt: ({ outputs }) => [
787
1297
  "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.",
1298
+ "Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
1299
+ "Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
789
1300
  `Review: ${JSON.stringify(outputs.assessReview)}`,
790
1301
  ].join("\n"),
791
1302
  expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
@@ -796,19 +1307,22 @@ export const autoimplementWorkflow = defineWorkflow({
796
1307
  statusDetail: "verifying P2 changes",
797
1308
  prompt: () => [
798
1309
  "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.",
1310
+ "Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
1311
+ "Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
1312
+ "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
1313
  "Report exact commands and outcomes.",
801
1314
  ].join("\n"),
802
- expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
803
- validate: (value) => requireRecord(value, "P2 verification"),
1315
+ 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" }] }`,
1316
+ validate: parseP2Verification,
804
1317
  }),
805
1318
  inspectComments: agent({
806
1319
  timeoutMs: 20 * 60_000,
807
1320
  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.",
1321
+ prompt: (context) => [
1322
+ "Inspect current inline review comments and PR issue comments for every published pull request.",
1323
+ "Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
1324
+ "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.",
1325
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
812
1326
  ].join("\n"),
813
1327
  expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
814
1328
  validate: (value) => parseRoute(value, ["redesign", "fix", "ci", "blocked"], "PR comment assessment"),
@@ -816,60 +1330,61 @@ export const autoimplementWorkflow = defineWorkflow({
816
1330
  inspectCi: agent({
817
1331
  timeoutMs: 10 * 60_000,
818
1332
  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.",
1333
+ prompt: (context) => [
1334
+ "Inspect every published pull request once without waiting for completion.",
1335
+ "Return one target per repository and current PR head.",
1336
+ "Choose green, failed, pending, or unavailable for each target.",
1337
+ "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.",
1338
+ "Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
1339
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
824
1340
  ].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
- },
1341
+ 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 } }] }`,
1342
+ validate: parseCiInspectionForPublished,
832
1343
  }),
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)),
1344
+ trackCi: action({
1345
+ statusDetail: "tracking pending CI commands",
1346
+ timeoutMs: (context) => {
1347
+ const inspected = latestOutput(context, ["inspectCi"]);
1348
+ const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
1349
+ return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
1350
+ },
1351
+ run: async (context) => {
1352
+ const inspected = latestOutput(context, ["inspectCi"]);
1353
+ const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
1354
+ const batch = await runAutoimplementBatch(context, "ciWatch", commands, concurrency(context).ciWatch);
1355
+ const needsRepair = batch.items.some((item) => (item.outcome === "failed" && item.exitCode === null) ||
1356
+ item.stdoutTruncated ||
1357
+ item.stderrTruncated);
1358
+ return { route: needsRepair ? "repair" : "assess", batch };
1359
+ },
837
1360
  }),
838
1361
  repairCiCommand: agent({
839
- statusDetail: "correcting CI tracking command",
1362
+ statusDetail: "repairing CI watch prerequisites",
840
1363
  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)}`,
1364
+ "One or more supported CI watch commands failed or returned truncated output.",
1365
+ "Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
1366
+ "Do not change the PR identity or substitute another command form.",
1367
+ "Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
1368
+ `Failure: ${JSON.stringify(context.outputs.trackCi)}`,
845
1369
  ].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
- },
1370
+ expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
1371
+ validate: (value) => parseRoute(value, ["retry", "blocked"], "CI command repair"),
860
1372
  }),
861
1373
  assessTrackedCi: agent({
862
1374
  statusDetail: "assessing tracked CI",
863
1375
  prompt: (context) => {
864
- const result = latestOutput(context, ["trackCi"]);
1376
+ const inspected = latestOutput(context, ["inspectCi"]);
1377
+ const execution = latestOutput(context, ["trackCi"]);
865
1378
  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)}`,
1379
+ "Assess every CI watch result without starting another wait.",
1380
+ "Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
1381
+ "A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
1382
+ `Initial inspection: ${JSON.stringify(inspected)}`,
1383
+ `Tracking results: ${JSON.stringify(execution.batch)}`,
869
1384
  ].join("\n");
870
1385
  },
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"),
1386
+ expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
1387
+ validate: parseTrackedCiAssessment,
873
1388
  }),
874
1389
  opportunisticTest: agent({
875
1390
  timeoutMs: 30 * 60_000,
@@ -895,30 +1410,23 @@ export const autoimplementWorkflow = defineWorkflow({
895
1410
  }),
896
1411
  finalizeDelivery: agent({
897
1412
  timeoutMs: 30 * 60_000,
898
- statusDetail: "finalizing PR",
899
- prompt: ({ input }) => {
900
- const request = input;
1413
+ statusDetail: "finalizing PRs",
1414
+ prompt: (context) => {
1415
+ const request = context.input;
901
1416
  return [
902
1417
  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.",
1418
+ ? "Leave every verified PR ready without merging because input disabled merge."
1419
+ : "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
1420
+ "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.",
1421
+ "Use each repository's required merge method.",
1422
+ "Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
1423
+ "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
1424
  "Return blocked instead of claiming completion when a required merge or report action fails.",
1425
+ `Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
908
1426
  ].join("\n");
909
1427
  },
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
- },
1428
+ 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" }] }`,
1429
+ validate: parseDeliveryResult,
922
1430
  }),
923
1431
  blocked: compute({
924
1432
  run: (context) => {
@@ -939,11 +1447,11 @@ export const autoimplementWorkflow = defineWorkflow({
939
1447
  status: "completed",
940
1448
  task: request.task,
941
1449
  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,
1450
+ implementation: latestOutput(context, ["implement"]),
1451
+ verification: latestOutput(context, ["verifyP2", "verify"]),
1452
+ reviewRounds: reviewRoundsForOutput(context),
1453
+ ci: ciForOutput(context),
1454
+ delivery: latestOutput(context, ["finalizeDelivery"]),
947
1455
  };
948
1456
  },
949
1457
  }),
@@ -986,13 +1494,51 @@ export const autoimplementWorkflow = defineWorkflow({
986
1494
  from: "replanGuard",
987
1495
  switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
988
1496
  },
989
- { from: "implement", to: "classifyImplementation" },
990
1497
  {
991
- from: "classifyImplementation",
1498
+ from: "timeoutFallbackGuard",
1499
+ switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
1500
+ },
1501
+ { from: "timeoutFallback", to: "routeTimeoutFallback" },
1502
+ { from: "propagateSupportedFailure", to: "blocked" },
1503
+ {
1504
+ from: "routeTimeoutFallback",
992
1505
  switch: {
993
1506
  on: "$.route",
994
1507
  cases: {
1508
+ implement: "implement",
1509
+ planVerification: "planVerification",
995
1510
  verify: "verify",
1511
+ fix: "fix",
1512
+ publish: "publish",
1513
+ addressP2: "addressP2",
1514
+ verifyP2: "verifyP2",
1515
+ inspectComments: "inspectComments",
1516
+ inspectCi: "inspectCi",
1517
+ opportunisticTest: "opportunisticTest",
1518
+ finalizeDelivery: "finalizeDelivery",
1519
+ selectReviewCommands: "selectReviewCommands",
1520
+ redesign: "redesign",
1521
+ blocked: "blocked",
1522
+ },
1523
+ },
1524
+ },
1525
+ {
1526
+ from: "implement",
1527
+ switch: {
1528
+ on: "$result.outcome",
1529
+ cases: {
1530
+ ok: "classifyImplementation",
1531
+ timed_out: "timeoutFallbackGuard",
1532
+ failed: "propagateSupportedFailure",
1533
+ },
1534
+ },
1535
+ },
1536
+ {
1537
+ from: "classifyImplementation",
1538
+ switch: {
1539
+ on: "$.route",
1540
+ cases: {
1541
+ verify: "planVerification",
996
1542
  redesign: "redesign",
997
1543
  fix: "fix",
998
1544
  blocked: "challengeBlockerGuard",
@@ -1007,7 +1553,29 @@ export const autoimplementWorkflow = defineWorkflow({
1007
1553
  from: "challengeBlocker",
1008
1554
  switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1009
1555
  },
1010
- { from: "verify", to: "classifyVerification" },
1556
+ {
1557
+ from: "planVerification",
1558
+ switch: {
1559
+ on: "$result.outcome",
1560
+ cases: {
1561
+ ok: "runVerification",
1562
+ timed_out: "timeoutFallbackGuard",
1563
+ failed: "propagateSupportedFailure",
1564
+ },
1565
+ },
1566
+ },
1567
+ { from: "runVerification", to: "verify" },
1568
+ {
1569
+ from: "verify",
1570
+ switch: {
1571
+ on: "$result.outcome",
1572
+ cases: {
1573
+ ok: "classifyVerification",
1574
+ timed_out: "timeoutFallbackGuard",
1575
+ failed: "propagateSupportedFailure",
1576
+ },
1577
+ },
1578
+ },
1011
1579
  {
1012
1580
  from: "classifyVerification",
1013
1581
  switch: {
@@ -1020,20 +1588,36 @@ export const autoimplementWorkflow = defineWorkflow({
1020
1588
  },
1021
1589
  },
1022
1590
  },
1023
- { from: "fix", to: "verify" },
1024
- { from: "publish", to: "authorReviewCommand" },
1025
- { from: "authorReviewCommand", to: "runReview" },
1026
1591
  {
1027
- from: "runReview",
1592
+ from: "fix",
1593
+ switch: {
1594
+ on: "$result.outcome",
1595
+ cases: {
1596
+ ok: "planVerification",
1597
+ timed_out: "timeoutFallbackGuard",
1598
+ failed: "propagateSupportedFailure",
1599
+ },
1600
+ },
1601
+ },
1602
+ {
1603
+ from: "publish",
1028
1604
  switch: {
1029
1605
  on: "$result.outcome",
1030
1606
  cases: {
1031
- ok: "assessReview",
1032
- failed: "repairReviewCommand",
1033
- timed_out: "repairReviewCommand",
1607
+ ok: "selectReviewCommands",
1608
+ timed_out: "timeoutFallbackGuard",
1609
+ failed: "propagateSupportedFailure",
1034
1610
  },
1035
1611
  },
1036
1612
  },
1613
+ {
1614
+ from: "selectReviewCommands",
1615
+ switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
1616
+ },
1617
+ {
1618
+ from: "runReview",
1619
+ switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
1620
+ },
1037
1621
  {
1038
1622
  from: "repairReviewCommand",
1039
1623
  switch: {
@@ -1057,13 +1641,45 @@ export const autoimplementWorkflow = defineWorkflow({
1057
1641
  from: "triageReview",
1058
1642
  switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
1059
1643
  },
1060
- { from: "addressP2", to: "verifyP2" },
1644
+ {
1645
+ from: "addressP2",
1646
+ switch: {
1647
+ on: "$result.outcome",
1648
+ cases: {
1649
+ ok: "verifyP2",
1650
+ timed_out: "timeoutFallbackGuard",
1651
+ failed: "propagateSupportedFailure",
1652
+ },
1653
+ },
1654
+ },
1061
1655
  {
1062
1656
  from: "verifyP2",
1657
+ switch: {
1658
+ on: "$result.outcome",
1659
+ cases: {
1660
+ ok: "routeVerifyP2Result",
1661
+ timed_out: "timeoutFallbackGuard",
1662
+ failed: "propagateSupportedFailure",
1663
+ },
1664
+ },
1665
+ },
1666
+ {
1667
+ from: "routeVerifyP2Result",
1063
1668
  switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
1064
1669
  },
1065
1670
  {
1066
1671
  from: "inspectComments",
1672
+ switch: {
1673
+ on: "$result.outcome",
1674
+ cases: {
1675
+ ok: "routeInspectCommentsResult",
1676
+ timed_out: "timeoutFallbackGuard",
1677
+ failed: "propagateSupportedFailure",
1678
+ },
1679
+ },
1680
+ },
1681
+ {
1682
+ from: "routeInspectCommentsResult",
1067
1683
  switch: {
1068
1684
  on: "$.route",
1069
1685
  cases: {
@@ -1076,6 +1692,17 @@ export const autoimplementWorkflow = defineWorkflow({
1076
1692
  },
1077
1693
  {
1078
1694
  from: "inspectCi",
1695
+ switch: {
1696
+ on: "$result.outcome",
1697
+ cases: {
1698
+ ok: "routeInspectCiResult",
1699
+ timed_out: "timeoutFallbackGuard",
1700
+ failed: "propagateSupportedFailure",
1701
+ },
1702
+ },
1703
+ },
1704
+ {
1705
+ from: "routeInspectCiResult",
1079
1706
  switch: {
1080
1707
  on: "$.route",
1081
1708
  cases: {
@@ -1088,10 +1715,7 @@ export const autoimplementWorkflow = defineWorkflow({
1088
1715
  },
1089
1716
  {
1090
1717
  from: "trackCi",
1091
- switch: {
1092
- on: "$result.outcome",
1093
- cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
1094
- },
1718
+ switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
1095
1719
  },
1096
1720
  {
1097
1721
  from: "repairCiCommand",
@@ -1112,7 +1736,17 @@ export const autoimplementWorkflow = defineWorkflow({
1112
1736
  },
1113
1737
  },
1114
1738
  },
1115
- { from: "opportunisticTest", to: "inspectCi" },
1739
+ {
1740
+ from: "opportunisticTest",
1741
+ switch: {
1742
+ on: "$result.outcome",
1743
+ cases: {
1744
+ ok: "inspectCi",
1745
+ timed_out: "timeoutFallbackGuard",
1746
+ failed: "propagateSupportedFailure",
1747
+ },
1748
+ },
1749
+ },
1116
1750
  {
1117
1751
  from: "classifyCi",
1118
1752
  switch: {
@@ -1127,6 +1761,17 @@ export const autoimplementWorkflow = defineWorkflow({
1127
1761
  },
1128
1762
  {
1129
1763
  from: "finalizeDelivery",
1764
+ switch: {
1765
+ on: "$result.outcome",
1766
+ cases: {
1767
+ ok: "routeFinalizeDeliveryResult",
1768
+ timed_out: "timeoutFallbackGuard",
1769
+ failed: "propagateSupportedFailure",
1770
+ },
1771
+ },
1772
+ },
1773
+ {
1774
+ from: "routeFinalizeDeliveryResult",
1130
1775
  switch: {
1131
1776
  on: "$.status",
1132
1777
  cases: { completed: "finalize", blocked: "challengeBlockerGuard" },