@revisium/orchestrator 0.2.0-alpha.3 → 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 (259) 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 +394 -1122
  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/pr/inputs/pr-readiness.input.js +6 -0
  61. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  62. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  63. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  64. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  65. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  67. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  68. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  69. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  70. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  71. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  72. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  73. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  74. package/dist/cli/commands/doctor-report.js +1 -13
  75. package/dist/cli/commands/doctor-report.js.map +1 -1
  76. package/dist/cli/commands/lifecycle.js +50 -65
  77. package/dist/cli/commands/lifecycle.js.map +1 -1
  78. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  79. package/dist/cli/commands/process-helpers.js.map +1 -0
  80. package/dist/config.js +17 -55
  81. package/dist/config.js.map +1 -1
  82. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  83. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  84. package/dist/control-plane/bootstrap.js +25 -38
  85. package/dist/control-plane/bootstrap.js.map +1 -1
  86. package/dist/control-plane/change-notifications.js +45 -14
  87. package/dist/control-plane/change-notifications.js.map +1 -1
  88. package/dist/control-plane/data-access.js +1 -106
  89. package/dist/control-plane/data-access.js.map +1 -1
  90. package/dist/control-plane/default-playbook-policy.js +497 -94
  91. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  92. package/dist/control-plane/definitions.js +4 -5
  93. package/dist/control-plane/definitions.js.map +1 -1
  94. package/dist/control-plane/engine-transport.js +381 -0
  95. package/dist/control-plane/engine-transport.js.map +1 -0
  96. package/dist/control-plane/inbox.js +15 -7
  97. package/dist/control-plane/inbox.js.map +1 -1
  98. package/dist/control-plane/index.js +0 -1
  99. package/dist/control-plane/index.js.map +1 -1
  100. package/dist/control-plane/merge-override-audit.js +2 -2
  101. package/dist/control-plane/merge-override-audit.js.map +1 -1
  102. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  103. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  104. package/dist/control-plane/query-types.js +2 -0
  105. package/dist/control-plane/query-types.js.map +1 -0
  106. package/dist/control-plane/run-profiles.js +89 -0
  107. package/dist/control-plane/run-profiles.js.map +1 -0
  108. package/dist/control-plane/schema-migration.js +0 -35
  109. package/dist/control-plane/schema-migration.js.map +1 -1
  110. package/dist/control-plane/seed-default-playbook.js +2 -31
  111. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  112. package/dist/control-plane/tables.js +8 -0
  113. package/dist/control-plane/tables.js.map +1 -1
  114. package/dist/control-plane/transport.js +16 -0
  115. package/dist/control-plane/transport.js.map +1 -0
  116. package/dist/control-plane/versioned-meaning.js +56 -14
  117. package/dist/control-plane/versioned-meaning.js.map +1 -1
  118. package/dist/e2e/kit/agents.js +60 -6
  119. package/dist/e2e/kit/agents.js.map +1 -1
  120. package/dist/e2e/kit/assertions.js +13 -1
  121. package/dist/e2e/kit/assertions.js.map +1 -1
  122. package/dist/e2e/kit/drive.js +46 -11
  123. package/dist/e2e/kit/drive.js.map +1 -1
  124. package/dist/e2e/kit/env.js +17 -1
  125. package/dist/e2e/kit/env.js.map +1 -1
  126. package/dist/e2e/kit/fake-integrator.js +25 -5
  127. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  128. package/dist/e2e/kit/gh-emulator.js +158 -24
  129. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  130. package/dist/e2e/kit/harness.js +44 -10
  131. package/dist/e2e/kit/harness.js.map +1 -1
  132. package/dist/e2e/kit/index.js +1 -0
  133. package/dist/e2e/kit/index.js.map +1 -1
  134. package/dist/e2e/kit/scenario.js +165 -0
  135. package/dist/e2e/kit/scenario.js.map +1 -0
  136. package/dist/e2e/kit/scenarios.js +12 -18
  137. package/dist/e2e/kit/scenarios.js.map +1 -1
  138. package/dist/engine/dbos.service.js +8 -1
  139. package/dist/engine/dbos.service.js.map +1 -1
  140. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  141. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  142. package/dist/host/daemon.js +32 -8
  143. package/dist/host/daemon.js.map +1 -1
  144. package/dist/host/ensure-host.js +3 -3
  145. package/dist/host/ensure-host.js.map +1 -1
  146. package/dist/host/host-runtime.js.map +1 -1
  147. package/dist/host/host.lifecycle.js +3 -16
  148. package/dist/host/host.lifecycle.js.map +1 -1
  149. package/dist/http/graphql-host.js +17 -8
  150. package/dist/http/graphql-host.js.map +1 -1
  151. package/dist/mcp/mcp-capabilities.js +3 -2
  152. package/dist/mcp/mcp-capabilities.js.map +1 -1
  153. package/dist/mcp/mcp-facade.service.js +21 -0
  154. package/dist/mcp/mcp-facade.service.js.map +1 -1
  155. package/dist/mcp/mcp-tools.js +35 -1
  156. package/dist/mcp/mcp-tools.js.map +1 -1
  157. package/dist/pipeline/data-driven-task.workflow.js +276 -57
  158. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  159. package/dist/pipeline/pipeline.service.js +27 -5
  160. package/dist/pipeline/pipeline.service.js.map +1 -1
  161. package/dist/pipeline/route-contract.js +123 -3
  162. package/dist/pipeline/route-contract.js.map +1 -1
  163. package/dist/pipeline-core/condition-scopes.js +15 -0
  164. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  165. package/dist/pipeline-core/index.js +2 -0
  166. package/dist/pipeline-core/index.js.map +1 -1
  167. package/dist/pipeline-core/kit/fixtures.js +81 -44
  168. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  169. package/dist/pipeline-core/materialize.js +221 -0
  170. package/dist/pipeline-core/materialize.js.map +1 -0
  171. package/dist/pipeline-core/types.js +5 -0
  172. package/dist/pipeline-core/types.js.map +1 -1
  173. package/dist/pipeline-core/validate-loops.js +2 -15
  174. package/dist/pipeline-core/validate-loops.js.map +1 -1
  175. package/dist/pipeline-core/validate-resilience.js +257 -0
  176. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  177. package/dist/pipeline-core/validate.js +2 -0
  178. package/dist/pipeline-core/validate.js.map +1 -1
  179. package/dist/playbook/catalog-loader.js +39 -2
  180. package/dist/playbook/catalog-loader.js.map +1 -1
  181. package/dist/playbook/catalog-schema-validator.js +438 -0
  182. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  183. package/dist/playbook/import-mapper.js +41 -0
  184. package/dist/playbook/import-mapper.js.map +1 -1
  185. package/dist/playbook/manifest.js +5 -0
  186. package/dist/playbook/manifest.js.map +1 -1
  187. package/dist/playbook/playbook-installer.js +17 -1
  188. package/dist/playbook/playbook-installer.js.map +1 -1
  189. package/dist/poller/pr-readiness-core.js +257 -93
  190. package/dist/poller/pr-readiness-core.js.map +1 -1
  191. package/dist/poller/pr-readiness.js +2 -2
  192. package/dist/poller/pr-readiness.js.map +1 -1
  193. package/dist/revisium/inbox.service.js +8 -8
  194. package/dist/revisium/inbox.service.js.map +1 -1
  195. package/dist/revisium/playbooks.service.js +116 -21
  196. package/dist/revisium/playbooks.service.js.map +1 -1
  197. package/dist/revisium/revisium.module.js +8 -8
  198. package/dist/revisium/revisium.module.js.map +1 -1
  199. package/dist/revisium/roles.service.js +3 -1
  200. package/dist/revisium/roles.service.js.map +1 -1
  201. package/dist/revisium/run.service.js +9 -8
  202. package/dist/revisium/run.service.js.map +1 -1
  203. package/dist/revisium/tokens.js +0 -1
  204. package/dist/revisium/tokens.js.map +1 -1
  205. package/dist/run/create-run.js +1 -1
  206. package/dist/run/create-run.js.map +1 -1
  207. package/dist/run/inspect-run.js +32 -28
  208. package/dist/run/inspect-run.js.map +1 -1
  209. package/dist/run/issue-ref.js +97 -2
  210. package/dist/run/issue-ref.js.map +1 -1
  211. package/dist/run/prisma-runtime-data-access.js +619 -0
  212. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  213. package/dist/run/run-outputs.js +39 -14
  214. package/dist/run/run-outputs.js.map +1 -1
  215. package/dist/run/terminal-run-status.js.map +1 -1
  216. package/dist/runners/integrator.js +657 -56
  217. package/dist/runners/integrator.js.map +1 -1
  218. package/dist/runners/worktree.service.js +10 -8
  219. package/dist/runners/worktree.service.js.map +1 -1
  220. package/dist/schema/ajv-errors.js +15 -0
  221. package/dist/schema/ajv-errors.js.map +1 -0
  222. package/dist/smoke/isolation.js +7 -16
  223. package/dist/smoke/isolation.js.map +1 -1
  224. package/dist/storage/ensure-storage.js +117 -0
  225. package/dist/storage/ensure-storage.js.map +1 -0
  226. package/dist/storage/revo-database.js +39 -0
  227. package/dist/storage/revo-database.js.map +1 -0
  228. package/dist/storage/revo-prisma.service.js +31 -0
  229. package/dist/storage/revo-prisma.service.js.map +1 -0
  230. package/dist/storage/revo-storage.module.js +18 -0
  231. package/dist/storage/revo-storage.module.js.map +1 -0
  232. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  233. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  234. package/dist/task-control-plane/run-watch.service.js +3 -6
  235. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  236. package/dist/task-control-plane/task-control-plane-api.service.js +225 -43
  237. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  238. package/dist/testing/runtime-data-access.js +175 -0
  239. package/dist/testing/runtime-data-access.js.map +1 -0
  240. package/dist/worker/build-context.js +178 -2
  241. package/dist/worker/build-context.js.map +1 -1
  242. package/dist/worker/claude-code-runner.js +22 -3
  243. package/dist/worker/claude-code-runner.js.map +1 -1
  244. package/dist/worker/codex-runner.js +6 -1
  245. package/dist/worker/codex-runner.js.map +1 -1
  246. package/package.json +20 -7
  247. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  248. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  249. package/prisma/migrations/migration_lock.toml +3 -0
  250. package/prisma/schema.prisma +339 -0
  251. package/prisma.config.ts +11 -0
  252. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  253. package/dist/control-plane/client-transport.js +0 -168
  254. package/dist/control-plane/client-transport.js.map +0 -1
  255. package/dist/engine/ensure-postgres.js +0 -46
  256. package/dist/engine/ensure-postgres.js.map +0 -1
  257. package/dist/host/ensure-revisium.js +0 -135
  258. package/dist/host/ensure-revisium.js.map +0 -1
  259. 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,21 +376,91 @@ 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
