@revisium/orchestrator 0.2.0-alpha.3 → 0.2.0-alpha.5

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 (323) hide show
  1. package/README.md +4 -4
  2. package/control-plane/bootstrap.config.json +108 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +446 -1123
  4. package/control-plane/default-playbook/catalog/roles.json +1 -1
  5. package/control-plane/default-playbook/catalog/run-profiles.json +338 -0
  6. package/control-plane/default-playbook/package.json +1 -1
  7. package/control-plane/default-playbook/playbook.json +2 -1
  8. package/control-plane/default-playbook/prompts/developer.md +6 -6
  9. package/control-plane/default-playbook/prompts/integrator.md +1 -1
  10. package/dist/__generated__/client/browser.js +18 -0
  11. package/dist/__generated__/client/browser.js.map +1 -0
  12. package/dist/__generated__/client/client.js +32 -0
  13. package/dist/__generated__/client/client.js.map +1 -0
  14. package/dist/__generated__/client/commonInputTypes.js +11 -0
  15. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  16. package/dist/__generated__/client/enums.js +19 -0
  17. package/dist/__generated__/client/enums.js.map +1 -0
  18. package/dist/__generated__/client/internal/class.js +53 -0
  19. package/dist/__generated__/client/internal/class.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespace.js +450 -0
  21. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  22. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +421 -0
  23. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  24. package/dist/__generated__/client/models/Branch.js +2 -0
  25. package/dist/__generated__/client/models/Branch.js.map +1 -0
  26. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  27. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  28. package/dist/__generated__/client/models/FileBlob.js +2 -0
  29. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  30. package/dist/__generated__/client/models/InboxItem.js +2 -0
  31. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  32. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  33. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  34. package/dist/__generated__/client/models/Revision.js +2 -0
  35. package/dist/__generated__/client/models/Revision.js.map +1 -0
  36. package/dist/__generated__/client/models/RevoProject.js +2 -0
  37. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  38. package/dist/__generated__/client/models/Row.js +2 -0
  39. package/dist/__generated__/client/models/Row.js.map +1 -0
  40. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  41. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  42. package/dist/__generated__/client/models/RunEvent.js +2 -0
  43. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  44. package/dist/__generated__/client/models/RunOutput.js +2 -0
  45. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  46. package/dist/__generated__/client/models/RunTask.js +2 -0
  47. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  48. package/dist/__generated__/client/models/Table.js +2 -0
  49. package/dist/__generated__/client/models/Table.js.map +1 -0
  50. package/dist/__generated__/client/models/TableMigration.js +2 -0
  51. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  52. package/dist/__generated__/client/models/TaskRun.js +2 -0
  53. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  54. package/dist/__generated__/client/models.js +2 -0
  55. package/dist/__generated__/client/models.js.map +1 -0
  56. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  57. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  59. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  60. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  61. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  62. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +9 -0
  63. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  64. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js +54 -0
  65. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js.map +1 -0
  66. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js +37 -0
  67. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js.map +1 -0
  68. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js +32 -0
  69. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js.map +1 -0
  70. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js +33 -0
  71. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js.map +1 -0
  72. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js +59 -0
  73. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js.map +1 -0
  74. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js +33 -0
  75. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js.map +1 -0
  76. package/dist/api/graphql-api/method/method.resolver.js +76 -1
  77. package/dist/api/graphql-api/method/method.resolver.js.map +1 -1
  78. package/dist/api/graphql-api/method/model/run-profile-connection.model.js +16 -0
  79. package/dist/api/graphql-api/method/model/run-profile-connection.model.js.map +1 -0
  80. package/dist/api/graphql-api/method/model/run-profile-status.model.js +6 -0
  81. package/dist/api/graphql-api/method/model/run-profile-status.model.js.map +1 -0
  82. package/dist/api/graphql-api/method/model/run-profile.model.js +79 -0
  83. package/dist/api/graphql-api/method/model/run-profile.model.js.map +1 -0
  84. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  85. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  86. package/dist/api/graphql-api/registerGraphqlEnums.js +6 -0
  87. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  88. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -1
  89. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  90. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -1
  91. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  92. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  93. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  94. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  95. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  96. package/dist/cli/commands/doctor-report.js +1 -13
  97. package/dist/cli/commands/doctor-report.js.map +1 -1
  98. package/dist/cli/commands/lifecycle.js +50 -65
  99. package/dist/cli/commands/lifecycle.js.map +1 -1
  100. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  101. package/dist/cli/commands/process-helpers.js.map +1 -0
  102. package/dist/config.js +17 -55
  103. package/dist/config.js.map +1 -1
  104. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  105. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  106. package/dist/control-plane/bootstrap.js +25 -38
  107. package/dist/control-plane/bootstrap.js.map +1 -1
  108. package/dist/control-plane/change-notifications.js +45 -14
  109. package/dist/control-plane/change-notifications.js.map +1 -1
  110. package/dist/control-plane/data-access.js +1 -106
  111. package/dist/control-plane/data-access.js.map +1 -1
  112. package/dist/control-plane/default-playbook-policy.js +488 -94
  113. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  114. package/dist/control-plane/definitions.js +4 -5
  115. package/dist/control-plane/definitions.js.map +1 -1
  116. package/dist/control-plane/engine-transport.js +383 -0
  117. package/dist/control-plane/engine-transport.js.map +1 -0
  118. package/dist/control-plane/inbox.js +15 -7
  119. package/dist/control-plane/inbox.js.map +1 -1
  120. package/dist/control-plane/index.js +0 -1
  121. package/dist/control-plane/index.js.map +1 -1
  122. package/dist/control-plane/json-fields.js +1 -1
  123. package/dist/control-plane/json-fields.js.map +1 -1
  124. package/dist/control-plane/merge-override-audit.js +2 -2
  125. package/dist/control-plane/merge-override-audit.js.map +1 -1
  126. package/dist/control-plane/pipeline-coverage-registry.js +486 -0
  127. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  128. package/dist/control-plane/query-types.js +2 -0
  129. package/dist/control-plane/query-types.js.map +1 -0
  130. package/dist/control-plane/run-profiles.js +175 -0
  131. package/dist/control-plane/run-profiles.js.map +1 -0
  132. package/dist/control-plane/schema-migration.js +0 -35
  133. package/dist/control-plane/schema-migration.js.map +1 -1
  134. package/dist/control-plane/seed-default-playbook.js +2 -31
  135. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  136. package/dist/control-plane/tables.js +8 -0
  137. package/dist/control-plane/tables.js.map +1 -1
  138. package/dist/control-plane/transport.js +16 -0
  139. package/dist/control-plane/transport.js.map +1 -0
  140. package/dist/control-plane/versioned-meaning.js +72 -15
  141. package/dist/control-plane/versioned-meaning.js.map +1 -1
  142. package/dist/e2e/kit/agents.js +63 -8
  143. package/dist/e2e/kit/agents.js.map +1 -1
  144. package/dist/e2e/kit/assertions.js +15 -3
  145. package/dist/e2e/kit/assertions.js.map +1 -1
  146. package/dist/e2e/kit/crash.js +8 -4
  147. package/dist/e2e/kit/crash.js.map +1 -1
  148. package/dist/e2e/kit/drive.js +46 -11
  149. package/dist/e2e/kit/drive.js.map +1 -1
  150. package/dist/e2e/kit/env.js +17 -1
  151. package/dist/e2e/kit/env.js.map +1 -1
  152. package/dist/e2e/kit/fake-integrator.js +9 -22
  153. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  154. package/dist/e2e/kit/gh-emulator.js +166 -26
  155. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  156. package/dist/e2e/kit/harness.js +44 -10
  157. package/dist/e2e/kit/harness.js.map +1 -1
  158. package/dist/e2e/kit/index.js +2 -0
  159. package/dist/e2e/kit/index.js.map +1 -1
  160. package/dist/e2e/kit/run-profiles.js +50 -0
  161. package/dist/e2e/kit/run-profiles.js.map +1 -0
  162. package/dist/e2e/kit/scenario.js +207 -0
  163. package/dist/e2e/kit/scenario.js.map +1 -0
  164. package/dist/e2e/kit/scenarios.js +38 -45
  165. package/dist/e2e/kit/scenarios.js.map +1 -1
  166. package/dist/e2e/recovery-crash-child.js +2 -2
  167. package/dist/e2e/recovery-crash-child.js.map +1 -1
  168. package/dist/e2e/recovery-dd-crash-child.js +8 -6
  169. package/dist/e2e/recovery-dd-crash-child.js.map +1 -1
  170. package/dist/engine/dbos.service.js +8 -1
  171. package/dist/engine/dbos.service.js.map +1 -1
  172. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  173. package/dist/features/method/commands/handlers/method-command.handlers.js +64 -0
  174. package/dist/features/method/commands/handlers/method-command.handlers.js.map +1 -0
  175. package/dist/features/method/commands/impl/create-run-profile.command.js +7 -0
  176. package/dist/features/method/commands/impl/create-run-profile.command.js.map +1 -0
  177. package/dist/features/method/commands/impl/deprecate-run-profile.command.js +7 -0
  178. package/dist/features/method/commands/impl/deprecate-run-profile.command.js.map +1 -0
  179. package/dist/features/method/commands/impl/update-run-profile.command.js +7 -0
  180. package/dist/features/method/commands/impl/update-run-profile.command.js.map +1 -0
  181. package/dist/features/method/commands/index.js +6 -1
  182. package/dist/features/method/commands/index.js.map +1 -1
  183. package/dist/features/method/method-api.service.js +31 -3
  184. package/dist/features/method/method-api.service.js.map +1 -1
  185. package/dist/features/method/queries/handlers/method-query.handlers.js +58 -1
  186. package/dist/features/method/queries/handlers/method-query.handlers.js.map +1 -1
  187. package/dist/features/method/queries/impl/get-run-profile.query.js +7 -0
  188. package/dist/features/method/queries/impl/get-run-profile.query.js.map +1 -0
  189. package/dist/features/method/queries/impl/list-run-profiles.query.js +7 -0
  190. package/dist/features/method/queries/impl/list-run-profiles.query.js.map +1 -0
  191. package/dist/features/method/queries/impl/validate-run-profile.query.js +7 -0
  192. package/dist/features/method/queries/impl/validate-run-profile.query.js.map +1 -0
  193. package/dist/features/method/queries/index.js +4 -1
  194. package/dist/features/method/queries/index.js.map +1 -1
  195. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  196. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  197. package/dist/features/runs/queries/impl/simulate-route.query.js.map +1 -1
  198. package/dist/features/shared/connection.js +6 -3
  199. package/dist/features/shared/connection.js.map +1 -1
  200. package/dist/host/daemon.js +32 -8
  201. package/dist/host/daemon.js.map +1 -1
  202. package/dist/host/ensure-host.js +3 -3
  203. package/dist/host/ensure-host.js.map +1 -1
  204. package/dist/host/host-runtime.js.map +1 -1
  205. package/dist/host/host.lifecycle.js +3 -16
  206. package/dist/host/host.lifecycle.js.map +1 -1
  207. package/dist/http/graphql-host.js +17 -8
  208. package/dist/http/graphql-host.js.map +1 -1
  209. package/dist/mcp/mcp-capabilities.js +8 -2
  210. package/dist/mcp/mcp-capabilities.js.map +1 -1
  211. package/dist/mcp/mcp-facade.service.js +53 -10
  212. package/dist/mcp/mcp-facade.service.js.map +1 -1
  213. package/dist/mcp/mcp-tool-result.js +22 -0
  214. package/dist/mcp/mcp-tool-result.js.map +1 -0
  215. package/dist/mcp/mcp-tools.js +108 -1
  216. package/dist/mcp/mcp-tools.js.map +1 -1
  217. package/dist/pipeline/await-human.js +27 -8
  218. package/dist/pipeline/await-human.js.map +1 -1
  219. package/dist/pipeline/data-driven-task.workflow.js +573 -105
  220. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  221. package/dist/pipeline/pipeline.service.js +28 -12
  222. package/dist/pipeline/pipeline.service.js.map +1 -1
  223. package/dist/pipeline/route-contract.js +80 -39
  224. package/dist/pipeline/route-contract.js.map +1 -1
  225. package/dist/pipeline-core/condition-scopes.js +15 -0
  226. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  227. package/dist/pipeline-core/index.js +2 -0
  228. package/dist/pipeline-core/index.js.map +1 -1
  229. package/dist/pipeline-core/kit/fixtures.js +81 -44
  230. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  231. package/dist/pipeline-core/materialize.js +221 -0
  232. package/dist/pipeline-core/materialize.js.map +1 -0
  233. package/dist/pipeline-core/types.js +5 -0
  234. package/dist/pipeline-core/types.js.map +1 -1
  235. package/dist/pipeline-core/validate-loops.js +2 -15
  236. package/dist/pipeline-core/validate-loops.js.map +1 -1
  237. package/dist/pipeline-core/validate-resilience.js +257 -0
  238. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  239. package/dist/pipeline-core/validate.js +2 -0
  240. package/dist/pipeline-core/validate.js.map +1 -1
  241. package/dist/playbook/catalog-loader.js +40 -3
  242. package/dist/playbook/catalog-loader.js.map +1 -1
  243. package/dist/playbook/catalog-schema-validator.js +468 -0
  244. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  245. package/dist/playbook/import-mapper.js +55 -3
  246. package/dist/playbook/import-mapper.js.map +1 -1
  247. package/dist/playbook/manifest.js +5 -0
  248. package/dist/playbook/manifest.js.map +1 -1
  249. package/dist/playbook/playbook-installer.js +17 -1
  250. package/dist/playbook/playbook-installer.js.map +1 -1
  251. package/dist/poller/pr-readiness-core.js +257 -93
  252. package/dist/poller/pr-readiness-core.js.map +1 -1
  253. package/dist/poller/pr-readiness.js +2 -2
  254. package/dist/poller/pr-readiness.js.map +1 -1
  255. package/dist/revisium/inbox.service.js +8 -8
  256. package/dist/revisium/inbox.service.js.map +1 -1
  257. package/dist/revisium/playbooks.service.js +311 -21
  258. package/dist/revisium/playbooks.service.js.map +1 -1
  259. package/dist/revisium/revisium.module.js +8 -8
  260. package/dist/revisium/revisium.module.js.map +1 -1
  261. package/dist/revisium/roles.service.js +3 -1
  262. package/dist/revisium/roles.service.js.map +1 -1
  263. package/dist/revisium/run.service.js +9 -8
  264. package/dist/revisium/run.service.js.map +1 -1
  265. package/dist/revisium/tokens.js +0 -1
  266. package/dist/revisium/tokens.js.map +1 -1
  267. package/dist/run/create-run.js +1 -4
  268. package/dist/run/create-run.js.map +1 -1
  269. package/dist/run/inspect-run.js +33 -29
  270. package/dist/run/inspect-run.js.map +1 -1
  271. package/dist/run/issue-ref.js +97 -2
  272. package/dist/run/issue-ref.js.map +1 -1
  273. package/dist/run/prisma-runtime-data-access.js +616 -0
  274. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  275. package/dist/run/run-outputs.js +39 -14
  276. package/dist/run/run-outputs.js.map +1 -1
  277. package/dist/run/terminal-run-status.js.map +1 -1
  278. package/dist/runners/gh-identity.js +29 -7
  279. package/dist/runners/gh-identity.js.map +1 -1
  280. package/dist/runners/integrator.js +692 -89
  281. package/dist/runners/integrator.js.map +1 -1
  282. package/dist/runners/worktree.service.js +10 -8
  283. package/dist/runners/worktree.service.js.map +1 -1
  284. package/dist/schema/ajv-errors.js +15 -0
  285. package/dist/schema/ajv-errors.js.map +1 -0
  286. package/dist/smoke/isolation.js +7 -16
  287. package/dist/smoke/isolation.js.map +1 -1
  288. package/dist/storage/ensure-storage.js +118 -0
  289. package/dist/storage/ensure-storage.js.map +1 -0
  290. package/dist/storage/revo-database.js +39 -0
  291. package/dist/storage/revo-database.js.map +1 -0
  292. package/dist/storage/revo-prisma.service.js +31 -0
  293. package/dist/storage/revo-prisma.service.js.map +1 -0
  294. package/dist/storage/revo-storage.module.js +18 -0
  295. package/dist/storage/revo-storage.module.js.map +1 -0
  296. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  297. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  298. package/dist/task-control-plane/run-watch.service.js +3 -6
  299. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  300. package/dist/task-control-plane/task-control-plane-api.service.js +518 -85
  301. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  302. package/dist/testing/runtime-data-access.js +175 -0
  303. package/dist/testing/runtime-data-access.js.map +1 -0
  304. package/dist/worker/build-context.js +178 -2
  305. package/dist/worker/build-context.js.map +1 -1
  306. package/dist/worker/claude-code-runner.js +23 -6
  307. package/dist/worker/claude-code-runner.js.map +1 -1
  308. package/dist/worker/codex-runner.js +6 -1
  309. package/dist/worker/codex-runner.js.map +1 -1
  310. package/package.json +20 -7
  311. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  312. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +234 -0
  313. package/prisma/migrations/migration_lock.toml +3 -0
  314. package/prisma/schema.prisma +338 -0
  315. package/prisma.config.ts +11 -0
  316. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  317. package/dist/control-plane/client-transport.js +0 -168
  318. package/dist/control-plane/client-transport.js.map +0 -1
  319. package/dist/engine/ensure-postgres.js +0 -46
  320. package/dist/engine/ensure-postgres.js.map +0 -1
  321. package/dist/host/ensure-revisium.js +0 -135
  322. package/dist/host/ensure-revisium.js.map +0 -1
  323. package/revisium.config.json +0 -10
