@revisium/orchestrator 0.2.0-alpha.2 → 0.2.0-alpha.4

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 (268) hide show
  1. package/README.md +3 -3
  2. package/control-plane/bootstrap.config.json +104 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +405 -1065
  4. package/control-plane/default-playbook/catalog/run-profiles.json +208 -0
  5. package/control-plane/default-playbook/package.json +1 -1
  6. package/control-plane/default-playbook/playbook.json +2 -1
  7. package/control-plane/default-playbook/prompts/developer.md +6 -6
  8. package/dist/__generated__/client/browser.js +18 -0
  9. package/dist/__generated__/client/browser.js.map +1 -0
  10. package/dist/__generated__/client/client.js +32 -0
  11. package/dist/__generated__/client/client.js.map +1 -0
  12. package/dist/__generated__/client/commonInputTypes.js +11 -0
  13. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  14. package/dist/__generated__/client/enums.js +19 -0
  15. package/dist/__generated__/client/enums.js.map +1 -0
  16. package/dist/__generated__/client/internal/class.js +53 -0
  17. package/dist/__generated__/client/internal/class.js.map +1 -0
  18. package/dist/__generated__/client/internal/prismaNamespace.js +451 -0
  19. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +422 -0
  21. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  22. package/dist/__generated__/client/models/Branch.js +2 -0
  23. package/dist/__generated__/client/models/Branch.js.map +1 -0
  24. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  25. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  26. package/dist/__generated__/client/models/FileBlob.js +2 -0
  27. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  28. package/dist/__generated__/client/models/InboxItem.js +2 -0
  29. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  30. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  31. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  32. package/dist/__generated__/client/models/Revision.js +2 -0
  33. package/dist/__generated__/client/models/Revision.js.map +1 -0
  34. package/dist/__generated__/client/models/RevoProject.js +2 -0
  35. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  36. package/dist/__generated__/client/models/Row.js +2 -0
  37. package/dist/__generated__/client/models/Row.js.map +1 -0
  38. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  39. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  40. package/dist/__generated__/client/models/RunEvent.js +2 -0
  41. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  42. package/dist/__generated__/client/models/RunOutput.js +2 -0
  43. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  44. package/dist/__generated__/client/models/RunTask.js +2 -0
  45. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  46. package/dist/__generated__/client/models/Table.js +2 -0
  47. package/dist/__generated__/client/models/Table.js.map +1 -0
  48. package/dist/__generated__/client/models/TableMigration.js +2 -0
  49. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  50. package/dist/__generated__/client/models/TaskRun.js +2 -0
  51. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  52. package/dist/__generated__/client/models.js +2 -0
  53. package/dist/__generated__/client/models.js.map +1 -0
  54. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  55. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  56. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  57. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  59. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  60. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +46 -0
  61. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  62. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  63. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  64. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  65. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  67. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  68. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  69. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  70. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  71. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  72. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  73. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  74. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  75. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  76. package/dist/cli/commands/doctor-report.js +1 -13
  77. package/dist/cli/commands/doctor-report.js.map +1 -1
  78. package/dist/cli/commands/lifecycle.js +50 -65
  79. package/dist/cli/commands/lifecycle.js.map +1 -1
  80. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  81. package/dist/cli/commands/process-helpers.js.map +1 -0
  82. package/dist/config.js +17 -55
  83. package/dist/config.js.map +1 -1
  84. package/dist/control-plane/audit-validation-helpers.js +31 -0
  85. package/dist/control-plane/audit-validation-helpers.js.map +1 -0
  86. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  87. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  88. package/dist/control-plane/bootstrap.js +25 -38
  89. package/dist/control-plane/bootstrap.js.map +1 -1
  90. package/dist/control-plane/change-notifications.js +45 -14
  91. package/dist/control-plane/change-notifications.js.map +1 -1
  92. package/dist/control-plane/data-access.js +1 -106
  93. package/dist/control-plane/data-access.js.map +1 -1
  94. package/dist/control-plane/default-playbook-policy.js +499 -78
  95. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  96. package/dist/control-plane/definitions.js +4 -5
  97. package/dist/control-plane/definitions.js.map +1 -1
  98. package/dist/control-plane/engine-transport.js +381 -0
  99. package/dist/control-plane/engine-transport.js.map +1 -0
  100. package/dist/control-plane/inbox.js +15 -7
  101. package/dist/control-plane/inbox.js.map +1 -1
  102. package/dist/control-plane/index.js +0 -1
  103. package/dist/control-plane/index.js.map +1 -1
  104. package/dist/control-plane/manual-adoption-audit.js +3 -24
  105. package/dist/control-plane/manual-adoption-audit.js.map +1 -1
  106. package/dist/control-plane/merge-override-audit.js +44 -0
  107. package/dist/control-plane/merge-override-audit.js.map +1 -0
  108. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  109. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  110. package/dist/control-plane/query-types.js +2 -0
  111. package/dist/control-plane/query-types.js.map +1 -0
  112. package/dist/control-plane/run-profiles.js +89 -0
  113. package/dist/control-plane/run-profiles.js.map +1 -0
  114. package/dist/control-plane/schema-migration.js +0 -35
  115. package/dist/control-plane/schema-migration.js.map +1 -1
  116. package/dist/control-plane/seed-default-playbook.js +2 -31
  117. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  118. package/dist/control-plane/tables.js +8 -0
  119. package/dist/control-plane/tables.js.map +1 -1
  120. package/dist/control-plane/transport.js +16 -0
  121. package/dist/control-plane/transport.js.map +1 -0
  122. package/dist/control-plane/versioned-meaning.js +56 -14
  123. package/dist/control-plane/versioned-meaning.js.map +1 -1
  124. package/dist/e2e/kit/agents.js +60 -6
  125. package/dist/e2e/kit/agents.js.map +1 -1
  126. package/dist/e2e/kit/assertions.js +13 -1
  127. package/dist/e2e/kit/assertions.js.map +1 -1
  128. package/dist/e2e/kit/drive.js +46 -11
  129. package/dist/e2e/kit/drive.js.map +1 -1
  130. package/dist/e2e/kit/env.js +17 -1
  131. package/dist/e2e/kit/env.js.map +1 -1
  132. package/dist/e2e/kit/fake-integrator.js +25 -5
  133. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  134. package/dist/e2e/kit/gh-emulator.js +172 -40
  135. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  136. package/dist/e2e/kit/harness.js +44 -10
  137. package/dist/e2e/kit/harness.js.map +1 -1
  138. package/dist/e2e/kit/index.js +1 -0
  139. package/dist/e2e/kit/index.js.map +1 -1
  140. package/dist/e2e/kit/scenario.js +165 -0
  141. package/dist/e2e/kit/scenario.js.map +1 -0
  142. package/dist/e2e/kit/scenarios.js +12 -18
  143. package/dist/e2e/kit/scenarios.js.map +1 -1
  144. package/dist/engine/dbos.service.js +8 -1
  145. package/dist/engine/dbos.service.js.map +1 -1
  146. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  147. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  148. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  149. package/dist/host/daemon.js +32 -8
  150. package/dist/host/daemon.js.map +1 -1
  151. package/dist/host/ensure-host.js +3 -3
  152. package/dist/host/ensure-host.js.map +1 -1
  153. package/dist/host/host-runtime.js.map +1 -1
  154. package/dist/host/host.lifecycle.js +3 -16
  155. package/dist/host/host.lifecycle.js.map +1 -1
  156. package/dist/http/graphql-host.js +17 -8
  157. package/dist/http/graphql-host.js.map +1 -1
  158. package/dist/mcp/mcp-capabilities.js +12 -11
  159. package/dist/mcp/mcp-capabilities.js.map +1 -1
  160. package/dist/mcp/mcp-facade.service.js +45 -5
  161. package/dist/mcp/mcp-facade.service.js.map +1 -1
  162. package/dist/mcp/mcp-tools.js +59 -5
  163. package/dist/mcp/mcp-tools.js.map +1 -1
  164. package/dist/mcp/monitoring-directive.js +43 -0
  165. package/dist/mcp/monitoring-directive.js.map +1 -0
  166. package/dist/pipeline/data-driven-task.workflow.js +393 -182
  167. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  168. package/dist/pipeline/pipeline.service.js +27 -5
  169. package/dist/pipeline/pipeline.service.js.map +1 -1
  170. package/dist/pipeline/route-contract.js +123 -3
  171. package/dist/pipeline/route-contract.js.map +1 -1
  172. package/dist/pipeline-core/condition-scopes.js +15 -0
  173. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  174. package/dist/pipeline-core/index.js +2 -0
  175. package/dist/pipeline-core/index.js.map +1 -1
  176. package/dist/pipeline-core/kit/fixtures.js +81 -44
  177. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  178. package/dist/pipeline-core/materialize.js +221 -0
  179. package/dist/pipeline-core/materialize.js.map +1 -0
  180. package/dist/pipeline-core/types.js +5 -0
  181. package/dist/pipeline-core/types.js.map +1 -1
  182. package/dist/pipeline-core/validate-loops.js +2 -15
  183. package/dist/pipeline-core/validate-loops.js.map +1 -1
  184. package/dist/pipeline-core/validate-resilience.js +257 -0
  185. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  186. package/dist/pipeline-core/validate.js +2 -0
  187. package/dist/pipeline-core/validate.js.map +1 -1
  188. package/dist/playbook/catalog-loader.js +39 -2
  189. package/dist/playbook/catalog-loader.js.map +1 -1
  190. package/dist/playbook/catalog-schema-validator.js +438 -0
  191. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  192. package/dist/playbook/import-mapper.js +41 -0
  193. package/dist/playbook/import-mapper.js.map +1 -1
  194. package/dist/playbook/manifest.js +5 -0
  195. package/dist/playbook/manifest.js.map +1 -1
  196. package/dist/playbook/playbook-installer.js +17 -1
  197. package/dist/playbook/playbook-installer.js.map +1 -1
  198. package/dist/poller/pr-readiness-core.js +280 -94
  199. package/dist/poller/pr-readiness-core.js.map +1 -1
  200. package/dist/poller/pr-readiness.js +2 -2
  201. package/dist/poller/pr-readiness.js.map +1 -1
  202. package/dist/revisium/inbox.service.js +8 -8
  203. package/dist/revisium/inbox.service.js.map +1 -1
  204. package/dist/revisium/playbooks.service.js +116 -21
  205. package/dist/revisium/playbooks.service.js.map +1 -1
  206. package/dist/revisium/revisium.module.js +8 -8
  207. package/dist/revisium/revisium.module.js.map +1 -1
  208. package/dist/revisium/roles.service.js +3 -1
  209. package/dist/revisium/roles.service.js.map +1 -1
  210. package/dist/revisium/run.service.js +9 -8
  211. package/dist/revisium/run.service.js.map +1 -1
  212. package/dist/revisium/tokens.js +0 -1
  213. package/dist/revisium/tokens.js.map +1 -1
  214. package/dist/run/create-run.js +1 -1
  215. package/dist/run/create-run.js.map +1 -1
  216. package/dist/run/inspect-run.js +32 -28
  217. package/dist/run/inspect-run.js.map +1 -1
  218. package/dist/run/issue-ref.js +97 -2
  219. package/dist/run/issue-ref.js.map +1 -1
  220. package/dist/run/prisma-runtime-data-access.js +619 -0
  221. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  222. package/dist/run/run-outputs.js +39 -14
  223. package/dist/run/run-outputs.js.map +1 -1
  224. package/dist/run/terminal-run-status.js.map +1 -1
  225. package/dist/runners/integrator.js +657 -56
  226. package/dist/runners/integrator.js.map +1 -1
  227. package/dist/runners/worktree.service.js +10 -8
  228. package/dist/runners/worktree.service.js.map +1 -1
  229. package/dist/schema/ajv-errors.js +15 -0
  230. package/dist/schema/ajv-errors.js.map +1 -0
  231. package/dist/smoke/isolation.js +7 -16
  232. package/dist/smoke/isolation.js.map +1 -1
  233. package/dist/storage/ensure-storage.js +117 -0
  234. package/dist/storage/ensure-storage.js.map +1 -0
  235. package/dist/storage/revo-database.js +39 -0
  236. package/dist/storage/revo-database.js.map +1 -0
  237. package/dist/storage/revo-prisma.service.js +31 -0
  238. package/dist/storage/revo-prisma.service.js.map +1 -0
  239. package/dist/storage/revo-storage.module.js +18 -0
  240. package/dist/storage/revo-storage.module.js.map +1 -0
  241. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  242. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  243. package/dist/task-control-plane/run-watch.service.js +3 -6
  244. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  245. package/dist/task-control-plane/task-control-plane-api.service.js +234 -43
  246. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  247. package/dist/testing/runtime-data-access.js +175 -0
  248. package/dist/testing/runtime-data-access.js.map +1 -0
  249. package/dist/worker/build-context.js +178 -2
  250. package/dist/worker/build-context.js.map +1 -1
  251. package/dist/worker/claude-code-runner.js +22 -3
  252. package/dist/worker/claude-code-runner.js.map +1 -1
  253. package/dist/worker/codex-runner.js +6 -1
  254. package/dist/worker/codex-runner.js.map +1 -1
  255. package/package.json +20 -7
  256. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  257. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  258. package/prisma/migrations/migration_lock.toml +3 -0
  259. package/prisma/schema.prisma +339 -0
  260. package/prisma.config.ts +11 -0
  261. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  262. package/dist/control-plane/client-transport.js +0 -168
  263. package/dist/control-plane/client-transport.js.map +0 -1
  264. package/dist/engine/ensure-postgres.js +0 -46
  265. package/dist/engine/ensure-postgres.js.map +0 -1
  266. package/dist/host/ensure-revisium.js +0 -135
  267. package/dist/host/ensure-revisium.js.map +0 -1
  268. package/revisium.config.json +0 -10
