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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/README.md +3 -3
  2. package/control-plane/bootstrap.config.json +104 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +394 -1122
  4. package/control-plane/default-playbook/catalog/run-profiles.json +208 -0
  5. package/control-plane/default-playbook/package.json +1 -1
  6. package/control-plane/default-playbook/playbook.json +2 -1
  7. package/control-plane/default-playbook/prompts/developer.md +6 -6
  8. package/dist/__generated__/client/browser.js +18 -0
  9. package/dist/__generated__/client/browser.js.map +1 -0
  10. package/dist/__generated__/client/client.js +32 -0
  11. package/dist/__generated__/client/client.js.map +1 -0
  12. package/dist/__generated__/client/commonInputTypes.js +11 -0
  13. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  14. package/dist/__generated__/client/enums.js +19 -0
  15. package/dist/__generated__/client/enums.js.map +1 -0
  16. package/dist/__generated__/client/internal/class.js +53 -0
  17. package/dist/__generated__/client/internal/class.js.map +1 -0
  18. package/dist/__generated__/client/internal/prismaNamespace.js +451 -0
  19. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +422 -0
  21. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  22. package/dist/__generated__/client/models/Branch.js +2 -0
  23. package/dist/__generated__/client/models/Branch.js.map +1 -0
  24. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  25. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  26. package/dist/__generated__/client/models/FileBlob.js +2 -0
  27. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  28. package/dist/__generated__/client/models/InboxItem.js +2 -0
  29. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  30. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  31. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  32. package/dist/__generated__/client/models/Revision.js +2 -0
  33. package/dist/__generated__/client/models/Revision.js.map +1 -0
  34. package/dist/__generated__/client/models/RevoProject.js +2 -0
  35. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  36. package/dist/__generated__/client/models/Row.js +2 -0
  37. package/dist/__generated__/client/models/Row.js.map +1 -0
  38. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  39. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  40. package/dist/__generated__/client/models/RunEvent.js +2 -0
  41. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  42. package/dist/__generated__/client/models/RunOutput.js +2 -0
  43. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  44. package/dist/__generated__/client/models/RunTask.js +2 -0
  45. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  46. package/dist/__generated__/client/models/Table.js +2 -0
  47. package/dist/__generated__/client/models/Table.js.map +1 -0
  48. package/dist/__generated__/client/models/TableMigration.js +2 -0
  49. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  50. package/dist/__generated__/client/models/TaskRun.js +2 -0
  51. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  52. package/dist/__generated__/client/models.js +2 -0
  53. package/dist/__generated__/client/models.js.map +1 -0
  54. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  55. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  56. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  57. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  59. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  60. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  61. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  62. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  63. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  64. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  65. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  67. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  68. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  69. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  70. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  71. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  72. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  73. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  74. package/dist/cli/commands/doctor-report.js +1 -13
  75. package/dist/cli/commands/doctor-report.js.map +1 -1
  76. package/dist/cli/commands/lifecycle.js +50 -65
  77. package/dist/cli/commands/lifecycle.js.map +1 -1
  78. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  79. package/dist/cli/commands/process-helpers.js.map +1 -0
  80. package/dist/config.js +17 -55
  81. package/dist/config.js.map +1 -1
  82. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  83. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  84. package/dist/control-plane/bootstrap.js +25 -38
  85. package/dist/control-plane/bootstrap.js.map +1 -1
  86. package/dist/control-plane/change-notifications.js +45 -14
  87. package/dist/control-plane/change-notifications.js.map +1 -1
  88. package/dist/control-plane/data-access.js +1 -106
  89. package/dist/control-plane/data-access.js.map +1 -1
  90. package/dist/control-plane/default-playbook-policy.js +497 -94
  91. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  92. package/dist/control-plane/definitions.js +4 -5
  93. package/dist/control-plane/definitions.js.map +1 -1
  94. package/dist/control-plane/engine-transport.js +381 -0
  95. package/dist/control-plane/engine-transport.js.map +1 -0
  96. package/dist/control-plane/inbox.js +15 -7
  97. package/dist/control-plane/inbox.js.map +1 -1
  98. package/dist/control-plane/index.js +0 -1
  99. package/dist/control-plane/index.js.map +1 -1
  100. package/dist/control-plane/merge-override-audit.js +2 -2
  101. package/dist/control-plane/merge-override-audit.js.map +1 -1
  102. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  103. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  104. package/dist/control-plane/query-types.js +2 -0
  105. package/dist/control-plane/query-types.js.map +1 -0
  106. package/dist/control-plane/run-profiles.js +89 -0
  107. package/dist/control-plane/run-profiles.js.map +1 -0
  108. package/dist/control-plane/schema-migration.js +0 -35
  109. package/dist/control-plane/schema-migration.js.map +1 -1
  110. package/dist/control-plane/seed-default-playbook.js +2 -31
  111. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  112. package/dist/control-plane/tables.js +8 -0
  113. package/dist/control-plane/tables.js.map +1 -1
  114. package/dist/control-plane/transport.js +16 -0
  115. package/dist/control-plane/transport.js.map +1 -0
  116. package/dist/control-plane/versioned-meaning.js +56 -14
  117. package/dist/control-plane/versioned-meaning.js.map +1 -1
  118. package/dist/e2e/kit/agents.js +60 -6
  119. package/dist/e2e/kit/agents.js.map +1 -1
  120. package/dist/e2e/kit/assertions.js +13 -1
  121. package/dist/e2e/kit/assertions.js.map +1 -1
  122. package/dist/e2e/kit/drive.js +46 -11
  123. package/dist/e2e/kit/drive.js.map +1 -1
  124. package/dist/e2e/kit/env.js +17 -1
  125. package/dist/e2e/kit/env.js.map +1 -1
  126. package/dist/e2e/kit/fake-integrator.js +25 -5
  127. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  128. package/dist/e2e/kit/gh-emulator.js +158 -24
  129. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  130. package/dist/e2e/kit/harness.js +44 -10
  131. package/dist/e2e/kit/harness.js.map +1 -1
  132. package/dist/e2e/kit/index.js +1 -0
  133. package/dist/e2e/kit/index.js.map +1 -1
  134. package/dist/e2e/kit/scenario.js +165 -0
  135. package/dist/e2e/kit/scenario.js.map +1 -0
  136. package/dist/e2e/kit/scenarios.js +12 -18
  137. package/dist/e2e/kit/scenarios.js.map +1 -1
  138. package/dist/engine/dbos.service.js +8 -1
  139. package/dist/engine/dbos.service.js.map +1 -1
  140. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  141. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  142. package/dist/host/daemon.js +32 -8
  143. package/dist/host/daemon.js.map +1 -1
  144. package/dist/host/ensure-host.js +3 -3
  145. package/dist/host/ensure-host.js.map +1 -1
  146. package/dist/host/host-runtime.js.map +1 -1
  147. package/dist/host/host.lifecycle.js +3 -16
  148. package/dist/host/host.lifecycle.js.map +1 -1
  149. package/dist/http/graphql-host.js +17 -8
  150. package/dist/http/graphql-host.js.map +1 -1
  151. package/dist/mcp/mcp-capabilities.js +3 -2
  152. package/dist/mcp/mcp-capabilities.js.map +1 -1
  153. package/dist/mcp/mcp-facade.service.js +21 -0
  154. package/dist/mcp/mcp-facade.service.js.map +1 -1
  155. package/dist/mcp/mcp-tools.js +35 -1
  156. package/dist/mcp/mcp-tools.js.map +1 -1
  157. package/dist/pipeline/data-driven-task.workflow.js +276 -57
  158. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  159. package/dist/pipeline/pipeline.service.js +27 -5
  160. package/dist/pipeline/pipeline.service.js.map +1 -1
  161. package/dist/pipeline/route-contract.js +123 -3
  162. package/dist/pipeline/route-contract.js.map +1 -1
  163. package/dist/pipeline-core/condition-scopes.js +15 -0
  164. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  165. package/dist/pipeline-core/index.js +2 -0
  166. package/dist/pipeline-core/index.js.map +1 -1
  167. package/dist/pipeline-core/kit/fixtures.js +81 -44
  168. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  169. package/dist/pipeline-core/materialize.js +221 -0
  170. package/dist/pipeline-core/materialize.js.map +1 -0
  171. package/dist/pipeline-core/types.js +5 -0
  172. package/dist/pipeline-core/types.js.map +1 -1
  173. package/dist/pipeline-core/validate-loops.js +2 -15
  174. package/dist/pipeline-core/validate-loops.js.map +1 -1
  175. package/dist/pipeline-core/validate-resilience.js +257 -0
  176. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  177. package/dist/pipeline-core/validate.js +2 -0
  178. package/dist/pipeline-core/validate.js.map +1 -1
  179. package/dist/playbook/catalog-loader.js +39 -2
  180. package/dist/playbook/catalog-loader.js.map +1 -1
  181. package/dist/playbook/catalog-schema-validator.js +438 -0
  182. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  183. package/dist/playbook/import-mapper.js +41 -0
  184. package/dist/playbook/import-mapper.js.map +1 -1
  185. package/dist/playbook/manifest.js +5 -0
  186. package/dist/playbook/manifest.js.map +1 -1
  187. package/dist/playbook/playbook-installer.js +17 -1
  188. package/dist/playbook/playbook-installer.js.map +1 -1
  189. package/dist/poller/pr-readiness-core.js +257 -93
  190. package/dist/poller/pr-readiness-core.js.map +1 -1
  191. package/dist/poller/pr-readiness.js +2 -2
  192. package/dist/poller/pr-readiness.js.map +1 -1
  193. package/dist/revisium/inbox.service.js +8 -8
  194. package/dist/revisium/inbox.service.js.map +1 -1
  195. package/dist/revisium/playbooks.service.js +116 -21
  196. package/dist/revisium/playbooks.service.js.map +1 -1
  197. package/dist/revisium/revisium.module.js +8 -8
  198. package/dist/revisium/revisium.module.js.map +1 -1
  199. package/dist/revisium/roles.service.js +3 -1
  200. package/dist/revisium/roles.service.js.map +1 -1
  201. package/dist/revisium/run.service.js +9 -8
  202. package/dist/revisium/run.service.js.map +1 -1
  203. package/dist/revisium/tokens.js +0 -1
  204. package/dist/revisium/tokens.js.map +1 -1
  205. package/dist/run/create-run.js +1 -1
  206. package/dist/run/create-run.js.map +1 -1
  207. package/dist/run/inspect-run.js +32 -28
  208. package/dist/run/inspect-run.js.map +1 -1
  209. package/dist/run/issue-ref.js +97 -2
  210. package/dist/run/issue-ref.js.map +1 -1
  211. package/dist/run/prisma-runtime-data-access.js +619 -0
  212. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  213. package/dist/run/run-outputs.js +39 -14
  214. package/dist/run/run-outputs.js.map +1 -1
  215. package/dist/run/terminal-run-status.js.map +1 -1
  216. package/dist/runners/integrator.js +657 -56
  217. package/dist/runners/integrator.js.map +1 -1
  218. package/dist/runners/worktree.service.js +10 -8
  219. package/dist/runners/worktree.service.js.map +1 -1
  220. package/dist/schema/ajv-errors.js +15 -0
  221. package/dist/schema/ajv-errors.js.map +1 -0
  222. package/dist/smoke/isolation.js +7 -16
  223. package/dist/smoke/isolation.js.map +1 -1
  224. package/dist/storage/ensure-storage.js +117 -0
  225. package/dist/storage/ensure-storage.js.map +1 -0
  226. package/dist/storage/revo-database.js +39 -0
  227. package/dist/storage/revo-database.js.map +1 -0
  228. package/dist/storage/revo-prisma.service.js +31 -0
  229. package/dist/storage/revo-prisma.service.js.map +1 -0
  230. package/dist/storage/revo-storage.module.js +18 -0
  231. package/dist/storage/revo-storage.module.js.map +1 -0
  232. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  233. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  234. package/dist/task-control-plane/run-watch.service.js +3 -6
  235. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  236. package/dist/task-control-plane/task-control-plane-api.service.js +225 -43
  237. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  238. package/dist/testing/runtime-data-access.js +175 -0
  239. package/dist/testing/runtime-data-access.js.map +1 -0
  240. package/dist/worker/build-context.js +178 -2
  241. package/dist/worker/build-context.js.map +1 -1
  242. package/dist/worker/claude-code-runner.js +22 -3
  243. package/dist/worker/claude-code-runner.js.map +1 -1
  244. package/dist/worker/codex-runner.js +6 -1
  245. package/dist/worker/codex-runner.js.map +1 -1
  246. package/package.json +20 -7
  247. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  248. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  249. package/prisma/migrations/migration_lock.toml +3 -0
  250. package/prisma/schema.prisma +339 -0
  251. package/prisma.config.ts +11 -0
  252. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  253. package/dist/control-plane/client-transport.js +0 -168
  254. package/dist/control-plane/client-transport.js.map +0 -1
  255. package/dist/engine/ensure-postgres.js +0 -46
  256. package/dist/engine/ensure-postgres.js.map +0 -1
  257. package/dist/host/ensure-revisium.js +0 -135
  258. package/dist/host/ensure-revisium.js.map +0 -1
  259. package/revisium.config.json +0 -10