@@ -1,14 +1,16 @@
1
1
  import { step as coreStep, initialState, validateTemplate, InterpretError, selectJoinWinner, reduceJoinVerdict, } from '../pipeline-core/index.js';
2
- import { runnerNeedsLivePreflight, runnerUsesRealIntegrator } from './route-contract.js';
2
+ import { resolveLaunchOverrides, runnerNeedsLivePreflight } from './route-contract.js';
3
3
  import { redactEventPayload } from '../run/append-event.js';
4
4
  import { redactSecrets } from '../control-plane/inbox.js';
5
5
  import { fnv1a64Hex } from '../control-plane/steps.js';
6
- import { normalizeIssueRef } from '../run/issue-ref.js';
6
+ import { normalizeIssueAction, normalizeIssueRef } from '../run/issue-ref.js';
7
7
  import { RUNNER_IDLE_TIMEOUT_KIND, RUNNER_WALL_CLOCK_LIMIT_KIND, } from '../worker/process-executor.js';
8
8
  export const RUN_PROGRESS_EVENT_KEY = 'run-progress';
9
+ export const INTEGRATOR_PROGRESS_EVENT_TYPES = ['integrate_succeeded', 'foreign_pr_adopted'];
9
10
  const MAX_STEPS = 1_000;
10
11
  const DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS = 2;