@@ -1,11 +1,12 @@
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, runnerUsesRealIntegrator } 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;
@@ -108,10 +109,12 @@ function producedChangeArtifact(value) {
108
109
  if (typeof headSha !== 'string' || headSha.trim().length === 0)
109
110
  return undefined;
110
111
  const issueRef = normalizeArtifactIssueRef(candidate.issueRef);
112
+ const issueAction = normalizeArtifactIssueAction(candidate.issueAction);
111
113
  return {
112
114
  branch,
113
115
  headSha,
114
116
  ...(issueRef ? { issueRef } : {}),
117
+ ...(issueAction ? { issueAction } : {}),
115
118
  ...(typeof candidate.worktreePath === 'string' && candidate.worktreePath.trim() ? { worktreePath: candidate.worktreePath } : {}),
116
119
  ...(typeof candidate.artifactRef === 'string' && candidate.artifactRef.trim() ? { artifactRef: candidate.artifactRef } : {}),
117
120
  ...(typeof candidate.prNumber === 'number' && Number.isSafeInteger(candidate.prNumber) ? { prNumber: candidate.prNumber } : {}),
@@ -125,7 +128,15 @@ function normalizeArtifactIssueRef(value) {
125
128
  return undefined;
126
129
  }
127
130
  }
128
- function changeWithRunIssueRef(change, issueRef) {
131
+ function normalizeArtifactIssueAction(value) {
132
+ try {
133
+ return normalizeIssueAction(value, 'change.issueAction');
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ function changeWithRunIssueContract(change, issueRef, issueAction) {
129
140
  const normalized = { ...change };
130
141
  if (issueRef) {
131
142
  normalized.issueRef = issueRef;
@@ -133,6 +144,12 @@ function changeWithRunIssueRef(change, issueRef) {
133
144
  else {
134
145
  delete normalized.issueRef;
135
146
  }
147
+ if (issueAction) {
148
+ normalized.issueAction = issueAction;
149
+ }
150
+ else {
151
+ delete normalized.issueAction;
152
+ }
136
153
  return normalized;
137
154
  }
138
155
  function producedChangeFromInputs(inputs) {
@@ -150,7 +167,16 @@ function mergeReadinessFromInputs(inputs) {
150
167
  const headSha = value.headSha;
151
168
  if (typeof headSha !== 'string' || headSha.trim().length === 0)
152
169
  return undefined;
153
- return { headSha };
170
+ const override = isRecord(value.override) && value.override.accepted === true
171
+ ? { accepted: true }
172
+ : undefined;
173
+ return { headSha, ...(override ? { override } : {}) };
174
+ }
175
+ function headShaFromPayload(value) {
176
+ if (!isRecord(value))
177
+ return undefined;
178
+ const headSha = value.headSha;
179
+ return typeof headSha === 'string' && headSha.trim().length > 0 ? headSha.trim() : undefined;
154
180
  }
155
181
  function attachProducedChange(output, change) {
156
182
  if (isRecord(output))
@@ -275,13 +301,24 @@ function gateVerdict(decision, outcomes) {
275
301
  return outcomes[0];
276
302
  return outcomes.length > 1 ? outcomes.at(-1) : undefined;
277
303
  }
278
- function gateResolutionOutput(decision, verdict, fallbackInboxId) {
304
+ function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHeadSha) {
305
+ const decisionRecord = isRecord(decision) ? decision : {};
306
+ const answer = isRecord(decision.answer) ? decision.answer : {};
307
+ const note = decision.note ?? (typeof answer.note === 'string' ? answer.note : undefined);
308
+ const resolvedBy = decision.resolvedBy ?? (typeof answer.resolvedBy === 'string' ? answer.resolvedBy : '');
309
+ const resolvedAt = decision.resolvedAt ?? (typeof answer.resolvedAt === 'string' ? answer.resolvedAt : '');
310
+ const inboxId = decision.inboxId ?? (typeof answer.inboxId === 'string' ? answer.inboxId : fallbackInboxId);
311
+ const mergeOverrideAudit = answer.mergeOverrideAudit ?? decisionRecord['mergeOverrideAudit'];
312
+ const adoptionAudit = answer.adoptionAudit ?? decisionRecord['adoptionAudit'];
279
313
  return {
280
314
  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,
315
+ ...(note !== undefined ? { note } : {}),
316
+ resolvedBy,
317
+ resolvedAt,
318
+ inboxId,
319
+ ...(trustedGateHeadSha !== undefined ? { trustedGateHeadSha } : {}),
320
+ ...(mergeOverrideAudit !== undefined ? { mergeOverrideAudit } : {}),
321
+ ...(adoptionAudit !== undefined ? { adoptionAudit } : {}),
285
322
  ...(decision.decision ? { decision: decision.decision } : {}),
286
323
  };
287
324
  }
@@ -317,9 +354,19 @@ function gateArtifactView(row, as) {
317
354
  payloadRef: `attempt:${row.attemptId ?? ''}`,
318
355
  };
319
356
  }
320
- export function buildGateSummary(decision, outputsByNode, lastVerdict) {
357
+ function freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput) {
358
+ if (decision.nodeId !== 'mergeGate')
359
+ return undefined;
360
+ if (lastProducedOutput?.nodeId === 'mergeRecheck' && lastVerdict === 'clean')
361
+ return lastProducedOutput;
362
+ if (lastProducedOutput?.nodeId === 'mergeApproveReverify' && lastVerdict === 'recheck')
363
+ return lastProducedOutput;
364
+ return undefined;
365
+ }
366
+ export function buildGateSummary(decision, outputsByNode, lastVerdict, lastProducedOutput) {
321
367
  const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
322
- const artRow = resolveGateRow(decision.gatedArtifact, outputsByNode);
368
+ const artRow = freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput)
369
+ ?? resolveGateRow(decision.gatedArtifact, outputsByNode);
323
370
  if (artRow)
324
371
  summary.gatedArtifact = gateArtifactView(artRow, decision.gatedArtifact?.as);
325
372
  const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
@@ -329,8 +376,279 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
329
376
  summary.reviewerVerdict = { verdict: lastVerdict };
330
377
  return summary;
331
378
  }
379
+ function approvedMergeGateHeadSha(decision, verdict, summary) {
380
+ if (decision.nodeId !== 'mergeGate')
381
+ return undefined;
382
+ if (verdict === 'approved' || verdict === 'override_merge') {
383
+ return headShaFromPayload(summary.gatedArtifact?.payload) ?? null;
384
+ }
385
+ return null;
386
+ }
387
+ function approvedHeadMove(decision, approvedHeadSha, pointer, verdict) {
388
+ if (decision.nodeId !== 'mergeApproveReverify' || verdict !== 'clean' || approvedHeadSha === undefined)
389
+ return undefined;
390
+ const reverifyHeadSha = headShaFromPayload(pointer);
391
+ if (reverifyHeadSha === undefined || reverifyHeadSha === approvedHeadSha)
392
+ return undefined;
393
+ return { approvedHeadSha, reverifyHeadSha };
394
+ }
395
+ function reapprovalRequiredFeedback(pointer, approvedHeadSha, reverifyHeadSha) {
396
+ if (!isRecord(pointer))
397
+ return pointer;
398
+ const evidence = Array.isArray(pointer.evidence)
399
+ ? pointer.evidence.filter((item) => typeof item === 'string')
400
+ : [];
401
+ return {
402
+ ...pointer,
403
+ verdict: 'recheck',
404
+ evidence: [
405
+ ...evidence,
406
+ `approved mergeGate headSha=${approvedHeadSha}; reverify headSha=${reverifyHeadSha}; reapproval required`,
407
+ ],
408
+ };
409
+ }
410
+ function reopenNodeState(state, nodeId) {
411
+ return { ...state, activeNodeIds: new Set([nodeId]), status: 'running' };
412
+ }
413
+ function integratorResultPointer(result) {
414
+ return {
415
+ prUrl: result.prUrl,
416
+ branch: result.branch,
417
+ prNumber: result.prNumber,
418
+ headSha: result.headSha,
419
+ status: result.status,
420
+ ...(result.issueRef ? { issueRef: result.issueRef } : {}),
421
+ ...(result.foreignPr ? { foreignPr: true } : {}),
422
+ ...(result.prAuthor ? { prAuthor: result.prAuthor } : {}),
423
+ ...(result.integratorAccount ? { integratorAccount: result.integratorAccount } : {}),
424
+ };
425
+ }
426
+ function integratorProgressEventType(result) {
427
+ return result.foreignPr ? 'foreign_pr_adopted' : 'integrate_succeeded';
428
+ }
429
+ function mergeOverrideEventPayload(result) {
430
+ const audit = result.override.audit;
431
+ return {
432
+ actor: result.override.actor,
433
+ note: result.override.note,
434
+ reason: audit?.reason ?? result.override.reason ?? '',
435
+ risk: audit?.risk ?? '',
436
+ verificationResponsibility: audit?.verificationResponsibility ?? '',
437
+ headSha: audit?.headSha ?? result.headSha,
438
+ freshHeadSha: result.headSha,
439
+ prNumber: result.prNumber,
440
+ source: result.override.source,
441
+ overriddenFacts: result.override.facts,
442
+ replied: result.override.replied,
443
+ resolved: result.override.resolved,
444
+ ...(result.override.reason ? { refusalReason: result.override.reason } : {}),
445
+ };
446
+ }
447
+ export function buildSystemScriptRegistry(deps) {
448
+ const { appendEvent, releaseWorktreeFn, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, overrideMergeFn, runOverrideStub, respondThreadsFn, runRespondStub, } = deps;
449
+ function buildIntegratorInput(runId, ctx, inputs) {
450
+ const { taskId, title, base, issueRef, issueAction } = ctx;
451
+ const change = producedChangeFromInputs(inputs);
452
+ const mergeReadiness = mergeReadinessFromInputs(inputs);
453
+ const changeForIntegrator = change ? changeWithRunIssueContract(change, issueRef, issueAction) : undefined;
454
+ return {
455
+ runId,
456
+ taskId,
457
+ title,
458
+ base,
459
+ ...(issueRef ? { issueRef } : {}),
460
+ ...(issueAction ? { issueAction } : {}),
461
+ ...(changeForIntegrator ? { change: changeForIntegrator } : {}),
462
+ ...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
463
+ ...(inputs.gateResolution === undefined ? {} : { gateResolution: inputs.gateResolution }),
464
+ ...(mergeReadiness ? { mergeReadiness } : {}),
465
+ };
466
+ }
467
+ function makeIntegratorScript(desc) {
468
+ return async ({ runId, decision, ctx, bindingByRef, stepKey, inputs }) => {
469
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs);
470
+ const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
471
+ const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
472
+ let result;
473
+ try {
474
+ result = useReal ? await desc.real(integratorInput) : desc.stub(integratorInput);
475
+ }
476
+ catch (err) {
477
+ const reason = err instanceof Error ? err.message : String(err);
478
+ await appendEvent({
479
+ runId, taskId: ctx.taskId, stepId: '', stepKey,
480
+ type: 'step_failed',
481
+ payload: { scriptRef: decision.scriptRef, error: reason },
482
+ });
483
+ return { outcome: 'failed', reason };
484
+ }
485
+ if ('needsHuman' in result) {
486
+ await appendEvent({
487
+ runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
488
+ type: 'pipeline_blocked',
489
+ payload: { reason: desc.blockedReason, lesson: result.lesson, nodeId: decision.nodeId },
490
+ });
491
+ return { outcome: 'blocked' };
492
+ }
493
+ const { eventType, payload, pointer, verdict } = desc.mapSuccess(result);
494
+ await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: eventType, payload });
495
+ return { outcome: 'ok', pointer, ...(verdict !== undefined ? { verdict } : {}) };
496
+ };
497
+ }
498
+ const cleanupWorktree = async ({ runId, decision, ctx, stepKey }) => {
499
+ let releaseResult;
500
+ try {
501
+ releaseResult = await releaseWorktreeFn(runId, ctx.taskId);
502
+ }
503
+ catch (err) {
504
+ const error = err instanceof Error ? err.message : String(err);
505
+ await appendEvent({
506
+ runId,
507
+ taskId: ctx.taskId,
508
+ stepId: '',
509
+ stepKey,
510
+ type: 'cleanup_failed',
511
+ payload: { nodeId: decision.nodeId, error, released: false },
512
+ });
513
+ return { outcome: 'ok', pointer: { released: false, error } };
514
+ }
515
+ if (!releaseResult.released) {
516
+ const pointer = {
517
+ released: false,
518
+ reason: releaseResult.reason,
519
+ worktreePath: releaseResult.worktreePath,
520
+ };
521
+ await appendEvent({
522
+ runId,
523
+ taskId: ctx.taskId,
524
+ stepId: '',
525
+ stepKey,
526
+ type: 'cleanup_failed',
527
+ payload: { nodeId: decision.nodeId, ...pointer },
528
+ });
529
+ return { outcome: 'ok', pointer };
530
+ }
531
+ await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
532
+ return { outcome: 'ok', pointer: { released: true } };
533
+ };
534
+ const integratorScript = makeIntegratorScript({
535
+ real: integrateFn,
536
+ stub: runStub,
537
+ blockedReason: 'integrate',
538
+ mapSuccess: (result) => ({
539
+ eventType: integratorProgressEventType(result),
540
+ payload: integratorResultPointer(result),
541
+ pointer: integratorResultPointer(result),
542
+ }),
543
+ });
544
+ const confirmMergeScript = makeIntegratorScript({
545
+ real: confirmMergeFn,
546
+ stub: runConfirmStub,
547
+ blockedReason: 'confirm-merge',
548
+ mapSuccess: (result) => ({
549
+ eventType: 'merge_confirmed',
550
+ payload: {
551
+ prNumber: result.prNumber,
552
+ prUrl: result.prUrl,
553
+ ...(result.issueRef ? { issueRef: result.issueRef } : {}),
554
+ },
555
+ pointer: {
556
+ merged: true,
557
+ prNumber: result.prNumber,
558
+ prUrl: result.prUrl,
559
+ ...(result.issueRef ? { issueRef: result.issueRef } : {}),
560
+ },
561
+ }),
562
+ });
563
+ const pollPrScript = makeIntegratorScript({
564
+ real: pollPrFn,
565
+ stub: runPollStub,
566
+ blockedReason: 'poll-pr',
567
+ mapSuccess: (result) => ({
568
+ eventType: 'pr_polled',
569
+ payload: {
570
+ prNumber: result.prNumber,
571
+ headSha: result.headSha,
572
+ verdict: result.verdict,
573
+ evidence: result.evidence,
574
+ ciFailures: result.ciFailures.length,
575
+ reviewThreads: result.reviewThreads.length,
576
+ ...(result.issueRef ? { issueRef: result.issueRef } : {}),
577
+ },
578
+ pointer: result,
579
+ verdict: result.verdict,
580
+ }),
581
+ });
582
+ const overrideMergeScript = async ({ runId, decision, ctx, bindingByRef, stepKey, inputs }) => {
583
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs);
584
+ const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
585
+ const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
586
+ let result;
587
+ try {
588
+ result = useReal ? await overrideMergeFn(integratorInput) : runOverrideStub(integratorInput);
589
+ }
590
+ catch (err) {
591
+ const reason = err instanceof Error ? err.message : String(err);
592
+ await appendEvent({
593
+ runId, taskId: ctx.taskId, stepId: '', stepKey,
594
+ type: 'step_failed',
595
+ payload: { scriptRef: decision.scriptRef, error: reason },
596
+ });
597
+ return { outcome: 'failed', reason };
598
+ }
599
+ if ('needsHuman' in result) {
600
+ await appendEvent({
601
+ runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
602
+ type: 'pipeline_blocked',
603
+ payload: { reason: 'override-merge', lesson: result.lesson, nodeId: decision.nodeId },
604
+ });
605
+ return { outcome: 'blocked' };
606
+ }
607
+ if (result.override.replied > 0 || result.override.resolved > 0) {
608
+ await appendEvent({
609
+ runId,
610
+ taskId: ctx.taskId,
611
+ stepId: '',
612
+ stepKey,
613
+ type: 'threads_responded',
614
+ payload: { replied: result.override.replied, resolved: result.override.resolved },
615
+ });
616
+ }
617
+ if (result.override.accepted || result.verdict !== 'merged') {
618
+ await appendEvent({
619
+ runId,
620
+ taskId: ctx.taskId,
621
+ stepId: '',
622
+ stepKey,
623
+ type: result.override.accepted ? 'merge_overridden' : 'merge_override_refused',
624
+ payload: mergeOverrideEventPayload(result),
625
+ });
626
+ }
627
+ return { outcome: 'ok', pointer: result, verdict: result.verdict };
628
+ };
629
+ const respondThreadsScript = makeIntegratorScript({
630
+ real: respondThreadsFn,
631
+ stub: runRespondStub,
632
+ blockedReason: 'respond-threads',
633
+ mapSuccess: (result) => ({
634
+ eventType: 'threads_responded',
635
+ payload: { replied: result.replied, resolved: result.resolved },
636
+ pointer: result,
637
+ }),
638
+ });
639
+ return new Map([
640
+ ['script:cleanupWorktree', cleanupWorktree],
641
+ ['script:confirmMerge', confirmMergeScript],
642
+ ['script:pollPr', pollPrScript],
643
+ ['script:overrideMerge', overrideMergeScript],
644
+ ['script:respondThreads', respondThreadsScript],
645
+ // Unknown refs fall through to this entry, matching the original else-branch behavior.
646
+ ['script:integrator', integratorScript],
647
+ ]);
648
+ }
332
649
  export function makeDataDrivenTask(runStepFn, deps) {
333
- const { appendEvent, appendRunOutput, awaitHuman, completeRun, failRun, blockRun, cancelRun, loadRunTaskContext, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub, captureChangeFn, preflightFn, createWorktreeFn, releaseWorktreeFn } = deps;
650
+ const { appendEvent, appendRunOutput, awaitHuman, completeRun, failRun, blockRun, cancelRun, loadRunTaskContext, captureChangeFn, preflightFn, createWorktreeFn } = deps;
651
+ const scriptRegistry = buildSystemScriptRegistry(deps);
334
652
  function resolveConsumes(node, outputsByNode) {
335
653
  const refs = 'consumes' in node ? (node.consumes ?? []) : [];
336
654
  const inputs = {};
@@ -363,7 +681,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
363
681
  }
364
682
  async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
365
683
  if (!('produces' in node) || !node.produces)
366
- return;
684
+ return undefined;
367
685
  const row = {
368
686
  runId,
369
687
  nodeId: node.id,
@@ -377,6 +695,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
377
695
  list.push(row);
378
696
  outputsByNode.set(node.id, list);
379
697
  await appendRunOutput(row);
698
+ return row;
380
699
  }
381
700
  return async function dataDrivenTaskImpl(runId, opts) {
382
701
  try {
@@ -399,7 +718,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
399
718
  if (diagnostics.length > 0) {
400
719
  throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
401
720
  }
402
- const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
721
+ const { taskId, title, base, issueRef, issueAction } = await loadRunTaskContext(runId);
403
722
  const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
404
723
  if (live) {
405
724
  const pf = await preflightFn(taskId, base);
@@ -410,9 +729,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
410
729
  if (live) {
411
730
  await createWorktreeFn(runId, taskId, title, base, issueRef);
412
731
  }
413
- return runGraph(runId, opts, taskId, title, base, issueRef, live);
732
+ return runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live);
414
733
  }
415
- async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
734
+ async function runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live) {
416
735
  const { route, template, runnerRetryPolicy } = opts;
417
736
  const bindingByRef = new Map();
418
737
  for (const binding of route.roleBindings) {
@@ -427,7 +746,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
427
746
  let state = initialState(template);
428
747
  let lastResult;
429
748
  let lastVerdict = '';
749
+ let lastProducedOutput;
430
750
  let lastFailureReason = '';
751
+ let approvedMergeGateHeadShaValue;
431
752
  let stepCount = 0;
432
753
  const effectOrdinalByNode = new Map();
433
754
  const outputsByNode = new Map();
@@ -439,10 +760,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
439
760
  return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
440
761
  }
441
762
  const eff = await applyDecision(decision, {
442
- runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef,
763
+ runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef, issueAction,
443
764
  effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
444
765
  live,
445
766
  lastVerdict,
767
+ lastProducedOutput,
768
+ approvedMergeGateHeadSha: approvedMergeGateHeadShaValue,
446
769
  });
447
770
  stepCount += eff.stepDelta;
448
771
  if (eff.terminal) {
@@ -453,9 +776,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
453
776
  await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
454
777
  }
455
778
  lastResult = eff.lastResult;
456
- if (eff.lastVerdict !== undefined)
457
- lastVerdict = eff.lastVerdict;
779
+ lastVerdict = eff.lastVerdict ?? lastVerdict;
780
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
458
781
  lastFailureReason = eff.failureReason ?? '';
782
+ if (eff.approvedMergeGateHeadSha !== undefined) {
783
+ approvedMergeGateHeadShaValue = eff.approvedMergeGateHeadSha ?? undefined;
784
+ }
459
785
  }
460
786
  throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
461
787
  }
@@ -510,6 +836,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
510
836
  };
511
837
  let lastResult;
512
838
  let lastVerdict = '';
839
+ let lastProducedOutput;
513
840
  let stepDelta = 0;
514
841
  for (let i = 0; i < MAX_STEPS; i++) {
515
842
  const next = coreStep(branchTemplate, state, lastResult);
@@ -532,6 +859,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
532
859
  template: branchTemplate,
533
860
  state,
534
861
  lastVerdict,
862
+ lastProducedOutput,
535
863
  });