+ }
332
447
  export function buildSystemScriptRegistry(deps) {
333
- const { appendEvent, releaseWorktreeFn, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub } = deps;
448
+ const { appendEvent, releaseWorktreeFn, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, overrideMergeFn, runOverrideStub, respondThreadsFn, runRespondStub, } = deps;
334
449
  function buildIntegratorInput(runId, ctx, inputs) {
335
- const { taskId, title, base, issueRef } = ctx;
450
+ const { taskId, title, base, issueRef, issueAction } = ctx;
336
451
  const change = producedChangeFromInputs(inputs);
337
452
  const mergeReadiness = mergeReadinessFromInputs(inputs);
338
- const changeForIntegrator = change ? changeWithRunIssueRef(change, issueRef) : undefined;
453
+ const changeForIntegrator = change ? changeWithRunIssueContract(change, issueRef, issueAction) : undefined;
339
454
  return {
340
455
  runId,
341
456
  taskId,
342
457
  title,
343
458
  base,
344
459
  ...(issueRef ? { issueRef } : {}),
460
+ ...(issueAction ? { issueAction } : {}),
345
461
  ...(changeForIntegrator ? { change: changeForIntegrator } : {}),
346
462
  ...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
463
+ ...(inputs.gateResolution === undefined ? {} : { gateResolution: inputs.gateResolution }),
347
464
  ...(mergeReadiness ? { mergeReadiness } : {}),
348
465
  };
349
466
  }
@@ -357,12 +474,13 @@ export function buildSystemScriptRegistry(deps) {
357
474
  result = useReal ? await desc.real(integratorInput) : desc.stub(integratorInput);
358
475
  }
359
476
  catch (err) {
477
+ const reason = err instanceof Error ? err.message : String(err);
360
478
  await appendEvent({
361
479
  runId, taskId: ctx.taskId, stepId: '', stepKey,
362
480
  type: 'step_failed',
363
- payload: { scriptRef: decision.scriptRef, error: err instanceof Error ? err.message : String(err) },
481
+ payload: { scriptRef: decision.scriptRef, error: reason },
364
482
  });
365
- return { outcome: 'failed' };
483
+ return { outcome: 'failed', reason };
366
484
  }
367
485
  if ('needsHuman' in result) {
368
486
  await appendEvent({
@@ -378,10 +496,38 @@ export function buildSystemScriptRegistry(deps) {
378
496
  };
379
497
  }
380
498
  const cleanupWorktree = async ({ runId, decision, ctx, stepKey }) => {
499
+ let releaseResult;
381
500
  try {
382
- await releaseWorktreeFn(runId, ctx.taskId);
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 };
383
530
  }
384
- catch { /* best-effort */ }
385
531
  await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
386
532
  return { outcome: 'ok', pointer: { released: true } };
387
533
  };
@@ -390,23 +536,9 @@ export function buildSystemScriptRegistry(deps) {
390
536
  stub: runStub,
391
537
  blockedReason: 'integrate',
392
538
  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
- },
539
+ eventType: integratorProgressEventType(result),
540
+ payload: integratorResultPointer(result),
541
+ pointer: integratorResultPointer(result),
410
542
  }),