11
12
  const DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS = 2_000;
13
+ const TRANSIENT_RETRY_GATE_OUTCOMES = ['retry', 'give_up'];
12
14
  function readPositiveIntegerEnv(env, key, fallback) {
13
15
  const raw = env[key];
14
16
  if (raw === undefined)
@@ -44,11 +46,12 @@ function physicalAttemptFor(runId, stepKey, attemptNo) {
44
46
  attemptId: `attempt_${fnv1a64Hex(attemptKey)}`,
45
47
  };
46
48
  }
47
- function stepInputForAttempt(nodeId, inputs, attempt) {
49
+ function stepInputForAttempt(nodeId, inputs, attempt, retryContext) {
48
50
  return {
49
51
  nodeId,
50
52
  attempt: { attemptNo: attempt.attemptNo, attemptId: attempt.attemptId },
51
53
  ...(Object.keys(inputs).length > 0 ? { inputs } : {}),
54
+ ...(retryContext ? { retryContext } : {}),
52
55
  };
53
56
  }
54
57
  function optionalTiming(timing) {
@@ -97,6 +100,19 @@ function nextOrdinal(byNode, nodeId) {
97
100
  function isRecord(value) {
98
101
  return value !== null && typeof value === 'object' && !Array.isArray(value);
99
102
  }
103
+ function transientRetryGateOutcome(decision) {
104
+ const answer = isRecord(decision.answer) ? decision.answer : {};
105
+ let outcome = '';
106
+ if (typeof decision.outcome === 'string') {
107
+ outcome = decision.outcome;
108
+ }
109
+ else if (typeof answer.outcome === 'string') {
110
+ outcome = answer.outcome;
111
+ }
112
+ if (outcome === 'retry')
113
+ return 'retry';
114
+ return 'give_up';
115
+ }
100
116
  function producedChangeArtifact(value) {
101
117
  if (!isRecord(value))
102
118
  return undefined;
@@ -108,10 +124,12 @@ function producedChangeArtifact(value) {
108
124
  if (typeof headSha !== 'string' || headSha.trim().length === 0)
109
125
  return undefined;
110
126
  const issueRef = normalizeArtifactIssueRef(candidate.issueRef);
127
+ const issueAction = normalizeArtifactIssueAction(candidate.issueAction);
111
128
  return {
112
129
  branch,
113
130
  headSha,
114
131
  ...(issueRef ? { issueRef } : {}),
132
+ ...(issueAction ? { issueAction } : {}),
115
133
  ...(typeof candidate.worktreePath === 'string' && candidate.worktreePath.trim() ? { worktreePath: candidate.worktreePath } : {}),
116
134
  ...(typeof candidate.artifactRef === 'string' && candidate.artifactRef.trim() ? { artifactRef: candidate.artifactRef } : {}),
117
135
  ...(typeof candidate.prNumber === 'number' && Number.isSafeInteger(candidate.prNumber) ? { prNumber: candidate.prNumber } : {}),
@@ -125,7 +143,15 @@ function normalizeArtifactIssueRef(value) {
125
143
  return undefined;
126
144
  }
127
145
  }
128
- function changeWithRunIssueRef(change, issueRef) {
146
+ function normalizeArtifactIssueAction(value) {
147
+ try {
148
+ return normalizeIssueAction(value, 'change.issueAction');
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ function changeWithRunIssueContract(change, issueRef, issueAction) {
129
155
  const normalized = { ...change };
130
156
  if (issueRef) {
131
157
  normalized.issueRef = issueRef;
@@ -133,6 +159,12 @@ function changeWithRunIssueRef(change, issueRef) {
133
159
  else {
134
160
  delete normalized.issueRef;
135
161
  }
162
+ if (issueAction) {
163
+ normalized.issueAction = issueAction;
164
+ }
165
+ else {
166
+ delete normalized.issueAction;
167
+ }
136
168
  return normalized;
137
169
  }
138
170
  function producedChangeFromInputs(inputs) {
@@ -150,7 +182,16 @@ function mergeReadinessFromInputs(inputs) {
150
182
  const headSha = value.headSha;
151
183
  if (typeof headSha !== 'string' || headSha.trim().length === 0)
152
184
  return undefined;
153
- return { headSha };
185
+ const override = isRecord(value.override) && value.override.accepted === true
186
+ ? { accepted: true }
187
+ : undefined;
188
+ return { headSha, ...(override ? { override } : {}) };
189
+ }
190
+ function headShaFromPayload(value) {
191
+ if (!isRecord(value))
192
+ return undefined;
193
+ const headSha = value.headSha;
194
+ return typeof headSha === 'string' && headSha.trim().length > 0 ? headSha.trim() : undefined;
154
195
  }
155
196
  function attachProducedChange(output, change) {
156
197
  if (isRecord(output))
@@ -162,9 +203,27 @@ function nodeProducesChange(node) {
162
203
  node.produces?.name === 'change' &&
163
204
  node.resultSchema === 'schema:change';
164
205
  }
206
+ function recoveryContextAfterNode(nodeId, recoveryContext) {
207
+ if (!recoveryContext)
208
+ return undefined;
209
+ return nodeId === 'classifyRecovery' ? recoveryContext : null;
210
+ }
165
211
  function runnerProducesWorktreeChanges(runnerId) {
166
212
  return runnerId === 'claude-code' || runnerId === 'codex';
167
213
  }
214
+ const LIVE_WORKTREE_SCRIPT_REFS = new Set([
215
+ 'script:integrator',
216
+ 'script:confirmMerge',
217
+ 'script:pollPr',
218
+ 'script:overrideMerge',
219
+ 'script:respondThreads',
220
+ ]);
221
+ function scriptRequiresLiveWorktree(scriptRef) {
222
+ return LIVE_WORKTREE_SCRIPT_REFS.has(scriptRef);
223
+ }
224
+ function templateRequiresLiveWorktree(template) {
225
+ return Object.values(template.nodes).some((node) => node.kind === 'script' && scriptRequiresLiveWorktree(node.scriptRef));
226
+ }
168
227
  function artifactRefFromResult(result) {
169
228
  const artifacts = result.artifacts;
170
229
  const processArtifact = isRecord(artifacts) && isRecord(artifacts.process) ? artifacts.process : artifacts;
@@ -199,6 +258,8 @@ function transientKind(reason) {
199
258
  return 'timeout';
200
259
  if (/\b429\b|rate.?limit|session limit/i.test(reason))
201
260
  return 'rate_limit';
261
+ if (/\b529\b|overloaded/i.test(reason))
262
+ return 'overloaded';
202
263
  if (isLegacyRetryableCrashReason(reason))
203
264
  return 'crash';
204
265
  return 'unknown';
@@ -275,16 +336,71 @@ function gateVerdict(decision, outcomes) {
275
336
  return outcomes[0];
276
337
  return outcomes.length > 1 ? outcomes.at(-1) : undefined;
277
338
  }
278
- function gateResolutionOutput(decision, verdict, fallbackInboxId) {
339
+ function gateResolutionMetadata(decision, fallbackInboxId) {
340
+ const decisionRecord = isRecord(decision) ? decision : {};
341
+ const answer = isRecord(decision.answer) ? decision.answer : {};
342
+ const note = decision.note ?? (typeof answer.note === 'string' ? answer.note : undefined);
343
+ const resolvedBy = decision.resolvedBy ?? (typeof answer.resolvedBy === 'string' ? answer.resolvedBy : '');
344
+ const resolvedAt = decision.resolvedAt ?? (typeof answer.resolvedAt === 'string' ? answer.resolvedAt : '');
345
+ const inboxId = decision.inboxId ?? (typeof answer.inboxId === 'string' ? answer.inboxId : fallbackInboxId);
346
+ const mergeOverrideAudit = answer.mergeOverrideAudit ?? decisionRecord['mergeOverrideAudit'];
347
+ const adoptionAudit = answer.adoptionAudit ?? decisionRecord['adoptionAudit'];
348
+ return {
349
+ ...(note !== undefined ? { note } : {}),
350
+ resolvedBy,
351
+ resolvedAt,
352
+ ...(inboxId !== undefined ? { inboxId } : {}),
353
+ ...(mergeOverrideAudit !== undefined ? { mergeOverrideAudit } : {}),
354
+ ...(adoptionAudit !== undefined ? { adoptionAudit } : {}),
355
+ };
356
+ }
357
+ function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHeadSha) {
358
+ const resolution = gateResolutionMetadata(decision, fallbackInboxId);
279
359
  return {
280
360
  outcome: decision.outcome ?? verdict ?? decision.decision,
281
- ...(decision.note !== undefined ? { note: decision.note } : {}),
282
- resolvedBy: decision.resolvedBy ?? '',
283
- resolvedAt: decision.resolvedAt ?? '',
284
- inboxId: decision.inboxId ?? fallbackInboxId,
361
+ ...(resolution.note !== undefined ? { note: resolution.note } : {}),
362
+ resolvedBy: resolution.resolvedBy,
363
+ resolvedAt: resolution.resolvedAt,
364
+ inboxId: resolution.inboxId ?? fallbackInboxId,
365
+ ...(trustedGateHeadSha !== undefined ? { trustedGateHeadSha } : {}),
366
+ ...(resolution.mergeOverrideAudit !== undefined ? { mergeOverrideAudit: resolution.mergeOverrideAudit } : {}),
367
+ ...(resolution.adoptionAudit !== undefined ? { adoptionAudit: resolution.adoptionAudit } : {}),
285
368
  ...(decision.decision ? { decision: decision.decision } : {}),
286
369
  };
287
370
  }
371
+ function nonEmptyString(value) {
372
+ if (typeof value !== 'string')
373
+ return undefined;
374
+ const trimmed = value.trim();
375
+ return trimmed.length > 0 ? trimmed : undefined;
376
+ }
377
+ function isSyntheticGateTimeoutFallback(decision) {
378
+ const answer = isRecord(decision.answer) ? decision.answer : {};
379
+ return decision.decision === 'reject' && answer.reason === 'gate-timeout';
380
+ }
381
+ function agentQuestionRetryContext(decision, nodeId, safeLesson) {
382
+ if (isSyntheticGateTimeoutFallback(decision))
383
+ return undefined;
384
+ const resolution = gateResolutionMetadata(decision);
385
+ const inboxId = nonEmptyString(resolution.inboxId);
386
+ const resolvedBy = nonEmptyString(resolution.resolvedBy);
387
+ if (!inboxId || !resolvedBy)
388
+ return undefined;
389
+ return {
390
+ kind: 'agent_question',
391
+ nodeId,
392
+ answer: decision.answer,
393
+ lesson: safeLesson,
394
+ inboxId,
395
+ resolvedBy,
396
+ };
397
+ }
398
+ function unresolvedAgentQuestionLesson(nodeId, safeLesson, decision) {
399
+ const reason = isSyntheticGateTimeoutFallback(decision)
400
+ ? 'agent question timed out'
401
+ : 'agent question resolution is missing inboxId or resolvedBy';
402
+ return `${reason} for ${nodeId}; original question: ${safeLesson}`;
403
+ }
288
404
  function gateTopicFor(reason) {
289
405
  if (/merge/i.test(reason))
290
406
  return 'merge';
@@ -317,9 +433,29 @@ function gateArtifactView(row, as) {
317
433
  payloadRef: `attempt:${row.attemptId ?? ''}`,
318
434
  };
319
435
  }
320
- export function buildGateSummary(decision, outputsByNode, lastVerdict) {
436
+ function freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput) {
437
+ if (decision.nodeId !== 'mergeGate')
438
+ return undefined;
439
+ if (lastProducedOutput?.nodeId === 'mergeRecheck' && lastVerdict === 'clean')
440
+ return lastProducedOutput;
441
+ if (lastProducedOutput?.nodeId === 'mergeApproveReverify' && lastVerdict === 'recheck')
442
+ return lastProducedOutput;
443
+ return undefined;
444
+ }
445
+ function latestBlockedScriptArtifact(decision, recoveryContext) {
446
+ if (decision.nodeId !== 'recoveryGate')
447
+ return undefined;
448
+ const payload = recoveryContext?.payload;
449
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
450
+ return undefined;
451
+ const record = payload;
452
+ return typeof record.lesson === 'string' && typeof record.reason === 'string' ? recoveryContext : undefined;
453
+ }
454
+ export function buildGateSummary(decision, outputsByNode, lastVerdict, lastProducedOutput, recoveryContext) {
321
455
  const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
322
- const artRow = resolveGateRow(decision.gatedArtifact, outputsByNode);
456
+ const artRow = latestBlockedScriptArtifact(decision, recoveryContext)
457
+ ?? freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput)
458
+ ?? resolveGateRow(decision.gatedArtifact, outputsByNode);
323
459
  if (artRow)
324
460
  summary.gatedArtifact = gateArtifactView(artRow, decision.gatedArtifact?.as);
325
461
  const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
@@ -329,48 +465,133 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
329
465
  summary.reviewerVerdict = { verdict: lastVerdict };
330
466
  return summary;
331
467
  }
468
+ function approvedMergeGateHeadSha(decision, verdict, summary) {
469
+ if (decision.nodeId !== 'mergeGate')
470
+ return undefined;
471
+ if (verdict === 'approved' || verdict === 'override_merge') {
472
+ return headShaFromPayload(summary.gatedArtifact?.payload) ?? null;
473
+ }
474
+ return null;
475
+ }
476
+ function approvedHeadMove(decision, approvedHeadSha, pointer, verdict) {
477
+ if (decision.nodeId !== 'mergeApproveReverify' || verdict !== 'clean' || approvedHeadSha === undefined)
478
+ return undefined;
479
+ const reverifyHeadSha = headShaFromPayload(pointer);
480
+ if (reverifyHeadSha === undefined || reverifyHeadSha === approvedHeadSha)
481
+ return undefined;
482
+ return { approvedHeadSha, reverifyHeadSha };
483
+ }
484
+ function reapprovalRequiredFeedback(pointer, approvedHeadSha, reverifyHeadSha) {
485
+ if (!isRecord(pointer))
486
+ return pointer;
487
+ const evidence = Array.isArray(pointer.evidence)
488
+ ? pointer.evidence.filter((item) => typeof item === 'string')
489
+ : [];
490
+ return {
491
+ ...pointer,
492
+ verdict: 'recheck',
493
+ evidence: [
494
+ ...evidence,
495
+ `approved mergeGate headSha=${approvedHeadSha}; reverify headSha=${reverifyHeadSha}; reapproval required`,
496
+ ],
497
+ };
498
+ }
499
+ function reopenNodeState(state, nodeId) {
500
+ return { ...state, activeNodeIds: new Set([nodeId]), status: 'running' };
501
+ }
502
+ function integratorResultPointer(result) {
503
+ return {
504
+ prUrl: result.prUrl,
505
+ branch: result.branch,
506
+ prNumber: result.prNumber,
507
+ headSha: result.headSha,
508
+ status: result.status,
509
+ ...(result.issueRef ? { issueRef: result.issueRef } : {}),
510
+ ...(result.foreignPr ? { foreignPr: true } : {}),
511
+ ...(result.prAuthor ? { prAuthor: result.prAuthor } : {}),
512
+ ...(result.integratorAccount ? { integratorAccount: result.integratorAccount } : {}),
513
+ };
514
+ }
515
+ function integratorProgressEventType(result) {
516
+ return result.foreignPr ? 'foreign_pr_adopted' : 'integrate_succeeded';
517
+ }
518
+ function mergeOverrideEventPayload(result) {
519
+ const audit = result.override.audit;
520
+ return {
521
+ actor: result.override.actor,
522
+ note: result.override.note,
523
+ reason: audit?.reason ?? result.override.reason ?? '',
524
+ risk: audit?.risk ?? '',
525
+ verificationResponsibility: audit?.verificationResponsibility ?? '',
526
+ headSha: audit?.headSha ?? result.headSha,
527
+ freshHeadSha: result.headSha,
528
+ prNumber: result.prNumber,
529
+ source: result.override.source,
530
+ overriddenFacts: result.override.facts,
531
+ replied: result.override.replied,
532
+ resolved: result.override.resolved,
533
+ ...(result.override.reason ? { refusalReason: result.override.reason } : {}),
534
+ };
535
+ }
536
+ function scriptGithubAccount(decision, launchBindings) {
537
+ if (!launchBindings)
538
+ return undefined;
539
+ for (let index = launchBindings.length - 1; index >= 0; index -= 1) {
540
+ const binding = launchBindings[index];
541
+ if (binding.match.nodeId === decision.nodeId)
542
+ return binding.accounts?.github;
543
+ }
544
+ return undefined;
545
+ }
332
546
  export function buildSystemScriptRegistry(deps) {
333
- const { appendEvent, releaseWorktreeFn, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub } = deps;
334
- function buildIntegratorInput(runId, ctx, inputs) {
335
- const { taskId, title, base, issueRef } = ctx;
547
+ const { appendEvent, releaseWorktreeFn, integrateFn, confirmMergeFn, pollPrFn, overrideMergeFn, respondThreadsFn, } = deps;
548
+ function buildIntegratorInput(runId, ctx, inputs, githubAccount) {
549
+ const { taskId, title, base, issueRef, issueAction } = ctx;
336
550
  const change = producedChangeFromInputs(inputs);
337
551
  const mergeReadiness = mergeReadinessFromInputs(inputs);
338
- const changeForIntegrator = change ? changeWithRunIssueRef(change, issueRef) : undefined;
552
+ const changeForIntegrator = change ? changeWithRunIssueContract(change, issueRef, issueAction) : undefined;
339
553
  return {
340
554
  runId,
341
555
  taskId,
342
556
  title,
343
557
  base,
558
+ ...(githubAccount ? { githubAccount } : {}),
344
559
  ...(issueRef ? { issueRef } : {}),
560
+ ...(issueAction ? { issueAction } : {}),
345
561
  ...(changeForIntegrator ? { change: changeForIntegrator } : {}),
346
562
  ...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
563
+ ...(inputs.gateResolution === undefined ? {} : { gateResolution: inputs.gateResolution }),
347
564
  ...(mergeReadiness ? { mergeReadiness } : {}),
348
565
  };
349
566
  }
350
567
  function makeIntegratorScript(desc) {
351
- return async ({ runId, decision, ctx, bindingByRef, stepKey, inputs }) => {
352
- const integratorInput = buildIntegratorInput(runId, ctx, inputs);
353
- const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
354
- const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
568
+ return async ({ runId, decision, ctx, launchBindings, stepKey, inputs }) => {
569
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs, scriptGithubAccount(decision, launchBindings));
355
570
  let result;
356
571
  try {
357
- result = useReal ? await desc.real(integratorInput) : desc.stub(integratorInput);
572
+ result = await desc.real(integratorInput);
358
573
  }
359
574
  catch (err) {
575
+ const reason = err instanceof Error ? err.message : String(err);
360
576
  await appendEvent({
361
577
  runId, taskId: ctx.taskId, stepId: '', stepKey,
362
578
  type: 'step_failed',
363
- payload: { scriptRef: decision.scriptRef, error: err instanceof Error ? err.message : String(err) },
579
+ payload: { scriptRef: decision.scriptRef, error: reason },
364
580
  });
365
- return { outcome: 'failed' };
581
+ return { outcome: 'failed', reason };
366
582
  }
367
583
  if ('needsHuman' in result) {
584
+ const pointer = {
585
+ reason: desc.blockedReason,
586
+ lesson: result.lesson,
587
+ nodeId: decision.nodeId,
588
+ };
368
589
  await appendEvent({
369
590
  runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
370
591
  type: 'pipeline_blocked',
371
- payload: { reason: desc.blockedReason, lesson: result.lesson, nodeId: decision.nodeId },
592
+ payload: pointer,
372
593
  });
373
- return { outcome: 'blocked' };
594
+ return { outcome: 'blocked', pointer };
374
595
  }
375
596
  const { eventType, payload, pointer, verdict } = desc.mapSuccess(result);
376
597
  await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: eventType, payload });
@@ -378,40 +599,52 @@ export function buildSystemScriptRegistry(deps) {
378
599
  };
379
600
  }
380
601
  const cleanupWorktree = async ({ runId, decision, ctx, stepKey }) => {
602
+ let releaseResult;
381
603
  try {
382
- await releaseWorktreeFn(runId, ctx.taskId);
604
+ releaseResult = await releaseWorktreeFn(runId, ctx.taskId);
605
+ }
606
+ catch (err) {
607
+ const error = err instanceof Error ? err.message : String(err);
608
+ await appendEvent({
609
+ runId,
610
+ taskId: ctx.taskId,
611
+ stepId: '',
612
+ stepKey,
613
+ type: 'cleanup_failed',
614
+ payload: { nodeId: decision.nodeId, error, released: false },
615
+ });
616
+ return { outcome: 'ok', pointer: { released: false, error } };
617
+ }
618
+ if (!releaseResult.released) {
619
+ const pointer = {
620
+ released: false,
621
+ reason: releaseResult.reason,
622
+ worktreePath: releaseResult.worktreePath,
623
+ };
624
+ await appendEvent({
625
+ runId,
626
+ taskId: ctx.taskId,
627
+ stepId: '',
628
+ stepKey,
629
+ type: 'cleanup_failed',
630
+ payload: { nodeId: decision.nodeId, ...pointer },
631
+ });
632
+ return { outcome: 'ok', pointer };
383
633
  }
384
- catch { /* best-effort */ }
385
634
  await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
386
635
  return { outcome: 'ok', pointer: { released: true } };
387
636
  };
388
637
  const integratorScript = makeIntegratorScript({
389
638
  real: integrateFn,
390
- stub: runStub,
391
639
  blockedReason: 'integrate',
392
640
  mapSuccess: (result) => ({
393
- eventType: 'integrate_succeeded',
394
- payload: {
395
- prUrl: result.prUrl,
396
- branch: result.branch,
397
- prNumber: result.prNumber,
398
- headSha: result.headSha,
399
- status: result.status,
400
- ...(result.issueRef ? { issueRef: result.issueRef } : {}),
401
- },
402
- pointer: {
403
- prUrl: result.prUrl,
404
- branch: result.branch,
405
- prNumber: result.prNumber,
406
- headSha: result.headSha,
407
- status: result.status,
408
- ...(result.issueRef ? { issueRef: result.issueRef } : {}),
409
- },
641
+ eventType: integratorProgressEventType(result),
642
+ payload: integratorResultPointer(result),
643
+ pointer: integratorResultPointer(result),
410
644
  }),
411
645
  });
412
646
  const confirmMergeScript = makeIntegratorScript({
413
647
  real: confirmMergeFn,
414
- stub: runConfirmStub,
415
648
  blockedReason: 'confirm-merge',
416
649
  mapSuccess: (result) => ({
417
650
  eventType: 'merge_confirmed',
@@ -430,7 +663,6 @@ export function buildSystemScriptRegistry(deps) {
430
663
  });
431
664
  const pollPrScript = makeIntegratorScript({
432
665
  real: pollPrFn,
433
- stub: runPollStub,
434
666
  blockedReason: 'poll-pr',
435
667
  mapSuccess: (result) => ({
436
668
  eventType: 'pr_polled',
@@ -447,9 +679,54 @@ export function buildSystemScriptRegistry(deps) {
447
679
  verdict: result.verdict,
448
680
  }),
449
681
  });
682
+ const overrideMergeScript = async ({ runId, decision, ctx, launchBindings, stepKey, inputs }) => {
683
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs, scriptGithubAccount(decision, launchBindings));
684
+ let result;
685
+ try {
686
+ result = await overrideMergeFn(integratorInput);
687
+ }
688
+ catch (err) {
689
+ const reason = err instanceof Error ? err.message : String(err);
690
+ await appendEvent({
691
+ runId, taskId: ctx.taskId, stepId: '', stepKey,
692
+ type: 'step_failed',
693
+ payload: { scriptRef: decision.scriptRef, error: reason },
694
+ });
695
+ return { outcome: 'failed', reason };
696
+ }
697
+ if ('needsHuman' in result) {
698
+ const pointer = { reason: 'override-merge', lesson: result.lesson, nodeId: decision.nodeId };
699
+ await appendEvent({
700
+ runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
701
+ type: 'pipeline_blocked',
702
+ payload: pointer,
703
+ });
704
+ return { outcome: 'blocked', pointer };
705
+ }
706
+ if (result.override.replied > 0 || result.override.resolved > 0) {
707
+ await appendEvent({
708
+ runId,
709
+ taskId: ctx.taskId,
710
+ stepId: '',
711
+ stepKey,
712
+ type: 'threads_responded',
713
+ payload: { replied: result.override.replied, resolved: result.override.resolved },
714
+ });
715
+ }
716
+ if (result.override.accepted || result.verdict !== 'merged') {
717
+ await appendEvent({
718
+ runId,
719
+ taskId: ctx.taskId,
720
+ stepId: '',
721
+ stepKey,
722
+ type: result.override.accepted ? 'merge_overridden' : 'merge_override_refused',
723
+ payload: mergeOverrideEventPayload(result),
724
+ });
725
+ }
726
+ return { outcome: 'ok', pointer: result, verdict: result.verdict };
727
+ };
450
728
  const respondThreadsScript = makeIntegratorScript({
451
729
  real: respondThreadsFn,
452
- stub: runRespondStub,
453
730
  blockedReason: 'respond-threads',
454
731
  mapSuccess: (result) => ({
455
732
  eventType: 'threads_responded',
@@ -461,8 +738,8 @@ export function buildSystemScriptRegistry(deps) {
461
738
  ['script:cleanupWorktree', cleanupWorktree],
462
739
  ['script:confirmMerge', confirmMergeScript],
463
740
  ['script:pollPr', pollPrScript],
741
+ ['script:overrideMerge', overrideMergeScript],
464
742
  ['script:respondThreads', respondThreadsScript],
465
- // Unknown refs fall through to this entry, matching the original else-branch behavior.
466
743
  ['script:integrator', integratorScript],
467
744
  ]);
468
745
  }
@@ -501,7 +778,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
501
778
  }
502
779
  async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
503
780
  if (!('produces' in node) || !node.produces)
504
- return;
781
+ return undefined;
505
782
  const row = {
506
783
  runId,
507
784
  nodeId: node.id,
@@ -515,6 +792,23 @@ export function makeDataDrivenTask(runStepFn, deps) {
515
792
  list.push(row);
516
793
  outputsByNode.set(node.id, list);
517
794
  await appendRunOutput(row);
795
+ return row;
796
+ }
797
+ async function recordRecoveryContext(runId, node, ordinal, attemptId, payload, outputsByNode) {
798
+ const row = {
799
+ runId,
800
+ nodeId: node.id,
801
+ ordinal,
802
+ name: 'recoveryContext',
803
+ schemaRef: 'schema:recoveryContext',
804
+ payload,
805
+ attemptId,
806
+ };
807
+ const list = outputsByNode.get(node.id) ?? [];
808
+ list.push(row);
809
+ outputsByNode.set(node.id, list);
810
+ await appendRunOutput(row);
811
+ return row;
518
812
  }
519
813
  return async function dataDrivenTaskImpl(runId, opts) {
520
814
  try {
@@ -537,8 +831,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
537
831
  if (diagnostics.length > 0) {
538
832
  throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
539
833
  }
540
- const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
541
- const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
834
+ const { taskId, title, base, issueRef, issueAction } = await loadRunTaskContext(runId);
835
+ const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId)) ||
836
+ templateRequiresLiveWorktree(template);
542
837
  if (live) {
543
838
  const pf = await preflightFn(taskId, base);
544
839
  if ('needsHuman' in pf) {
@@ -548,27 +843,27 @@ export function makeDataDrivenTask(runStepFn, deps) {
548
843
  if (live) {
549
844
  await createWorktreeFn(runId, taskId, title, base, issueRef);
550
845
  }
551
- return runGraph(runId, opts, taskId, title, base, issueRef, live);
846
+ return runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live);
552
847
  }
553
- async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
848
+ async function runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live) {
554
849
  const { route, template, runnerRetryPolicy } = opts;
555
850
  const bindingByRef = new Map();
556
851
  for (const binding of route.roleBindings) {
557
852
  bindingByRef.set(`role:${binding.roleId}`, binding);
558
- bindingByRef.set(`script:${binding.roleId}`, binding);
559
853
  bindingByRef.set(binding.roleId, binding);
560
- if (runnerUsesRealIntegrator(binding.resolvedRunnerId) && !bindingByRef.has('script:integrator')) {
561
- bindingByRef.set('script:integrator', binding);
562
- }
563
854
  }
564
- const executionProfile = route.executionProfile;
855
+ const launchBindings = route.launchBindings ?? [];
565
856
  let state = initialState(template);
566
857
  let lastResult;
567
858
  let lastVerdict = '';
859
+ let lastProducedOutput;
860
+ let recoveryContext;
568
861
  let lastFailureReason = '';
862
+ let approvedMergeGateHeadShaValue;
569
863
  let stepCount = 0;
570
864
  const effectOrdinalByNode = new Map();
571
865
  const outputsByNode = new Map();
866
+ const agentQuestionRetryContextByNode = new Map();
572
867
  for (let i = 0; i < MAX_STEPS; i++) {
573
868
  const { state: nextState, decision } = coreStep(template, state, lastResult);
574
869
  state = nextState;
@@ -577,10 +872,13 @@ export function makeDataDrivenTask(runStepFn, deps) {
577
872
  return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
578
873
  }
579
874
  const eff = await applyDecision(decision, {
580
- runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef,
581
- effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
875
+ runId, template, state, bindingByRef, launchBindings, taskId, title, base, issueRef, issueAction,
876
+ effectOrdinalByNode, outputsByNode, runnerRetryPolicy, agentQuestionRetryContextByNode,
582
877
  live,
583
878
  lastVerdict,
879
+ lastProducedOutput,
880
+ recoveryContext,
881
+ approvedMergeGateHeadSha: approvedMergeGateHeadShaValue,
584
882
  });
585
883
  stepCount += eff.stepDelta;
586
884
  if (eff.terminal) {
@@ -591,12 +889,30 @@ export function makeDataDrivenTask(runStepFn, deps) {
591
889
  await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
592
890
  }
593
891
  lastResult = eff.lastResult;
594
- if (eff.lastVerdict !== undefined)
595
- lastVerdict = eff.lastVerdict;
892
+ lastVerdict = eff.lastVerdict ?? lastVerdict;
893
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
894
+ if ('recoveryContext' in eff)
895
+ recoveryContext = eff.recoveryContext ?? undefined;
596
896
  lastFailureReason = eff.failureReason ?? '';
897
+ if (eff.approvedMergeGateHeadSha !== undefined) {
898
+ approvedMergeGateHeadShaValue = eff.approvedMergeGateHeadSha ?? undefined;
899
+ }
597
900
  }
598
901
  throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
599
902
  }
903
+ function forkBranchArrival(decision, branch, seq, status, lastVerdict) {
904
+ if (status !== 'succeeded') {
905
+ throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${status} before join ${decision.joinId}`);
906
+ }
907
+ return {
908
+ branchId: branch.id,
909
+ seq,
910
+ ...(lastVerdict ? { verdict: lastVerdict } : {}),
911
+ };
912
+ }
913
+ function recoveryContextFromEffect(current, effect) {
914
+ return 'recoveryContext' in effect ? effect.recoveryContext ?? undefined : current;
915
+ }
600
916
  function branchTemplateForJoin(template, joinId) {
601
917
  const join = resolveNode(template, joinId);
602
918
  if (join.kind !== 'join')
@@ -648,20 +964,15 @@ export function makeDataDrivenTask(runStepFn, deps) {
648
964
  };
649
965
  let lastResult;
650
966
  let lastVerdict = '';
967
+ let lastProducedOutput;
968
+ let recoveryContext;
651
969
  let stepDelta = 0;
652
970
  for (let i = 0; i < MAX_STEPS; i++) {
653
971
  const next = coreStep(branchTemplate, state, lastResult);
654
972
  state = next.state;
655
973
  if (next.decision.type === 'complete') {
656
- if (next.decision.status !== 'succeeded') {
657
- throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${next.decision.status} before join ${decision.joinId}`);
658
- }
659
974
  return {
660
- arrival: {
661
- branchId: branch.id,
662
- seq,
663
- ...(lastVerdict ? { verdict: lastVerdict } : {}),
664
- },
975
+ arrival: forkBranchArrival(decision, branch, seq, next.decision.status, lastVerdict),
665
976
  stepDelta,
666
977
  };
667
978
  }
@@ -670,15 +981,17 @@ export function makeDataDrivenTask(runStepFn, deps) {
670
981
  template: branchTemplate,
671
982
  state,
672
983
  lastVerdict,
984
+ lastProducedOutput,
985
+ recoveryContext,
673
986
  });
674
987
  stepDelta += eff.stepDelta;
675
988
  if (eff.terminal)
676
989
  return { terminal: eff.terminal, stepDelta };
677
- if (eff.stateOverride)
678
- state = eff.stateOverride;
990
+ state = eff.stateOverride ?? state;
679
991
  lastResult = eff.lastResult;
680
- if (eff.lastVerdict !== undefined)
681
- lastVerdict = eff.lastVerdict;
992
+ lastVerdict = eff.lastVerdict ?? lastVerdict;
993
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
994
+ recoveryContext = recoveryContextFromEffect(recoveryContext, eff);
682
995
  }
683
996
  throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
684
997
  }
@@ -691,15 +1004,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
691
1004
  const stepKey = stepKeyFor(node.id, ordinal);
692
1005
  const resolved = resolveConsumes(node, ctx.outputsByNode);
693
1006
  if ('missing' in resolved) {
1007
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
694
1008
  await appendEvent({
695
1009
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
696
- payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
1010
+ payload: { nodeId: node.id, error: reason },
697
1011
  });
698
- return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
1012
+ return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
699
1013
  }
700
1014
  const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
1015
+ if ('question' in result) {
1016
+ ctx.agentQuestionRetryContextByNode.set(node.id, result.retryContext);
1017
+ return {
1018
+ lastResult: undefined,
1019
+ stateOverride: reopenNodeState(ctx.state, node.id),
1020
+ stepDelta: result.attemptsMade,
1021
+ };
1022
+ }
701
1023
  if ('blocked' in result) {
702
1024
  if (result.recovery) {
1025
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
703
1026
  const recovery = await awaitVerificationRecoveryGate(runId, ctx, result.recovery);
704
1027
  return {
705
1028
  lastResult: undefined,
@@ -711,6 +1034,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
711
1034
  stepDelta: result.attemptsMade,
712
1035
  };
713
1036
  }
1037
+ if (result.retry?.attemptsExhausted) {
1038
+ const retryOutcome = await awaitTransientRetryGate(runId, ctx, {
1039
+ node,
1040
+ stepKey,
1041
+ binding: resolveRoleBinding(ctx, decision),
1042
+ retry: result.retry,
1043
+ });
1044
+ if (retryOutcome === 'retry') {
1045
+ return {
1046
+ lastResult: undefined,
1047
+ stateOverride: reopenNodeState(ctx.state, node.id),
1048
+ stepDelta: result.attemptsMade,
1049
+ };
1050
+ }
1051
+ }
1052
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
714
1053
  return {
715
1054
  lastResult: undefined,
716
1055
  terminal: {
@@ -723,18 +1062,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
723
1062
  };
724
1063
  }
725
1064
  if (result.failed) {
1065
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
726
1066
  await appendEvent({
727
1067
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
728
1068
  idempotencyKey: result.attemptId,
729
- payload: { nodeId: node.id, error: result.errorCode },
1069
+ payload: { nodeId: node.id, error: result.errorCode, reason: result.reason },
730
1070
  });
731
1071
  return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
732
1072
  }
733
- await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
1073
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
1074
+ const producedOutput = await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
734
1075
  const verdict = result.verdict;
735
1076
  return {
736
1077
  lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
737
1078
  ...(verdict ? { lastVerdict: verdict } : {}),
1079
+ ...(producedOutput ? { producedOutput } : {}),
1080
+ ...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
738
1081
  stepDelta: result.attemptsMade,
739
1082
  };
740
1083
  }
@@ -743,30 +1086,64 @@ export function makeDataDrivenTask(runStepFn, deps) {
743
1086
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, node.id);
744
1087
  const resolved = resolveConsumes(node, ctx.outputsByNode);
745
1088
  if ('missing' in resolved) {
1089
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
746
1090
  await appendEvent({
747
1091
  runId, taskId, stepId: '', stepKey: stepKeyFor(node.id, ordinal), type: 'step_failed',
748
- payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
1092
+ payload: { nodeId: node.id, error: reason },
749
1093
  });
750
- return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
1094
+ return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
751
1095
  }
752
- const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
1096
+ const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef, issueAction: ctx.issueAction }, bindingByRef, ctx.launchBindings, stepKeyFor(node.id, ordinal), resolved.inputs);
753
1097
  if (scriptResult.outcome === 'blocked') {
754
- return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
1098
+ const recoveryContext = scriptResult.pointer === undefined
1099
+ ? undefined
1100
+ : await recordRecoveryContext(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
1101
+ return {
1102
+ lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED },
1103
+ lastVerdict: 'blocked',
1104
+ ...(recoveryContext ? { recoveryContext } : {}),
1105
+ stepDelta: 1,
1106
+ };
755
1107
  }
756
1108
  if (scriptResult.outcome === 'failed') {
757
- return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
1109
+ const reason = scriptResult.reason ? `${REVO_SCRIPT_FAILED}: ${scriptResult.reason}` : REVO_SCRIPT_FAILED;
1110
+ return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', recoveryContext: null, failureReason: reason, stepDelta: 1 };
1111
+ }
1112
+ let pointer = scriptResult.pointer;
1113
+ let sv = scriptResult.verdict;
1114
+ let stateOverride;
1115
+ const headMove = approvedHeadMove(decision, ctx.approvedMergeGateHeadSha, pointer, sv);
1116
+ if (headMove) {
1117
+ pointer = reapprovalRequiredFeedback(pointer, headMove.approvedHeadSha, headMove.reverifyHeadSha);
1118
+ sv = 'recheck';
1119
+ stateOverride = reopenNodeState(ctx.state, 'mergeGate');
758
1120
  }
759
- await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
760
- const sv = scriptResult.verdict;
761
- return { lastResult: { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) }, ...(sv ? { lastVerdict: sv } : {}), stepDelta: 1 };
1121
+ const producedOutput = await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), pointer, ctx.outputsByNode);
1122
+ return {
1123
+ lastResult: stateOverride ? undefined : { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) },
1124
+ ...(sv ? { lastVerdict: sv } : {}),
1125
+ ...(producedOutput ? { producedOutput } : {}),
1126
+ ...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
1127
+ ...(stateOverride ? { stateOverride } : {}),
1128
+ stepDelta: 1,
1129
+ };
762
1130
  }
763
1131
  case 'awaitGate': {
764
1132
  const topic = gateTopicFor(decision.reason);
765
1133
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
766
- const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict), decision.outcomes);
1134
+ const summary = buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict, ctx.lastProducedOutput, ctx.recoveryContext);
1135
+ const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, summary, decision.outcomes);
767
1136
  const verdict = gateVerdict(human, decision.outcomes);
768
- await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal)), ctx.outputsByNode);
769
- return { lastResult: verdict ? { verdict } : {}, ...(verdict ? { lastVerdict: verdict } : {}), stepDelta: 0 };
1137
+ const approvedHeadSha = approvedMergeGateHeadSha(decision, verdict, summary);
1138
+ const producedOutput = await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal), approvedHeadSha), ctx.outputsByNode);
1139
+ return {
1140
+ lastResult: verdict ? { verdict } : {},
1141
+ ...(verdict ? { lastVerdict: verdict } : {}),
1142
+ ...(producedOutput ? { producedOutput } : {}),
1143
+ ...(decision.nodeId === 'recoveryGate' ? { recoveryContext: null } : {}),
1144
+ ...(approvedHeadSha !== undefined ? { approvedMergeGateHeadSha: approvedHeadSha } : {}),
1145
+ stepDelta: 0,
1146
+ };
770
1147
  }