536
864
  stepDelta += eff.stepDelta;
537
865
  if (eff.terminal)
@@ -541,6 +869,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
541
869
  lastResult = eff.lastResult;
542
870
  if (eff.lastVerdict !== undefined)
543
871
  lastVerdict = eff.lastVerdict;
872
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
544
873
  }
545
874
  throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
546
875
  }
@@ -553,11 +882,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
553
882
  const stepKey = stepKeyFor(node.id, ordinal);
554
883
  const resolved = resolveConsumes(node, ctx.outputsByNode);
555
884
  if ('missing' in resolved) {
885
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
556
886
  await appendEvent({
557
887
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
558
- payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
888
+ payload: { nodeId: node.id, error: reason },
559
889
  });
560
- return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
890
+ return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
561
891
  }
562
892
  const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
563
893
  if ('blocked' in result) {
@@ -588,15 +918,16 @@ export function makeDataDrivenTask(runStepFn, deps) {
588
918
  await appendEvent({
589
919
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
590
920
  idempotencyKey: result.attemptId,
591
- payload: { nodeId: node.id, error: result.errorCode },
921
+ payload: { nodeId: node.id, error: result.errorCode, reason: result.reason },
592
922
  });
593
923
  return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
594
924
  }
595
- await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
925
+ const producedOutput = await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
596
926
  const verdict = result.verdict;
597
927
  return {
598
928
  lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
599
929
  ...(verdict ? { lastVerdict: verdict } : {}),
930
+ ...(producedOutput ? { producedOutput } : {}),
600
931
  stepDelta: result.attemptsMade,
601
932
  };
602
933
  }