411
543
  });
412
544
  const confirmMergeScript = makeIntegratorScript({
@@ -447,6 +579,53 @@ export function buildSystemScriptRegistry(deps) {
447
579
  verdict: result.verdict,
448
580
  }),
449
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
+ };
450
629
  const respondThreadsScript = makeIntegratorScript({
451
630
  real: respondThreadsFn,
452
631
  stub: runRespondStub,
@@ -461,6 +640,7 @@ export function buildSystemScriptRegistry(deps) {
461
640
  ['script:cleanupWorktree', cleanupWorktree],
462
641
  ['script:confirmMerge', confirmMergeScript],
463
642
  ['script:pollPr', pollPrScript],
643
+ ['script:overrideMerge', overrideMergeScript],
464
644
  ['script:respondThreads', respondThreadsScript],
465
645
  // Unknown refs fall through to this entry, matching the original else-branch behavior.
466
646
  ['script:integrator', integratorScript],
@@ -501,7 +681,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
501
681
  }
502
682
  async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
503
683
  if (!('produces' in node) || !node.produces)
504
- return;
684
+ return undefined;
505
685
  const row = {
506
686
  runId,
507
687
  nodeId: node.id,
@@ -515,6 +695,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
515
695
  list.push(row);
516
696
  outputsByNode.set(node.id, list);
517
697
  await appendRunOutput(row);
698
+ return row;
518
699
  }
519
700
  return async function dataDrivenTaskImpl(runId, opts) {
520
701
  try {
@@ -537,7 +718,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
537
718
  if (diagnostics.length > 0) {
538
719
  throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
539
720
  }
540
- const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
721
+ const { taskId, title, base, issueRef, issueAction } = await loadRunTaskContext(runId);
541
722
  const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
542
723
  if (live) {
543
724
  const pf = await preflightFn(taskId, base);
@@ -548,9 +729,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
548
729
  if (live) {
549
730
  await createWorktreeFn(runId, taskId, title, base, issueRef);
550
731
  }
551
- return runGraph(runId, opts, taskId, title, base, issueRef, live);
732
+ return runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live);
552
733
  }
553
- async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
734
+ async function runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live) {
554
735
  const { route, template, runnerRetryPolicy } = opts;
555
736
  const bindingByRef = new Map();
556
737
  for (const binding of route.roleBindings) {
@@ -565,7 +746,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
565
746
  let state = initialState(template);
566
747
  let lastResult;
567
748
  let lastVerdict = '';
749
+ let lastProducedOutput;
568
750
  let lastFailureReason = '';
751
+ let approvedMergeGateHeadShaValue;
569
752
  let stepCount = 0;
570
753
  const effectOrdinalByNode = new Map();
571
754
  const outputsByNode = new Map();
@@ -577,10 +760,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
577
760
  return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
578
761
  }
579
762
  const eff = await applyDecision(decision, {
580
- runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef,
763
+ runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef, issueAction,
581
764
  effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
582
765
  live,
583
766
  lastVerdict,
767
+ lastProducedOutput,
768
+ approvedMergeGateHeadSha: approvedMergeGateHeadShaValue,
584
769
  });
585
770
  stepCount += eff.stepDelta;
586
771
  if (eff.terminal) {
@@ -591,9 +776,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
591
776
  await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
592
777
  }
593
778
  lastResult = eff.lastResult;
594
- if (eff.lastVerdict !== undefined)
595
- lastVerdict = eff.lastVerdict;
779
+ lastVerdict = eff.lastVerdict ?? lastVerdict;
780
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
596
781
  lastFailureReason = eff.failureReason ?? '';
782
+ if (eff.approvedMergeGateHeadSha !== undefined) {
783
+ approvedMergeGateHeadShaValue = eff.approvedMergeGateHeadSha ?? undefined;
784
+ }
597
785
  }
598
786
  throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
599
787
  }
@@ -648,6 +836,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
648
836
  };