771
1148
  case 'fork': {
772
1149
  await appendEvent({
@@ -783,9 +1160,43 @@ export function makeDataDrivenTask(runStepFn, deps) {
783
1160
  return { lastResult: {}, stepDelta: 0 };
784
1161
  }
785
1162
  }
1163
+ async function awaitTransientRetryGate(runId, ctx, input) {
1164
+ const outcomes = [...TRANSIENT_RETRY_GATE_OUTCOMES];
1165
+ const decision = await awaitHuman(runId, 'retry', `transientRetry:${input.stepKey}`, `Retry ${input.node.id} after transient runner failure`, {
1166
+ kind: 'transient_retry',
1167
+ runId,
1168
+ taskId: ctx.taskId,
1169
+ nodeId: input.node.id,
1170
+ step: input.stepKey,
1171
+ role: input.binding.rowId,
1172
+ runner: input.binding.resolvedRunnerId,
1173
+ reason: input.retry.reason,
1174
+ lesson: input.retry.lesson,
1175
+ attemptsExhausted: input.retry.attemptsExhausted,
1176
+ attemptsMade: input.retry.attemptsMade,
1177
+ maxAttempts: input.retry.maxAttempts,
1178
+ attemptIds: input.retry.attemptIds,
1179
+ lastAttemptId: input.retry.lastAttemptId,
1180
+ ...(input.retry.failureKind ? { failureKind: input.retry.failureKind } : {}),
1181
+ ...(input.retry.transientKind ? { transientKind: input.retry.transientKind } : {}),
1182
+ ...(input.retry.timing !== undefined ? { timing: input.retry.timing } : {}),
1183
+ reconcile: { default: 'keep', supported: ['keep'] },
1184
+ outcomes,
1185
+ }, outcomes);
1186
+ return transientRetryGateOutcome(decision);
1187
+ }
786
1188
  async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
787
1189
  const binding = resolveRoleBinding(ctx, decision);
788
- return invokeRoleAttempts({ runId, decision, node, ctx, inputs, stepKey, binding });
1190
+ return invokeRoleAttempts({
1191
+ runId,
1192
+ decision,
1193
+ node,
1194
+ ctx,
1195
+ inputs,
1196
+ stepKey,
1197
+ binding,
1198
+ retryContext: ctx.agentQuestionRetryContextByNode.get(node.id),
1199
+ });
789
1200
  }
790
1201
  function resolveRoleBinding(ctx, decision) {
791
1202
  const binding = ctx.bindingByRef.get(decision.roleRef);
@@ -797,20 +1208,24 @@ export function makeDataDrivenTask(runStepFn, deps) {
797
1208
  async function invokeRoleAttempts(input) {
798
1209
  const { runId, decision, node, ctx, inputs, stepKey, binding } = input;
799
1210
  const attemptIds = [];
1211
+ const retryContext = input.retryContext;
800
1212
  for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
801
1213
  const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
802
1214
  attemptIds.push(physicalAttempt.attemptId);
803
- const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain);
1215
+ const launchOverrides = resolveLaunchOverrides(binding, decision.nodeId, ctx.launchBindings);
1216
+ const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt, retryContext), binding.resolvedRunnerId, physicalAttempt, ctx.template.verdicts.domain, launchOverrides);
804
1217
  const needsHuman = await maybeHandleNeedsHumanRoleResult({
805
1218
  ...input,
806
1219
  attemptIds,
807
1220
  physicalAttempt,
808
1221
  result,
809
1222
  });
810
- if (needsHuman === 'retry')
811
- continue;
812
- if (needsHuman)
1223
+ if (needsHuman) {
1224
+ if ('action' in needsHuman) {
1225
+ continue;
1226
+ }
813
1227
  return needsHuman;
1228
+ }
814
1229
  const failed = roleValidationFailure(ctx.template, node, result, physicalAttempt);
815
1230
  if (failed)
816
1231
  return failed;
@@ -830,6 +1245,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
830
1245
  nodeId: node.id,
831
1246
  attemptId: physicalAttempt.attemptId,
832
1247
  ...(ctx.issueRef ? { issueRef: ctx.issueRef } : {}),
1248
+ ...(ctx.issueAction ? { issueAction: ctx.issueAction } : {}),
833
1249
  ...(artifactRef ? { artifactRef } : {}),
834
1250
  });