@@ -605,30 +936,54 @@ export function makeDataDrivenTask(runStepFn, deps) {
605
936
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, node.id);
606
937
  const resolved = resolveConsumes(node, ctx.outputsByNode);
607
938
  if ('missing' in resolved) {
939
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
608
940
  await appendEvent({
609
941
  runId, taskId, stepId: '', stepKey: stepKeyFor(node.id, ordinal), type: 'step_failed',
610
- payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
942
+ payload: { nodeId: node.id, error: reason },
611
943
  });
612
- return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
944
+ return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
613
945
  }
614
- const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
946
+ const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef, issueAction: ctx.issueAction }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
615
947
  if (scriptResult.outcome === 'blocked') {
616
948
  return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
617
949
  }
618
950
  if (scriptResult.outcome === 'failed') {
619
- return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
951
+ const reason = scriptResult.reason ? `${REVO_SCRIPT_FAILED}: ${scriptResult.reason}` : REVO_SCRIPT_FAILED;
952
+ return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
953
+ }
954
+ let pointer = scriptResult.pointer;
955
+ let sv = scriptResult.verdict;
956
+ let stateOverride;
957
+ const headMove = approvedHeadMove(decision, ctx.approvedMergeGateHeadSha, pointer, sv);
958
+ if (headMove) {
959
+ pointer = reapprovalRequiredFeedback(pointer, headMove.approvedHeadSha, headMove.reverifyHeadSha);
960
+ sv = 'recheck';
961
+ stateOverride = reopenNodeState(ctx.state, 'mergeGate');
620
962
  }