@@ -1,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(', ');
109
107
  return {
110
108
  needsHuman: true,
111
- lesson: `Ambiguous: ${onBase.length} open PRs for head branch "${headBranch}" targeting base "${baseBranch}" - candidates #${candidates} - manual review needed`,
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);
135
+ return {
136
+ needsHuman: true,
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,36 +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
+ }
213
274
  // gh api graphql wraps responses in {"data":{...}}; unwrap tolerantly so unit fixtures still work.
214
275
  function unwrapGraphqlData(raw) {
215
276
  const top = asRecord(raw);
216
277
  const data = top ? asRecord(top['data']) : null;
217
278
  return data ?? raw;
218
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
+ }
219
291
  function mapReviewThreads(raw) {
220
- const root = asRecord(unwrapGraphqlData(raw));
221
- const repository = asRecord(root?.['repository']);
222
- const pullRequest = asRecord(repository?.['pullRequest']);
223
- const reviewThreads = asRecord(pullRequest?.['reviewThreads']);
224
- const nodes = Array.isArray(reviewThreads?.['nodes']) ? reviewThreads.nodes : [];
225
- return nodes.flatMap((node) => {
226
- const thread = asRecord(node);
227
- if (!thread)
228
- return [];
229
- const comments = asRecord(thread.comments);
230
- const firstComment = Array.isArray(comments?.['nodes']) ? asRecord(comments.nodes[0]) : null;
231
- const author = asRecord(firstComment?.['author']);
232
- return [{
233
- id: asStr(thread.id),
234
- isResolved: Boolean(thread.isResolved),
235
- isOutdated: Boolean(thread.isOutdated),
236
- path: asStr(thread.path) || undefined,
237
- line: typeof thread.line === 'number' ? thread.line : undefined,
238
- author: asStr(author?.['login']) || undefined,
239
- body: compactBody(asStr(firstComment?.['body'])),
240
- url: asStr(firstComment?.['url']) || undefined,
241
- }];
242
- });
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 };
243
339
  }