835
1251
  output = attachProducedChange(output, change);
@@ -870,17 +1286,57 @@ export function makeDataDrivenTask(runStepFn, deps) {
870
1286
  }
871
1287
  const transient = transientRunnerFailure(result);
872
1288
  if (transient === undefined) {
873
- return {
874
- blocked: true,
875
- reason: 'agent-needs-human',
876
- lesson: safeLesson,
877
- attemptsMade: physicalAttempt.attemptNo,
878
- };
1289
+ const question = await awaitAgentQuestion({ ...input, safeLesson, physicalAttempt });
1290
+ if ('unresolved' in question) {
1291
+ return {
1292
+ blocked: true,
1293
+ reason: question.reason,
1294
+ lesson: question.lesson,
1295
+ attemptsMade: physicalAttempt.attemptNo,
1296
+ };
1297
+ }
1298
+ return { question: true, retryContext: question, attemptsMade: physicalAttempt.attemptNo };
879
1299
  }
880
1300
  return handleTransientRoleResult({ ...input, transient });
881
1301
  }
882
1302
  return undefined;
883
1303
  }
1304
+ async function awaitAgentQuestion(input) {
1305
+ const { runId, node, ctx, stepKey, binding, physicalAttempt, safeLesson } = input;
1306
+ const question = await awaitHuman(runId, 'question', `agentQuestion:${stepKey}:attempt${physicalAttempt.attemptNo}`, `${node.id} question`, {
1307
+ kind: 'agent_question',
1308
+ runId,
1309
+ taskId: ctx.taskId,
1310
+ nodeId: node.id,
1311
+ step: stepKey,
1312
+ role: binding.rowId,
1313
+ runner: binding.resolvedRunnerId,
1314
+ lesson: safeLesson,
1315
+ attemptId: physicalAttempt.attemptId,
1316
+ }, undefined, 'question');
1317
+ const retryContext = agentQuestionRetryContext(question, node.id, safeLesson);
1318
+ if (retryContext === undefined) {
1319
+ return {
1320
+ unresolved: true,
1321
+ reason: 'agent-question-unresolved',
1322
+ lesson: unresolvedAgentQuestionLesson(node.id, safeLesson, question),
1323
+ };
1324
+ }
1325
+ await appendEvent({
1326
+ runId,
1327
+ taskId: ctx.taskId,
1328
+ stepId: '',
1329
+ stepKey,
1330
+ type: 'agent_question_resolved',
1331
+ idempotencyKey: `${physicalAttempt.attemptId}:question`,
1332
+ payload: {
1333
+ nodeId: node.id,
1334
+ inboxId: retryContext.inboxId,
1335
+ resolvedBy: retryContext.resolvedBy,
1336
+ },
1337
+ });
1338
+ return retryContext;
1339
+ }
884
1340
  async function handleTransientRoleResult(input) {
885
1341
  const { runId, node, ctx, stepKey, attemptIds, physicalAttempt, transient } = input;
886
1342
  const policy = ctx.runnerRetryPolicy;
@@ -898,7 +1354,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
898
1354
  });