621
- await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
622
- const sv = scriptResult.verdict;
623
- return { lastResult: { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) }, ...(sv ? { lastVerdict: sv } : {}), stepDelta: 1 };
963
+ const producedOutput = await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), pointer, ctx.outputsByNode);
964
+ return {
965
+ lastResult: stateOverride ? undefined : { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) },
966
+ ...(sv ? { lastVerdict: sv } : {}),
967
+ ...(producedOutput ? { producedOutput } : {}),
968
+ ...(stateOverride ? { stateOverride } : {}),
969
+ stepDelta: 1,
970
+ };
624
971
  }
625
972
  case 'awaitGate': {
626
973
  const topic = gateTopicFor(decision.reason);
627
974
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
628
- const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict), decision.outcomes);
975
+ const summary = buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict, ctx.lastProducedOutput);
976
+ const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, summary, decision.outcomes);
629
977
  const verdict = gateVerdict(human, decision.outcomes);
630
- await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal)), ctx.outputsByNode);
631
- return { lastResult: verdict ? { verdict } : {}, ...(verdict ? { lastVerdict: verdict } : {}), stepDelta: 0 };
978
+ const approvedHeadSha = approvedMergeGateHeadSha(decision, verdict, summary);
979
+ const producedOutput = await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal), approvedHeadSha), ctx.outputsByNode);
980
+ return {
981
+ lastResult: verdict ? { verdict } : {},
982
+ ...(verdict ? { lastVerdict: verdict } : {}),
983
+ ...(producedOutput ? { producedOutput } : {}),
984
+ ...(approvedHeadSha !== undefined ? { approvedMergeGateHeadSha: approvedHeadSha } : {}),
985
+ stepDelta: 0,
986
+ };
632
987
  }