244
340
  function splitRepo(repo) {
245
341
  const [owner, name] = repo.split('/');
@@ -251,7 +347,7 @@ function fetchReviewThreads(repo, prNumber, execGh) {
251
347
  const { owner, name } = splitRepo(repo);
252
348
  const raw = execGh([
253
349
  'api', 'graphql',
254
- '-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}}}}}}}}',
255
351
  '-f', `owner=${owner}`,
256
352
  '-f', `name=${name}`,
257
353
  '-F', `number=${prNumber}`,
@@ -259,12 +355,15 @@ function fetchReviewThreads(repo, prNumber, execGh) {
259
355
  return mapReviewThreads(parseGhJson(raw, `review-threads #${prNumber}`));
260
356
  }
261
357
  function mapRequiredCheckNames(raw) {
262
- const root = asRecord(unwrapGraphqlData(raw));
263
- const repository = asRecord(root?.['repository']);
264
- const pullRequest = asRecord(repository?.['pullRequest']);
265
- const rollup = asRecord(pullRequest?.['statusCheckRollup']);
266
- const contexts = asRecord(rollup?.['contexts']);
267
- 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');
268
367
  const required = new Set();
269
368
  for (const node of nodes) {
270
369
  const ctx = asRecord(node);
@@ -287,13 +386,56 @@ export function fetchRequiredCheckNames(repo, prNumber, execGh) {
287
386
  ]);
288
387
  return mapRequiredCheckNames(parseGhJson(raw, `required-checks #${prNumber}`));
289
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
+ }
290
419
  function collectReviewThreads(input, prNumber, execGh) {
291
- const threads = input.includeReviewThreads === false ? [] : fetchReviewThreads(input.repo, prNumber, execGh);
292
- 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);
293
427
  return {
294
- included: input.includeReviewThreads !== false,
295
- unresolvedCount: unresolved.length,
296
- 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),
297
439
  };
298
440
  }
299
441
  function fetchComments(input, prNumber, execGh) {
@@ -419,7 +561,7 @@ function compactCheckLists(checks) {
419
561
  const pass = [];
420
562
  const fail = [];
421
563
  for (const check of checks) {
422
- if (['QUEUED', 'IN_PROGRESS', 'PENDING'].includes(check.result)) {
564
+ if (['EXPECTED', 'QUEUED', 'IN_PROGRESS', 'PENDING'].includes(check.result)) {
423
565
  pending.push(check.name);
424
566
  continue;
425
567
  }
@@ -467,7 +609,7 @@ function providerState(checks, botComments) {
467
609
  }
468
610
  function locationOf(item) {
469
611
  const path = item.path ?? item.component ?? '';
470
- return item.line ? `${path}:${item.line}` : path;
612
+ return typeof item.line === 'number' ? `${path}:${item.line}` : path;
471
613
  }
472
614
  function providerWaitFeedback(state, botComments) {
473
615
  const codeRabbit = state.codeRabbit;
@@ -482,9 +624,6 @@ function providerWaitFeedback(state, botComments) {
482
624
  resumeAfter: extractResumeHint(botComments),
483
625
  }];
484
626
  }
485
- function escapeRegExp(value) {
486
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
487
- }
488
627
  function issueRefTitleTag(issueRef, repo) {
489
628
  return issueRef.repo.toLowerCase() === repo.toLowerCase()
490
629
  ? `#${issueRef.number}`
@@ -493,25 +632,15 @@ function issueRefTitleTag(issueRef, repo) {
493
632
  function hasIssueBranchToken(head, issueRef) {
494
633
  return new RegExp(`(?:^|[/-])issue-${issueRef.number}(?:-|$)`).test(head ?? '');
495
634
  }
496
- function hasIssueTitleToken(title, issueRef, repo) {
497
- const boundaryBefore = String.raw `(?:^|[\s([{"'])`;
498
- const boundaryAfter = String.raw `(?=$|[^A-Za-z0-9_-])`;
499
- const qualified = new RegExp(`${boundaryBefore}${escapeRegExp(`${issueRef.repo}#${issueRef.number}`)}${boundaryAfter}`, 'i');
500
- if (qualified.test(title ?? ''))
501
- return true;
502
- if (issueRef.repo.toLowerCase() !== repo.toLowerCase())
503
- return false;
504
- return new RegExp(`${boundaryBefore}#${issueRef.number}${boundaryAfter}`).test(title ?? '');
505
- }
506
- function issueLinkageFeedback(issueRef, repo, pr) {
507
- if (!issueRef)
635
+ function issueLinkageFeedback(issueRef, issueAction, repo, pr) {
636
+ if (!issueRef || issueAction === 'none')
508
637
  return [];
509
638
  const issueBranch = `issue-${issueRef.number}`;
510
639
  const issueTag = issueRefTitleTag(issueRef, repo);
511
640
  const missing = [];
512
641
  if (!hasIssueBranchToken(pr.head, issueRef))
513
642
  missing.push(`branch missing ${issueBranch}`);
514
- if (!hasIssueTitleToken(pr.title, issueRef, repo))
643
+ if (!hasIssueRefToken(pr.title, issueRef, repo))
515
644
  missing.push(`title missing ${issueTag}`);
516
645
  if (missing.length === 0)
517
646
  return [];
@@ -521,6 +650,16 @@ function issueLinkageFeedback(issueRef, repo, pr) {
521
650
  evidence: `${missing.join('; ')} (head=${pr.head ?? ''}, title=${pr.title ?? ''})`,
522
651
  }];
523
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
+ }
524
663
  // Bot logins whose comments are purely informational and safe to suppress.
525
664
  // Anything not in this list surfaces in developerFixes to avoid silent suppression.
526
665
  const KNOWN_INFORMATIONAL_BOTS = new Set([
@@ -554,6 +693,13 @@ function buildFeedback(input) {
554
693
  author: review.user?.login ?? '',
555
694
  evidence: review.state,
556
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
+ : []),
557
703
  ...input.reviewThreads.items.map((thread) => ({
558
704
  source: 'review_thread',
559
705
  summary: thread.body,
@@ -564,6 +710,7 @@ function buildFeedback(input) {
564
710
  ...codeRabbitActionableFeedback(input.coderabbitReviews ?? [], input.botComments, new Set(input.reviewThreads.items.map((thread) => locationOf(thread)).filter((loc) => loc !== ''))),
565
711
  ...input.botComments
566
712
  .filter((comment) => !isCodeRabbit(comment.user) && !KNOWN_INFORMATIONAL_BOTS.has(comment.user?.login ?? ''))
713
+ .filter((comment) => !isStaleThreadFeedback(comment, input.staleThreadEvidence))
567
714
  .map((comment) => ({
568
715
  source: 'bot_comment',
569
716
  summary: compactBody(comment.body),
@@ -585,7 +732,8 @@ function buildFeedback(input) {
585
732
  ? [{ source: 'github_review_decision', summary: `Review decision is ${input.reviewDecision}` }]
586
733
  : []),
587
734
  ...(input.sonar.unavailable ? [{ source: 'sonar', summary: 'Sonar was configured but unavailable.' }] : []),
588
- ...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),
589
737
  ];
590
738
  const ignoredNoise = input.botComments
591
739
  .filter((comment) => !isCodeRabbit(comment.user) && KNOWN_INFORMATIONAL_BOTS.has(comment.user?.login ?? ''))
@@ -642,7 +790,9 @@ function buildEmptyFeedback(input) {
642
790
  humanComments: [],
643
791
  botComments: [],
644
792
  issueRef: input.issueRef,
645
- repo: '',
793
+ issueAction: input.issueAction,
794
+ repo: input.repo ?? '',
795
+ prView: input.prView,
646
796
  });
647
797
  }
648
798
  function buildMergedReadiness(input, resolved) {
@@ -658,7 +808,7 @@ function buildMergedReadiness(input, resolved) {
658
808
  sonar: emptySonar(sonarConfigured),
659
809
  nextAction: 'none',
660
810
  evidence: [`PR #${resolved.prNumber} is merged.`],
661
- feedback: buildEmptyFeedback({ sonarConfigured, includeReviewThreads }),
811
+ feedback: buildEmptyFeedback({ sonarConfigured, includeReviewThreads, issueRef: input.issueRef, issueAction: input.issueAction, repo: input.repo, prView: resolved.prView }),
662
812
  ciSummary: { ...emptyCiSummary(true), ...(input.issueRef ? { issueRef: input.issueRef } : {}) },
663
813
  ...(input.issueRef ? { issueRef: input.issueRef } : {}),
664
814
  };
@@ -666,7 +816,7 @@ function buildMergedReadiness(input, resolved) {
666
816
  function buildNeedsHumanReadiness(input, resolved) {
667
817
  const includeReviewThreads = input.includeReviewThreads !== false;
668
818
  const sonarConfigured = Boolean(input.sonarProject);
669
- const feedback = buildEmptyFeedback({ sonarConfigured, includeReviewThreads });
819
+ const feedback = buildEmptyFeedback({ sonarConfigured, includeReviewThreads, issueRef: input.issueRef, issueAction: input.issueAction, repo: input.repo, prView: resolved.prView });
670
820
  return {
671
821
  verdict: resolved.verdict === 'closed' ? 'closed' : 'needs_human',
672
822
  pr: resolved.prNumber ? prFromView(resolved.prNumber, resolved.prView) : emptyPr(input.prNumber, resolved.verdict),
@@ -683,6 +833,9 @@ function buildNeedsHumanReadiness(input, resolved) {
683
833
  };
684
834
  }
685
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;
686
839
  const pendingCodeRabbit = input.ci.pendingNames.find((name) => name.toLowerCase().includes('coderabbit'));
687
840
  const pendingProviderState = pendingCodeRabbit
688
841
  ? {
@@ -695,7 +848,7 @@ function buildWaitingReadiness(input) {
695
848
  }
696
849
  : {};
697
850
  const feedback = buildFeedback({
698
- checks: input.checkLists,
851
+ checks: checkLists,
699
852
  providerState: pendingProviderState,
700
853
  sonar: emptySonar(input.sonarConfigured),
701
854
  reviewDecision: input.prView.reviewDecision ?? '',
@@ -704,8 +857,10 @@ function buildWaitingReadiness(input) {
704
857
  humanComments: [],
705
858
  botComments: [],
706
859
  issueRef: input.issueRef,
860
+ issueAction: input.issueAction,
707
861
  repo: input.repo,
708
862
  pr: { head: input.prView.headRefName, title: input.prView.title },
863
+ prView: input.prView,
709
864
  });
710
865
  const providerWait = pendingCodeRabbit
711
866
  ? [{
@@ -720,7 +875,7 @@ function buildWaitingReadiness(input) {
720
875
  return {
721
876
  verdict: 'waiting',
722
877
  pr: prFromView(input.prNumber, input.prView),
723
- checks: input.checkLists,
878
+ checks: checkLists,
724
879
  reviewDecision: input.prView.reviewDecision ?? '',
725
880
  reviewThreads: input.reviewThreads,
726
881
  providerState: pendingProviderState,
@@ -755,6 +910,7 @@ function readinessAction(input) {
755
910
  return { verdict: 'ready', nextAction: 'ready_for_merge_gate' };
756
911
  }
757
912
  export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSonar = defaultFetchSonar) {
913
+ input = { ...input, issueAction: input.issueAction ?? defaultIssueAction(input.issueRef) };
758
914
  const baseBranch = input.baseBranch ?? 'master';
759
915
  const resolved = resolveOpenPr(input, baseBranch, execGh);
760
916
  if (resolved.kind === 'merged') {
@@ -765,9 +921,9 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
765
921
  }
766
922
  const { prNumber, prView } = resolved;
767
923
  const checks = prView.statusCheckRollup ?? [];
768
- const ci = collectCiChecks(checks);
924
+ const ci = collectCiChecks(checks, { emptyIsPending: prView.statusCheckRollup === null });
769
925
  const checkLists = compactCheckLists(ci.checks);
770
- const reviewThreads = collectReviewThreads(input, prNumber, execGh);
926
+ const { reviewThreads, staleThreadEvidence } = collectReviewThreads(input, prNumber, execGh);
771
927
  const sonarConfigured = Boolean(input.sonarProject);
772
928
  if (prView.isDraft === true) {
773
929
  return buildWaitingReadiness({
@@ -779,6 +935,7 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
779
935
  reviewThreads,
780
936
  sonarConfigured,
781
937
  issueRef: input.issueRef,
938
+ issueAction: input.issueAction,
782
939
  evidence: [`PR #${prNumber} is still draft.`],
783
940
  isDraft: true,
784
941
  });
@@ -793,6 +950,7 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
793
950
  reviewThreads,
794
951
  sonarConfigured,
795
952
  issueRef: input.issueRef,
953
+ issueAction: input.issueAction,
796
954
  evidence: ci.pendingNames.length > 0 ? [`Pending checks: ${ci.pendingNames.join(', ')}`] : ['No check rollup entries are registered yet.'],
797
955
  });
798
956
  }
@@ -816,10 +974,13 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
816
974
  humanReviews: comments.human_reviews,
817
975
  humanComments: comments.human_comments,
818
976
  botComments: comments.bot_comments,
977
+ staleThreadEvidence,
819
978
  coderabbitReviews: comments.coderabbit_reviews,
820
979
  issueRef: input.issueRef,
980
+ issueAction: input.issueAction,
821
981
  repo: input.repo,
822
982
  pr: { head: prView.headRefName, title: prView.title },
983
+ prView,
823
984
  });
824
985
  const ciSummary = {
825
986
  ci_passed: ci.ci_passed,
@@ -848,7 +1009,9 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
848
1009
  nextAction,
849
1010
  evidence: [
850
1011
  `PR #${prNumber} state=${prView.state ?? 'unknown'} draft=${Boolean(prView.isDraft)}`,
851
- `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}`,
852
1015
  ...(input.issueRef ? [`issueRef ${issueRefTitleTag(input.issueRef, input.repo)} expected in branch and title`] : []),
853
1016
  ...(providers.codeRabbit?.reason === 'provider_limit' ? ['CodeRabbit provider/rate limit comment detected.'] : []),
854
1017
  ...(verdict === 'ready' && hasInformationalProviderWait
@@ -868,6 +1031,7 @@ export function toReadinessInput(input) {
868
1031
  baseBranch: input.base_branch,
869
1032
  sonarProject: input.sonar_project,
870
1033
  issueRef: input.issue_ref,
1034
+ issueAction: input.issue_action,
871
1035
  includeComments: true,
872
1036
  includeReviewThreads: false,
873
1037
  };