@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
@@ -12,9 +12,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  import { Inject, Injectable } from '@nestjs/common';
14
14
  import { execFileSync } from 'node:child_process';
15
- import { collectPrReadiness, fetchRequiredCheckNames, } from '../poller/pr-readiness-core.js';
15
+ import { collectPrReadiness, fetchRequiredCheckNames, GITHUB_CHECK_ROLLUP_UNAVAILABLE, } from '../poller/pr-readiness-core.js';
16
16
  import { RunService } from '../revisium/run.service.js';
17
- import { issueRefTag } from '../run/issue-ref.js';
17
+ import { hasClosingIssueReference, hasIssueRefToken, issueBodyWithClosingReference, issueRefTag, } from '../run/issue-ref.js';
18
18
  import { resolveGhAccount, resolvePinnedGh } from './gh-identity.js';
19
19
  import { gitAbsPath, branchExists, countAhead } from './integrator-git.js';
20
20
  import { resolveOwnerRepo } from './integrator-remote.js';
@@ -22,14 +22,28 @@ import { branchName } from './integrator-branch-naming.js';
22
22
  export { resolveExecutable } from './integrator-git.js';
23
23
  export { branchName };
24
24
  export { parseOwnerRepo } from './integrator-remote.js';
25
- function issueBoundTitle(title, issueRef, ownerRepo) {
25
+ function issueBoundTitle(title, issueRef, ownerRepo, issueAction = issueRef ? 'close' : 'none') {
26
+ if (!issueRef || issueAction === 'none')
27
+ return title;
28
+ if (hasIssueRefToken(title, issueRef, ownerRepo))
29
+ return title;
26
30
  const tag = issueRefTag(issueRef, ownerRepo);
27
31
  return tag ? `${tag} ${title}` : title;
28
32
  }
29
- function commitMessage(title, issueRef, ownerRepo) {
33
+ function commitMessage(title, issueRef, ownerRepo, issueAction = issueRef ? 'close' : 'none') {
34
+ if (!issueRef || issueAction === 'none')
35
+ return `feat: ${title}`;
30
36
  const tag = issueRefTag(issueRef, ownerRepo);
31
37
  return tag ? `feat: ${tag} ${title}` : `feat: ${title}`;
32
38
  }
39
+ function prBody(body, issueRef, ownerRepo, issueAction) {
40
+ if (issueAction !== 'close')
41
+ return body ?? '';
42
+ return issueBodyWithClosingReference(body, issueRef, ownerRepo);
43
+ }
44
+ function resolvedIssueAction(issueRef, issueAction) {
45
+ return issueAction ?? (issueRef ? 'close' : undefined);
46
+ }
33
47
  function parsePrList(raw) {
34
48
  try {
35
49
  return JSON.parse(raw);
@@ -38,6 +52,22 @@ function parsePrList(raw) {
38
52
  throw new Error(`gh pr list returned non-JSON: ${raw.slice(0, 200)}`);
39
53
  }
40
54
  }
55
+ function prAuthorLogin(author) {
56
+ if (typeof author === 'string' && author.trim().length > 0)
57
+ return author.trim();
58
+ if (author && typeof author === 'object' && typeof author.login === 'string' && author.login.trim().length > 0) {
59
+ return author.login.trim();
60
+ }
61
+ return undefined;
62
+ }
63
+ function asRecord(value) {
64
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
65
+ return undefined;
66
+ return value;
67
+ }
68
+ function nonBlank(value) {
69
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
70
+ }
41
71
  function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
42
72
  const raw = execGh([
43
73
  'pr',
@@ -56,7 +86,14 @@ function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
56
86
  const pr = matching[0];
57
87
  if (!pr)
58
88
  throw new Error('unexpected empty match');
59
- return { prUrl: pr.url, prNumber: pr.number, ...(pr.headRefOid ? { headSha: pr.headRefOid } : {}) };
89
+ return {
90
+ prUrl: pr.url,
91
+ prNumber: pr.number,
92
+ ...(pr.headRefOid ? { headSha: pr.headRefOid } : {}),
93
+ ...(pr.title !== undefined ? { title: pr.title } : {}),
94
+ ...(pr.body !== undefined ? { body: pr.body } : {}),
95
+ ...(prAuthorLogin(pr.author) ? { author: prAuthorLogin(pr.author) } : {}),
96
+ };
60
97
  }
61
98
  if (matching.length > 1) {
62
99
  const candidates = matching.map((p) => `#${p.number}`).join(', ');
@@ -67,7 +104,9 @@ function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
67
104
  }
68
105
  return null;
69
106
  }
70
- function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
107
+ function createPr(ownerRepo, branch, base, title, issueRef, issueAction, execGh) {
108
+ const resolvedTitle = issueBoundTitle(title, issueRef, ownerRepo, issueAction);
109
+ const resolvedBody = prBody(undefined, issueRef, ownerRepo, issueAction);
71
110
  const createOut = execGh([
72
111
  'pr',
73
112
  'create',
@@ -79,9 +118,9 @@ function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
79
118
  '--head',
80
119
  branch,
81
120
  '--title',
82
- issueBoundTitle(title, issueRef, ownerRepo),
121
+ resolvedTitle,
83
122
  '--body',
84
- '',
123
+ resolvedBody,
85
124
  ]);
86
125
  const createdUrl = createOut.trim();
87
126
  const viewRaw = execGh([
@@ -104,19 +143,66 @@ function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
104
143
  'check if the PR was created and update the run manually',
105
144
  };
106
145
  }
107
- return { prUrl: viewData.url, prNumber: viewData.number };
146
+ return { prUrl: viewData.url, prNumber: viewData.number, title: resolvedTitle, body: resolvedBody };
108
147
  }
109
- function findOrCreatePr(ownerRepo, branch, base, title, issueRef, execGh) {
110
- const existing = matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName');
148
+ function repairPr(ownerRepo, prNumber, issueRef, issueAction, title, body, desiredTitle, desiredBody, execGh) {
149
+ if (!issueRef && issueAction !== 'close')
150
+ return;
151
+ const args = ['pr', 'edit', String(prNumber), '--repo', ownerRepo];
152
+ if ((title ?? '') !== desiredTitle)
153
+ args.push('--title', desiredTitle);
154
+ if ((body ?? '') !== desiredBody)
155
+ args.push('--body', desiredBody);
156
+ if (args.length > 5)
157
+ execGh(args);
158
+ }
159
+ function findOrCreatePr(ownerRepo, branch, base, title, issueRef, issueAction, execGh) {
160
+ const existing = matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,title,body');
111
161
  if (existing && !('needsHuman' in existing)) {
162
+ repairPr(ownerRepo, existing.prNumber, issueRef, issueAction, existing.title, existing.body, issueBoundTitle(existing.title || title, issueRef, ownerRepo, issueAction), prBody(existing.body, issueRef, ownerRepo, issueAction), execGh);
112
163
  return { prUrl: existing.prUrl, prNumber: existing.prNumber };
113
164
  }
114
165
  if (existing)
115
166
  return existing;
116
- return createPr(ownerRepo, branch, base, title, issueRef, execGh);
167
+ return createPr(ownerRepo, branch, base, title, issueRef, issueAction, execGh);
117
168
  }
118
169
  function findExistingPrWithHead(ownerRepo, branch, base, execGh) {
119
- return matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,headRefOid');
170
+ return matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,headRefOid,title,body,author');
171
+ }
172
+ function foreignPrProvenance(author) {
173
+ const integratorAccount = resolveGhAccount();
174
+ if (!author || author.toLowerCase() === integratorAccount.toLowerCase())
175
+ return {};
176
+ return { foreignPr: true, prAuthor: author, integratorAccount };
177
+ }
178
+ function repairProducedChangePr(context, existing, provenance) {
179
+ if (provenance.foreignPr)
180
+ return;
181
+ repairPr(context.ownerRepo, existing.prNumber, context.issueRef, context.issueAction, existing.title, existing.body, issueBoundTitle(existing.title || context.title, context.issueRef, context.ownerRepo, context.issueAction), prBody(existing.body, context.issueRef, context.ownerRepo, context.issueAction), context.execGh);
182
+ }
183
+ function existingProducedChangeOutput(context, existing, provenance, status) {
184
+ return {
185
+ prUrl: existing.prUrl,
186
+ branch: context.branch,
187
+ prNumber: existing.prNumber,
188
+ ...(context.issueRef ? { issueRef: context.issueRef } : {}),
189
+ headSha: context.change.headSha,
190
+ status,
191
+ ...(status === 'noop' ? { message: 'nothing to integrate — produced head already pushed and equals PR head' } : {}),
192
+ ...provenance,
193
+ };
194
+ }
195
+ function reuseExistingProducedChangePr(context, existing) {
196
+ if (existing.headSha !== context.change.headSha)
197
+ return null;
198
+ const provenance = foreignPrProvenance(existing.author);
199
+ repairProducedChangePr(context, existing, provenance);
200
+ return existingProducedChangeOutput(context, existing, provenance, 'noop');
201
+ }
202
+ function updateExistingProducedChangePr(context, existing) {
203
+ const provenance = foreignPrProvenance(existing.author);
204
+ repairProducedChangePr(context, existing, provenance);
205
+ return existingProducedChangeOutput(context, existing, provenance, 'pushed');
120
206
  }
121
207
  export async function preflightLive(taskId, base, deps) {
122
208
  const { execGit, resolveTaskCwd } = deps;
@@ -186,6 +272,7 @@ export async function captureProducedChange(input, deps) {
186
272
  const branch = branchName(input.taskId, input.title, input.issueRef);
187
273
  const ownerRepoResult = resolveOwnerRepo(git, cwd);
188
274
  const ownerRepo = 'needsHuman' in ownerRepoResult ? undefined : ownerRepoResult.ownerRepo;
275
+ const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
189
276
  if (branchExists(git, cwd, branch)) {
190
277
  git(['switch', branch], cwd);
191
278
  }
@@ -194,13 +281,14 @@ export async function captureProducedChange(input, deps) {
194
281
  }
195
282
  git(['add', '-A'], cwd);
196
283
  if (stagedDiffPresent(git, cwd)) {
197
- git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo)], cwd);
284
+ git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo, issueAction)], cwd);
198
285
  }