899
1355
  if (policy.backoffMs > 0)
900
1356
  await deps.sleep(policy.backoffMs);
901
- return 'retry';
1357
+ return { action: 'retry' };
902
1358
  }
903
1359
  const retry = runnerRetryBlockPayload({
904
1360
  transient,
@@ -1021,9 +1477,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
1021
1477
  lesson: `Human selected ${outcome}; recovery is recorded but no Revo worktree patch was adopted by the main session.`,
1022
1478
  };
1023
1479
  }
1024
- async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
1025
- const handler = scriptRegistry.get(decision.scriptRef) ?? scriptRegistry.get('script:integrator');
1026
- return handler({ runId, decision, ctx, bindingByRef, stepKey, inputs });
1480
+ async function invokeScript(runId, decision, ctx, bindingByRef, launchBindings, stepKey, inputs) {
1481
+ const handler = scriptRegistry.get(decision.scriptRef);
1482
+ if (!handler) {
1483
+ const reason = `script handler is not registered: ${decision.scriptRef}`;
1484
+ await appendEvent({
1485
+ runId,
1486
+ taskId: ctx.taskId,
1487
+ stepId: '',
1488
+ stepKey,
1489
+ type: 'step_failed',
1490
+ payload: { scriptRef: decision.scriptRef, error: reason },
1491
+ });
1492
+ return { outcome: 'failed', reason };
1493
+ }
1494
+ return handler({ runId, decision, ctx, bindingByRef, launchBindings, stepKey, inputs });
1027
1495
  }
1028
1496
  async function finish(runId, status, verdict, steps, failureReason = '') {
1029
1497
  if (status === 'succeeded') {