649
837
  let lastResult;
650
838
  let lastVerdict = '';
839
+ let lastProducedOutput;
651
840
  let stepDelta = 0;
652
841
  for (let i = 0; i < MAX_STEPS; i++) {
653
842
  const next = coreStep(branchTemplate, state, lastResult);
@@ -670,6 +859,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
670
859
  template: branchTemplate,
671
860
  state,
672
861
  lastVerdict,
862
+ lastProducedOutput,
673
863
  });
674
864
  stepDelta += eff.stepDelta;
675
865
  if (eff.terminal)
@@ -679,6 +869,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
679
869
  lastResult = eff.lastResult;
680
870
  if (eff.lastVerdict !== undefined)
681
871
  lastVerdict = eff.lastVerdict;
872
+ lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
682
873
  }
683
874
  throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
684
875
  }
@@ -691,11 +882,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
691
882
  const stepKey = stepKeyFor(node.id, ordinal);
692
883
  const resolved = resolveConsumes(node, ctx.outputsByNode);
693
884
  if ('missing' in resolved) {
885
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
694
886
  await appendEvent({
695
887
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
696
- payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
888
+ payload: { nodeId: node.id, error: reason },
697
889
  });
698
- 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 };
699
891
  }
700
892
  const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
701
893
  if ('blocked' in result) {
@@ -726,15 +918,16 @@ export function makeDataDrivenTask(runStepFn, deps) {
726
918
  await appendEvent({
727
919
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
728
920
  idempotencyKey: result.attemptId,
729
- payload: { nodeId: node.id, error: result.errorCode },
921
+ payload: { nodeId: node.id, error: result.errorCode, reason: result.reason },
730
922
  });
731
923
  return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
732
924
  }
733
- 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);
734
926
  const verdict = result.verdict;