199
286
  const headSha = git(['rev-parse', 'HEAD'], cwd).trim();
200
287
  return {
201
288
  branch,
202
289
  headSha,
203
290
  ...(input.issueRef ? { issueRef: input.issueRef } : {}),
291
+ ...(issueAction ? { issueAction } : {}),
204
292
  worktreePath: cwd,
205
293
  ...(input.artifactRef ? { artifactRef: input.artifactRef } : {}),
206
294
  };
@@ -224,6 +312,7 @@ export async function integrate(input, deps) {
224
312
  if ('needsHuman' in ownerRepoResult)
225
313
  return ownerRepoResult;
226
314
  const { ownerRepo } = ownerRepoResult;
315
+ const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
227
316
  git(['fetch', 'origin', input.base], cwd);
228
317
  if (branchExists(git, cwd, branch)) {
229
318
  git(['switch', branch], cwd);
@@ -233,7 +322,7 @@ export async function integrate(input, deps) {
233
322
  }
234
323
  git(['add', '-A'], cwd);
235
324
  if (stagedDiffPresent(git, cwd)) {
236
- const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo);
325
+ const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo, issueAction);
237
326
  git(['commit', '-m', commitMsg], cwd);
238
327
  }
239
328
  else {
@@ -256,7 +345,7 @@ export async function integrate(input, deps) {
256
345
  }
257
346
  }
258
347
  git(['push', '-u', 'origin', branch], cwd);
259
- const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, gh);
348
+ const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, issueAction, gh);
260
349
  if ('needsHuman' in prResult)
261
350
  return prResult;
