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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +3 -3
  2. package/control-plane/bootstrap.config.json +104 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +405 -1065
  4. package/control-plane/default-playbook/catalog/run-profiles.json +208 -0
  5. package/control-plane/default-playbook/package.json +1 -1
  6. package/control-plane/default-playbook/playbook.json +2 -1
  7. package/control-plane/default-playbook/prompts/developer.md +6 -6
  8. package/dist/__generated__/client/browser.js +18 -0
  9. package/dist/__generated__/client/browser.js.map +1 -0
  10. package/dist/__generated__/client/client.js +32 -0
  11. package/dist/__generated__/client/client.js.map +1 -0
  12. package/dist/__generated__/client/commonInputTypes.js +11 -0
  13. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  14. package/dist/__generated__/client/enums.js +19 -0
  15. package/dist/__generated__/client/enums.js.map +1 -0
  16. package/dist/__generated__/client/internal/class.js +53 -0
  17. package/dist/__generated__/client/internal/class.js.map +1 -0
  18. package/dist/__generated__/client/internal/prismaNamespace.js +451 -0
  19. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +422 -0
  21. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  22. package/dist/__generated__/client/models/Branch.js +2 -0
  23. package/dist/__generated__/client/models/Branch.js.map +1 -0
  24. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  25. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  26. package/dist/__generated__/client/models/FileBlob.js +2 -0
  27. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  28. package/dist/__generated__/client/models/InboxItem.js +2 -0
  29. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  30. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  31. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  32. package/dist/__generated__/client/models/Revision.js +2 -0
  33. package/dist/__generated__/client/models/Revision.js.map +1 -0
  34. package/dist/__generated__/client/models/RevoProject.js +2 -0
  35. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  36. package/dist/__generated__/client/models/Row.js +2 -0
  37. package/dist/__generated__/client/models/Row.js.map +1 -0
  38. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  39. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  40. package/dist/__generated__/client/models/RunEvent.js +2 -0
  41. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  42. package/dist/__generated__/client/models/RunOutput.js +2 -0
  43. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  44. package/dist/__generated__/client/models/RunTask.js +2 -0
  45. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  46. package/dist/__generated__/client/models/Table.js +2 -0
  47. package/dist/__generated__/client/models/Table.js.map +1 -0
  48. package/dist/__generated__/client/models/TableMigration.js +2 -0
  49. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  50. package/dist/__generated__/client/models/TaskRun.js +2 -0
  51. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  52. package/dist/__generated__/client/models.js +2 -0
  53. package/dist/__generated__/client/models.js.map +1 -0
  54. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  55. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  56. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  57. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  59. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  60. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +46 -0
  61. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  62. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  63. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  64. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  65. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  67. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  68. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  69. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  70. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  71. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  72. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  73. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  74. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  75. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  76. package/dist/cli/commands/doctor-report.js +1 -13
  77. package/dist/cli/commands/doctor-report.js.map +1 -1
  78. package/dist/cli/commands/lifecycle.js +50 -65
  79. package/dist/cli/commands/lifecycle.js.map +1 -1
  80. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  81. package/dist/cli/commands/process-helpers.js.map +1 -0
  82. package/dist/config.js +17 -55
  83. package/dist/config.js.map +1 -1
  84. package/dist/control-plane/audit-validation-helpers.js +31 -0
  85. package/dist/control-plane/audit-validation-helpers.js.map +1 -0
  86. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  87. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  88. package/dist/control-plane/bootstrap.js +25 -38
  89. package/dist/control-plane/bootstrap.js.map +1 -1
  90. package/dist/control-plane/change-notifications.js +45 -14
  91. package/dist/control-plane/change-notifications.js.map +1 -1
  92. package/dist/control-plane/data-access.js +1 -106
  93. package/dist/control-plane/data-access.js.map +1 -1
  94. package/dist/control-plane/default-playbook-policy.js +499 -78
  95. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  96. package/dist/control-plane/definitions.js +4 -5
  97. package/dist/control-plane/definitions.js.map +1 -1
  98. package/dist/control-plane/engine-transport.js +381 -0
  99. package/dist/control-plane/engine-transport.js.map +1 -0
  100. package/dist/control-plane/inbox.js +15 -7
  101. package/dist/control-plane/inbox.js.map +1 -1
  102. package/dist/control-plane/index.js +0 -1
  103. package/dist/control-plane/index.js.map +1 -1
  104. package/dist/control-plane/manual-adoption-audit.js +3 -24
  105. package/dist/control-plane/manual-adoption-audit.js.map +1 -1
  106. package/dist/control-plane/merge-override-audit.js +44 -0
  107. package/dist/control-plane/merge-override-audit.js.map +1 -0
  108. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  109. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  110. package/dist/control-plane/query-types.js +2 -0
  111. package/dist/control-plane/query-types.js.map +1 -0
  112. package/dist/control-plane/run-profiles.js +89 -0
  113. package/dist/control-plane/run-profiles.js.map +1 -0
  114. package/dist/control-plane/schema-migration.js +0 -35
  115. package/dist/control-plane/schema-migration.js.map +1 -1
  116. package/dist/control-plane/seed-default-playbook.js +2 -31
  117. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  118. package/dist/control-plane/tables.js +8 -0
  119. package/dist/control-plane/tables.js.map +1 -1
  120. package/dist/control-plane/transport.js +16 -0
  121. package/dist/control-plane/transport.js.map +1 -0
  122. package/dist/control-plane/versioned-meaning.js +56 -14
  123. package/dist/control-plane/versioned-meaning.js.map +1 -1
  124. package/dist/e2e/kit/agents.js +60 -6
  125. package/dist/e2e/kit/agents.js.map +1 -1
  126. package/dist/e2e/kit/assertions.js +13 -1
  127. package/dist/e2e/kit/assertions.js.map +1 -1
  128. package/dist/e2e/kit/drive.js +46 -11
  129. package/dist/e2e/kit/drive.js.map +1 -1
  130. package/dist/e2e/kit/env.js +17 -1
  131. package/dist/e2e/kit/env.js.map +1 -1
  132. package/dist/e2e/kit/fake-integrator.js +25 -5
  133. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  134. package/dist/e2e/kit/gh-emulator.js +172 -40
  135. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  136. package/dist/e2e/kit/harness.js +44 -10
  137. package/dist/e2e/kit/harness.js.map +1 -1
  138. package/dist/e2e/kit/index.js +1 -0
  139. package/dist/e2e/kit/index.js.map +1 -1
  140. package/dist/e2e/kit/scenario.js +165 -0
  141. package/dist/e2e/kit/scenario.js.map +1 -0
  142. package/dist/e2e/kit/scenarios.js +12 -18
  143. package/dist/e2e/kit/scenarios.js.map +1 -1
  144. package/dist/engine/dbos.service.js +8 -1
  145. package/dist/engine/dbos.service.js.map +1 -1
  146. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  147. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  148. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  149. package/dist/host/daemon.js +32 -8
  150. package/dist/host/daemon.js.map +1 -1
  151. package/dist/host/ensure-host.js +3 -3
  152. package/dist/host/ensure-host.js.map +1 -1
  153. package/dist/host/host-runtime.js.map +1 -1
  154. package/dist/host/host.lifecycle.js +3 -16
  155. package/dist/host/host.lifecycle.js.map +1 -1
  156. package/dist/http/graphql-host.js +17 -8
  157. package/dist/http/graphql-host.js.map +1 -1
  158. package/dist/mcp/mcp-capabilities.js +12 -11
  159. package/dist/mcp/mcp-capabilities.js.map +1 -1
  160. package/dist/mcp/mcp-facade.service.js +45 -5
  161. package/dist/mcp/mcp-facade.service.js.map +1 -1
  162. package/dist/mcp/mcp-tools.js +59 -5
  163. package/dist/mcp/mcp-tools.js.map +1 -1
  164. package/dist/mcp/monitoring-directive.js +43 -0
  165. package/dist/mcp/monitoring-directive.js.map +1 -0
  166. package/dist/pipeline/data-driven-task.workflow.js +393 -182
  167. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  168. package/dist/pipeline/pipeline.service.js +27 -5
  169. package/dist/pipeline/pipeline.service.js.map +1 -1
  170. package/dist/pipeline/route-contract.js +123 -3
  171. package/dist/pipeline/route-contract.js.map +1 -1
  172. package/dist/pipeline-core/condition-scopes.js +15 -0
  173. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  174. package/dist/pipeline-core/index.js +2 -0
  175. package/dist/pipeline-core/index.js.map +1 -1
  176. package/dist/pipeline-core/kit/fixtures.js +81 -44
  177. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  178. package/dist/pipeline-core/materialize.js +221 -0
  179. package/dist/pipeline-core/materialize.js.map +1 -0
  180. package/dist/pipeline-core/types.js +5 -0
  181. package/dist/pipeline-core/types.js.map +1 -1
  182. package/dist/pipeline-core/validate-loops.js +2 -15
  183. package/dist/pipeline-core/validate-loops.js.map +1 -1
  184. package/dist/pipeline-core/validate-resilience.js +257 -0
  185. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  186. package/dist/pipeline-core/validate.js +2 -0
  187. package/dist/pipeline-core/validate.js.map +1 -1
  188. package/dist/playbook/catalog-loader.js +39 -2
  189. package/dist/playbook/catalog-loader.js.map +1 -1
  190. package/dist/playbook/catalog-schema-validator.js +438 -0
  191. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  192. package/dist/playbook/import-mapper.js +41 -0
  193. package/dist/playbook/import-mapper.js.map +1 -1
  194. package/dist/playbook/manifest.js +5 -0
  195. package/dist/playbook/manifest.js.map +1 -1
  196. package/dist/playbook/playbook-installer.js +17 -1
  197. package/dist/playbook/playbook-installer.js.map +1 -1
  198. package/dist/poller/pr-readiness-core.js +280 -94
  199. package/dist/poller/pr-readiness-core.js.map +1 -1
  200. package/dist/poller/pr-readiness.js +2 -2
  201. package/dist/poller/pr-readiness.js.map +1 -1
  202. package/dist/revisium/inbox.service.js +8 -8
  203. package/dist/revisium/inbox.service.js.map +1 -1
  204. package/dist/revisium/playbooks.service.js +116 -21
  205. package/dist/revisium/playbooks.service.js.map +1 -1
  206. package/dist/revisium/revisium.module.js +8 -8
  207. package/dist/revisium/revisium.module.js.map +1 -1
  208. package/dist/revisium/roles.service.js +3 -1
  209. package/dist/revisium/roles.service.js.map +1 -1
  210. package/dist/revisium/run.service.js +9 -8
  211. package/dist/revisium/run.service.js.map +1 -1
  212. package/dist/revisium/tokens.js +0 -1
  213. package/dist/revisium/tokens.js.map +1 -1
  214. package/dist/run/create-run.js +1 -1
  215. package/dist/run/create-run.js.map +1 -1
  216. package/dist/run/inspect-run.js +32 -28
  217. package/dist/run/inspect-run.js.map +1 -1
  218. package/dist/run/issue-ref.js +97 -2
  219. package/dist/run/issue-ref.js.map +1 -1
  220. package/dist/run/prisma-runtime-data-access.js +619 -0
  221. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  222. package/dist/run/run-outputs.js +39 -14
  223. package/dist/run/run-outputs.js.map +1 -1
  224. package/dist/run/terminal-run-status.js.map +1 -1
  225. package/dist/runners/integrator.js +657 -56
  226. package/dist/runners/integrator.js.map +1 -1
  227. package/dist/runners/worktree.service.js +10 -8
  228. package/dist/runners/worktree.service.js.map +1 -1
  229. package/dist/schema/ajv-errors.js +15 -0
  230. package/dist/schema/ajv-errors.js.map +1 -0
  231. package/dist/smoke/isolation.js +7 -16
  232. package/dist/smoke/isolation.js.map +1 -1
  233. package/dist/storage/ensure-storage.js +117 -0
  234. package/dist/storage/ensure-storage.js.map +1 -0
  235. package/dist/storage/revo-database.js +39 -0
  236. package/dist/storage/revo-database.js.map +1 -0
  237. package/dist/storage/revo-prisma.service.js +31 -0
  238. package/dist/storage/revo-prisma.service.js.map +1 -0
  239. package/dist/storage/revo-storage.module.js +18 -0
  240. package/dist/storage/revo-storage.module.js.map +1 -0
  241. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  242. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  243. package/dist/task-control-plane/run-watch.service.js +3 -6
  244. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  245. package/dist/task-control-plane/task-control-plane-api.service.js +234 -43
  246. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  247. package/dist/testing/runtime-data-access.js +175 -0
  248. package/dist/testing/runtime-data-access.js.map +1 -0
  249. package/dist/worker/build-context.js +178 -2
  250. package/dist/worker/build-context.js.map +1 -1
  251. package/dist/worker/claude-code-runner.js +22 -3
  252. package/dist/worker/claude-code-runner.js.map +1 -1
  253. package/dist/worker/codex-runner.js +6 -1
  254. package/dist/worker/codex-runner.js.map +1 -1
  255. package/package.json +20 -7
  256. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  257. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  258. package/prisma/migrations/migration_lock.toml +3 -0
  259. package/prisma/schema.prisma +339 -0
  260. package/prisma.config.ts +11 -0
  261. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  262. package/dist/control-plane/client-transport.js +0 -168
  263. package/dist/control-plane/client-transport.js.map +0 -1
  264. package/dist/engine/ensure-postgres.js +0 -46
  265. package/dist/engine/ensure-postgres.js.map +0 -1
  266. package/dist/host/ensure-revisium.js +0 -135
  267. package/dist/host/ensure-revisium.js.map +0 -1
  268. package/revisium.config.json +0 -10
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { accessSync, constants } from 'node:fs';
3
+ import { defaultIssueAction, hasClosingIssueReference, hasIssueRefToken } from '../run/issue-ref.js';
3
4
  const GH_EXECUTABLE_CANDIDATES = ['/opt/homebrew/bin/gh', '/usr/local/bin/gh', '/usr/bin/gh'];