735
927
  return {
736
928
  lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
737
929
  ...(verdict ? { lastVerdict: verdict } : {}),
930
+ ...(producedOutput ? { producedOutput } : {}),
738
931
  stepDelta: result.attemptsMade,
739
932
  };
740
933
  }
@@ -743,30 +936,54 @@ export function makeDataDrivenTask(runStepFn, deps) {
743
936
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, node.id);
744
937
  const resolved = resolveConsumes(node, ctx.outputsByNode);
745
938
  if ('missing' in resolved) {
939
+ const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
746
940
  await appendEvent({
747
941
  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` },
942
+ payload: { nodeId: node.id, error: reason },
749
943
  });
750
- 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 };
751
945
  }
752
- 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);
753
947
  if (scriptResult.outcome === 'blocked') {
754
948
  return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
755
949
  }
756
950
  if (scriptResult.outcome === 'failed') {
757
- 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 };
758
953
  }
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 };
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');
962
+ }
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
+ };
762
971
  }
763
972
  case 'awaitGate': {
764
973
  const topic = gateTopicFor(decision.reason);
765
974
  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);
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);
767
977
  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 };
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
+ };
770
987
  }
771
988
  case 'fork': {
772
989
  await appendEvent({
@@ -800,7 +1017,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
800
1017
  for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
801
1018
  const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
802
1019
  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);
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);
804
1022
  const needsHuman = await maybeHandleNeedsHumanRoleResult({
805
1023
  ...input,
806
1024
  attemptIds,
@@ -830,6 +1048,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
830
1048
  nodeId: node.id,
831
1049
  attemptId: physicalAttempt.attemptId,
832
1050
  ...(ctx.issueRef ? { issueRef: ctx.issueRef } : {}),
1051
+ ...(ctx.issueAction ? { issueAction: ctx.issueAction } : {}),
833
1052
  ...(artifactRef ? { artifactRef } : {}),
834
1053
  });
835
1054
  output = attachProducedChange(output, change);