633
988
  case 'fork': {
634
989
  await appendEvent({
@@ -662,7 +1017,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
662
1017
  for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
663
1018
  const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
664
1019
  attemptIds.push(physicalAttempt.attemptId);
665
- const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain);
1020
+ const launchOverrides = resolveLaunchOverrides(binding, decision.nodeId, ctx.executionProfile);
1021
+ const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain, launchOverrides);
666
1022
  const needsHuman = await maybeHandleNeedsHumanRoleResult({
667
1023
  ...input,
668
1024
  attemptIds,
@@ -692,6 +1048,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
692
1048
  nodeId: node.id,
693
1049
  attemptId: physicalAttempt.attemptId,
694
1050
  ...(ctx.issueRef ? { issueRef: ctx.issueRef } : {}),
1051
+ ...(ctx.issueAction ? { issueAction: ctx.issueAction } : {}),
695
1052
  ...(artifactRef ? { artifactRef } : {}),
696
1053
  });
697
1054
  output = attachProducedChange(output, change);
@@ -884,154 +1241,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
884
1241
  };
885
1242
  }
886
1243
  async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
887
- if (decision.scriptRef === 'script:cleanupWorktree') {
888
- try {
889
- await releaseWorktreeFn(runId, ctx.taskId);
890
- }
891
- catch { /* best-effort */ }
892
- await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
893
- return { outcome: 'ok', pointer: { released: true } };
894
- }
895
- const isConfirmMerge = decision.scriptRef === 'script:confirmMerge';
896
- const isPollPr = decision.scriptRef === 'script:pollPr';
897
- const isRespondThreads = decision.scriptRef === 'script:respondThreads';
898
- const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
899
- const change = producedChangeFromInputs(inputs);
900
- const mergeReadiness = mergeReadinessFromInputs(inputs);
901
- const issueRef = ctx.issueRef;
902
- const changeForIntegrator = change ? changeWithRunIssueRef(change, issueRef) : undefined;
903
- const integratorInput = {
904
- runId,
905
- taskId: ctx.taskId,
906
- title: ctx.title,
907
- base: ctx.base,
908
- ...(issueRef ? { issueRef } : {}),
909
- ...(changeForIntegrator ? { change: changeForIntegrator } : {}),
910
- ...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
911
- ...(mergeReadiness ? { mergeReadiness } : {}),
912
- };
913
- const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
914
- let result;
915
- try {
916
- if (isConfirmMerge) {
917
- result = useReal ? await confirmMergeFn(integratorInput) : runConfirmStub(integratorInput);
918
- }
919
- else if (isPollPr) {
920
- result = useReal ? await pollPrFn(integratorInput) : runPollStub(integratorInput);
921
- }
922
- else if (isRespondThreads) {
923
- result = useReal ? await respondThreadsFn(integratorInput) : runRespondStub(integratorInput);
924
- }
925
- else {
926
- result = useReal ? await integrateFn(integratorInput) : runStub(integratorInput);
927
- }
928
- }
929
- catch (err) {
930
- await appendEvent({
931
- runId,
932
- taskId: ctx.taskId,
933
- stepId: '',
934
- stepKey,
935
- type: 'step_failed',
936
- payload: { scriptRef: decision.scriptRef, error: err instanceof Error ? err.message : String(err) },
937
- });
938
- return { outcome: 'failed' };
939
- }
940
- if ('needsHuman' in result) {
941
- const reason = isConfirmMerge ? 'confirm-merge' : isPollPr ? 'poll-pr' : isRespondThreads ? 'respond-threads' : 'integrate';
942
- await appendEvent({
943
- runId,
944
- taskId: ctx.taskId,
945
- stepId: '',
946
- stepKey: 'pipeline',
947
- type: 'pipeline_blocked',
948
- payload: { reason, lesson: result.lesson, nodeId: decision.nodeId },
949
- });
950
- return { outcome: 'blocked' };
951
- }
952
- if (isConfirmMerge) {
953
- const merged = result;
954
- await appendEvent({
955
- runId,
956
- taskId: ctx.taskId,
957
- stepId: '',
958
- stepKey,
959
- type: 'merge_confirmed',
960
- payload: {
961
- prNumber: merged.prNumber,
962
- prUrl: merged.prUrl,
963
- ...(merged.issueRef ? { issueRef: merged.issueRef } : {}),
964
- },
965
- });
966
- return {
967
- outcome: 'ok',
968
- pointer: {
969
- merged: true,
970
- prNumber: merged.prNumber,
971
- prUrl: merged.prUrl,
972
- ...(merged.issueRef ? { issueRef: merged.issueRef } : {}),
973
- },
974
- };
975
- }
976
- if (isPollPr) {
977
- const feedback = result;
978
- await appendEvent({
979
- runId,
980
- taskId: ctx.taskId,
981
- stepId: '',
982
- stepKey,
983
- type: 'pr_polled',
984
- payload: {
985
- prNumber: feedback.prNumber,
986
- headSha: feedback.headSha,
987
- verdict: feedback.verdict,
988
- evidence: feedback.evidence,
989
- ciFailures: feedback.ciFailures.length,
990
- reviewThreads: feedback.reviewThreads.length,
991
- ...(feedback.issueRef ? { issueRef: feedback.issueRef } : {}),
992
- },
993
- });
994
- return { outcome: 'ok', pointer: feedback, verdict: feedback.verdict };
995
- }
996
- if (isRespondThreads) {
997
- const responded = result;
998
- await appendEvent({
999
- runId,
1000
- taskId: ctx.taskId,
1001
- stepId: '',
1002
- stepKey,
1003
- type: 'threads_responded',
1004
- payload: { replied: responded.replied, resolved: responded.resolved },
1005
- });
1006
- return { outcome: 'ok', pointer: responded };
1007
- }
1008
- const integrated = result;
1009
- await appendEvent({
1010
- runId,
1011
- taskId: ctx.taskId,
1012
- stepId: '',
1013
- stepKey,
1014
- type: 'integrate_succeeded',
1015
- payload: {
1016
- prUrl: integrated.prUrl,
1017
- branch: integrated.branch,
1018
- prNumber: integrated.prNumber,
1019
- headSha: integrated.headSha,
1020
- status: integrated.status,
1021
- ...(integrated.issueRef ? { issueRef: integrated.issueRef } : {}),
1022
- },
1023
- });
1024
- return {
1025
- outcome: 'ok',
1026
- pointer: {
1027
- prUrl: integrated.prUrl,
1028
- branch: integrated.branch,
1029
- prNumber: integrated.prNumber,
1030
- headSha: integrated.headSha,
1031
- status: integrated.status,
1032
- ...(integrated.issueRef ? { issueRef: integrated.issueRef } : {}),
1033
- },
1034
- };
1244
+ const handler = scriptRegistry.get(decision.scriptRef) ?? scriptRegistry.get('script:integrator');
1245
+ return handler({ runId, decision, ctx, bindingByRef, stepKey, inputs });
1035
1246
  }
1036
1247
  async function finish(runId, status, verdict, steps, failureReason = '') {
1037
1248
  if (status === 'succeeded') {