262
351
  return { prUrl: prResult.prUrl, branch, prNumber: prResult.prNumber, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
@@ -266,6 +355,7 @@ async function integrateProducedChange(input, deps, change) {
266
355
  const cwd = change.worktreePath ?? await deps.resolveRunCwd(input.runId, input.taskId);
267
356
  const branch = change.branch;
268
357
  const issueRef = change.issueRef ?? input.issueRef;
358
+ const issueAction = resolvedIssueAction(issueRef, change.issueAction ?? input.issueAction);
269
359
  const ownerRepoResult = resolveOwnerRepo(git, cwd);
270
360
  if ('needsHuman' in ownerRepoResult)
271
361
  return ownerRepoResult;
@@ -274,16 +364,11 @@ async function integrateProducedChange(input, deps, change) {
274
364
  const existing = findExistingPrWithHead(ownerRepo, branch, input.base, gh);
275
365
  if (existing && 'needsHuman' in existing)
276
366
  return existing;
277
- if (existing?.headSha === change.headSha) {
278
- return {
279
- prUrl: existing.prUrl,
280
- branch,
281
- prNumber: existing.prNumber,
282
- ...(issueRef ? { issueRef } : {}),
283
- headSha: change.headSha,
284
- status: 'noop',
285
- message: 'nothing to integrate — produced head already pushed and equals PR head',
286
- };
367
+ const prContext = { ownerRepo, branch, title: input.title, issueRef, issueAction, change, execGh: gh };
368
+ if (existing) {
369
+ const reused = reuseExistingProducedChangePr(prContext, existing);
370
+ if (reused)
371
+ return reused;
287
372
  }
288
373
  if (!existing && countAhead(git, cwd, change.headSha, input.base) === 0) {
289
374
  return {
@@ -294,16 +379,9 @@ async function integrateProducedChange(input, deps, change) {
294
379
  }
295
380
  git(['push', 'origin', `${change.headSha}:refs/heads/${branch}`], cwd);
296
381
  if (existing) {
297
- return {
298
- prUrl: existing.prUrl,
299
- branch,
300
- prNumber: existing.prNumber,
301
- ...(issueRef ? { issueRef } : {}),
302
- headSha: change.headSha,
303
- status: 'pushed',
304
- };
382
+ return updateExistingProducedChangePr(prContext, existing);
305
383
  }
306
- const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, gh);
384
+ const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, issueAction, gh);
307
385
  if ('needsHuman' in created)
308
386
  return created;
309
387
  return {
@@ -315,6 +393,13 @@ async function integrateProducedChange(input, deps, change) {
315
393
  status: 'pushed',
316
394
  };
317
395
  }
396
+ function mergeabilityForConfirmMerge(pr, overrideAccepted) {
397
+ if (!overrideAccepted) {
398
+ return pr.mergeStateStatus === 'CLEAN' ? 'clean' : 'blocked';
399
+ }
400
+ const mergeable = pr.mergeable ?? (pr.mergeStateStatus === 'CLEAN' ? 'MERGEABLE' : undefined);
401
+ return mergeSignal(pr.mergeStateStatus, mergeable);
402
+ }
318
403
  export async function confirmMerge(input, deps) {
319
404
  const { execGit: git, execGh: gh, resolveRunCwd } = deps;
320
405
  const cwd = await resolveRunCwd(input.runId, input.taskId);
@@ -323,8 +408,9 @@ export async function confirmMerge(input, deps) {
323
408
  if ('needsHuman' in ownerRepoResult)
324
409
  return ownerRepoResult;
325
410
  const { ownerRepo } = ownerRepoResult;
411
+ const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
326
412
  const view = () => {
327
- const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus']);
413
+ const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus,mergeable,closingIssuesReferences']);
328
414
  try {
329
415
  return JSON.parse(raw);
330
416
  }
@@ -338,7 +424,9 @@ export async function confirmMerge(input, deps) {
338
424
  if (pr.state !== 'OPEN') {
339
425
  return { needsHuman: true, lesson: `PR #${pr.number} is ${pr.state} (not OPEN) and not merged — resolve manually` };
340
426
  }
341
- if (pr.mergeStateStatus !== 'CLEAN') {
427
+ const overrideAccepted = input.mergeReadiness?.override?.accepted === true;
428
+ const mergeability = mergeabilityForConfirmMerge(pr, overrideAccepted);
429
+ if (mergeability !== 'clean') {
342
430
  return {
343
431
  needsHuman: true,
344
432
  lesson: `PR #${pr.number} is not auto-mergeable (mergeStateStatus=${pr.mergeStateStatus}) — CI not green, ` +
@@ -352,6 +440,12 @@ export async function confirmMerge(input, deps) {
352
440
  lesson: `PR #${pr.number} merge requires a fresh merge readiness headSha guard — re-run readiness before approving merge`,
353
441
  };
354
442
  }
443
+ if (issueAction === 'close' && input.issueRef && !hasClosingIssueReference(pr.closingIssuesReferences, input.issueRef)) {
444
+ return {
445
+ needsHuman: true,
446
+ lesson: `PR #${pr.number} is expected to close ${issueRefTag(input.issueRef, ownerRepo)} but GitHub closingIssuesReferences does not include it`,
447
+ };
448
+ }
355
449
  if (pr.isDraft) {
356
450
  gh(['pr', 'ready', branch, '--repo', ownerRepo]);
357
451
  }
@@ -375,14 +469,22 @@ function envInt(name, fallback) {
375
469
  const n = Number.parseInt(process.env[name] ?? '', 10);
376
470
  return Number.isFinite(n) && n > 0 ? n : fallback;
377
471
  }
378
- function defaultCollect(repo, branch, base, execGh, issueRef) {
379
- return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
472
+ function defaultCollect(repo, branch, base, execGh, issueRef, issueAction) {
473
+ return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, issueAction, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
380
474
  pr: { number: r.pr.number, headSha: r.pr.headSha },
381
475
  checks: { pending: r.checks.pending, fail: r.checks.fail, list: r.checks.list },
382
- reviewThreads: { items: r.reviewThreads.items },
476
+ reviewThreads: {
477
+ items: r.reviewThreads.items,
478
+ unresolvedCount: r.reviewThreads.unresolvedCount,
479
+ ...(r.reviewThreads.truncated ? { truncated: true } : {}),
480
+ },
383
481
  readinessVerdict: r.verdict,
384
482
  nextAction: r.nextAction,
385
483
  evidence: r.evidence,
484
+ mergeStateStatus: r.pr.mergeState,
485
+ mergeable: r.ciSummary.mergeable,
486
+ draft: r.pr.draft,
487
+ feedback: r.feedback,
386
488
  }));
387
489
  }
388
490
  const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -428,8 +530,364 @@ function unsettledReadinessFeedback(input, readiness, reason) {
428
530
  verdict: 'recheck',
429
531
  ciFailures: ciFailuresFrom(readiness),
430
532
  reviewThreads,
533
+ ...(readiness.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
534
+ ...(readiness.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
535
+ };
536
+ }
537
+ // UNSTABLE/HAS_HOOKS: GitHub marks a PR mergeable even when non-required checks are unsettled.
538
+ // Required-check gating is handled upstream; this quirk is intentional for advisory-only scenarios.
539
+ export function mergeSignal(mergeStateStatus, mergeable) {
540
+ const ms = (mergeStateStatus ?? '').toUpperCase();
541
+ const mg = (mergeable ?? '').toUpperCase();
542
+ if (mg === 'CONFLICTING' || ms === 'DIRTY' || ms === 'BLOCKED' || ms === 'BEHIND')
543
+ return 'blocked';
544
+ if (mg === 'MERGEABLE' && (ms === 'CLEAN' || ms === 'UNSTABLE' || ms === 'HAS_HOOKS'))
545
+ return 'clean';
546
+ return 'unknown';
547
+ }
548
+ function mergeOverrideGate(input) {
549
+ const resolution = asRecord(input.gateResolution);
550
+ const audit = asRecord(resolution?.['mergeOverrideAudit']);
551
+ const note = nonBlank(resolution?.['note']);
552
+ if (!resolution || !note || !audit) {
553
+ return {
554
+ needsHuman: true,
555
+ lesson: 'override_merge requires a non-empty note and mergeOverrideAudit from the merge gate',
556
+ };
557
+ }
558
+ const actor = nonBlank(audit.actor) ?? nonBlank(resolution['resolvedBy']) ?? 'operator';
559
+ const trustedHeadSha = nonBlank(resolution['trustedGateHeadSha']);
560
+ return {
561
+ note,
562
+ audit,
563
+ actor,
564
+ ...(trustedHeadSha ? { trustedHeadSha } : {}),
565
+ source: { gate: 'mergeGate', inboxId: nonBlank(resolution['inboxId']) ?? '' },
566
+ };
567
+ }
568
+ function prReviewThreads(readiness) {
569
+ return readiness.reviewThreads.items.map((thread) => ({
570
+ threadId: thread.id,
571
+ path: thread.path,
572
+ line: thread.line,
573
+ author: thread.author,
574
+ body: thread.body,
575
+ }));
576
+ }
577
+ function checkFact(severity, kind, name, result) {
578
+ return { severity, kind, name, summary: `${name}: ${result}`, evidence: result };
579
+ }
580
+ function emptyFactBuckets() {
581
+ return { hard: [], advisory: [] };
582
+ }
583
+ function pushFact(buckets, fact) {
584
+ buckets[fact.severity].push(fact);
585
+ }
586
+ function reviewThreadFact(thread) {
587
+ return {
588
+ severity: 'advisory',
589
+ kind: 'review_thread',
590
+ threadId: thread.id,
591
+ summary: thread.body,
592
+ evidence: [thread.path, thread.line].filter((item) => item !== undefined).join(':') || thread.url || thread.id,
593
+ };
594
+ }
595
+ function feedbackEvidence(item) {
596
+ return item.evidence ?? item.location ?? item.author ?? item.summary;
597
+ }
598
+ function classifyFeedbackFacts(readiness) {
599
+ const facts = emptyFactBuckets();
600
+ const feedback = readiness.feedback;
601
+ if (!feedback)
602
+ return facts;
603
+ for (const fix of feedback.developerFixes) {
604
+ if (fix.source === 'ci' || fix.source === 'review_thread')
605
+ continue;
606
+ const fact = {
607
+ severity: 'advisory',
608
+ kind: fix.source,
609
+ summary: fix.summary,
610
+ evidence: feedbackEvidence(fix),
611
+ };
612
+ pushFact(facts, fix.source === 'human_review' ? { ...fact, severity: 'hard' } : fact);
613
+ }
614
+ for (const question of feedback.reviewerQuestions) {
615
+ pushFact(facts, {
616
+ severity: 'hard',
617
+ kind: 'reviewer_question',
618
+ summary: question.summary,
619
+ evidence: feedbackEvidence(question),
620
+ });
621
+ }
622
+ for (const decision of feedback.humanDecisions) {
623
+ pushFact(facts, {
624
+ severity: 'hard',
625
+ kind: decision.source,
626
+ summary: decision.summary,
627
+ });
628
+ }
629
+ for (const wait of feedback.providerWait) {
630
+ pushFact(facts, {
631
+ severity: 'advisory',
632
+ kind: `provider_wait:${wait.provider}`,
633
+ summary: wait.evidence,
634
+ evidence: wait.reason,
635
+ });
636
+ }
637
+ return facts;
638
+ }
639
+ function checkStatusFact(check, required, status) {
640
+ const severity = required ? 'hard' : 'advisory';
641
+ const kind = `${required ? 'required' : 'non_required'}_check_${status}`;
642
+ return checkFact(severity, kind, check.name, check.result);
643
+ }
644
+ function pushCheckStatusFact(facts, check, names, required, status) {
645
+ if (!names.includes(check.name))
646
+ return;
647
+ pushFact(facts, checkStatusFact(check, required, status));
648
+ }
649
+ function factsFromRequiredChecks(readiness, required) {
650
+ const facts = emptyFactBuckets();
651
+ for (const check of readiness.checks.list) {
652
+ const isRequired = required.has(check.name);
653
+ pushCheckStatusFact(facts, check, readiness.checks.fail, isRequired, 'failed');
654
+ pushCheckStatusFact(facts, check, readiness.checks.pending, isRequired, 'pending');
655
+ }
656
+ if (readiness.checks.list.length === 0) {
657
+ pushFact(facts, {
658
+ severity: 'advisory',
659
+ kind: 'checks_none_registered',
660
+ summary: 'No registered checks were reported for the PR.',
661
+ evidence: 'checks: none registered',
662
+ });
663
+ }
664
+ return facts;
665
+ }
666
+ function checkRollupUnavailableFact(readiness) {
667
+ const unavailable = readiness.checks.pending.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE)
668
+ || readiness.evidence.some((item) => item.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE));
669
+ if (!unavailable)
670
+ return undefined;
671
+ return {
672
+ severity: 'hard',
673
+ kind: 'check_rollup_unavailable',
674
+ summary: 'GitHub check rollup is unavailable; override cannot distinguish pending checks from no registered checks.',
675
+ evidence: GITHUB_CHECK_ROLLUP_UNAVAILABLE,
676
+ };
677
+ }
678
+ function reviewThreadsIncompleteFact(readiness) {
679
+ if (readiness.reviewThreads.truncated !== true)
680
+ return undefined;
681
+ return {
682
+ severity: 'hard',
683
+ kind: 'review_threads_incomplete',
684
+ summary: 'Review thread data is incomplete; override cannot safely resolve every unresolved thread.',
685
+ evidence: `unresolvedCount=${readiness.reviewThreads.unresolvedCount ?? readiness.reviewThreads.items.length}`,
431
686
  };
432
687
  }
688
+ function mergeOverrideOutput(input) {
689
+ const readiness = input.readiness;
690
+ const evidence = [
691
+ ...(readiness?.evidence ?? []),
692
+ ...(readiness ? readinessEvidence(readiness) : []),
693
+ ...input.facts.map((fact) => `${fact.severity} ${fact.kind}: ${fact.summary}`),
694
+ input.reason,
695
+ `PR headSha=${readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha}`,
696
+ `overrideMerge verdict=${input.verdict}`,
697
+ ].filter((item) => typeof item === 'string' && item.length > 0);
698
+ return {
699
+ prNumber: readiness?.pr.number ?? null,
700
+ headSha: readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha,
701
+ evidence,
702
+ verdict: input.verdict,
703
+ ciFailures: readiness ? ciFailuresFrom(readiness) : [],
704
+ reviewThreads: readiness ? prReviewThreads(readiness) : [],
705
+ ...(readiness?.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
706
+ ...(readiness?.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
707
+ override: {
708
+ accepted: input.accepted,
709
+ actor: input.gate.actor,
710
+ note: input.gate.note,
711
+ audit: {
712
+ reason: input.gate.audit.reason,
713
+ risk: input.gate.audit.risk,
714
+ verificationResponsibility: input.gate.audit.verificationResponsibility,
715
+ headSha: input.gate.audit.headSha,
716
+ },
717
+ source: input.gate.source,
718
+ facts: input.facts,
719
+ replied: input.response?.replied ?? 0,
720
+ resolved: input.response?.resolved ?? 0,
721
+ ...(input.reason ? { reason: input.reason } : {}),
722
+ },
723
+ };
724
+ }
725
+ function hardMergeabilityFact(readiness) {
726
+ const signal = mergeSignal(readiness.mergeStateStatus, readiness.mergeable);
727
+ if (signal === 'clean')
728
+ return undefined;
729
+ return {
730
+ severity: 'hard',
731
+ kind: signal === 'blocked' ? 'mergeability_blocked' : 'mergeability_unknown',
732
+ summary: `mergeStateStatus=${readiness.mergeStateStatus ?? ''} mergeable=${readiness.mergeable ?? ''}`,
733
+ evidence: `mergeStateStatus=${readiness.mergeStateStatus ?? ''}; mergeable=${readiness.mergeable ?? ''}`,
734
+ };
735
+ }
736
+ function movedHeadFact(readiness, gate) {
737
+ const approvedHeadSha = gate.trustedHeadSha;
738
+ if (!approvedHeadSha) {
739
+ return {
740
+ severity: 'hard',
741
+ kind: 'trusted_gate_head_unavailable',
742
+ summary: 'Trusted merge gate head is unavailable.',
743
+ evidence: `audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
744
+ };
745
+ }
746
+ if (readiness.pr.headSha === approvedHeadSha)
747
+ return undefined;
748
+ return {
749
+ severity: 'hard',
750
+ kind: 'head_moved',
751
+ summary: `approved head ${approvedHeadSha} no longer matches fresh head ${readiness.pr.headSha}`,
752
+ evidence: `approved=${approvedHeadSha}; audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
753
+ };
754
+ }
755
+ function auditHeadMismatchFact(gate) {
756
+ if (!gate.trustedHeadSha)
757
+ return undefined;
758
+ if (gate.audit.headSha === gate.trustedHeadSha)
759
+ return undefined;
760
+ return {
761
+ severity: 'hard',
762
+ kind: 'audit_head_mismatch',
763
+ summary: `audit head ${gate.audit.headSha} does not match trusted merge gate head ${gate.trustedHeadSha}`,
764
+ evidence: `audit=${gate.audit.headSha}; approved=${gate.trustedHeadSha}`,
765
+ };
766
+ }
767
+ function initialMergeOverrideHardFacts(readiness, gate, unavailableRollup) {
768
+ return [
769
+ readiness.draft === true ? { severity: 'hard', kind: 'draft_pr', summary: 'PR is still draft.' } : undefined,
770
+ auditHeadMismatchFact(gate),
771
+ movedHeadFact(readiness, gate),
772
+ hardMergeabilityFact(readiness),
773
+ unavailableRollup,
774
+ reviewThreadsIncompleteFact(readiness),
775
+ ].filter((fact) => fact !== undefined);
776
+ }
777
+ function appendFeedbackFacts(buckets, readiness) {
778
+ for (const thread of readiness.reviewThreads.items) {
779
+ buckets.advisory.push(reviewThreadFact(thread));
780
+ }
781
+ const feedbackFacts = classifyFeedbackFacts(readiness);
782
+ buckets.hard.push(...feedbackFacts.hard);
783
+ buckets.advisory.push(...feedbackFacts.advisory);
784
+ }
785
+ function classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks) {
786
+ if (readiness.checks.fail.length === 0 && readiness.checks.pending.length === 0) {
787
+ return readiness.checks.list.length === 0 ? factsFromRequiredChecks(readiness, new Set()) : emptyFactBuckets();
788
+ }
789
+ if (readiness.pr.number === null) {
790
+ return {
791
+ hard: [{ severity: 'hard', kind: 'pr_number_unavailable', summary: 'Cannot classify required checks without a PR number.' }],
792
+ advisory: [],
793
+ };
794
+ }
795
+ try {
796
+ return factsFromRequiredChecks(readiness, requiredChecks(ownerRepo, readiness.pr.number, execGh));
797
+ }
798
+ catch (err) {
799
+ return {
800
+ hard: [{
801
+ severity: 'hard',
802
+ kind: 'required_check_names_unavailable',
803
+ summary: 'Required check names are unavailable.',
804
+ evidence: err instanceof Error ? err.message : String(err),
805
+ }],
806
+ advisory: [],
807
+ };
808
+ }
809
+ }
810
+ async function classifyMergeOverrideFacts(readiness, gate, ownerRepo, execGh, requiredChecks) {
811
+ const unavailableRollup = checkRollupUnavailableFact(readiness);
812
+ const facts = {
813
+ hard: initialMergeOverrideHardFacts(readiness, gate, unavailableRollup),
814
+ advisory: [],
815
+ };
816
+ if (!unavailableRollup) {
817
+ const checkFacts = classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks);
818
+ facts.hard.push(...checkFacts.hard);
819
+ facts.advisory.push(...checkFacts.advisory);
820
+ }
821
+ appendFeedbackFacts(facts, readiness);
822
+ return facts;
823
+ }
824
+ function overrideThreadTriage(readiness, note) {
825
+ return {
826
+ items: readiness.reviewThreads.items.map((thread) => ({
827
+ threadId: thread.id,
828
+ decision: 'wontfix',
829
+ replyText: `merged by operator override: ${note}`,
830
+ })),
831
+ };
832
+ }
833
+ export async function overrideMerge(input, deps) {
834
+ const gate = mergeOverrideGate(input);
835
+ if ('needsHuman' in gate)
836
+ return gate;
837
+ const { execGit: git, execGh: gh, resolveRunCwd } = deps;
838
+ const cwd = await resolveRunCwd(input.runId, input.taskId);
839
+ const branch = branchName(input.taskId, input.title, input.issueRef);
840
+ const ownerRepoResult = resolveOwnerRepo(git, cwd);
841
+ if ('needsHuman' in ownerRepoResult)
842
+ return ownerRepoResult;
843
+ const { ownerRepo } = ownerRepoResult;
844
+ let readiness;
845
+ try {
846
+ readiness = await (deps.collect ?? defaultCollect)(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
847
+ }
848
+ catch (err) {
849
+ const fact = {
850
+ severity: 'hard',
851
+ kind: 'readiness_transport_degraded',
852
+ summary: 'Fresh override reverify could not read PR readiness.',
853
+ evidence: err instanceof Error ? err.message : String(err),
854
+ };
855
+ return mergeOverrideOutput({
856
+ gate,
857
+ verdict: 'recheck',
858
+ facts: [fact],
859
+ accepted: false,
860
+ reason: fact.summary,
861
+ });
862
+ }
863
+ if (readiness.readinessVerdict === 'merged') {
864
+ return mergeOverrideOutput({ gate, readiness, verdict: 'merged', facts: [], accepted: false, reason: 'PR already merged externally.' });
865
+ }
866
+ if (readiness.readinessVerdict === 'closed') {
867
+ const fact = { severity: 'hard', kind: 'pr_closed_externally', summary: 'PR is closed without being merged.' };
868
+ return mergeOverrideOutput({ gate, readiness, verdict: 'closed', facts: [fact], accepted: false, reason: fact.summary });
869
+ }
870
+ const facts = await classifyMergeOverrideFacts(readiness, gate, ownerRepo, gh, deps.requiredChecks ?? fetchRequiredCheckNames);
871
+ if (facts.hard.length > 0) {
872
+ return mergeOverrideOutput({
873
+ gate,
874
+ readiness,
875
+ verdict: 'recheck',
876
+ facts: facts.hard,
877
+ accepted: false,
878
+ reason: 'override_merge refused because hard blockers remain',
879
+ });
880
+ }
881
+ const response = await respondThreads(overrideThreadTriage(readiness, gate.note), { execGh: gh });
882
+ return mergeOverrideOutput({
883
+ gate,
884
+ readiness,
885
+ verdict: 'clean',
886
+ facts: facts.advisory,
887
+ accepted: true,
888
+ response,
889
+ });
890
+ }
433
891
  export async function pollPr(input, deps) {
434
892
  const { execGit: git, execGh: gh, resolveRunCwd } = deps;
435
893
  const collect = deps.collect ?? defaultCollect;
@@ -446,9 +904,9 @@ export async function pollPr(input, deps) {
446
904
  let readiness;
447
905
  let lastReadiness;
448
906
  for (let i = 0; i < maxPolls; i++) {
449
- readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
907
+ readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
450
908
  lastReadiness = readiness;
451
- if (readiness.checks.pending.length === 0 && readiness.checks.list.length > 0)
909
+ if (readiness.checks.pending.length === 0)
452
910
  break;
453
911
  readiness = undefined;
454
912
  if (i < maxPolls - 1)
@@ -468,6 +926,51 @@ export async function pollPr(input, deps) {
468
926
  return { needsHuman: true, lesson: `pollPr timed out after ${maxPolls} polls before reading PR readiness for ${branch}` };
469
927
  }
470
928
  let settled = readiness;
929
+ const makeTerminalFeedback = (verdict) => ({
930
+ prNumber: settled.pr.number ?? null,
931
+ headSha: settled.pr.headSha,
932
+ evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
933
+ ...(input.issueRef ? { issueRef: input.issueRef } : {}),
934
+ verdict,
935
+ ciFailures: ciFailuresFrom(settled),
936
+ reviewThreads: [],
937
+ ...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
938
+ ...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
939
+ });
940
+ if (settled.readinessVerdict === 'merged')
941
+ return makeTerminalFeedback('merged');
942
+ if (settled.readinessVerdict === 'closed')
943
+ return makeTerminalFeedback('closed');
944
+ const standardCheckResults = new Set('ACTION_REQUIRED CANCELLED ERROR EXPECTED FAILURE IN_PROGRESS NEUTRAL PENDING QUEUED SKIPPED STALE STARTUP_FAILURE SUCCESS TIMED_OUT UNKNOWN'.split(' '));
945
+ const standardMergeStateStatuses = new Set('BEHIND BLOCKED CLEAN DIRTY DRAFT HAS_HOOKS UNKNOWN UNSTABLE'.split(' '));
946
+ const standardMergeableStates = new Set('CONFLICTING MERGEABLE UNKNOWN'.split(' '));
947
+ const addUnclassifiableState = (states, label, value, known) => {
948
+ const state = (value ?? '').trim().toUpperCase();
949
+ if (state !== '' && !known.has(state))
950
+ states.push(`${label} ${state}`);
951
+ };
952
+ const unclassifiablePollState = (snapshot) => {
953
+ const states = [];
954
+ for (const check of snapshot.checks.list) {
955
+ addUnclassifiableState(states, 'check result', check.result, standardCheckResults);
956
+ }
957
+ addUnclassifiableState(states, 'mergeStateStatus', snapshot.mergeStateStatus, standardMergeStateStatuses);
958
+ addUnclassifiableState(states, 'mergeable', snapshot.mergeable, standardMergeableStates);
959
+ return states;
960
+ };
961
+ const unclassifiableReadinessBlock = (snapshot, states) => ({
962
+ needsHuman: true,
963
+ lesson: [
964
+ ...snapshot.evidence,
965
+ ...readinessEvidence(snapshot),
966
+ `pollPr unclassifiable readiness state: ${states.join(', ')}`,
967
+ `PR headSha=${snapshot.pr.headSha}`,
968
+ ].join('; '),
969
+ });
970
+ const unclassifiable = unclassifiablePollState(settled);
971
+ if (unclassifiable.length > 0) {
972
+ return unclassifiableReadinessBlock(settled, unclassifiable);
973
+ }
471
974
  const initialCiFailures = ciFailuresFrom(settled);
472
975
  if (initialCiFailures.length === 0) {
473
976
  try {
@@ -478,14 +981,19 @@ export async function pollPr(input, deps) {
478
981
  const reviewGracePolls = deps.reviewGracePolls ?? envInt('REVO_POLL_PR_REVIEW_GRACE_POLLS', 4);
479
982
  for (let i = 0; i < reviewGracePolls && settled.reviewThreads.items.length === 0; i++) {
480
983
  await sleep(intervalMs);
481
- settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
984
+ settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
482
985
  }
483
986
  }
484
- if (settled.checks.pending.length > 0 || settled.checks.list.length === 0) {
485
- const detail = settled.checks.pending.length > 0
486
- ? `pending checks: ${settled.checks.pending.join(', ')}`
487
- : 'no checks registered';
488
- return unsettledReadinessFeedback(input, settled, `pollPr found unsettled readiness after readying ${branch}: ${detail}`);
987
+ if (settled.readinessVerdict === 'merged')
988
+ return makeTerminalFeedback('merged');
989
+ if (settled.readinessVerdict === 'closed')
990
+ return makeTerminalFeedback('closed');
991
+ const finalUnclassifiable = unclassifiablePollState(settled);
992
+ if (finalUnclassifiable.length > 0) {
993
+ return unclassifiableReadinessBlock(settled, finalUnclassifiable);
994
+ }
995
+ if (settled.checks.pending.length > 0) {
996
+ return unsettledReadinessFeedback(input, settled, `pollPr found unsettled readiness after readying ${branch}: pending checks: ${settled.checks.pending.join(', ')}`);
489
997
  }
490
998
  const reviewThreads = settled.reviewThreads.items.map((t) => ({
491
999
  threadId: t.id,
@@ -496,27 +1004,51 @@ export async function pollPr(input, deps) {
496
1004
  }));
497
1005
  const ciFailures = ciFailuresFrom(settled);
498
1006
  let ciVerdictFailures = ciFailures;
1007
+ let requiredCheckFetchFailure;
499
1008
  if (ciFailures.length > 0 && settled.pr.number !== null) {
500
1009
  try {
501
1010
  const required = requiredChecks(ownerRepo, settled.pr.number, gh);
502
1011
  if (required.size > 0)
503
1012
  ciVerdictFailures = ciFailures.filter((f) => required.has(f.name));
504
1013
  }
505
- catch {
1014
+ catch (err) {
1015
+ requiredCheckFetchFailure = err instanceof Error ? err.message : String(err);
506
1016
  }
507
1017
  }
508
- const verdict = reviewThreads.length > 0 || readinessRequiresReview(settled)
509
- ? 'review_changes'
510
- : ciVerdictFailures.length > 0 ? 'ci_changes' : 'clean';
511
- return {
1018
+ const makeFeedback = (verdict, extraEvidence = []) => ({
512
1019
  prNumber: settled.pr.number ?? null,
513
1020
  headSha: settled.pr.headSha,
514
- evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
1021
+ evidence: [...settled.evidence, ...readinessEvidence(settled), ...extraEvidence, `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
515
1022
  ...(input.issueRef ? { issueRef: input.issueRef } : {}),
516
1023
  verdict,
517
1024
  ciFailures,
518
1025
  reviewThreads,
519
- };
1026
+ ...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
1027
+ ...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
1028
+ });
1029
+ if (reviewThreads.length > 0 || readinessRequiresReview(settled))
1030
+ return makeFeedback('review_changes');
1031
+ if (requiredCheckFetchFailure) {
1032
+ return makeFeedback('recheck', [`Required check names unavailable: ${requiredCheckFetchFailure}`]);
1033
+ }
1034
+ if (ciVerdictFailures.length > 0)
1035
+ return makeFeedback('ci_changes');
1036
+ const signal = mergeSignal(settled.mergeStateStatus, settled.mergeable);
1037
+ if (signal === 'blocked') {
1038
+ return {
1039
+ needsHuman: true,
1040
+ lesson: [
1041
+ ...settled.evidence,
1042
+ ...readinessEvidence(settled),
1043
+ `pollPr merge readiness blocked (mergeStateStatus=${settled.mergeStateStatus ?? ''} mergeable=${settled.mergeable ?? ''})`,
1044
+ `PR headSha=${settled.pr.headSha}`,
1045
+ ].join('; '),
1046
+ };
1047
+ }
1048
+ if (signal === 'unknown') {
1049
+ return unsettledReadinessFeedback(input, settled, `pollPr merge readiness unknown (mergeable=${settled.mergeable ?? 'undefined'})`);
1050
+ }
1051
+ return makeFeedback('clean');
520
1052
  }
521
1053
  const TRIAGE_DECISIONS = new Set(['fix', 'wontfix', 'question']);
522
1054
  export function asTriage(value) {
@@ -531,6 +1063,37 @@ export function asTriage(value) {
531
1063
  : [];
532
1064
  return { items };
533
1065
  }
1066
+ function questionGateResolution(value) {
1067
+ if (value === null || typeof value !== 'object')
1068
+ return undefined;
1069
+ const outcome = value.outcome;
1070
+ if (outcome !== 'fix' && outcome !== 'wontfix')
1071
+ return undefined;
1072
+ const rawNote = value.note;
1073
+ const note = typeof rawNote === 'string' ? rawNote.trim() : '';
1074
+ return { decision: outcome, ...(note ? { note } : {}) };
1075
+ }
1076
+ function replyTextForQuestionResolution(decision, note) {
1077
+ if (decision === 'fix')
1078
+ return note ? `Addressed: ${note}` : 'Addressed.';
1079
+ return note ? `Won't fix: ${note}` : "Won't fix.";
1080
+ }
1081
+ export function triageForRespondThreads(input) {
1082
+ const triage = asTriage(input.triage);
1083
+ const resolution = questionGateResolution(input.gateResolution);
1084
+ if (!resolution)
1085
+ return triage;
1086
+ return {
1087
+ ...triage,
1088
+ items: triage.items.map((item) => item.decision === 'question'
1089
+ ? {
1090
+ ...item,
1091
+ decision: resolution.decision,
1092
+ replyText: replyTextForQuestionResolution(resolution.decision, resolution.note),
1093
+ }
1094
+ : item),
1095
+ };
1096
+ }
534
1097
  export async function respondThreads(triage, deps) {
535
1098
  const { execGh: gh } = deps;
536
1099
  let replied = 0;
@@ -609,13 +1172,51 @@ let IntegratorService = class IntegratorService {
609
1172
  runPollStub = (_input) => {
610
1173
  return { prNumber: null, headSha: 'stub', evidence: ['stub pollPr readiness: clean'], verdict: 'clean', ciFailures: [], reviewThreads: [] };
611
1174
  };
1175
+ runOverrideMerge = (input) => {
1176
+ const pinned = resolvePinnedGh();
1177
+ if ('needsHuman' in pinned) {
1178
+ console.warn(`[override-merge] ${pinned.lesson}`);
1179
+ return Promise.resolve(pinned);
1180
+ }
1181
+ return overrideMerge(input, { ...this.deps, execGh: pinned.execGh });
1182
+ };
1183
+ runOverrideStub = (input) => {
1184
+ const gate = mergeOverrideGate(input);
1185
+ if ('needsHuman' in gate) {
1186
+ return {
1187
+ prNumber: null,
1188
+ headSha: 'stub',
1189
+ evidence: [gate.lesson, 'overrideMerge verdict=recheck'],
1190
+ verdict: 'recheck',
1191
+ ciFailures: [],
1192
+ reviewThreads: [],
1193
+ override: {
1194
+ accepted: false,
1195
+ actor: 'stub',
1196
+ note: '',
1197
+ source: { gate: 'mergeGate', inboxId: '' },
1198
+ facts: [{ severity: 'hard', kind: 'override_input_missing', summary: gate.lesson }],
1199
+ replied: 0,
1200
+ resolved: 0,
1201
+ reason: gate.lesson,
1202
+ },
1203
+ };
1204
+ }
1205
+ return mergeOverrideOutput({
1206
+ gate,
1207
+ verdict: 'clean',
1208
+ facts: [],
1209
+ accepted: true,
1210
+ fallbackHeadSha: gate.audit.headSha || 'stub',
1211
+ });
1212
+ };
612
1213
  runRespondThreads = (input) => {
613
1214
  const pinned = resolvePinnedGh();
614
1215
  if ('needsHuman' in pinned) {
615
1216
  console.warn(`[respond-threads] ${pinned.lesson}`);
616
1217
  return Promise.resolve(pinned);
617
1218
  }
618
- return respondThreads(asTriage(input.triage), { execGh: pinned.execGh });
1219
+ return respondThreads(triageForRespondThreads(input), { execGh: pinned.execGh });
619
1220
  };
620
1221
  runRespondStub = (_input) => {
621
1222
  return { replied: 0, resolved: 0 };