4
5
  function resolveGhExecutable() {
5
6
  for (const candidate of GH_EXECUTABLE_CANDIDATES) {
@@ -93,46 +94,96 @@ function parseGhJson(raw, label) {
93
94
  }
94
95
  }
95
96
  const NOT_FOUND_RE = /could not resolve|could not find|no pull requests? found|not found/i;
96
- function resolvePrByBranch(repo, headBranch, baseBranch, execGh) {
97
+ function listPrsByBranch(repo, headBranch, state, execGh) {
97
98
  const raw = execGh([
98
- 'pr', 'list', '--repo', repo, '--head', headBranch, '--state', 'open',
99
+ 'pr', 'list', '--repo', repo, '--head', headBranch, '--state', state,
99
100
  '--json', 'number,baseRefName,state',
100
101
  ]);
101
- const prs = parseGhJson(raw, `pr list --head ${headBranch}`);
102
- const onBase = prs.filter((p) => p.baseRefName === baseBranch);
103
- if (onBase.length === 0) {
104
- return { needsHuman: true, lesson: `No open PR for head branch "${headBranch}" with base "${baseBranch}" in ${repo} - manual review needed` };
105
- }
106
- if (onBase.length === 1)
107
- return { pr_number: onBase[0].number };
108
- const candidates = onBase.map((p) => p.number).join(', ');
102
+ const stateLabel = state === 'all' ? ' --state all' : '';
103
+ return parseGhJson(raw, `pr list --head ${headBranch}${stateLabel}`);
104
+ }
105
+ function ambiguousBranchPrs(scope, matches, headBranch, baseBranch) {
106
+ const candidates = matches.map((p) => p.number).join(', ');
107
+ return {
108
+ needsHuman: true,
109
+ lesson: `Ambiguous: ${matches.length} ${scope} PRs for head branch "${headBranch}" targeting base "${baseBranch}" - candidates #${candidates} - manual review needed`,
110
+ };
111
+ }
112
+ function resolvePrByBranch(repo, headBranch, baseBranch, execGh) {
113
+ const openPrs = listPrsByBranch(repo, headBranch, 'open', execGh);
114
+ const openOnBase = openPrs.filter((p) => p.baseRefName === baseBranch);
115
+ if (openOnBase.length === 1)
116
+ return { kind: 'open', prNumber: openOnBase[0].number };
117
+ if (openOnBase.length > 1)
118
+ return ambiguousBranchPrs('open', openOnBase, headBranch, baseBranch);
119
+ const allPrs = listPrsByBranch(repo, headBranch, 'all', execGh);
120
+ const terminalOnBase = allPrs.filter((p) => p.baseRefName === baseBranch && p.state.toUpperCase() !== 'OPEN');
121
+ if (terminalOnBase.length === 1) {
122
+ const pr = terminalOnBase[0];
123
+ const state = pr.state.toUpperCase();
124
+ if (state === 'MERGED')
125
+ return { kind: 'merged', prNumber: pr.number };
126
+ if (state === 'CLOSED')
127
+ return { kind: 'closed', prNumber: pr.number };
128
+ return {
129
+ needsHuman: true,
130
+ lesson: `Unexpected terminal PR state ${pr.state} for head branch "${headBranch}" targeting base "${baseBranch}" - manual review needed`,
131
+ };
132
+ }
133
+ if (terminalOnBase.length > 1)
134
+ return ambiguousBranchPrs('terminal', terminalOnBase, headBranch, baseBranch);
109
135
  return {
110
136
  needsHuman: true,
111
- lesson: `Ambiguous: ${onBase.length} open PRs for head branch "${headBranch}" targeting base "${baseBranch}" - candidates #${candidates} - manual review needed`,
137
+ lesson: `No open PR for head branch "${headBranch}" with base "${baseBranch}" in ${repo} - manual review needed`,
112
138
  };
113
139
  }
114
140
  function fetchPrView(prNumber, repo, execGh) {
115
141
  const raw = execGh([
116
142
  'pr', 'view', String(prNumber), '--repo', repo,
117
- '--json', 'number,url,state,isDraft,baseRefName,headRefName,headRefOid,title,statusCheckRollup,mergeStateStatus,reviewDecision,mergeable',
143
+ '--json', 'number,url,state,isDraft,baseRefName,headRefName,headRefOid,title,statusCheckRollup,mergeStateStatus,reviewDecision,mergeable,closingIssuesReferences',
118
144
  ]);
119
145
  return parseGhJson(raw, `pr view #${prNumber}`);
120
146
  }
147
+ function terminalBranchResult(result, headBranch, prView) {
148
+ if (result.kind === 'merged')
149
+ return { kind: 'merged', prNumber: result.prNumber, prView };
150
+ return {
151
+ kind: 'needsHuman',
152
+ verdict: 'closed',
153
+ lesson: `pr_closed_externally: PR #${result.prNumber} for "${headBranch}" was closed without merging - manual review needed`,
154
+ prNumber: result.prNumber,
155
+ prView,
156
+ };
157
+ }
158
+ function resolveBranchForOpenPr(input, headBranch, baseBranch, execGh) {
159
+ const result = resolvePrByBranch(input.repo, headBranch, baseBranch, execGh);
160
+ if ('needsHuman' in result) {
161
+ return { kind: 'needsHuman', verdict: 'unresolved', lesson: result.lesson };
162
+ }
163
+ if (result.kind === 'open')
164
+ return { kind: 'open', prNumber: result.prNumber };
165
+ return terminalBranchResult(result, headBranch, fetchPrView(result.prNumber, input.repo, execGh));
166
+ }
167
+ function canRecoverFromMissingPr(headBranch, resolvedFromBranch, err) {
168
+ return Boolean(headBranch && !resolvedFromBranch && NOT_FOUND_RE.test(String(err)));
169
+ }
121
170
  function handleClosedPr(prNumber, resolvedFromBranch, input, baseBranch, execGh, prView) {
122
171
  if (!input.headBranch || resolvedFromBranch) {
123
- return { kind: 'needsHuman', verdict: 'closed', lesson: `PR #${prNumber} was closed without merging - manual review needed`, prNumber, prView };
172
+ return { kind: 'needsHuman', verdict: 'closed', lesson: `pr_closed_externally: PR #${prNumber} was closed without merging - manual review needed`, prNumber, prView };
124
173
  }
125
174
  const r = resolvePrByBranch(input.repo, input.headBranch, baseBranch, execGh);
126
175
  if ('needsHuman' in r) {
127
- return { kind: 'needsHuman', verdict: 'closed', lesson: `PR #${prNumber} was closed; ${r.lesson}`, prNumber, prView };
176
+ return { kind: 'needsHuman', verdict: 'closed', lesson: `pr_closed_externally: PR #${prNumber} was closed; ${r.lesson}`, prNumber, prView };
128
177
  }
129
- const newPrView = fetchPrView(r.pr_number, input.repo, execGh);
178
+ const newPrView = fetchPrView(r.prNumber, input.repo, execGh);
179
+ if (r.kind !== 'open')
180
+ return terminalBranchResult(r, input.headBranch, newPrView);
130
181
  if (newPrView.state === 'MERGED')
131
- return { kind: 'merged', prNumber: r.pr_number, prView: newPrView };
182
+ return { kind: 'merged', prNumber: r.prNumber, prView: newPrView };
132
183
  if (newPrView.state === 'CLOSED') {
133
- return { kind: 'needsHuman', verdict: 'closed', lesson: `PR #${r.pr_number} (recovered via "${input.headBranch}") is also closed - manual review needed`, prNumber: r.pr_number, prView: newPrView };
184
+ return { kind: 'needsHuman', verdict: 'closed', lesson: `pr_closed_externally: PR #${r.prNumber} (recovered via "${input.headBranch}") is also closed - manual review needed`, prNumber: r.prNumber, prView: newPrView };
134
185
  }
135
- return { kind: 'open', prNumber: r.pr_number, prView: newPrView };
186
+ return { kind: 'open', prNumber: r.prNumber, prView: newPrView };
136
187
  }
137
188
  function resolveOpenPr(input, baseBranch, execGh) {
138
189
  let prNumber = input.prNumber;
@@ -141,10 +192,10 @@ function resolveOpenPr(input, baseBranch, execGh) {
141
192
  if (!input.headBranch) {
142
193
  return { kind: 'needsHuman', verdict: 'unresolved', lesson: 'ci-poller step has neither pr_number nor head_branch - cannot identify a PR to watch' };
143
194
  }
144
- const r = resolvePrByBranch(input.repo, input.headBranch, baseBranch, execGh);
145
- if ('needsHuman' in r)
146
- return { kind: 'needsHuman', verdict: 'unresolved', lesson: r.lesson };
147
- prNumber = r.pr_number;
195
+ const resolved = resolveBranchForOpenPr(input, input.headBranch, baseBranch, execGh);
196
+ if (resolved.kind !== 'open')
197
+ return resolved;
198
+ prNumber = resolved.prNumber;
148
199
  resolvedFromBranch = true;
149
200
  }
150
201
  let prView;
@@ -152,13 +203,13 @@ function resolveOpenPr(input, baseBranch, execGh) {
152
203
  prView = fetchPrView(prNumber, input.repo, execGh);
153
204
  }
154
205
  catch (err) {
155
- if (!input.headBranch || resolvedFromBranch || !NOT_FOUND_RE.test(String(err))) {
206
+ const recoveryHeadBranch = input.headBranch;
207
+ if (!canRecoverFromMissingPr(recoveryHeadBranch, resolvedFromBranch, err))
156
208
  throw err;
157
- }
158
- const r = resolvePrByBranch(input.repo, input.headBranch, baseBranch, execGh);
159
- if ('needsHuman' in r)
160
- return { kind: 'needsHuman', verdict: 'unresolved', lesson: r.lesson };
161
- prNumber = r.pr_number;
209
+ const resolved = resolveBranchForOpenPr(input, recoveryHeadBranch, baseBranch, execGh);
210
+ if (resolved.kind !== 'open')
211
+ return resolved;
212
+ prNumber = resolved.prNumber;
162
213
  resolvedFromBranch = true;
163
214
  prView = fetchPrView(prNumber, input.repo, execGh);
164
215
  }
@@ -171,7 +222,7 @@ function resolveOpenPr(input, baseBranch, execGh) {
171
222
  function isTerminal(item) {
172
223
  if (item.__typename === 'CheckRun')
173
224
  return item.status === 'COMPLETED';
174
- return item.state !== 'PENDING';
225
+ return item.state !== 'EXPECTED' && item.state !== 'PENDING';
175
226
  }
176
227
  function isPassed(item) {
177
228
  if (item.__typename === 'CheckRun') {
@@ -193,12 +244,15 @@ function checkResult(item) {
193
244
  function isBot(user) {
194
245
  return user?.type === 'Bot';
195
246
  }
196
- export function collectCiChecks(items) {
197
- const pending = items.length === 0 || items.some((item) => !isTerminal(item));
247
+ export const GITHUB_CHECK_ROLLUP_UNAVAILABLE = 'GitHub check rollup unavailable (re-polling for checks)';
248
+ export function collectCiChecks(items, opts = {}) {
249
+ const pending = (opts.emptyIsPending === true && items.length === 0) || items.some((item) => !isTerminal(item));
198
250
  const ci_passed = !pending && items.every((item) => isPassed(item));
199
251
  const pendingNames = items
200
252
  .filter((item) => !isTerminal(item))
201
253
  .map(checkName);
254
+ if (opts.emptyIsPending === true && items.length === 0)
255
+ pendingNames.push(GITHUB_CHECK_ROLLUP_UNAVAILABLE);
202
256
  const checks = items.map((item) => ({ name: checkName(item), result: checkResult(item) }));
203
257
  return { pending, ci_passed, checks, pendingNames };
204
258
  }
@@ -210,30 +264,78 @@ function asRecord(value) {
210
264
  return null;
211
265
  return value;
212
266
  }
267
+ function firstNumber(...values) {
268
+ for (const value of values) {
269
+ if (typeof value === 'number')
270
+ return value;
271
+ }
272
+ return undefined;
273
+ }
274
+ // gh api graphql wraps responses in {"data":{...}}; unwrap tolerantly so unit fixtures still work.
275
+ function unwrapGraphqlData(raw) {
276
+ const top = asRecord(raw);
277
+ const data = top ? asRecord(top['data']) : null;
278
+ return data ?? raw;
279
+ }
280
+ function requireGraphqlRecord(value, context, path) {
281
+ const record = asRecord(value);
282
+ if (!record)
283
+ throw new Error(`invalid GraphQL shape in ${context} response: missing ${path}`);
284
+ return record;
285
+ }
286
+ function requireGraphqlArray(value, context, path) {
287
+ if (!Array.isArray(value))
288
+ throw new Error(`invalid GraphQL shape in ${context} response: missing ${path}`);
289
+ return value;
290
+ }
213
291
  function mapReviewThreads(raw) {
214
- const root = asRecord(raw);
215
- const repository = asRecord(root?.['repository']);
216
- const pullRequest = asRecord(repository?.['pullRequest']);
217
- const reviewThreads = asRecord(pullRequest?.['reviewThreads']);
218
- const nodes = Array.isArray(reviewThreads?.['nodes']) ? reviewThreads.nodes : [];
219
- return nodes.flatMap((node) => {
220
- const thread = asRecord(node);
221
- if (!thread)
222
- return [];
223
- const comments = asRecord(thread.comments);
224
- const firstComment = Array.isArray(comments?.['nodes']) ? asRecord(comments.nodes[0]) : null;
225
- const author = asRecord(firstComment?.['author']);
226
- return [{
227
- id: asStr(thread.id),
228
- isResolved: Boolean(thread.isResolved),
229
- isOutdated: Boolean(thread.isOutdated),
230
- path: asStr(thread.path) || undefined,
231
- line: typeof thread.line === 'number' ? thread.line : undefined,
232
- author: asStr(author?.['login']) || undefined,
233
- body: compactBody(asStr(firstComment?.['body'])),
234
- url: asStr(firstComment?.['url']) || undefined,
235
- }];
236
- });
292
+ const context = 'reviewThreads';
293
+ const root = requireGraphqlRecord(unwrapGraphqlData(raw), context, 'root');
294
+ const repository = requireGraphqlRecord(root['repository'], context, 'repository');
295
+ const pullRequest = requireGraphqlRecord(repository['pullRequest'], context, 'repository.pullRequest');
296
+ const reviewThreads = requireGraphqlRecord(pullRequest['reviewThreads'], context, 'repository.pullRequest.reviewThreads');
297
+ const nodes = requireGraphqlArray(reviewThreads['nodes'], context, 'repository.pullRequest.reviewThreads.nodes');
298
+ const pageInfo = asRecord(reviewThreads['pageInfo']);
299
+ const hasNextPage = pageInfo?.['hasNextPage'] === true;
300
+ return { threads: nodes.flatMap((node) => {
301
+ const thread = asRecord(node);
302
+ if (!thread)
303
+ return [];
304
+ const comments = asRecord(thread.comments);
305
+ const commentNodes = Array.isArray(comments?.['nodes']) ? comments.nodes.flatMap((comment) => {
306
+ const record = asRecord(comment);
307
+ return record ? [record] : [];
308
+ }) : [];
309
+ const firstComment = commentNodes[0] ?? null;
310
+ const author = asRecord(firstComment?.['author']);
311
+ const path = asStr(thread.path) || undefined;
312
+ const line = firstNumber(thread.line, firstComment?.['line']);
313
+ const originalLine = firstNumber(thread.originalLine, firstComment?.['originalLine']);
314
+ return [{
315
+ item: {
316
+ id: asStr(thread.id),
317
+ isResolved: Boolean(thread.isResolved),
318
+ isOutdated: Boolean(thread.isOutdated),
319
+ path,
320
+ line,
321
+ originalLine,
322
+ author: asStr(author?.['login']) || undefined,
323
+ body: compactBody(asStr(firstComment?.['body'])),
324
+ url: asStr(firstComment?.['url']) || undefined,
325
+ },
326
+ comments: commentNodes.map((comment) => {
327
+ const commentAuthor = asRecord(comment['author']);
328
+ return {
329
+ path,
330
+ line: firstNumber(comment['line'], thread.line),
331
+ originalLine: firstNumber(comment['originalLine'], thread.originalLine),
332
+ author: asStr(commentAuthor?.['login']) || undefined,
333
+ body: asStr(comment['body']),
334
+ url: asStr(comment['url']) || undefined,
335
+ };
336
+ }),
337
+ }];
338
+ }), hasNextPage };
237
339
  }
238
340
  function splitRepo(repo) {
239
341
  const [owner, name] = repo.split('/');
@@ -245,7 +347,7 @@ function fetchReviewThreads(repo, prNumber, execGh) {
245
347
  const { owner, name } = splitRepo(repo);
246
348
  const raw = execGh([
247
349
  'api', 'graphql',
248
- '-f', 'query=query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100){nodes{id,isResolved,isOutdated,path,line,comments(first:1){nodes{body,url,author{login}}}}}}}}',
350
+ '-f', 'query=query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100){pageInfo{hasNextPage}nodes{id,isResolved,isOutdated,path,line,originalLine,comments(first:100){nodes{body,url,line,originalLine,author{login}}}}}}}}',
249
351
  '-f', `owner=${owner}`,
250
352
  '-f', `name=${name}`,
251
353
  '-F', `number=${prNumber}`,
@@ -253,12 +355,15 @@ function fetchReviewThreads(repo, prNumber, execGh) {
253
355
  return mapReviewThreads(parseGhJson(raw, `review-threads #${prNumber}`));
254
356
  }
255
357
  function mapRequiredCheckNames(raw) {
256
- const root = asRecord(raw);
257
- const repository = asRecord(root?.['repository']);
258
- const pullRequest = asRecord(repository?.['pullRequest']);
259
- const rollup = asRecord(pullRequest?.['statusCheckRollup']);
260
- const contexts = asRecord(rollup?.['contexts']);
261
- const nodes = Array.isArray(contexts?.['nodes']) ? contexts.nodes : [];
358
+ const context = 'required-checks';
359
+ const root = requireGraphqlRecord(unwrapGraphqlData(raw), context, 'root');
360
+ const repository = requireGraphqlRecord(root['repository'], context, 'repository');
361
+ const pullRequest = requireGraphqlRecord(repository['pullRequest'], context, 'repository.pullRequest');
362
+ if (pullRequest['statusCheckRollup'] === null)
363
+ throw new Error(GITHUB_CHECK_ROLLUP_UNAVAILABLE);
364
+ const rollup = requireGraphqlRecord(pullRequest['statusCheckRollup'], context, 'repository.pullRequest.statusCheckRollup');
365
+ const contexts = requireGraphqlRecord(rollup['contexts'], context, 'repository.pullRequest.statusCheckRollup.contexts');
366
+ const nodes = requireGraphqlArray(contexts['nodes'], context, 'repository.pullRequest.statusCheckRollup.contexts.nodes');
262
367
  const required = new Set();
263
368
  for (const node of nodes) {
264
369
  const ctx = asRecord(node);
@@ -281,13 +386,56 @@ export function fetchRequiredCheckNames(repo, prNumber, execGh) {
281
386
  ]);
282
387
  return mapRequiredCheckNames(parseGhJson(raw, `required-checks #${prNumber}`));
283
388
  }
389
+ function reviewLineOf(item) {
390
+ return firstNumber(item.line, item.originalLine, item.original_line);
391
+ }
392
+ function preciseReviewLocationOf(item) {
393
+ const line = reviewLineOf(item);
394
+ return item.path && typeof line === 'number' ? `${item.path}:${line}` : '';
395
+ }
396
+ function feedbackMatchEvidence(item) {
397
+ const location = preciseReviewLocationOf(item);
398
+ if (location === '')
399
+ return null;
400
+ const url = item.html_url ?? item.url ?? '';
401
+ const author = (item.author ?? item.user?.login ?? '').toLowerCase();
402
+ const body = compactBody(item.body);
403
+ const contentKey = author !== '' && body !== '' ? [author, body].join('\0') : '';
404
+ return { location, url, contentKey };
405
+ }
406
+ function isStaleThreadFeedback(comment, staleThreadEvidence) {
407
+ const commentEvidence = feedbackMatchEvidence(comment);
408
+ if (!commentEvidence || !staleThreadEvidence)
409
+ return false;
410
+ return staleThreadEvidence
411
+ .filter((staleEvidence) => staleEvidence.location === commentEvidence.location)
412
+ .some((staleEvidence) => {
413
+ if (commentEvidence.url !== '' && staleEvidence.url !== '') {
414
+ return commentEvidence.url === staleEvidence.url;
415
+ }
416
+ return commentEvidence.contentKey !== '' && commentEvidence.contentKey === staleEvidence.contentKey;
417
+ });
418
+ }
284
419
  function collectReviewThreads(input, prNumber, execGh) {
285
- const threads = input.includeReviewThreads === false ? [] : fetchReviewThreads(input.repo, prNumber, execGh);
286
- const unresolved = threads.filter((thread) => !thread.isResolved && !thread.isOutdated);
420
+ const fetched = input.includeReviewThreads === false
421
+ ? { threads: [], hasNextPage: false }
422
+ : fetchReviewThreads(input.repo, prNumber, execGh);
423
+ const threads = fetched.threads;
424
+ const unresolved = threads
425
+ .filter((thread) => !thread.item.isResolved && !thread.item.isOutdated)
426
+ .map((thread) => thread.item);
287
427
  return {
288
- included: input.includeReviewThreads !== false,
289
- unresolvedCount: unresolved.length,
290
- items: unresolved.slice(0, 20),
428
+ reviewThreads: {
429
+ included: input.includeReviewThreads !== false,
430
+ unresolvedCount: unresolved.length,
431
+ items: unresolved,
432
+ ...(fetched.hasNextPage ? { truncated: true } : {}),
433
+ },
434
+ staleThreadEvidence: threads
435
+ .filter((thread) => thread.item.isResolved || thread.item.isOutdated)
436
+ .flatMap((thread) => thread.comments)
437
+ .map(feedbackMatchEvidence)
438
+ .filter((evidence) => evidence !== null),
291
439
  };
292
440
  }
293
441
  function fetchComments(input, prNumber, execGh) {
@@ -413,7 +561,7 @@ function compactCheckLists(checks) {
413
561
  const pass = [];
414
562
  const fail = [];
415
563
  for (const check of checks) {
416
- if (['QUEUED', 'IN_PROGRESS', 'PENDING'].includes(check.result)) {
564
+ if (['EXPECTED', 'QUEUED', 'IN_PROGRESS', 'PENDING'].includes(check.result)) {
417
565
  pending.push(check.name);
418
566
  continue;
419
567
  }
@@ -461,7 +609,7 @@ function providerState(checks, botComments) {
461
609
  }
462
610
  function locationOf(item) {
463
611
  const path = item.path ?? item.component ?? '';
464
- return item.line ? `${path}:${item.line}` : path;
612
+ return typeof item.line === 'number' ? `${path}:${item.line}` : path;
465
613
  }
466
614
  function providerWaitFeedback(state, botComments) {
467
615
  const codeRabbit = state.codeRabbit;
@@ -476,9 +624,6 @@ function providerWaitFeedback(state, botComments) {
476
624
  resumeAfter: extractResumeHint(botComments),
477
625
  }];
478
626
  }
479
- function escapeRegExp(value) {
480
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
481
- }
482
627
  function issueRefTitleTag(issueRef, repo) {
483
628
  return issueRef.repo.toLowerCase() === repo.toLowerCase()
484
629
  ? `#${issueRef.number}`
@@ -487,25 +632,15 @@ function issueRefTitleTag(issueRef, repo) {
487
632
  function hasIssueBranchToken(head, issueRef) {
488
633
  return new RegExp(`(?:^|[/-])issue-${issueRef.number}(?:-|$)`).test(head ?? '');
489
634
  }
490
- function hasIssueTitleToken(title, issueRef, repo) {
491
- const boundaryBefore = String.raw `(?:^|[\s([{"'])`;
492
- const boundaryAfter = String.raw `(?=$|[^A-Za-z0-9_-])`;
493
- const qualified = new RegExp(`${boundaryBefore}${escapeRegExp(`${issueRef.repo}#${issueRef.number}`)}${boundaryAfter}`, 'i');
494
- if (qualified.test(title ?? ''))
495
- return true;
496
- if (issueRef.repo.toLowerCase() !== repo.toLowerCase())
497
- return false;
498
- return new RegExp(`${boundaryBefore}#${issueRef.number}${boundaryAfter}`).test(title ?? '');
499
- }
500
- function issueLinkageFeedback(issueRef, repo, pr) {
501
- if (!issueRef)
635
+ function issueLinkageFeedback(issueRef, issueAction, repo, pr) {
636
+ if (!issueRef || issueAction === 'none')
502
637
  return [];
503
638
  const issueBranch = `issue-${issueRef.number}`;
504
639
  const issueTag = issueRefTitleTag(issueRef, repo);
505
640
  const missing = [];
506
641
  if (!hasIssueBranchToken(pr.head, issueRef))
507
642
  missing.push(`branch missing ${issueBranch}`);
508
- if (!hasIssueTitleToken(pr.title, issueRef, repo))
643
+ if (!hasIssueRefToken(pr.title, issueRef, repo))
509
644
  missing.push(`title missing ${issueTag}`);
510
645
  if (missing.length === 0)
511
646
  return [];
@@ -515,6 +650,24 @@ function issueLinkageFeedback(issueRef, repo, pr) {
515
650
  evidence: `${missing.join('; ')} (head=${pr.head ?? ''}, title=${pr.title ?? ''})`,
516
651
  }];
517
652
  }
653
+ function issueCloseFeedback(issueRef, issueAction, repo, prView) {
654
+ if (issueAction !== 'close' || !issueRef || hasClosingIssueReference(prView?.closingIssuesReferences, issueRef))
655
+ return [];
656
+ const issueTag = issueRefTitleTag(issueRef, repo);
657
+ return [{
658
+ source: 'issue_auto_close',
659
+ summary: `PR does not auto-close ${issueTag}`,
660
+ evidence: `GitHub closingIssuesReferences does not include ${issueTag}`,
661
+ }];
662
+ }
663
+ // Bot logins whose comments are purely informational and safe to suppress.
664
+ // Anything not in this list surfaces in developerFixes to avoid silent suppression.
665
+ const KNOWN_INFORMATIONAL_BOTS = new Set([
666
+ 'sonarqubecloud[bot]',
667
+ 'cursor[bot]',
668
+ 'linear-app[bot]',
669
+ 'deepsource-autofix[bot]',
670
+ ]);
518
671
  function buildFeedback(input) {
519
672
  const developerFixes = [
520
673
  ...input.checks.fail.map((name) => ({ source: 'ci', summary: `Fix failing check: ${name}`, evidence: name })),
@@ -540,6 +693,13 @@ function buildFeedback(input) {
540
693
  author: review.user?.login ?? '',
541
694
  evidence: review.state,
542
695
  })),
696
+ ...(input.reviewThreads.truncated
697
+ ? [{
698
+ source: 'review_threads_truncated',
699
+ summary: 'Review thread data is incomplete; fetch all review thread pages before declaring the PR ready.',
700
+ evidence: 'GitHub reviewThreads pageInfo.hasNextPage=true',
701
+ }]
702
+ : []),
543
703
  ...input.reviewThreads.items.map((thread) => ({
544
704
  source: 'review_thread',
545
705
  summary: thread.body,
@@ -548,6 +708,15 @@ function buildFeedback(input) {
548
708
  evidence: thread.url ?? thread.id,
549
709
  })),
550
710
  ...codeRabbitActionableFeedback(input.coderabbitReviews ?? [], input.botComments, new Set(input.reviewThreads.items.map((thread) => locationOf(thread)).filter((loc) => loc !== ''))),
711
+ ...input.botComments
712
+ .filter((comment) => !isCodeRabbit(comment.user) && !KNOWN_INFORMATIONAL_BOTS.has(comment.user?.login ?? ''))
713
+ .filter((comment) => !isStaleThreadFeedback(comment, input.staleThreadEvidence))
714
+ .map((comment) => ({
715
+ source: 'bot_comment',
716
+ summary: compactBody(comment.body),
717
+ author: comment.user?.login ?? 'bot',
718
+ evidence: comment.user?.login ?? 'bot',
719
+ })),
551
720
  ];
552
721
  const reviewerQuestions = input.humanComments
553
722
  .filter((comment) => comment.body.includes('?'))
@@ -563,10 +732,11 @@ function buildFeedback(input) {
563
732
  ? [{ source: 'github_review_decision', summary: `Review decision is ${input.reviewDecision}` }]
564
733
  : []),
565
734
  ...(input.sonar.unavailable ? [{ source: 'sonar', summary: 'Sonar was configured but unavailable.' }] : []),
566
- ...issueLinkageFeedback(input.issueRef, input.repo, input.pr ?? {}),
735
+ ...issueLinkageFeedback(input.issueRef, input.issueAction, input.repo, input.pr ?? {}),
736
+ ...issueCloseFeedback(input.issueRef, input.issueAction, input.repo, input.prView),
567
737
  ];
568
738
  const ignoredNoise = input.botComments
569
- .filter((comment) => !comment.user?.login.toLowerCase().includes('coderabbit'))
739
+ .filter((comment) => !isCodeRabbit(comment.user) && KNOWN_INFORMATIONAL_BOTS.has(comment.user?.login ?? ''))
570
740
  .map((comment) => ({ source: comment.user?.login ?? 'bot', summary: compactBody(comment.body) }));
571
741
  const residualRisks = [
572
742
  ...(input.reviewThreads.included ? [] : ['Review threads were not requested.']),
@@ -620,7 +790,9 @@ function buildEmptyFeedback(input) {
620
790
  humanComments: [],
621
791
  botComments: [],
622
792
  issueRef: input.issueRef,
623
- repo: '',
793
+ issueAction: input.issueAction,
794
+ repo: input.repo ?? '',
795
+ prView: input.prView,
624
796
  });
625
797
  }
626
798
  function buildMergedReadiness(input, resolved) {
@@ -636,7 +808,7 @@ function buildMergedReadiness(input, resolved) {
636
808
  sonar: emptySonar(sonarConfigured),
637
809
  nextAction: 'none',
638
810
  evidence: [`PR #${resolved.prNumber} is merged.`],
639
- feedback: buildEmptyFeedback({ sonarConfigured, includeReviewThreads }),
811
+ feedback: buildEmptyFeedback({ sonarConfigured, includeReviewThreads, issueRef: input.issueRef, issueAction: input.issueAction, repo: input.repo, prView: resolved.prView }),
640
812
  ciSummary: { ...emptyCiSummary(true), ...(input.issueRef ? { issueRef: input.issueRef } : {}) },
641
813
  ...(input.issueRef ? { issueRef: input.issueRef } : {}),
642
814
  };
@@ -644,7 +816,7 @@ function buildMergedReadiness(input, resolved) {
644
816
  function buildNeedsHumanReadiness(input, resolved) {
645
817
  const includeReviewThreads = input.includeReviewThreads !== false;
646
818
  const sonarConfigured = Boolean(input.sonarProject);
647
- const feedback = buildEmptyFeedback({ sonarConfigured, includeReviewThreads });
819
+ const feedback = buildEmptyFeedback({ sonarConfigured, includeReviewThreads, issueRef: input.issueRef, issueAction: input.issueAction, repo: input.repo, prView: resolved.prView });
648
820
  return {
649
821
  verdict: resolved.verdict === 'closed' ? 'closed' : 'needs_human',
650
822
  pr: resolved.prNumber ? prFromView(resolved.prNumber, resolved.prView) : emptyPr(input.prNumber, resolved.verdict),
@@ -661,6 +833,9 @@ function buildNeedsHumanReadiness(input, resolved) {
661
833
  };
662
834
  }
663
835
  function buildWaitingReadiness(input) {
836
+ const checkLists = input.checkLists.list.length === 0 && input.ci.pendingNames.length > 0
837
+ ? { ...input.checkLists, pending: input.ci.pendingNames }
838
+ : input.checkLists;
664
839
  const pendingCodeRabbit = input.ci.pendingNames.find((name) => name.toLowerCase().includes('coderabbit'));
665
840
  const pendingProviderState = pendingCodeRabbit
666
841
  ? {
@@ -673,7 +848,7 @@ function buildWaitingReadiness(input) {
673
848
  }
674
849
  : {};
675
850
  const feedback = buildFeedback({
676
- checks: input.checkLists,
851
+ checks: checkLists,
677
852
  providerState: pendingProviderState,
678
853
  sonar: emptySonar(input.sonarConfigured),
679
854
  reviewDecision: input.prView.reviewDecision ?? '',
@@ -682,8 +857,10 @@ function buildWaitingReadiness(input) {
682
857
  humanComments: [],
683
858
  botComments: [],
684
859
  issueRef: input.issueRef,
860
+ issueAction: input.issueAction,
685
861
  repo: input.repo,
686
862
  pr: { head: input.prView.headRefName, title: input.prView.title },
863
+ prView: input.prView,
687
864
  });
688
865
  const providerWait = pendingCodeRabbit
689
866
  ? [{
@@ -698,7 +875,7 @@ function buildWaitingReadiness(input) {
698
875
  return {
699
876
  verdict: 'waiting',
700
877
  pr: prFromView(input.prNumber, input.prView),
701
- checks: input.checkLists,
878
+ checks: checkLists,
702
879
  reviewDecision: input.prView.reviewDecision ?? '',
703
880
  reviewThreads: input.reviewThreads,
704
881
  providerState: pendingProviderState,
@@ -733,6 +910,7 @@ function readinessAction(input) {
733
910
  return { verdict: 'ready', nextAction: 'ready_for_merge_gate' };
734
911
  }
735
912
  export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSonar = defaultFetchSonar) {
913
+ input = { ...input, issueAction: input.issueAction ?? defaultIssueAction(input.issueRef) };
736
914
  const baseBranch = input.baseBranch ?? 'master';
737
915
  const resolved = resolveOpenPr(input, baseBranch, execGh);
738
916
  if (resolved.kind === 'merged') {
@@ -743,9 +921,9 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
743
921
  }
744
922
  const { prNumber, prView } = resolved;
745
923
  const checks = prView.statusCheckRollup ?? [];
746
- const ci = collectCiChecks(checks);
924
+ const ci = collectCiChecks(checks, { emptyIsPending: prView.statusCheckRollup === null });
747
925
  const checkLists = compactCheckLists(ci.checks);
748
- const reviewThreads = collectReviewThreads(input, prNumber, execGh);
926
+ const { reviewThreads, staleThreadEvidence } = collectReviewThreads(input, prNumber, execGh);
749
927
  const sonarConfigured = Boolean(input.sonarProject);
750
928
  if (prView.isDraft === true) {
751
929
  return buildWaitingReadiness({
@@ -757,6 +935,7 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
757
935
  reviewThreads,
758
936
  sonarConfigured,
759
937
  issueRef: input.issueRef,
938
+ issueAction: input.issueAction,
760
939
  evidence: [`PR #${prNumber} is still draft.`],
761
940
  isDraft: true,
762
941
  });
@@ -771,6 +950,7 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
771
950
  reviewThreads,
772
951
  sonarConfigured,
773
952
  issueRef: input.issueRef,
953
+ issueAction: input.issueAction,
774
954
  evidence: ci.pendingNames.length > 0 ? [`Pending checks: ${ci.pendingNames.join(', ')}`] : ['No check rollup entries are registered yet.'],
775
955
  });
776
956
  }
@@ -794,10 +974,13 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
794
974
  humanReviews: comments.human_reviews,
795
975
  humanComments: comments.human_comments,
796
976
  botComments: comments.bot_comments,
977
+ staleThreadEvidence,
797
978
  coderabbitReviews: comments.coderabbit_reviews,
798
979
  issueRef: input.issueRef,
980
+ issueAction: input.issueAction,
799
981
  repo: input.repo,
800
982
  pr: { head: prView.headRefName, title: prView.title },
983
+ prView,
801
984
  });
802
985
  const ciSummary = {
803
986
  ci_passed: ci.ci_passed,
@@ -826,7 +1009,9 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
826
1009
  nextAction,
827
1010
  evidence: [
828
1011
  `PR #${prNumber} state=${prView.state ?? 'unknown'} draft=${Boolean(prView.isDraft)}`,
829
- `checks pass=${checkLists.pass.length} fail=${checkLists.fail.length} pending=${checkLists.pending.length}`,
1012
+ checkLists.list.length === 0
1013
+ ? 'checks: none registered'
1014
+ : `checks pass=${checkLists.pass.length} fail=${checkLists.fail.length} pending=${checkLists.pending.length}`,
830
1015
  ...(input.issueRef ? [`issueRef ${issueRefTitleTag(input.issueRef, input.repo)} expected in branch and title`] : []),
831
1016
  ...(providers.codeRabbit?.reason === 'provider_limit' ? ['CodeRabbit provider/rate limit comment detected.'] : []),
832
1017
  ...(verdict === 'ready' && hasInformationalProviderWait
@@ -846,6 +1031,7 @@ export function toReadinessInput(input) {
846
1031
  baseBranch: input.base_branch,
847
1032
  sonarProject: input.sonar_project,
848
1033
  issueRef: input.issue_ref,
1034
+ issueAction: input.issue_action,
849
1035
  includeComments: true,
850
1036
  includeReviewThreads: false,
851
1037
  };