@kbediako/codex-orchestrator 0.1.37 → 0.2.0

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 (302) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +73 -291
  3. package/bin/codex-orchestrator.js +161 -0
  4. package/codex.orchestrator.json +149 -13
  5. package/dist/bin/codex-orchestrator.js +795 -1154
  6. package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +22 -4
  7. package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +3 -3
  8. package/dist/orchestrator/src/cli/adapters/CommandTester.js +2 -2
  9. package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +183 -11
  10. package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
  11. package/dist/orchestrator/src/cli/coStatusCliShell.js +429 -0
  12. package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
  13. package/dist/orchestrator/src/cli/codexCliShell.js +72 -0
  14. package/dist/orchestrator/src/cli/codexDefaultsSetup.js +49 -11
  15. package/dist/orchestrator/src/cli/config/delegationConfig.js +317 -5
  16. package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +2 -3
  17. package/dist/orchestrator/src/cli/config/userConfig.js +28 -13
  18. package/dist/orchestrator/src/cli/control/authenticatedControlRouteGate.js +69 -0
  19. package/dist/orchestrator/src/cli/control/authenticatedRouteComposition.js +267 -0
  20. package/dist/orchestrator/src/cli/control/authenticatedRouteController.js +5 -0
  21. package/dist/orchestrator/src/cli/control/authenticatedRouteDispatcher.js +41 -0
  22. package/dist/orchestrator/src/cli/control/compatibilityIssuePresenter.js +1035 -0
  23. package/dist/orchestrator/src/cli/control/confirmationApproveController.js +62 -0
  24. package/dist/orchestrator/src/cli/control/confirmationCreateController.js +69 -0
  25. package/dist/orchestrator/src/cli/control/confirmationIssueConsumeController.js +43 -0
  26. package/dist/orchestrator/src/cli/control/confirmationListController.js +22 -0
  27. package/dist/orchestrator/src/cli/control/confirmationValidateController.js +58 -0
  28. package/dist/orchestrator/src/cli/control/confirmations.js +25 -3
  29. package/dist/orchestrator/src/cli/control/controlActionCancelConfirmation.js +65 -0
  30. package/dist/orchestrator/src/cli/control/controlActionController.js +77 -0
  31. package/dist/orchestrator/src/cli/control/controlActionControllerSequencing.js +161 -0
  32. package/dist/orchestrator/src/cli/control/controlActionExecution.js +142 -0
  33. package/dist/orchestrator/src/cli/control/controlActionFinalization.js +43 -0
  34. package/dist/orchestrator/src/cli/control/controlActionOutcome.js +60 -0
  35. package/dist/orchestrator/src/cli/control/controlActionPreflight.js +476 -0
  36. package/dist/orchestrator/src/cli/control/controlAuthenticatedRouteHandoff.js +57 -0
  37. package/dist/orchestrator/src/cli/control/controlBootstrapAssembly.js +39 -0
  38. package/dist/orchestrator/src/cli/control/controlBootstrapMetadataPersistence.js +16 -0
  39. package/dist/orchestrator/src/cli/control/controlEventTransport.js +49 -0
  40. package/dist/orchestrator/src/cli/control/controlExpiryLifecycle.js +102 -0
  41. package/dist/orchestrator/src/cli/control/controlHostOwnership.js +480 -0
  42. package/dist/orchestrator/src/cli/control/controlHostSupervision.js +608 -0
  43. package/dist/orchestrator/src/cli/control/controlOversightFacade.js +8 -0
  44. package/dist/orchestrator/src/cli/control/controlOversightReadContract.js +1 -0
  45. package/dist/orchestrator/src/cli/control/controlOversightReadService.js +16 -0
  46. package/dist/orchestrator/src/cli/control/controlOversightUpdateContract.js +1 -0
  47. package/dist/orchestrator/src/cli/control/controlPersistenceFiles.js +6 -0
  48. package/dist/orchestrator/src/cli/control/controlQuestionChildResolution.js +18 -0
  49. package/dist/orchestrator/src/cli/control/controlRequestContext.js +42 -0
  50. package/dist/orchestrator/src/cli/control/controlRequestController.js +9 -0
  51. package/dist/orchestrator/src/cli/control/controlRequestPredispatch.js +17 -0
  52. package/dist/orchestrator/src/cli/control/controlRequestRouteDispatch.js +44 -0
  53. package/dist/orchestrator/src/cli/control/controlRuntime.js +992 -0
  54. package/dist/orchestrator/src/cli/control/controlServer.js +23 -1456
  55. package/dist/orchestrator/src/cli/control/controlServerAuditAndErrorHelpers.js +115 -0
  56. package/dist/orchestrator/src/cli/control/controlServerAuthenticatedRouteBranch.js +29 -0
  57. package/dist/orchestrator/src/cli/control/controlServerBootstrapLifecycle.js +30 -0
  58. package/dist/orchestrator/src/cli/control/controlServerBootstrapStartSequence.js +21 -0
  59. package/dist/orchestrator/src/cli/control/controlServerOwnedRuntimeLifecycle.js +67 -0
  60. package/dist/orchestrator/src/cli/control/controlServerPublicLifecycle.js +756 -0
  61. package/dist/orchestrator/src/cli/control/controlServerPublicRouteHelpers.js +86 -0
  62. package/dist/orchestrator/src/cli/control/controlServerReadyInstanceLifecycle.js +25 -0
  63. package/dist/orchestrator/src/cli/control/controlServerReadyInstanceStartup.js +18 -0
  64. package/dist/orchestrator/src/cli/control/controlServerRequestBodyHelpers.js +37 -0
  65. package/dist/orchestrator/src/cli/control/controlServerRequestShell.js +40 -0
  66. package/dist/orchestrator/src/cli/control/controlServerRequestShellBinding.js +17 -0
  67. package/dist/orchestrator/src/cli/control/controlServerSeedLoading.js +27 -0
  68. package/dist/orchestrator/src/cli/control/controlServerSeededRuntimeAssembly.js +186 -0
  69. package/dist/orchestrator/src/cli/control/controlServerStartupInputPreparation.js +31 -0
  70. package/dist/orchestrator/src/cli/control/controlServerStartupSequence.js +49 -0
  71. package/dist/orchestrator/src/cli/control/controlState.js +233 -2
  72. package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +1899 -0
  73. package/dist/orchestrator/src/cli/control/controlTelegramBridgeBootstrapLifecycle.js +22 -0
  74. package/dist/orchestrator/src/cli/control/controlTelegramBridgeLifecycle.js +67 -0
  75. package/dist/orchestrator/src/cli/control/controlTelegramBridgeOversightFacadeFactory.js +8 -0
  76. package/dist/orchestrator/src/cli/control/controlTelegramCommandController.js +49 -0
  77. package/dist/orchestrator/src/cli/control/controlTelegramDispatchRead.js +40 -0
  78. package/dist/orchestrator/src/cli/control/controlTelegramPollingController.js +89 -0
  79. package/dist/orchestrator/src/cli/control/controlTelegramProjectionNotificationController.js +29 -0
  80. package/dist/orchestrator/src/cli/control/controlTelegramPushState.js +63 -0
  81. package/dist/orchestrator/src/cli/control/controlTelegramQuestionRead.js +13 -0
  82. package/dist/orchestrator/src/cli/control/controlTelegramReadController.js +216 -0
  83. package/dist/orchestrator/src/cli/control/controlTelegramUpdateHandler.js +63 -0
  84. package/dist/orchestrator/src/cli/control/controlWatcher.js +73 -5
  85. package/dist/orchestrator/src/cli/control/delegationRegisterController.js +35 -0
  86. package/dist/orchestrator/src/cli/control/dynamicToolBridgePolicy.js +139 -0
  87. package/dist/orchestrator/src/cli/control/eventsSseController.js +12 -0
  88. package/dist/orchestrator/src/cli/control/linearBudgetState.js +1789 -0
  89. package/dist/orchestrator/src/cli/control/linearDispatchSource.js +1137 -0
  90. package/dist/orchestrator/src/cli/control/linearGraphqlClient.js +150 -0
  91. package/dist/orchestrator/src/cli/control/linearRateLimit.js +102 -0
  92. package/dist/orchestrator/src/cli/control/linearWebhookController.js +499 -0
  93. package/dist/orchestrator/src/cli/control/liveLinearAdvisoryRuntime.js +70 -0
  94. package/dist/orchestrator/src/cli/control/observabilityApiController.js +173 -0
  95. package/dist/orchestrator/src/cli/control/observabilityReadModel.js +500 -0
  96. package/dist/orchestrator/src/cli/control/observabilitySurface.js +284 -0
  97. package/dist/orchestrator/src/cli/control/observabilityUpdateNotifier.js +22 -0
  98. package/dist/orchestrator/src/cli/control/operatorDashboardPresenter.js +252 -0
  99. package/dist/orchestrator/src/cli/control/providerAgentCapacity.js +70 -0
  100. package/dist/orchestrator/src/cli/control/providerControlHostFreshnessGauge.js +1068 -0
  101. package/dist/orchestrator/src/cli/control/providerIntakeState.js +473 -0
  102. package/dist/orchestrator/src/cli/control/providerIssueHandoff.js +6811 -0
  103. package/dist/orchestrator/src/cli/control/providerIssueObservability.js +1348 -0
  104. package/dist/orchestrator/src/cli/control/providerIssueRetryQueue.js +84 -0
  105. package/dist/orchestrator/src/cli/control/providerLinearRuntimeProof.js +588 -0
  106. package/dist/orchestrator/src/cli/control/providerLinearScreenshotProof.js +473 -0
  107. package/dist/orchestrator/src/cli/control/providerLinearWorkerTruth.js +383 -0
  108. package/dist/orchestrator/src/cli/control/providerLinearWorkflowAudit.js +254 -0
  109. package/dist/orchestrator/src/cli/control/providerLinearWorkflowFacade.js +5573 -0
  110. package/dist/orchestrator/src/cli/control/providerLinearWorkflowStates.js +115 -0
  111. package/dist/orchestrator/src/cli/control/providerMergeCloseout.js +1868 -0
  112. package/dist/orchestrator/src/cli/control/providerOperatorAutopilot.js +1580 -0
  113. package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLifecycle.js +154 -0
  114. package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLocalRolloutExecution.js +1006 -0
  115. package/dist/orchestrator/src/cli/control/providerPollingHealth.js +435 -0
  116. package/dist/orchestrator/src/cli/control/providerTerminalCleanup.js +516 -0
  117. package/dist/orchestrator/src/cli/control/providerWorkerHosts.js +191 -0
  118. package/dist/orchestrator/src/cli/control/providerWorkflowConfigStore.js +515 -0
  119. package/dist/orchestrator/src/cli/control/questionChildResolutionAdapter.js +361 -0
  120. package/dist/orchestrator/src/cli/control/questionQueueController.js +181 -0
  121. package/dist/orchestrator/src/cli/control/questionReadRetryDeduplication.js +9 -0
  122. package/dist/orchestrator/src/cli/control/questionReadSequence.js +10 -0
  123. package/dist/orchestrator/src/cli/control/securityViolationController.js +27 -0
  124. package/dist/orchestrator/src/cli/control/selectedRunProjection.js +1838 -0
  125. package/dist/orchestrator/src/cli/control/telegramOversightApiClient.js +48 -0
  126. package/dist/orchestrator/src/cli/control/telegramOversightBridge.js +180 -0
  127. package/dist/orchestrator/src/cli/control/telegramOversightBridgeProjectionDeliveryQueue.js +25 -0
  128. package/dist/orchestrator/src/cli/control/telegramOversightBridgeRuntimeLifecycle.js +45 -0
  129. package/dist/orchestrator/src/cli/control/telegramOversightBridgeStateStore.js +77 -0
  130. package/dist/orchestrator/src/cli/control/telegramOversightControlActionApiClient.js +45 -0
  131. package/dist/orchestrator/src/cli/control/trackerDispatchPilot.js +439 -0
  132. package/dist/orchestrator/src/cli/control/uiDataController.js +34 -0
  133. package/dist/orchestrator/src/cli/control/uiSessionController.js +100 -0
  134. package/dist/orchestrator/src/cli/controlHostCliShell.js +860 -0
  135. package/dist/orchestrator/src/cli/controlHostFreshnessGaugeCliShell.js +129 -0
  136. package/dist/orchestrator/src/cli/controlHostSupervisionCliShell.js +2127 -0
  137. package/dist/orchestrator/src/cli/delegationCliShell.js +62 -0
  138. package/dist/orchestrator/src/cli/delegationServer.js +567 -678
  139. package/dist/orchestrator/src/cli/delegationServerCliShell.js +52 -0
  140. package/dist/orchestrator/src/cli/delegationServerQuestionFlowShell.js +228 -0
  141. package/dist/orchestrator/src/cli/delegationServerToolDispatchShell.js +411 -0
  142. package/dist/orchestrator/src/cli/delegationServerTransport.js +274 -0
  143. package/dist/orchestrator/src/cli/delegationSetup.js +51 -171
  144. package/dist/orchestrator/src/cli/devtoolsCliShell.js +34 -0
  145. package/dist/orchestrator/src/cli/doctor.js +542 -122
  146. package/dist/orchestrator/src/cli/doctorCliRequestShell.js +72 -0
  147. package/dist/orchestrator/src/cli/doctorCliShell.js +138 -0
  148. package/dist/orchestrator/src/cli/doctorUsage.js +136 -16
  149. package/dist/orchestrator/src/cli/exec/experience.js +16 -2
  150. package/dist/orchestrator/src/cli/exec/summary.js +3 -0
  151. package/dist/orchestrator/src/cli/execCliShell.js +51 -0
  152. package/dist/orchestrator/src/cli/flowCliRequestShell.js +44 -0
  153. package/dist/orchestrator/src/cli/flowCliShell.js +239 -0
  154. package/dist/orchestrator/src/cli/frontendTestCliRequestShell.js +80 -0
  155. package/dist/orchestrator/src/cli/frontendTestCliShell.js +41 -0
  156. package/dist/orchestrator/src/cli/init.js +1 -0
  157. package/dist/orchestrator/src/cli/initCliShell.js +50 -0
  158. package/dist/orchestrator/src/cli/linearCliShell.js +1200 -0
  159. package/dist/orchestrator/src/cli/mcpEnableCliShell.js +132 -0
  160. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +3 -2
  161. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +56 -0
  162. package/dist/orchestrator/src/cli/orchestrator.js +66 -1376
  163. package/dist/orchestrator/src/cli/planCliShell.js +19 -0
  164. package/dist/orchestrator/src/cli/prCliShell.js +41 -0
  165. package/dist/orchestrator/src/cli/providerLinearChildLanePhaseContract.js +204 -0
  166. package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +1772 -0
  167. package/dist/orchestrator/src/cli/providerLinearChildLaneShell.js +2420 -0
  168. package/dist/orchestrator/src/cli/providerLinearChildStreamShell.js +385 -0
  169. package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +5738 -0
  170. package/dist/orchestrator/src/cli/resumeCliShell.js +14 -0
  171. package/dist/orchestrator/src/cli/reviewCliLaunchShell.js +72 -0
  172. package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
  173. package/dist/orchestrator/src/cli/rlm/context.js +94 -7
  174. package/dist/orchestrator/src/cli/rlm/rlmCodexRuntimeShell.js +546 -0
  175. package/dist/orchestrator/src/cli/rlm/symbolic.js +4 -2
  176. package/dist/orchestrator/src/cli/rlmCliRequestShell.js +42 -0
  177. package/dist/orchestrator/src/cli/rlmCompletionCliShell.js +46 -0
  178. package/dist/orchestrator/src/cli/rlmLaunchCliShell.js +51 -0
  179. package/dist/orchestrator/src/cli/rlmRunner.js +83 -523
  180. package/dist/orchestrator/src/cli/run/blockMemory.js +500 -0
  181. package/dist/orchestrator/src/cli/run/manifest.js +410 -73
  182. package/dist/orchestrator/src/cli/run/manifestPersister.js +45 -14
  183. package/dist/orchestrator/src/cli/run/runMemoryController.js +216 -0
  184. package/dist/orchestrator/src/cli/run/source0.js +690 -0
  185. package/dist/orchestrator/src/cli/run/workspacePath.js +101 -0
  186. package/dist/orchestrator/src/cli/runtime/mode.js +2 -1
  187. package/dist/orchestrator/src/cli/runtime/provider.js +39 -2
  188. package/dist/orchestrator/src/cli/selfCheckCliShell.js +12 -0
  189. package/dist/orchestrator/src/cli/services/commandRunner.js +668 -18
  190. package/dist/orchestrator/src/cli/services/execRuntime.js +66 -1
  191. package/dist/orchestrator/src/cli/services/orchestratorAutoScoutEvidenceRecorder.js +71 -0
  192. package/dist/orchestrator/src/cli/services/orchestratorCloudBranchResolution.js +8 -0
  193. package/dist/orchestrator/src/cli/services/orchestratorCloudEnvironmentResolution.js +22 -0
  194. package/dist/orchestrator/src/cli/services/orchestratorCloudExecutionLifecycleShell.js +39 -0
  195. package/dist/orchestrator/src/cli/services/orchestratorCloudPromptBuilder.js +37 -0
  196. package/dist/orchestrator/src/cli/services/orchestratorCloudRouteFallbackContract.js +45 -0
  197. package/dist/orchestrator/src/cli/services/orchestratorCloudRouteShell.js +36 -0
  198. package/dist/orchestrator/src/cli/services/orchestratorCloudTargetExecutor.js +277 -0
  199. package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycle.js +98 -0
  200. package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycleShell.js +54 -0
  201. package/dist/orchestrator/src/cli/services/orchestratorExecutionLifecycle.js +112 -0
  202. package/dist/orchestrator/src/cli/services/orchestratorExecutionModePolicy.js +27 -0
  203. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteAdapterShell.js +59 -0
  204. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteDecisionShell.js +57 -0
  205. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteState.js +21 -0
  206. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouter.js +2 -0
  207. package/dist/orchestrator/src/cli/services/orchestratorLocalPipelineExecutor.js +149 -0
  208. package/dist/orchestrator/src/cli/services/orchestratorLocalRouteShell.js +63 -0
  209. package/dist/orchestrator/src/cli/services/orchestratorPlanShell.js +54 -0
  210. package/dist/orchestrator/src/cli/services/orchestratorPlanTargetTracker.js +16 -0
  211. package/dist/orchestrator/src/cli/services/orchestratorResumePreparationShell.js +84 -0
  212. package/dist/orchestrator/src/cli/services/orchestratorResumeTokenValidation.js +15 -0
  213. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleCompletion.js +31 -0
  214. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleExecutionRegistration.js +37 -0
  215. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleOrchestrationShell.js +83 -0
  216. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleTaskManagerShell.js +37 -0
  217. package/dist/orchestrator/src/cli/services/orchestratorRuntimeManifestMutation.js +20 -0
  218. package/dist/orchestrator/src/cli/services/orchestratorStartPreparationShell.js +56 -0
  219. package/dist/orchestrator/src/cli/services/orchestratorStatusShell.js +70 -0
  220. package/dist/orchestrator/src/cli/services/pipelineResolver.js +7 -3
  221. package/dist/orchestrator/src/cli/services/plannerMemory.js +119 -0
  222. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -3
  223. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +9 -0
  224. package/dist/orchestrator/src/cli/setupBootstrapShell.js +114 -0
  225. package/dist/orchestrator/src/cli/setupCliShell.js +51 -0
  226. package/dist/orchestrator/src/cli/skillsCliShell.js +56 -0
  227. package/dist/orchestrator/src/cli/startCliRequestShell.js +53 -0
  228. package/dist/orchestrator/src/cli/startCliShell.js +68 -0
  229. package/dist/orchestrator/src/cli/statusCliShell.js +22 -0
  230. package/dist/orchestrator/src/cli/utils/authProvenanceFingerprint.js +27 -0
  231. package/dist/orchestrator/src/cli/utils/cloudPreflight.js +83 -1
  232. package/dist/orchestrator/src/cli/utils/delegationConfigParser.js +250 -0
  233. package/dist/orchestrator/src/cli/utils/delegationMcpHealth.js +1382 -0
  234. package/dist/orchestrator/src/cli/utils/devtools.js +2 -54
  235. package/dist/orchestrator/src/cli/utils/mcpServerEntry.js +53 -0
  236. package/dist/orchestrator/src/cli/utils/packageProgramResolver.js +151 -0
  237. package/dist/orchestrator/src/cli/utils/providerOverrideEnv.js +71 -0
  238. package/dist/orchestrator/src/cli/utils/trailingJsonObject.js +59 -0
  239. package/dist/orchestrator/src/learning/crystalizer.js +2 -2
  240. package/dist/orchestrator/src/persistence/ExperienceStore.js +233 -49
  241. package/dist/orchestrator/src/persistence/TaskStateStore.js +6 -6
  242. package/dist/orchestrator/src/persistence/lockFile.js +70 -4
  243. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +39 -0
  244. package/dist/orchestrator/src/sync/createCloudSyncWorker.js +3 -2
  245. package/dist/orchestrator/src/utils/atomicWrite.js +17 -2
  246. package/dist/packages/orchestrator/src/exec/unified-exec.js +99 -6
  247. package/dist/packages/orchestrator/src/instructions/promptPacks.js +150 -19
  248. package/dist/packages/sdk-node/src/orchestrator.js +137 -13
  249. package/dist/packages/shared/config/designConfig.js +8 -1
  250. package/dist/packages/shared/streams/stdio.js +1 -1
  251. package/dist/scripts/design/pipeline/permit.js +15 -0
  252. package/dist/scripts/lib/docs-catalog.js +365 -0
  253. package/dist/scripts/lib/docs-helpers.js +87 -5
  254. package/dist/scripts/lib/pr-watch-merge.js +1088 -80
  255. package/dist/scripts/lib/provider-run-contract.js +26 -0
  256. package/dist/scripts/lib/review-command-intent-classification.js +532 -0
  257. package/dist/scripts/lib/review-command-probe-classification.js +385 -0
  258. package/dist/scripts/lib/review-execution-boundary-preflight.js +279 -0
  259. package/dist/scripts/lib/review-execution-runtime.js +753 -0
  260. package/dist/scripts/lib/review-execution-state.js +1144 -0
  261. package/dist/scripts/lib/review-execution-telemetry.js +215 -0
  262. package/dist/scripts/lib/review-inspection-target-parsing.js +78 -0
  263. package/dist/scripts/lib/review-launch-attempt.js +601 -0
  264. package/dist/scripts/lib/review-meta-surface-boundary-analysis.js +300 -0
  265. package/dist/scripts/lib/review-meta-surface-normalization.js +746 -0
  266. package/dist/scripts/lib/review-non-interactive-handoff.js +61 -0
  267. package/dist/scripts/lib/review-prompt-context.js +376 -0
  268. package/dist/scripts/lib/review-scope-advisory.js +286 -0
  269. package/dist/scripts/lib/review-scope-paths.js +123 -0
  270. package/dist/scripts/lib/review-shell-command-parser.js +389 -0
  271. package/dist/scripts/lib/review-shell-env-interpreter.js +340 -0
  272. package/dist/scripts/lib/run-manifests.js +192 -36
  273. package/dist/scripts/lib/spark-policy-classifier.js +593 -0
  274. package/dist/scripts/run-review.js +507 -1777
  275. package/docs/public/downstream-setup.md +106 -0
  276. package/docs/public/provider-onboarding.md +173 -0
  277. package/package.json +30 -11
  278. package/plugins/codex-orchestrator/.codex-plugin/plugin.json +30 -0
  279. package/plugins/codex-orchestrator/.mcp.json +13 -0
  280. package/plugins/codex-orchestrator/launcher.mjs +359 -0
  281. package/schemas/manifest.json +395 -0
  282. package/skills/chrome-devtools/SKILL.md +1 -1
  283. package/skills/codex-orchestrator/SKILL.md +83 -0
  284. package/skills/collab-subagents-first/SKILL.md +2 -1
  285. package/skills/delegation-usage/DELEGATION_GUIDE.md +24 -11
  286. package/skills/delegation-usage/SKILL.md +20 -13
  287. package/skills/land/SKILL.md +77 -0
  288. package/skills/linear/SKILL.md +255 -0
  289. package/skills/release/SKILL.md +47 -3
  290. package/skills/standalone-review/SKILL.md +6 -1
  291. package/templates/README.md +4 -2
  292. package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
  293. package/templates/codex/.codex/agents/explorer-fast.toml +1 -0
  294. package/templates/codex/.codex/agents/worker-complex.toml +1 -1
  295. package/templates/codex/.codex/config.toml +3 -4
  296. package/templates/codex/.codex/providers/README.md +13 -0
  297. package/templates/codex/.codex/providers/control.example.json +18 -0
  298. package/templates/codex/.codex/providers/provider.env.example +15 -0
  299. package/templates/codex/AGENTS.md +12 -7
  300. package/templates/codex/mcp-client.json +5 -1
  301. package/docs/README.md +0 -307
  302. package/docs/assets/setup.gif +0 -0
@@ -14,7 +14,9 @@ const REQUIRED_BUCKET_PENDING = new Set(['pending']);
14
14
  const REQUIRED_BUCKET_FAILED = new Set(['fail', 'cancel', 'skipping']);
15
15
  const MERGEABLE_STATES = new Set(['CLEAN', 'HAS_HOOKS', 'UNSTABLE']);
16
16
  const ACTION_REQUIRED_MERGE_STATES = new Set(['BEHIND', 'DIRTY']);
17
- const BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
17
+ const AUTOMATIC_BRANCH_RECOVERY_REASONS = new Set(['merge_state=BEHIND', 'merge_state=DIRTY']);
18
+ const MERGE_BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
19
+ const REVIEW_HANDOFF_BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED']);
18
20
  const DO_NOT_MERGE_LABEL = /do[\s_-]*not[\s_-]*merge/i;
19
21
  const ACTIONABLE_BOT_LOGINS = new Set([
20
22
  'chatgpt-codex-connector',
@@ -32,6 +34,31 @@ const BOT_MENTION_PATTERNS = {
32
34
  };
33
35
  const BOT_IN_PROGRESS_REACTION_CONTENT = new Set(['eyes']);
34
36
  const BOT_COMPLETE_REACTION_CONTENT = new Set(['+1', 'hooray', 'heart', 'rocket', 'laugh', 'confused']);
37
+ const CODERABBIT_ISSUE_COMMENT_COMPLETION_PATTERNS = [
38
+ /No actionable comments were generated in the recent review/iu,
39
+ /Everything is clean\b/iu,
40
+ /PR is ready to merge\b/iu
41
+ ];
42
+ const CODERABBIT_STATUS_NAMES = new Set(['coderabbit', 'coderabbitai', 'code rabbit', 'code rabbit ai']);
43
+ function normalizeReadinessMode(rawValue) {
44
+ return typeof rawValue === 'string' && rawValue.trim().toLowerCase() === 'review' ? 'review' : 'merge';
45
+ }
46
+ function resolveBlockedReviewDecisions(readinessMode) {
47
+ return readinessMode === 'review'
48
+ ? REVIEW_HANDOFF_BLOCKED_REVIEW_DECISIONS
49
+ : MERGE_BLOCKED_REVIEW_DECISIONS;
50
+ }
51
+ function isReviewDecisionBlocked(reviewDecision, readinessMode) {
52
+ return resolveBlockedReviewDecisions(readinessMode).has(reviewDecision);
53
+ }
54
+ function doesMergeStateBlockReady(mergeStateStatus, readinessMode) {
55
+ return readinessMode === 'review'
56
+ ? ACTION_REQUIRED_MERGE_STATES.has(mergeStateStatus)
57
+ : !MERGEABLE_STATES.has(mergeStateStatus);
58
+ }
59
+ function doesMergeStateRequireAuthorAction(mergeStateStatus) {
60
+ return ACTION_REQUIRED_MERGE_STATES.has(mergeStateStatus);
61
+ }
35
62
  class PrWatchMergeExitError extends Error {
36
63
  constructor(message, exitCode = 1) {
37
64
  super(message);
@@ -39,6 +66,24 @@ class PrWatchMergeExitError extends Error {
39
66
  this.exitCode = exitCode;
40
67
  }
41
68
  }
69
+ class GhCommandError extends Error {
70
+ constructor(args, result) {
71
+ const detail = result.stderr || result.stdout || `exit code ${result.exitCode}`;
72
+ super(`gh ${args.join(' ')} failed: ${detail}`);
73
+ this.name = 'GhCommandError';
74
+ this.args = [...args];
75
+ this.exitCode = result.exitCode;
76
+ this.stdout = result.stdout;
77
+ this.stderr = result.stderr;
78
+ }
79
+ }
80
+ class GitHubRateLimitError extends Error {
81
+ constructor(rateLimit, message = null) {
82
+ super(message || formatGitHubRateLimitStatus(rateLimit));
83
+ this.name = 'GitHubRateLimitError';
84
+ this.githubRateLimit = rateLimit;
85
+ }
86
+ }
42
87
  const PR_QUERY = `
43
88
  query($owner:String!, $repo:String!, $number:Int!) {
44
89
  repository(owner:$owner, name:$repo) {
@@ -74,11 +119,14 @@ query($owner:String!, $repo:String!, $number:Int!) {
74
119
  name
75
120
  status
76
121
  conclusion
122
+ startedAt
123
+ completedAt
77
124
  detailsUrl
78
125
  }
79
126
  ... on StatusContext {
80
127
  context
81
128
  state
129
+ createdAt
82
130
  targetUrl
83
131
  }
84
132
  }
@@ -141,6 +189,255 @@ function formatDuration(ms) {
141
189
  function log(message) {
142
190
  console.log(`[${new Date().toISOString()}] ${message}`);
143
191
  }
192
+ function sanitizeRateLimitMessage(value) {
193
+ if (typeof value !== 'string') {
194
+ return null;
195
+ }
196
+ const normalized = value.replace(/\s+/gu, ' ').trim();
197
+ return normalized.length > 0 ? normalized.slice(0, 500) : null;
198
+ }
199
+ function inferGitHubApiSurfaceFromArgs(args) {
200
+ if (!Array.isArray(args)) {
201
+ return 'unknown';
202
+ }
203
+ if (args[0] === 'api' && args.includes('graphql')) {
204
+ return 'graphql';
205
+ }
206
+ if (args[0] === 'api' || (args[0] === 'pr' && args[1] === 'checks')) {
207
+ return 'rest';
208
+ }
209
+ return 'unknown';
210
+ }
211
+ function extractTextFromRateLimitInput(input) {
212
+ if (input instanceof Error) {
213
+ const pieces = [input.message];
214
+ if (typeof input.stderr === 'string') {
215
+ pieces.push(input.stderr);
216
+ }
217
+ if (typeof input.stdout === 'string') {
218
+ pieces.push(input.stdout);
219
+ }
220
+ return pieces.filter(Boolean).join('\n');
221
+ }
222
+ if (typeof input === 'string') {
223
+ return input;
224
+ }
225
+ if (input && typeof input === 'object') {
226
+ const pieces = [];
227
+ if (typeof input.message === 'string') {
228
+ pieces.push(input.message);
229
+ }
230
+ if (typeof input.stderr === 'string') {
231
+ pieces.push(input.stderr);
232
+ }
233
+ if (typeof input.stdout === 'string') {
234
+ pieces.push(input.stdout);
235
+ }
236
+ if (Array.isArray(input.errors)) {
237
+ for (const error of input.errors) {
238
+ if (typeof error?.message === 'string') {
239
+ pieces.push(error.message);
240
+ }
241
+ if (typeof error?.type === 'string') {
242
+ pieces.push(error.type);
243
+ }
244
+ }
245
+ }
246
+ return pieces.filter(Boolean).join('\n');
247
+ }
248
+ return '';
249
+ }
250
+ function parseHttpStatusFromText(text) {
251
+ const match = text.match(/\bHTTP\s+(?<status>403|429)\b/iu) ||
252
+ text.match(/\bstatus(?:\s+code)?["':=\s]+(?<status>403|429)\b/iu) ||
253
+ text.match(/"status"\s*:\s*(?<status>403|429)\b/iu);
254
+ const parsed = match?.groups?.status ? Number.parseInt(match.groups.status, 10) : null;
255
+ return Number.isInteger(parsed) ? parsed : null;
256
+ }
257
+ function parseHeaderSeconds(text, headerName) {
258
+ const escapedHeaderName = headerName.replace(/[\\^$*+?.()|[\]{}]/gu, '\\$&');
259
+ const pattern = new RegExp(`${escapedHeaderName}["'\\s:=]+(?<value>\\d+)`, 'iu');
260
+ const match = text.match(pattern);
261
+ if (!match?.groups?.value) {
262
+ return null;
263
+ }
264
+ const parsed = Number.parseInt(match.groups.value, 10);
265
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
266
+ }
267
+ function parseIsoTimestampFromText(text) {
268
+ const match = text.match(/\b(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\b/u);
269
+ return match?.groups?.timestamp ?? null;
270
+ }
271
+ function isoFromEpochSeconds(value) {
272
+ if (!Number.isFinite(value) || value <= 0) {
273
+ return null;
274
+ }
275
+ const millis = value * 1000;
276
+ return isoFromMillis(millis);
277
+ }
278
+ function isoFromMillis(value) {
279
+ if (!Number.isFinite(value)) {
280
+ return null;
281
+ }
282
+ try {
283
+ return new Date(value).toISOString();
284
+ }
285
+ catch {
286
+ return null;
287
+ }
288
+ }
289
+ function futureDelayMsFromSeconds(nowMs, seconds) {
290
+ if (typeof seconds !== 'number' || !Number.isFinite(seconds)) {
291
+ return null;
292
+ }
293
+ const delayMs = seconds * 1000;
294
+ if (!Number.isFinite(delayMs) || delayMs <= 0) {
295
+ return null;
296
+ }
297
+ const targetMs = nowMs + delayMs;
298
+ return isoFromMillis(targetMs) ? delayMs : null;
299
+ }
300
+ function computeRetryAt(nowMs, retryAfterSeconds, resetAt) {
301
+ const retryDelayMs = futureDelayMsFromSeconds(nowMs, retryAfterSeconds);
302
+ if (retryDelayMs !== null) {
303
+ return isoFromMillis(nowMs + retryDelayMs);
304
+ }
305
+ if (typeof resetAt === 'string' && resetAt.trim().length > 0) {
306
+ return resetAt;
307
+ }
308
+ return null;
309
+ }
310
+ function hasRateLimitSignal(text) {
311
+ return (/\b(api\s+rate\s+limit\s+exceeded|rate\s+limit\s+exceeded|secondary\s+(?:rate\s+)?limit|RATE_LIMITED)\b/iu.test(text)
312
+ || /\b(?:retry-after|x-ratelimit-reset)\b/iu.test(text)
313
+ || /\bHTTP\s+429\b/iu.test(text));
314
+ }
315
+ export function resolveGitHubRateLimitStatus(input, options = {}) {
316
+ if (input instanceof GitHubRateLimitError && input.githubRateLimit) {
317
+ return input.githubRateLimit;
318
+ }
319
+ if (input && typeof input === 'object' && input.kind === 'github_rate_limited') {
320
+ return input;
321
+ }
322
+ const text = extractTextFromRateLimitInput(input);
323
+ const isCommandOrRawTextInput = typeof input === 'string'
324
+ || input instanceof Error
325
+ || (input
326
+ && typeof input === 'object'
327
+ && (Array.isArray(input.args)
328
+ || typeof input.exitCode === 'number'
329
+ || typeof input.stderr === 'string'
330
+ || typeof input.stdout === 'string'));
331
+ const structuredStatus = input && typeof input === 'object' && Number.isInteger(input.status)
332
+ ? Number(input.status)
333
+ : null;
334
+ const status = structuredStatus === 403 || structuredStatus === 429
335
+ ? structuredStatus
336
+ : isCommandOrRawTextInput ? parseHttpStatusFromText(text) : null;
337
+ const retryAfterSeconds = isCommandOrRawTextInput ? parseHeaderSeconds(text, 'retry-after') : null;
338
+ const resetEpochSeconds = isCommandOrRawTextInput ? parseHeaderSeconds(text, 'x-ratelimit-reset') : null;
339
+ const remainingRequests = isCommandOrRawTextInput ? parseHeaderSeconds(text, 'x-ratelimit-remaining') : null;
340
+ const hasRateLimitText = /\b(api\s+rate\s+limit\s+exceeded|secondary\s+(?:rate\s+)?limit|RATE_LIMITED)\b/iu.test(text);
341
+ const hasGraphqlRateLimitPayload = Array.isArray(input?.errors) &&
342
+ input.errors.some((error) => typeof error?.type === 'string' && error.type.trim().toUpperCase() === 'RATE_LIMITED');
343
+ const hasProtocolRateLimitEvidence = hasGraphqlRateLimitPayload
344
+ || status === 429
345
+ || retryAfterSeconds !== null
346
+ || (resetEpochSeconds !== null && remainingRequests === 0)
347
+ || (isCommandOrRawTextInput && hasRateLimitText);
348
+ if (!hasProtocolRateLimitEvidence ||
349
+ (!hasRateLimitSignal(text) && status !== 429)) {
350
+ return null;
351
+ }
352
+ const args = Array.isArray(input?.args) ? input.args : [];
353
+ const surface = options.surface ??
354
+ (/\bgraphql\b/iu.test(text) ? 'graphql' : inferGitHubApiSurfaceFromArgs(args));
355
+ const resetAt = isoFromEpochSeconds(resetEpochSeconds) ?? parseIsoTimestampFromText(text);
356
+ const nowMs = typeof options.nowMs === 'number' && Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
357
+ const limitType = /\bsecondary\b/iu.test(text) ? 'secondary' : 'primary';
358
+ return {
359
+ kind: 'github_rate_limited',
360
+ surface,
361
+ limit_type: limitType,
362
+ status,
363
+ reset_at: resetAt,
364
+ retry_after_seconds: retryAfterSeconds,
365
+ retry_at: computeRetryAt(nowMs, retryAfterSeconds, resetAt),
366
+ message: sanitizeRateLimitMessage(text)
367
+ };
368
+ }
369
+ export function formatGitHubRateLimitStatus(rateLimit) {
370
+ if (!rateLimit || typeof rateLimit !== 'object') {
371
+ return 'GitHub API rate limit is active.';
372
+ }
373
+ const parts = [
374
+ 'GitHub API rate limit',
375
+ `surface=${rateLimit.surface ?? 'unknown'}`,
376
+ `type=${rateLimit.limit_type ?? 'unknown'}`
377
+ ];
378
+ if (rateLimit.status) {
379
+ parts.push(`status=${rateLimit.status}`);
380
+ }
381
+ if (rateLimit.retry_at) {
382
+ parts.push(`retry_at=${rateLimit.retry_at}`);
383
+ }
384
+ else if (rateLimit.reset_at) {
385
+ parts.push(`reset_at=${rateLimit.reset_at}`);
386
+ }
387
+ return parts.join(' | ');
388
+ }
389
+ function throwIfGitHubRateLimited(input, options = {}) {
390
+ const rateLimit = resolveGitHubRateLimitStatus(input, options);
391
+ if (rateLimit) {
392
+ throw new GitHubRateLimitError(rateLimit);
393
+ }
394
+ }
395
+ function stableJitterMs(seed, maxJitterMs) {
396
+ if (!maxJitterMs || maxJitterMs <= 0) {
397
+ return 0;
398
+ }
399
+ const text = String(seed ?? 'github-rate-limit');
400
+ let hash = 0;
401
+ for (let index = 0; index < text.length; index += 1) {
402
+ hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
403
+ }
404
+ return hash % (maxJitterMs + 1);
405
+ }
406
+ export function planGitHubRateLimitBackoff(rateLimit, options = {}) {
407
+ const nowMs = typeof options.nowMs === 'number' && Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
408
+ const fallbackMs = typeof options.fallbackMs === 'number' && Number.isFinite(options.fallbackMs) && options.fallbackMs > 0
409
+ ? options.fallbackMs
410
+ : DEFAULT_INTERVAL_SECONDS * 1000;
411
+ const maxJitterMs = typeof options.maxJitterMs === 'number' && Number.isFinite(options.maxJitterMs) && options.maxJitterMs >= 0
412
+ ? Math.round(options.maxJitterMs)
413
+ : 5000;
414
+ const candidates = [];
415
+ const retryAfterMs = futureDelayMsFromSeconds(nowMs, rateLimit?.retry_after_seconds);
416
+ if (retryAfterMs !== null) {
417
+ candidates.push(retryAfterMs);
418
+ }
419
+ const retryAtMs = parseTimestampMs(rateLimit?.retry_at);
420
+ if (retryAtMs !== null) {
421
+ const retryDelayMs = retryAtMs - nowMs;
422
+ if (retryDelayMs > 0) {
423
+ candidates.push(retryDelayMs);
424
+ }
425
+ }
426
+ const resetAtMs = parseTimestampMs(rateLimit?.reset_at);
427
+ if (resetAtMs !== null) {
428
+ const resetDelayMs = resetAtMs - nowMs;
429
+ if (resetDelayMs > 0) {
430
+ candidates.push(resetDelayMs);
431
+ }
432
+ }
433
+ const baseMs = candidates.length > 0 ? Math.max(...candidates) : fallbackMs;
434
+ const jitterMs = stableJitterMs(options.jitterSeed, maxJitterMs);
435
+ const plannedMs = Math.max(1000, baseMs + jitterMs);
436
+ const remainingMs = typeof options.remainingMs === 'number' && Number.isFinite(options.remainingMs)
437
+ ? Math.max(0, options.remainingMs)
438
+ : null;
439
+ return remainingMs === null ? plannedMs : Math.min(plannedMs, remainingMs);
440
+ }
144
441
  function parseTimestampMs(value) {
145
442
  if (typeof value !== 'string' || value.trim().length === 0) {
146
443
  return null;
@@ -148,6 +445,12 @@ function parseTimestampMs(value) {
148
445
  const parsed = Date.parse(value);
149
446
  return Number.isFinite(parsed) ? parsed : null;
150
447
  }
448
+ export function isNoRequiredChecksReportedErrorMessage(value) {
449
+ if (typeof value !== 'string' || value.trim().length === 0) {
450
+ return false;
451
+ }
452
+ return /no required checks reported\b/iu.test(value);
453
+ }
151
454
  function maxTimestamp(values) {
152
455
  let max = null;
153
456
  for (const value of values) {
@@ -160,6 +463,12 @@ function maxTimestamp(values) {
160
463
  }
161
464
  return max;
162
465
  }
466
+ function isCoderabbitStatusName(value) {
467
+ const normalized = typeof value === 'string'
468
+ ? value.trim().toLowerCase().replace(/\s+/gu, ' ')
469
+ : '';
470
+ return CODERABBIT_STATUS_NAMES.has(normalized);
471
+ }
163
472
  function resolveBotKindFromLogin(login) {
164
473
  const normalized = normalizeLogin(login);
165
474
  if (!normalized) {
@@ -243,16 +552,22 @@ function parseMergeMethod(rawValue) {
243
552
  return normalized;
244
553
  }
245
554
  export function printPrWatchMergeHelp(options = {}) {
555
+ const readinessMode = normalizeReadinessMode(options.readinessMode);
556
+ const isReviewMode = readinessMode === 'review';
246
557
  const usageCommand = typeof options.usage === 'string' && options.usage.trim().length > 0
247
558
  ? options.usage.trim()
248
- : 'codex-orchestrator pr watch-merge';
559
+ : isReviewMode
560
+ ? 'codex-orchestrator pr ready-review'
561
+ : 'codex-orchestrator pr watch-merge';
249
562
  const defaultAutoMerge = typeof options.defaultAutoMerge === 'boolean'
250
563
  ? options.defaultAutoMerge
251
564
  : envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, false);
252
565
  const defaultExitOnActionRequired = Boolean(options.defaultExitOnActionRequired);
253
566
  console.log(`Usage: ${usageCommand} [options]
254
567
 
255
- Monitor PR checks/reviews with polling and optionally merge after a quiet window.
568
+ ${isReviewMode
569
+ ? 'Monitor PR checks/reviews with polling and report when review handoff is safe after a bounded automated-feedback drain.'
570
+ : 'Monitor PR checks/reviews with polling and optionally merge after a quiet window.'}
256
571
 
257
572
  Options:
258
573
  --pr <number> PR number (default: PR for current branch)
@@ -261,25 +576,29 @@ Options:
261
576
  --interval-seconds <n> Poll interval in seconds (default: ${DEFAULT_INTERVAL_SECONDS})
262
577
  --quiet-minutes <n> Required quiet window after ready state (default: ${DEFAULT_QUIET_MINUTES})
263
578
  --timeout-minutes <n> Max monitor duration before failing (default: ${DEFAULT_TIMEOUT_MINUTES})
264
- --merge-method <method> merge|squash|rebase (default: ${DEFAULT_MERGE_METHOD})
579
+ ${isReviewMode ? '' : ` --merge-method <method> merge|squash|rebase (default: ${DEFAULT_MERGE_METHOD})
265
580
  --auto-merge Merge automatically after quiet window
266
581
  --no-auto-merge Never merge automatically (monitor only)
267
582
  --delete-branch Delete remote branch when merging
268
583
  --no-delete-branch Keep remote branch after merge
584
+ `}
269
585
  --exit-on-action-required Exit non-zero when author action is required
270
586
  --no-exit-on-action-required Keep monitoring even when author action is required
271
- --dry-run Never call gh pr merge (report only)
587
+ --dry-run Never call gh pr merge/update-branch (report only)
272
588
  -h, --help Show this help message
273
589
 
274
590
  Environment:
275
- PR_MONITOR_AUTO_MERGE=1 Default auto-merge on (current default: ${defaultAutoMerge ? 'on' : 'off'})
276
- PR_MONITOR_DELETE_BRANCH=1 Default delete branch on merge
277
591
  PR_MONITOR_QUIET_MINUTES=<n> Override quiet window default
278
592
  PR_MONITOR_INTERVAL_SECONDS=<n>
279
- PR_MONITOR_TIMEOUT_MINUTES=<n>
280
- PR_MONITOR_MERGE_METHOD=<method>`);
593
+ PR_MONITOR_TIMEOUT_MINUTES=<n>${isReviewMode ? '' : `
594
+ PR_MONITOR_AUTO_MERGE=1 Default auto-merge on (current default: ${defaultAutoMerge ? 'on' : 'off'})
595
+ PR_MONITOR_DELETE_BRANCH=1 Default delete branch on merge
596
+ PR_MONITOR_MERGE_METHOD=<method>`}`);
281
597
  if (defaultExitOnActionRequired) {
282
- console.log(' resolve-merge default: exit-on-action-required is on');
598
+ console.log(` ${isReviewMode ? 'ready-review' : 'resolve-merge'} default: exit-on-action-required is on`);
599
+ }
600
+ if (isReviewMode) {
601
+ console.log(' ready-review treats REVIEW_REQUIRED as informational; CHANGES_REQUESTED and actionable machine feedback still block handoff.');
283
602
  }
284
603
  }
285
604
  async function runGh(args, { allowFailure = false } = {}) {
@@ -315,8 +634,7 @@ async function runGh(args, { allowFailure = false } = {}) {
315
634
  resolve(result);
316
635
  return;
317
636
  }
318
- const detail = result.stderr || result.stdout || `exit code ${exitCode}`;
319
- reject(new Error(`gh ${args.join(' ')} failed: ${detail}`));
637
+ reject(new GhCommandError(args, result));
320
638
  });
321
639
  });
322
640
  }
@@ -354,20 +672,49 @@ async function runGit(args, { allowFailure = false } = {}) {
354
672
  });
355
673
  }
356
674
  async function runGhJson(args) {
357
- const result = await runGh(args);
675
+ let result;
676
+ const surface = inferGitHubApiSurfaceFromArgs(args);
677
+ try {
678
+ result = await runGh(args);
679
+ }
680
+ catch (error) {
681
+ throwIfGitHubRateLimited(error, { surface });
682
+ throw error;
683
+ }
358
684
  try {
359
- return JSON.parse(result.stdout);
685
+ const parsed = JSON.parse(result.stdout);
686
+ throwIfGitHubRateLimited(parsed, { surface });
687
+ return parsed;
360
688
  }
361
689
  catch (error) {
690
+ if (error instanceof GitHubRateLimitError) {
691
+ throw error;
692
+ }
693
+ throwIfGitHubRateLimited(result.stdout, { surface });
362
694
  throw new Error(`Failed to parse JSON from gh ${args.join(' ')}: ${error instanceof Error ? error.message : String(error)}`);
363
695
  }
364
696
  }
365
697
  async function runGhJsonSlurped(args) {
366
- const result = await runGh([...args, '--paginate', '--slurp']);
698
+ const ghArgs = [...args, '--paginate', '--slurp'];
699
+ let result;
700
+ const surface = inferGitHubApiSurfaceFromArgs(ghArgs);
367
701
  try {
368
- return JSON.parse(result.stdout);
702
+ result = await runGh(ghArgs);
369
703
  }
370
704
  catch (error) {
705
+ throwIfGitHubRateLimited(error, { surface });
706
+ throw error;
707
+ }
708
+ try {
709
+ const parsed = JSON.parse(result.stdout);
710
+ throwIfGitHubRateLimited(parsed, { surface });
711
+ return parsed;
712
+ }
713
+ catch (error) {
714
+ if (error instanceof GitHubRateLimitError) {
715
+ throw error;
716
+ }
717
+ throwIfGitHubRateLimited(result.stdout, { surface });
371
718
  throw new Error(`Failed to parse paginated JSON from gh ${args.join(' ')}: ${error instanceof Error ? error.message : String(error)}`);
372
719
  }
373
720
  }
@@ -436,6 +783,9 @@ export function buildPrNumberViewArgs(owner, repo) {
436
783
  }
437
784
  return args;
438
785
  }
786
+ export function buildPrUpdateBranchArgs({ owner, repo, prNumber }) {
787
+ return ['pr', 'update-branch', String(prNumber), '--repo', `${owner}/${repo}`];
788
+ }
439
789
  async function resolvePrNumber(prArg, owner, repo) {
440
790
  if (prArg !== undefined) {
441
791
  return parseInteger('pr', prArg, null);
@@ -502,6 +852,189 @@ function summarizeChecks(nodes) {
502
852
  }
503
853
  return summary;
504
854
  }
855
+ function normalizeRollupCheckState(node) {
856
+ if (!node || typeof node !== 'object') {
857
+ return null;
858
+ }
859
+ const typeName = typeof node.__typename === 'string' ? node.__typename : '';
860
+ if (typeName === 'CheckRun') {
861
+ const name = typeof node.name === 'string' && node.name.trim() ? node.name.trim() : 'check-run';
862
+ const status = normalizeEnum(node.status);
863
+ const startedAt = typeof node.startedAt === 'string' ? node.startedAt : null;
864
+ const completedAt = typeof node.completedAt === 'string' ? node.completedAt : null;
865
+ const observedAt = status === 'COMPLETED' ? completedAt ?? startedAt : startedAt;
866
+ const observedAtMs = parseTimestampMs(observedAt);
867
+ if (status !== 'COMPLETED') {
868
+ return {
869
+ name,
870
+ state: 'pending',
871
+ signal: status || 'PENDING',
872
+ observedAt,
873
+ observedAtMs,
874
+ detailsUrl: typeof node.detailsUrl === 'string' ? node.detailsUrl : null
875
+ };
876
+ }
877
+ const conclusion = normalizeEnum(node.conclusion);
878
+ if (conclusion === 'SUCCESS') {
879
+ return {
880
+ name,
881
+ state: 'success',
882
+ signal: conclusion || 'SUCCESS',
883
+ observedAt,
884
+ observedAtMs,
885
+ detailsUrl: typeof node.detailsUrl === 'string' ? node.detailsUrl : null
886
+ };
887
+ }
888
+ return {
889
+ name,
890
+ state: 'failed',
891
+ signal: conclusion || 'UNKNOWN',
892
+ observedAt,
893
+ observedAtMs,
894
+ detailsUrl: typeof node.detailsUrl === 'string' ? node.detailsUrl : null
895
+ };
896
+ }
897
+ if (typeName === 'StatusContext') {
898
+ const name = typeof node.context === 'string' && node.context.trim() ? node.context.trim() : 'status-context';
899
+ const state = normalizeEnum(node.state);
900
+ const observedAt = typeof node.createdAt === 'string' ? node.createdAt : null;
901
+ const observedAtMs = parseTimestampMs(observedAt);
902
+ if (STATUS_CONTEXT_PENDING_STATES.has(state)) {
903
+ return {
904
+ name,
905
+ state: 'pending',
906
+ signal: state || 'PENDING',
907
+ observedAt,
908
+ observedAtMs,
909
+ detailsUrl: typeof node.targetUrl === 'string' ? node.targetUrl : null
910
+ };
911
+ }
912
+ if (STATUS_CONTEXT_PASS_STATES.has(state)) {
913
+ return {
914
+ name,
915
+ state: 'success',
916
+ signal: state || 'SUCCESS',
917
+ observedAt,
918
+ observedAtMs,
919
+ detailsUrl: typeof node.targetUrl === 'string' ? node.targetUrl : null
920
+ };
921
+ }
922
+ return {
923
+ name,
924
+ state: 'failed',
925
+ signal: state || 'UNKNOWN',
926
+ observedAt,
927
+ observedAtMs,
928
+ detailsUrl: typeof node.targetUrl === 'string' ? node.targetUrl : null
929
+ };
930
+ }
931
+ return null;
932
+ }
933
+ function summarizeCoderabbitStatusCheckRollup(nodes) {
934
+ const contexts = [];
935
+ if (Array.isArray(nodes)) {
936
+ for (const node of nodes) {
937
+ const context = normalizeRollupCheckState(node);
938
+ if (!context || !isCoderabbitStatusName(context.name)) {
939
+ continue;
940
+ }
941
+ contexts.push(context);
942
+ }
943
+ }
944
+ let state = 'missing';
945
+ if (contexts.some((context) => context.state === 'pending')) {
946
+ state = 'pending';
947
+ }
948
+ else if (contexts.some((context) => context.state === 'failed')) {
949
+ state = 'failed';
950
+ }
951
+ else if (contexts.some((context) => context.state === 'success')) {
952
+ state = 'success';
953
+ }
954
+ const latestSuccessAtMs = maxTimestamp(contexts
955
+ .filter((context) => context.state === 'success')
956
+ .map((context) => context.observedAtMs));
957
+ return {
958
+ state,
959
+ contexts,
960
+ latestSuccessAtMs
961
+ };
962
+ }
963
+ function resolveCoderabbitPendingBlockerSignal(coderabbitStatusCheckRollup, requestAtMs = null) {
964
+ if (!coderabbitStatusCheckRollup || typeof coderabbitStatusCheckRollup !== 'object') {
965
+ return 'status_check_rollup=unknown';
966
+ }
967
+ const names = Array.isArray(coderabbitStatusCheckRollup.contexts)
968
+ ? coderabbitStatusCheckRollup.contexts.map((context) => context.name).filter(Boolean)
969
+ : [];
970
+ const suffix = names.length > 0 ? `:${names.join('+')}` : '';
971
+ const baseSignal = `status_check_rollup=${coderabbitStatusCheckRollup.state || 'unknown'}${suffix}`;
972
+ if (coderabbitStatusCheckRollup.state !== 'success') {
973
+ return baseSignal;
974
+ }
975
+ if (typeof requestAtMs !== 'number' || !Number.isFinite(requestAtMs)) {
976
+ return `${baseSignal};request_time=unknown`;
977
+ }
978
+ const latestSuccessAtMs = coderabbitStatusCheckRollup.latestSuccessAtMs;
979
+ if (typeof latestSuccessAtMs !== 'number' || !Number.isFinite(latestSuccessAtMs)) {
980
+ return `${baseSignal};success_time=unknown`;
981
+ }
982
+ if (latestSuccessAtMs <= requestAtMs) {
983
+ return `${baseSignal};success_before_request`;
984
+ }
985
+ return baseSignal;
986
+ }
987
+ function resolveEffectiveBotRereviewSignals({ pendingBots, coderabbitStatusCheckRollup, requestTimesByBot, hasUnresolvedThread, unacknowledgedBotFeedbackCount, botFeedbackFetchError }) {
988
+ const rawPendingBots = Array.isArray(pendingBots) ? pendingBots : [];
989
+ const effectivePendingBots = [];
990
+ const clearedPendingBots = [];
991
+ const canTrustResolvedFeedbackTruth = !hasUnresolvedThread && unacknowledgedBotFeedbackCount === 0 && botFeedbackFetchError !== true;
992
+ const requestTimes = requestTimesByBot && typeof requestTimesByBot === 'object' ? requestTimesByBot : {};
993
+ const coderabbitRequestAtMs = typeof requestTimes[BOT_KIND_LABELS.coderabbit] === 'number' &&
994
+ Number.isFinite(requestTimes[BOT_KIND_LABELS.coderabbit])
995
+ ? requestTimes[BOT_KIND_LABELS.coderabbit]
996
+ : null;
997
+ const coderabbitSuccessAtMs = typeof coderabbitStatusCheckRollup?.latestSuccessAtMs === 'number' &&
998
+ Number.isFinite(coderabbitStatusCheckRollup.latestSuccessAtMs)
999
+ ? coderabbitStatusCheckRollup.latestSuccessAtMs
1000
+ : null;
1001
+ const coderabbitSuccessAfterRequest = coderabbitStatusCheckRollup?.state === 'success' &&
1002
+ coderabbitRequestAtMs !== null &&
1003
+ coderabbitSuccessAtMs !== null &&
1004
+ coderabbitSuccessAtMs > coderabbitRequestAtMs;
1005
+ for (const bot of rawPendingBots) {
1006
+ const isCoderabbit = bot === BOT_KIND_LABELS.coderabbit;
1007
+ if (isCoderabbit &&
1008
+ coderabbitSuccessAfterRequest &&
1009
+ canTrustResolvedFeedbackTruth) {
1010
+ clearedPendingBots.push(bot);
1011
+ continue;
1012
+ }
1013
+ effectivePendingBots.push(bot);
1014
+ }
1015
+ return {
1016
+ rawPendingBots,
1017
+ effectivePendingBots,
1018
+ clearedPendingBots,
1019
+ coderabbit: {
1020
+ statusCheckRollup: coderabbitStatusCheckRollup,
1021
+ stalePendingCleared: clearedPendingBots.includes(BOT_KIND_LABELS.coderabbit),
1022
+ latestRequestAtMs: coderabbitRequestAtMs,
1023
+ latestSuccessAtMs: coderabbitSuccessAtMs,
1024
+ successAfterRequest: coderabbitSuccessAfterRequest,
1025
+ pendingBlockerSignal: resolveCoderabbitPendingBlockerSignal(coderabbitStatusCheckRollup, coderabbitRequestAtMs)
1026
+ }
1027
+ };
1028
+ }
1029
+ function formatBotRereviewPendingGateReason(pendingBots, diagnostics) {
1030
+ const parts = pendingBots.map((bot) => {
1031
+ if (bot === BOT_KIND_LABELS.coderabbit) {
1032
+ return `${bot}(${diagnostics?.coderabbit?.pendingBlockerSignal ?? 'status_check_rollup=unknown'})`;
1033
+ }
1034
+ return bot;
1035
+ });
1036
+ return `bot_rereview_pending=${parts.join(',')}`;
1037
+ }
505
1038
  export function summarizeRequiredChecks(entries) {
506
1039
  const summary = {
507
1040
  total: 0,
@@ -569,17 +1102,90 @@ export function resolveCachedRequiredChecksSummary(previousCache, currentHeadOid
569
1102
  if (!previousCache || typeof previousCache !== 'object') {
570
1103
  return null;
571
1104
  }
572
- const cachedHeadOid = typeof previousCache.headOid === 'string' ? previousCache.headOid : null;
1105
+ const requiredChecksCache = previousCache.requiredChecksForNextPoll && typeof previousCache.requiredChecksForNextPoll === 'object'
1106
+ ? previousCache.requiredChecksForNextPoll
1107
+ : previousCache;
1108
+ const cachedHeadOid = typeof requiredChecksCache.headOid === 'string' ? requiredChecksCache.headOid : null;
573
1109
  if (!cachedHeadOid || !currentHeadOid || cachedHeadOid !== currentHeadOid) {
574
1110
  return null;
575
1111
  }
576
- return hasRequiredChecksSummary(previousCache.summary) ? previousCache.summary : null;
1112
+ return hasRequiredChecksSummary(requiredChecksCache.summary) ? requiredChecksCache.summary : null;
577
1113
  }
578
- export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFeedback = null) {
1114
+ function resolveReusableFanoutCache(previousCache, currentHeadOid, currentUpdatedAt) {
1115
+ if (!previousCache || typeof previousCache !== 'object') {
1116
+ return null;
1117
+ }
1118
+ const cachedHeadOid = typeof previousCache.headOid === 'string' ? previousCache.headOid : null;
1119
+ const cachedUpdatedAt = typeof previousCache.updatedAt === 'string' ? previousCache.updatedAt : null;
1120
+ if (!cachedHeadOid || !cachedUpdatedAt || !currentHeadOid || !currentUpdatedAt) {
1121
+ return null;
1122
+ }
1123
+ if (cachedHeadOid !== currentHeadOid || cachedUpdatedAt !== currentUpdatedAt) {
1124
+ return null;
1125
+ }
1126
+ if (previousCache.requiredChecksFetchError === true
1127
+ || previousCache.inlineBotFeedback?.fetchError === true
1128
+ || previousCache.botRereviewSignals?.fetchError === true) {
1129
+ return null;
1130
+ }
1131
+ if (!isReusableBotFanoutClean(previousCache.inlineBotFeedback, previousCache.botRereviewSignals)) {
1132
+ return null;
1133
+ }
1134
+ return previousCache;
1135
+ }
1136
+ function isReusableBotFanoutClean(inlineBotFeedback, botRereviewSignals) {
1137
+ if (!inlineBotFeedback || typeof inlineBotFeedback !== 'object') {
1138
+ return false;
1139
+ }
1140
+ if (!botRereviewSignals || typeof botRereviewSignals !== 'object') {
1141
+ return false;
1142
+ }
1143
+ if (inlineBotFeedback.fetchError === true || botRereviewSignals.fetchError === true) {
1144
+ return false;
1145
+ }
1146
+ if (inlineBotFeedback.unacknowledgedCount !== 0) {
1147
+ return false;
1148
+ }
1149
+ if (Array.isArray(botRereviewSignals.pendingBots) &&
1150
+ botRereviewSignals.pendingBots.length > 0) {
1151
+ return false;
1152
+ }
1153
+ if (Array.isArray(botRereviewSignals.inProgressBots) &&
1154
+ botRereviewSignals.inProgressBots.length > 0) {
1155
+ return false;
1156
+ }
1157
+ return true;
1158
+ }
1159
+ function buildReusableFanoutCache(input) {
1160
+ const requiredChecksForNextPoll = input.requiredChecks
1161
+ ? {
1162
+ headOid: input.headOid,
1163
+ summary: input.requiredChecks
1164
+ }
1165
+ : null;
1166
+ if (input.requiredChecksResult?.fetchError === true
1167
+ || input.inlineBotFeedback?.fetchError === true
1168
+ || input.botRereviewSignals?.fetchError === true) {
1169
+ return requiredChecksForNextPoll;
1170
+ }
1171
+ if (!isReusableBotFanoutClean(input.inlineBotFeedback, input.botRereviewSignals)) {
1172
+ return requiredChecksForNextPoll;
1173
+ }
1174
+ return {
1175
+ headOid: input.headOid,
1176
+ updatedAt: input.updatedAt,
1177
+ requiredChecksFetchError: false,
1178
+ requiredChecksForNextPoll,
1179
+ inlineBotFeedback: input.inlineBotFeedback,
1180
+ botRereviewSignals: input.botRereviewSignals
1181
+ };
1182
+ }
1183
+ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFeedback = null, options = {}) {
579
1184
  const pr = response?.data?.repository?.pullRequest;
580
1185
  if (!pr) {
581
1186
  throw new Error('GraphQL response missing pullRequest payload.');
582
1187
  }
1188
+ const readinessMode = normalizeReadinessMode(options.readinessMode);
583
1189
  const labels = Array.isArray(pr.labels?.nodes)
584
1190
  ? pr.labels.nodes
585
1191
  .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
@@ -592,6 +1198,7 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
592
1198
  const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes;
593
1199
  const checkNodes = Array.isArray(contexts) ? contexts : [];
594
1200
  const checks = summarizeChecks(checkNodes);
1201
+ const coderabbitStatusCheckRollup = summarizeCoderabbitStatusCheckRollup(checkNodes);
595
1202
  const requiredCheckSummary = requiredChecks && typeof requiredChecks === 'object' && requiredChecks.total > 0 ? requiredChecks : null;
596
1203
  const unacknowledgedBotFeedbackCount = inlineBotFeedback && typeof inlineBotFeedback.unacknowledgedCount === 'number'
597
1204
  ? inlineBotFeedback.unacknowledgedCount
@@ -601,8 +1208,13 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
601
1208
  ? inlineBotFeedback.rereview
602
1209
  : null;
603
1210
  const botRereviewFetchError = botRereview?.fetchError === true;
604
- const botRereviewPending = Array.isArray(botRereview?.pendingBots) ? botRereview.pendingBots : [];
1211
+ const rawBotRereviewPending = Array.isArray(botRereview?.pendingBots) ? botRereview.pendingBots : [];
605
1212
  const botRereviewInProgress = Array.isArray(botRereview?.inProgressBots) ? botRereview.inProgressBots : [];
1213
+ const requiredChecksQueryFailed = options.requiredChecksQueryFailed === true;
1214
+ const githubRateLimits = Array.isArray(options.githubRateLimits)
1215
+ ? options.githubRateLimits.map((entry) => resolveGitHubRateLimitStatus(entry)).filter(Boolean)
1216
+ : [];
1217
+ const githubRateLimit = githubRateLimits[0] ?? null;
606
1218
  const coderabbitReviewMeta = botRereview?.coderabbit && typeof botRereview.coderabbit === 'object'
607
1219
  ? botRereview.coderabbit
608
1220
  : { actionableCount: 0, outsideDiffCount: 0, nitpickCount: 0 };
@@ -612,6 +1224,15 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
612
1224
  const mergeStateStatus = normalizeEnum(pr.mergeStateStatus);
613
1225
  const state = normalizeEnum(pr.state);
614
1226
  const isDraft = Boolean(pr.isDraft);
1227
+ const botRereviewDiagnostics = resolveEffectiveBotRereviewSignals({
1228
+ pendingBots: rawBotRereviewPending,
1229
+ coderabbitStatusCheckRollup,
1230
+ requestTimesByBot: botRereview?.requestTimesByBot,
1231
+ hasUnresolvedThread,
1232
+ unacknowledgedBotFeedbackCount,
1233
+ botFeedbackFetchError
1234
+ });
1235
+ const botRereviewPending = botRereviewDiagnostics.effectivePendingBots;
615
1236
  const gateReasons = [];
616
1237
  if (state !== 'OPEN') {
617
1238
  gateReasons.push(`state=${state || 'UNKNOWN'}`);
@@ -627,10 +1248,17 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
627
1248
  ? `required_checks_pending=${gateChecks.pending.length}`
628
1249
  : `checks_pending=${gateChecks.pending.length}`);
629
1250
  }
630
- if (!MERGEABLE_STATES.has(mergeStateStatus)) {
1251
+ if (gateChecksSource === 'required' && gateChecks.failed.length > 0) {
1252
+ gateReasons.push(`required_checks_failed=${gateChecks.failed.length}`);
1253
+ }
1254
+ if (requiredChecksQueryFailed) {
1255
+ gateReasons.push('required_checks_query_failed');
1256
+ }
1257
+ const mergeStateBlocksReady = doesMergeStateBlockReady(mergeStateStatus, readinessMode);
1258
+ if (mergeStateBlocksReady) {
631
1259
  gateReasons.push(`merge_state=${mergeStateStatus || 'UNKNOWN'}`);
632
1260
  }
633
- if (BLOCKED_REVIEW_DECISIONS.has(reviewDecision)) {
1261
+ if (isReviewDecisionBlocked(reviewDecision, readinessMode)) {
634
1262
  gateReasons.push(`review=${reviewDecision}`);
635
1263
  }
636
1264
  if (hasUnresolvedThread) {
@@ -646,7 +1274,7 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
646
1274
  gateReasons.push('bot_rereview=unknown');
647
1275
  }
648
1276
  else if (botRereviewPending.length > 0) {
649
- gateReasons.push(`bot_rereview_pending=${botRereviewPending.join(',')}`);
1277
+ gateReasons.push(formatBotRereviewPendingGateReason(botRereviewPending, botRereviewDiagnostics));
650
1278
  }
651
1279
  return {
652
1280
  number: Number(pr.number),
@@ -665,32 +1293,40 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
665
1293
  botRereviewFetchError,
666
1294
  botRereviewPending,
667
1295
  botRereviewInProgress,
1296
+ botRereviewDiagnostics,
668
1297
  coderabbitReviewMeta,
669
1298
  checks,
670
1299
  requiredChecks: requiredCheckSummary,
1300
+ requiredChecksQueryFailed,
671
1301
  gateChecksSource,
672
1302
  gateReasons,
1303
+ readinessMode,
673
1304
  readyToMerge: gateReasons.length === 0,
674
- headOid: pr.commits?.nodes?.[0]?.commit?.oid || null
1305
+ headOid: pr.commits?.nodes?.[0]?.commit?.oid || null,
1306
+ fanoutCacheHit: options.fanoutCacheHit === true,
1307
+ githubRateLimit,
1308
+ githubRateLimits
675
1309
  };
676
1310
  }
677
- export function resolveActionRequiredReasons(snapshot) {
1311
+ export function resolveActionRequiredReasons(snapshot, options = {}) {
678
1312
  if (!snapshot || typeof snapshot !== 'object') {
679
1313
  return ['snapshot=unknown'];
680
1314
  }
1315
+ const readinessMode = normalizeReadinessMode(options.readinessMode ?? snapshot.readinessMode);
681
1316
  const reasons = [];
682
1317
  const reviewDecision = normalizeEnum(snapshot.reviewDecision);
683
1318
  const mergeStateStatus = normalizeEnum(snapshot.mergeStateStatus);
1319
+ const mergeStateRequiresAuthorAction = doesMergeStateRequireAuthorAction(mergeStateStatus);
684
1320
  if (Boolean(snapshot.isDraft)) {
685
1321
  reasons.push('draft');
686
1322
  }
687
1323
  if (Boolean(snapshot.hasDoNotMergeLabel)) {
688
1324
  reasons.push('label:do-not-merge');
689
1325
  }
690
- if (BLOCKED_REVIEW_DECISIONS.has(reviewDecision)) {
1326
+ if (isReviewDecisionBlocked(reviewDecision, readinessMode)) {
691
1327
  reasons.push(`review=${reviewDecision}`);
692
1328
  }
693
- if (ACTION_REQUIRED_MERGE_STATES.has(mergeStateStatus)) {
1329
+ if (mergeStateRequiresAuthorAction) {
694
1330
  reasons.push(`merge_state=${mergeStateStatus}`);
695
1331
  }
696
1332
  if (typeof snapshot.unresolvedThreadCount === 'number' && snapshot.unresolvedThreadCount > 0) {
@@ -701,6 +1337,9 @@ export function resolveActionRequiredReasons(snapshot) {
701
1337
  reasons.push(`unacknowledged_bot_feedback=${snapshot.unacknowledgedBotFeedbackCount}`);
702
1338
  }
703
1339
  const requiredChecks = snapshot.requiredChecks && typeof snapshot.requiredChecks === 'object' ? snapshot.requiredChecks : null;
1340
+ if (snapshot.requiredChecksQueryFailed === true) {
1341
+ reasons.push('required_checks_query_failed');
1342
+ }
704
1343
  const requiredFailedCount = Array.isArray(requiredChecks?.failed) ? requiredChecks.failed.length : 0;
705
1344
  if (requiredFailedCount > 0 && snapshot.readyToMerge === false) {
706
1345
  reasons.push(`required_checks_failed=${requiredFailedCount}`);
@@ -708,12 +1347,93 @@ export function resolveActionRequiredReasons(snapshot) {
708
1347
  else {
709
1348
  const rollupFailedCount = Array.isArray(snapshot.checks?.failed) ? snapshot.checks.failed.length : 0;
710
1349
  const rollupPendingCount = Array.isArray(snapshot.checks?.pending) ? snapshot.checks.pending.length : 0;
711
- if (!requiredChecks && !MERGEABLE_STATES.has(mergeStateStatus) && rollupPendingCount === 0 && rollupFailedCount > 0) {
1350
+ if (!requiredChecks
1351
+ && snapshot.requiredChecksQueryFailed !== true
1352
+ && readinessMode !== 'review'
1353
+ && !mergeStateRequiresAuthorAction
1354
+ && !MERGEABLE_STATES.has(mergeStateStatus)
1355
+ && rollupPendingCount === 0
1356
+ && rollupFailedCount > 0) {
712
1357
  reasons.push(`checks_failed=${rollupFailedCount}`);
713
1358
  }
714
1359
  }
715
1360
  return reasons;
716
1361
  }
1362
+ function readPrecomputedRecoveryReasonList(snapshotOrReasons) {
1363
+ if (Array.isArray(snapshotOrReasons)) {
1364
+ return snapshotOrReasons.filter((reason) => typeof reason === 'string' && reason.trim().length > 0);
1365
+ }
1366
+ const precomputedReasons = snapshotOrReasons?.action_required_reasons;
1367
+ return Array.isArray(precomputedReasons)
1368
+ ? precomputedReasons.filter((reason) => typeof reason === 'string' && reason.trim().length > 0)
1369
+ : [];
1370
+ }
1371
+ function readRecoveryGateReasons(snapshotOrReasons) {
1372
+ if (!snapshotOrReasons || Array.isArray(snapshotOrReasons) || typeof snapshotOrReasons !== 'object') {
1373
+ return [];
1374
+ }
1375
+ const rawGateReasons = Array.isArray(snapshotOrReasons.gateReasons)
1376
+ ? snapshotOrReasons.gateReasons
1377
+ : snapshotOrReasons.gate_reasons;
1378
+ return Array.isArray(rawGateReasons)
1379
+ ? rawGateReasons.filter((reason) => typeof reason === 'string' && reason.trim().length > 0)
1380
+ : [];
1381
+ }
1382
+ export function resolveAutomaticBranchRecoveryReason(snapshotOrReasons, options = {}) {
1383
+ const reasons = readPrecomputedRecoveryReasonList(snapshotOrReasons);
1384
+ const resolvedReasons = reasons.length > 0
1385
+ ? reasons
1386
+ : resolveActionRequiredReasons(snapshotOrReasons, options);
1387
+ const recoveryReason = reasons.find((reason) => AUTOMATIC_BRANCH_RECOVERY_REASONS.has(reason));
1388
+ const selectedReason = typeof recoveryReason === 'string'
1389
+ ? recoveryReason
1390
+ : resolvedReasons.find((reason) => AUTOMATIC_BRANCH_RECOVERY_REASONS.has(reason));
1391
+ if (typeof selectedReason !== 'string') {
1392
+ return null;
1393
+ }
1394
+ if (options.requireExclusive === true) {
1395
+ if (resolvedReasons.length === 0
1396
+ || resolvedReasons.some((reason) => !AUTOMATIC_BRANCH_RECOVERY_REASONS.has(reason))) {
1397
+ return null;
1398
+ }
1399
+ const gateReasons = readRecoveryGateReasons(snapshotOrReasons);
1400
+ if (gateReasons.some((reason) => !AUTOMATIC_BRANCH_RECOVERY_REASONS.has(reason))) {
1401
+ return null;
1402
+ }
1403
+ }
1404
+ return selectedReason;
1405
+ }
1406
+ export function shouldAttemptAutomaticBranchRecovery(snapshotOrReasons, options = {}) {
1407
+ const reasons = readPrecomputedRecoveryReasonList(snapshotOrReasons);
1408
+ const resolvedReasons = reasons.length > 0
1409
+ ? reasons
1410
+ : resolveActionRequiredReasons(snapshotOrReasons, options);
1411
+ const recoveryReason = resolveAutomaticBranchRecoveryReason(snapshotOrReasons, {
1412
+ ...options,
1413
+ requireExclusive: true
1414
+ });
1415
+ return (typeof recoveryReason === 'string'
1416
+ && resolvedReasons.length === 1
1417
+ && resolvedReasons[0] === recoveryReason);
1418
+ }
1419
+ export function isConflictLikeBranchRecoveryFailureMessage(value) {
1420
+ if (typeof value !== 'string' || value.trim().length === 0) {
1421
+ return false;
1422
+ }
1423
+ return (/\bconflict(?:ing|s)?\b/iu.test(value)
1424
+ || /\bcannot be (?:cleanly )?(?:rebased|merged)\b/iu.test(value)
1425
+ || /\bmerge conflict\b/iu.test(value));
1426
+ }
1427
+ export function shouldSucceedAfterTimeout(snapshot, options = {}) {
1428
+ if (!snapshot || typeof snapshot !== 'object') {
1429
+ return false;
1430
+ }
1431
+ if (options.pollingHealthy === false) {
1432
+ return false;
1433
+ }
1434
+ const readinessMode = normalizeReadinessMode(options.readinessMode ?? snapshot.readinessMode);
1435
+ return readinessMode === 'review' && snapshot.readyToMerge === true;
1436
+ }
717
1437
  function formatStatusLine(snapshot, quietRemainingMs) {
718
1438
  const requiredChecks = snapshot.requiredChecks;
719
1439
  const failedNames = snapshot.checks.failed.map((item) => `${item.name}:${item.state}`).join(', ') || '-';
@@ -723,11 +1443,25 @@ function formatStatusLine(snapshot, quietRemainingMs) {
723
1443
  : '-';
724
1444
  const requiredPendingNames = requiredChecks ? requiredChecks.pending.join(', ') || '-' : '-';
725
1445
  const reasons = snapshot.gateReasons.join(', ') || 'none';
1446
+ const githubRateLimit = snapshot.githubRateLimit
1447
+ ? `${snapshot.githubRateLimit.surface ?? 'unknown'}/${snapshot.githubRateLimit.limit_type ?? 'unknown'}`
1448
+ : 'none';
1449
+ const coderabbitRollup = snapshot.botRereviewDiagnostics?.coderabbit?.statusCheckRollup;
1450
+ const coderabbitRollupState = coderabbitRollup?.state ?? 'unknown';
1451
+ const coderabbitRollupNames = Array.isArray(coderabbitRollup?.contexts)
1452
+ ? coderabbitRollup.contexts.map((context) => context.name).filter(Boolean).join('+') || '-'
1453
+ : '-';
1454
+ const clearedRereviewPending = Array.isArray(snapshot.botRereviewDiagnostics?.clearedPendingBots)
1455
+ ? snapshot.botRereviewDiagnostics.clearedPendingBots.join(', ') || '-'
1456
+ : '-';
726
1457
  return [
727
1458
  `PR #${snapshot.number}`,
728
1459
  `state=${snapshot.state}`,
729
1460
  `merge_state=${snapshot.mergeStateStatus}`,
730
1461
  `review=${snapshot.reviewDecision}`,
1462
+ `target=${normalizeReadinessMode(snapshot.readinessMode)}`,
1463
+ `fanout_cache=${snapshot.fanoutCacheHit ? 'hit' : 'miss'}`,
1464
+ `github_rate_limit=${githubRateLimit}`,
731
1465
  `gate_checks=${snapshot.gateChecksSource}`,
732
1466
  `checks_ok=${snapshot.checks.successCount}/${snapshot.checks.total}`,
733
1467
  `checks_pending=${snapshot.checks.pending.length}`,
@@ -741,6 +1475,9 @@ function formatStatusLine(snapshot, quietRemainingMs) {
741
1475
  `bot_rereview_fetch_error=${snapshot.botRereviewFetchError ? 'yes' : 'no'}`,
742
1476
  `bot_rereview_pending=[${snapshot.botRereviewPending.join(', ') || '-'}]`,
743
1477
  `bot_rereview_in_progress=[${snapshot.botRereviewInProgress.join(', ') || '-'}]`,
1478
+ `bot_rereview_cleared=[${clearedRereviewPending}]`,
1479
+ `coderabbit_rollup=${coderabbitRollupState}`,
1480
+ `coderabbit_rollup_contexts=[${coderabbitRollupNames}]`,
744
1481
  `coderabbit_actionable=${snapshot.coderabbitReviewMeta.actionableCount}`,
745
1482
  `coderabbit_out_of_diff=${snapshot.coderabbitReviewMeta.outsideDiffCount}`,
746
1483
  `coderabbit_nitpick=${snapshot.coderabbitReviewMeta.nitpickCount}`,
@@ -752,6 +1489,16 @@ function formatStatusLine(snapshot, quietRemainingMs) {
752
1489
  `required_failed=[${requiredFailedNames}]`
753
1490
  ].join(' | ');
754
1491
  }
1492
+ function planPollingRateLimitSleepMs(rateLimit, { owner, repo, prNumber, intervalMs, deadline }) {
1493
+ const nowMs = Date.now();
1494
+ const remainingMs = Math.max(0, deadline - nowMs);
1495
+ return planGitHubRateLimitBackoff(rateLimit, {
1496
+ nowMs,
1497
+ fallbackMs: intervalMs,
1498
+ remainingMs,
1499
+ jitterSeed: `${owner}/${repo}#${prNumber}:${rateLimit?.surface ?? 'unknown'}:${rateLimit?.limit_type ?? 'unknown'}`
1500
+ });
1501
+ }
755
1502
  async function fetchRequiredChecks(owner, repo, prNumber) {
756
1503
  try {
757
1504
  const result = await runGhJson([
@@ -768,13 +1515,24 @@ async function fetchRequiredChecks(owner, repo, prNumber) {
768
1515
  const summary = summarizeRequiredChecks(entries);
769
1516
  return {
770
1517
  summary: summary.total > 0 ? summary : null,
771
- fetchError: false
1518
+ fetchError: false,
1519
+ rateLimit: null
772
1520
  };
773
1521
  }
774
- catch {
1522
+ catch (error) {
1523
+ const message = error instanceof Error ? error.message : String(error);
1524
+ if (isNoRequiredChecksReportedErrorMessage(message)) {
1525
+ return {
1526
+ summary: null,
1527
+ fetchError: false,
1528
+ rateLimit: null
1529
+ };
1530
+ }
1531
+ const rateLimit = resolveGitHubRateLimitStatus(error, { surface: 'rest' });
775
1532
  return {
776
1533
  summary: null,
777
- fetchError: true
1534
+ fetchError: true,
1535
+ rateLimit
778
1536
  };
779
1537
  }
780
1538
  }
@@ -904,18 +1662,27 @@ function maxCommentTimestampForKind(issueComments, kind, requestAtMs, headOid) {
904
1662
  if (resolveBotKindFromLogin(comment.user?.login) !== kind) {
905
1663
  continue;
906
1664
  }
907
- if (comment.__source !== 'pull') {
908
- continue;
1665
+ if (comment.__source === 'pull') {
1666
+ const commentCommitId = typeof comment.commit_id === 'string' ? comment.commit_id : null;
1667
+ if (headOid && commentCommitId && commentCommitId !== headOid) {
1668
+ continue;
1669
+ }
909
1670
  }
910
- const commentCommitId = typeof comment.commit_id === 'string' ? comment.commit_id : null;
911
- if (headOid && commentCommitId && commentCommitId !== headOid) {
1671
+ else if (!(kind === 'coderabbit' &&
1672
+ typeof comment.body === 'string' &&
1673
+ typeof headOid === 'string' &&
1674
+ headOid.length > 0 &&
1675
+ comment.body.toLowerCase().includes(headOid.toLowerCase()) &&
1676
+ CODERABBIT_ISSUE_COMMENT_COMPLETION_PATTERNS.some((pattern) => pattern.test(comment.body)))) {
912
1677
  continue;
913
1678
  }
914
1679
  const createdAtMs = parseTimestampMs(comment.created_at);
915
- if (createdAtMs === null || createdAtMs <= requestAtMs) {
1680
+ const updatedAtMs = parseTimestampMs(comment.updated_at);
1681
+ const effectiveAtMs = comment.__source === 'issue' ? maxTimestamp([createdAtMs, updatedAtMs]) : createdAtMs;
1682
+ if (effectiveAtMs === null || effectiveAtMs <= requestAtMs) {
916
1683
  continue;
917
1684
  }
918
- timestamps.push(createdAtMs);
1685
+ timestamps.push(effectiveAtMs);
919
1686
  }
920
1687
  return maxTimestamp(timestamps);
921
1688
  }
@@ -1001,6 +1768,7 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1001
1768
  if (requestedKinds.length === 0) {
1002
1769
  return {
1003
1770
  fetchError: false,
1771
+ rateLimit: null,
1004
1772
  pendingBots: [],
1005
1773
  inProgressBots: [],
1006
1774
  coderabbit
@@ -1008,7 +1776,9 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1008
1776
  }
1009
1777
  const pendingBots = [];
1010
1778
  const inProgressBots = [];
1779
+ const requestTimesByBot = {};
1011
1780
  let hadSignalFetchError = false;
1781
+ let signalRateLimit = null;
1012
1782
  for (const kind of requestedKinds) {
1013
1783
  const request = rereviewRequests[kind];
1014
1784
  if (!request) {
@@ -1019,8 +1789,9 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1019
1789
  try {
1020
1790
  requestCommentReactions = await fetchCommentReactionsBySource(owner, repo, request.source, request.commentId);
1021
1791
  }
1022
- catch {
1792
+ catch (error) {
1023
1793
  hadSignalFetchError = true;
1794
+ signalRateLimit = signalRateLimit ?? resolveGitHubRateLimitStatus(error, { surface: 'rest' });
1024
1795
  requestCommentReactions = [];
1025
1796
  }
1026
1797
  }
@@ -1035,6 +1806,7 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1035
1806
  });
1036
1807
  const hasActiveInProgress = inProgressAtMs !== null && (completeAtMs === null || inProgressAtMs > completeAtMs);
1037
1808
  const label = BOT_KIND_LABELS[kind] ?? kind;
1809
+ requestTimesByBot[label] = request.createdAtMs;
1038
1810
  if (hasActiveInProgress) {
1039
1811
  inProgressBots.push(label);
1040
1812
  }
@@ -1044,14 +1816,17 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1044
1816
  }
1045
1817
  return {
1046
1818
  fetchError: hadSignalFetchError,
1819
+ rateLimit: signalRateLimit,
1047
1820
  pendingBots,
1048
1821
  inProgressBots,
1822
+ requestTimesByBot,
1049
1823
  coderabbit
1050
1824
  };
1051
1825
  }
1052
- catch {
1826
+ catch (error) {
1053
1827
  return {
1054
1828
  fetchError: true,
1829
+ rateLimit: resolveGitHubRateLimitStatus(error, { surface: 'rest' }),
1055
1830
  pendingBots: [],
1056
1831
  inProgressBots: [],
1057
1832
  coderabbit: {
@@ -1064,7 +1839,7 @@ async function fetchBotRereviewSignals(owner, repo, prNumber, headOid) {
1064
1839
  }
1065
1840
  async function fetchInlineBotFeedback(owner, repo, prNumber, headOid) {
1066
1841
  if (!headOid) {
1067
- return { fetchError: false, unacknowledgedCount: 0 };
1842
+ return { fetchError: false, rateLimit: null, unacknowledgedCount: 0 };
1068
1843
  }
1069
1844
  try {
1070
1845
  const pagedPayload = await runGhJsonSlurped([
@@ -1111,13 +1886,17 @@ async function fetchInlineBotFeedback(owner, repo, prNumber, headOid) {
1111
1886
  unacknowledgedCount += 1;
1112
1887
  }
1113
1888
  }
1114
- return { fetchError: false, unacknowledgedCount };
1889
+ return { fetchError: false, rateLimit: null, unacknowledgedCount };
1115
1890
  }
1116
- catch {
1117
- return { fetchError: true, unacknowledgedCount: 0 };
1891
+ catch (error) {
1892
+ return {
1893
+ fetchError: true,
1894
+ rateLimit: resolveGitHubRateLimitStatus(error, { surface: 'rest' }),
1895
+ unacknowledgedCount: 0
1896
+ };
1118
1897
  }
1119
1898
  }
1120
- async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache = null) {
1899
+ async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache = null, options = {}) {
1121
1900
  const response = await runGhJson([
1122
1901
  'api',
1123
1902
  'graphql',
@@ -1131,26 +1910,65 @@ async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache
1131
1910
  `number=${prNumber}`
1132
1911
  ]);
1133
1912
  const currentHeadOid = response?.data?.repository?.pullRequest?.commits?.nodes?.[0]?.commit?.oid || null;
1913
+ const currentUpdatedAt = response?.data?.repository?.pullRequest?.updatedAt || null;
1914
+ const cachedFanout = resolveReusableFanoutCache(previousRequiredChecksCache, currentHeadOid, currentUpdatedAt);
1134
1915
  const previousRequiredChecks = resolveCachedRequiredChecksSummary(previousRequiredChecksCache, currentHeadOid);
1135
- const [requiredChecksResult, inlineBotFeedback, botRereviewSignals] = await Promise.all([
1136
- fetchRequiredChecks(owner, repo, prNumber),
1137
- fetchInlineBotFeedback(owner, repo, prNumber, currentHeadOid),
1138
- fetchBotRereviewSignals(owner, repo, prNumber, currentHeadOid)
1139
- ]);
1916
+ let requiredChecksResult;
1917
+ let inlineBotFeedback;
1918
+ let botRereviewSignals;
1919
+ let fanoutCacheHit = false;
1920
+ if (cachedFanout) {
1921
+ requiredChecksResult = await fetchRequiredChecks(owner, repo, prNumber);
1922
+ inlineBotFeedback = cachedFanout.inlineBotFeedback;
1923
+ botRereviewSignals = cachedFanout.botRereviewSignals;
1924
+ fanoutCacheHit = true;
1925
+ }
1926
+ else {
1927
+ [requiredChecksResult, inlineBotFeedback, botRereviewSignals] = await Promise.all([
1928
+ fetchRequiredChecks(owner, repo, prNumber),
1929
+ fetchInlineBotFeedback(owner, repo, prNumber, currentHeadOid),
1930
+ fetchBotRereviewSignals(owner, repo, prNumber, currentHeadOid)
1931
+ ]);
1932
+ }
1140
1933
  const requiredChecks = resolveRequiredChecksSummary(requiredChecksResult.summary, previousRequiredChecks, requiredChecksResult.fetchError);
1934
+ const githubRateLimits = [
1935
+ requiredChecksResult.rateLimit,
1936
+ inlineBotFeedback?.rateLimit,
1937
+ botRereviewSignals?.rateLimit
1938
+ ].filter(Boolean);
1141
1939
  return {
1142
1940
  snapshot: buildStatusSnapshot(response, requiredChecks, {
1143
1941
  ...inlineBotFeedback,
1144
1942
  rereview: botRereviewSignals
1943
+ }, {
1944
+ ...options,
1945
+ fanoutCacheHit,
1946
+ githubRateLimits,
1947
+ requiredChecksQueryFailed: requiredChecksResult.fetchError
1145
1948
  }),
1146
- requiredChecksForNextPoll: requiredChecks
1147
- ? {
1148
- headOid: currentHeadOid,
1149
- summary: requiredChecks
1150
- }
1151
- : null
1949
+ requiredChecksForNextPoll: buildReusableFanoutCache({
1950
+ headOid: currentHeadOid,
1951
+ updatedAt: currentUpdatedAt,
1952
+ requiredChecks,
1953
+ requiredChecksResult,
1954
+ inlineBotFeedback,
1955
+ botRereviewSignals
1956
+ })
1152
1957
  };
1153
1958
  }
1959
+ export async function fetchPrStatusSnapshot(input) {
1960
+ const owner = typeof input?.owner === 'string' ? input.owner.trim() : '';
1961
+ const repo = typeof input?.repo === 'string' ? input.repo.trim() : '';
1962
+ const prNumber = Number(input?.prNumber);
1963
+ if (!owner || !repo || !Number.isInteger(prNumber) || prNumber <= 0) {
1964
+ throw new Error('fetchPrStatusSnapshot requires owner, repo, and a positive integer prNumber.');
1965
+ }
1966
+ const readinessMode = normalizeReadinessMode(input?.readinessMode);
1967
+ const { snapshot } = await fetchSnapshot(owner, repo, prNumber, null, {
1968
+ readinessMode
1969
+ });
1970
+ return snapshot;
1971
+ }
1154
1972
  export function buildPrMergeArgs({ owner, repo, prNumber, mergeMethod, deleteBranch, headOid }) {
1155
1973
  // gh pr merge has no --yes flag; rely on non-interactive stdio + explicit merge method.
1156
1974
  const args = ['pr', 'merge', String(prNumber), `--${mergeMethod}`, '--repo', `${owner}/${repo}`];
@@ -1166,8 +1984,26 @@ async function attemptMerge({ owner, repo, prNumber, mergeMethod, deleteBranch,
1166
1984
  const args = buildPrMergeArgs({ owner, repo, prNumber, mergeMethod, deleteBranch, headOid });
1167
1985
  return await runGh(args, { allowFailure: true });
1168
1986
  }
1987
+ async function attemptUpdateBranch({ owner, repo, prNumber }) {
1988
+ const args = buildPrUpdateBranchArgs({ owner, repo, prNumber });
1989
+ return await runGh(args, { allowFailure: true });
1990
+ }
1991
+ export function buildAutomaticBranchRecoveryKey(snapshot, recoveryReason) {
1992
+ return [
1993
+ recoveryReason,
1994
+ snapshot?.headOid || 'no-head'
1995
+ ].join('|');
1996
+ }
1997
+ function describeAutomaticBranchRecovery(recoveryReason) {
1998
+ if (recoveryReason === 'merge_state=DIRTY') {
1999
+ return 'conflict recovery';
2000
+ }
2001
+ return 'branch refresh';
2002
+ }
1169
2003
  async function runPrWatchMergeOrThrow(argv, options) {
1170
2004
  const { args, positionals } = parseArgs(argv);
2005
+ const readinessMode = normalizeReadinessMode(options.readinessMode);
2006
+ const isReviewMode = readinessMode === 'review';
1171
2007
  if (hasFlag(args, 'h') || hasFlag(args, 'help')) {
1172
2008
  printPrWatchMergeHelp(options);
1173
2009
  return;
@@ -1195,6 +2031,21 @@ async function runPrWatchMergeOrThrow(argv, options) {
1195
2031
  const label = unknownFlags[0] ? `--${unknownFlags[0]}` : positionals[0];
1196
2032
  throw new Error(`Unknown option: ${label}`);
1197
2033
  }
2034
+ if (isReviewMode) {
2035
+ const unsupportedFlags = [];
2036
+ if (Object.prototype.hasOwnProperty.call(args, 'merge-method')) {
2037
+ unsupportedFlags.push('--merge-method');
2038
+ }
2039
+ if (hasFlag(args, 'auto-merge') || hasFlag(args, 'no-auto-merge')) {
2040
+ unsupportedFlags.push('--auto-merge/--no-auto-merge');
2041
+ }
2042
+ if (hasFlag(args, 'delete-branch') || hasFlag(args, 'no-delete-branch')) {
2043
+ unsupportedFlags.push('--delete-branch/--no-delete-branch');
2044
+ }
2045
+ if (unsupportedFlags.length > 0) {
2046
+ throw new Error(`ready-review does not support merge flags: ${unsupportedFlags.join(', ')}`);
2047
+ }
2048
+ }
1198
2049
  const intervalSeconds = parseNumber('interval-seconds', typeof args['interval-seconds'] === 'string'
1199
2050
  ? args['interval-seconds']
1200
2051
  : process.env.PR_MONITOR_INTERVAL_SECONDS, DEFAULT_INTERVAL_SECONDS);
@@ -1204,25 +2055,33 @@ async function runPrWatchMergeOrThrow(argv, options) {
1204
2055
  const timeoutMinutes = parseNumber('timeout-minutes', typeof args['timeout-minutes'] === 'string'
1205
2056
  ? args['timeout-minutes']
1206
2057
  : process.env.PR_MONITOR_TIMEOUT_MINUTES, DEFAULT_TIMEOUT_MINUTES);
1207
- const mergeMethod = parseMergeMethod(typeof args['merge-method'] === 'string'
1208
- ? args['merge-method']
1209
- : process.env.PR_MONITOR_MERGE_METHOD || DEFAULT_MERGE_METHOD);
2058
+ const mergeMethod = isReviewMode
2059
+ ? DEFAULT_MERGE_METHOD
2060
+ : parseMergeMethod(typeof args['merge-method'] === 'string'
2061
+ ? args['merge-method']
2062
+ : process.env.PR_MONITOR_MERGE_METHOD || DEFAULT_MERGE_METHOD);
1210
2063
  const defaultAutoMergeFallback = typeof options.defaultAutoMerge === 'boolean' ? options.defaultAutoMerge : false;
1211
2064
  const defaultAutoMerge = envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, defaultAutoMergeFallback);
1212
2065
  const defaultDeleteBranch = envFlagEnabled(process.env.PR_MONITOR_DELETE_BRANCH, true);
1213
- let autoMerge = defaultAutoMerge;
1214
- if (hasFlag(args, 'auto-merge')) {
1215
- autoMerge = true;
1216
- }
1217
- if (hasFlag(args, 'no-auto-merge')) {
1218
- autoMerge = false;
1219
- }
1220
- let deleteBranch = defaultDeleteBranch;
1221
- if (hasFlag(args, 'delete-branch')) {
1222
- deleteBranch = true;
2066
+ let autoMerge = false;
2067
+ if (!isReviewMode) {
2068
+ autoMerge = defaultAutoMerge;
2069
+ if (hasFlag(args, 'auto-merge')) {
2070
+ autoMerge = true;
2071
+ }
2072
+ if (hasFlag(args, 'no-auto-merge')) {
2073
+ autoMerge = false;
2074
+ }
1223
2075
  }
1224
- if (hasFlag(args, 'no-delete-branch')) {
1225
- deleteBranch = false;
2076
+ let deleteBranch = false;
2077
+ if (!isReviewMode) {
2078
+ deleteBranch = defaultDeleteBranch;
2079
+ if (hasFlag(args, 'delete-branch')) {
2080
+ deleteBranch = true;
2081
+ }
2082
+ if (hasFlag(args, 'no-delete-branch')) {
2083
+ deleteBranch = false;
2084
+ }
1226
2085
  }
1227
2086
  let exitOnActionRequired = Boolean(options.defaultExitOnActionRequired);
1228
2087
  if (hasFlag(args, 'exit-on-action-required')) {
@@ -1239,24 +2098,54 @@ async function runPrWatchMergeOrThrow(argv, options) {
1239
2098
  const quietMs = Math.round(quietMinutes * 60 * 1000);
1240
2099
  const timeoutMs = Math.round(timeoutMinutes * 60 * 1000);
1241
2100
  const deadline = Date.now() + timeoutMs;
1242
- log(`Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, auto_merge=${autoMerge ? 'on' : 'off'}, exit_on_action_required=${exitOnActionRequired ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`);
2101
+ const automaticBranchRecoveryEnabled = isReviewMode
2102
+ || autoMerge
2103
+ || Boolean(options.enableAutomaticBranchRecovery);
2104
+ log(isReviewMode
2105
+ ? `Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, target=review_handoff, exit_on_action_required=${exitOnActionRequired ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`
2106
+ : `Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, auto_merge=${autoMerge ? 'on' : 'off'}, exit_on_action_required=${exitOnActionRequired ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`);
1243
2107
  let quietWindowStartedAt = null;
1244
2108
  let quietWindowAnchorUpdatedAt = null;
1245
2109
  let quietWindowAnchorHeadOid = null;
1246
2110
  let lastMergeAttemptHeadOid = null;
1247
- let requiredChecksForNextPollCache = null;
2111
+ let pendingAutomaticBranchRecoveryKey = null;
2112
+ let attemptedAutomaticBranchRecoveryKey = null;
2113
+ let fanoutForNextPollCache = null;
2114
+ let latestSnapshot = null;
2115
+ let pollingHealthySinceLatestSnapshot = false;
1248
2116
  while (Date.now() <= deadline) {
1249
2117
  let snapshot;
1250
2118
  try {
1251
- const fetched = await fetchSnapshot(owner, repo, prNumber, requiredChecksForNextPollCache);
2119
+ const fetched = await fetchSnapshot(owner, repo, prNumber, fanoutForNextPollCache, {
2120
+ readinessMode
2121
+ });
1252
2122
  snapshot = fetched.snapshot;
1253
- requiredChecksForNextPollCache = fetched.requiredChecksForNextPoll;
2123
+ fanoutForNextPollCache = fetched.requiredChecksForNextPoll;
1254
2124
  }
1255
2125
  catch (error) {
2126
+ pollingHealthySinceLatestSnapshot = false;
2127
+ const rateLimit = resolveGitHubRateLimitStatus(error);
2128
+ if (rateLimit) {
2129
+ const sleepMs = planPollingRateLimitSleepMs(rateLimit, {
2130
+ owner,
2131
+ repo,
2132
+ prNumber,
2133
+ intervalMs,
2134
+ deadline
2135
+ });
2136
+ if (sleepMs <= 0) {
2137
+ break;
2138
+ }
2139
+ log(`Polling GitHub API rate limit: ${formatGitHubRateLimitStatus(rateLimit)} (retrying in ${formatDuration(sleepMs)}).`);
2140
+ await sleep(sleepMs);
2141
+ continue;
2142
+ }
1256
2143
  log(`Polling error: ${error instanceof Error ? error.message : String(error)} (retrying).`);
1257
2144
  await sleep(intervalMs);
1258
2145
  continue;
1259
2146
  }
2147
+ latestSnapshot = snapshot;
2148
+ pollingHealthySinceLatestSnapshot = true;
1260
2149
  if (snapshot.state === 'MERGED' || snapshot.mergedAt) {
1261
2150
  log(`PR #${prNumber} is merged.`);
1262
2151
  if (snapshot.url) {
@@ -1290,20 +2179,132 @@ async function runPrWatchMergeOrThrow(argv, options) {
1290
2179
  const quietElapsedMs = quietWindowStartedAt ? Date.now() - quietWindowStartedAt : 0;
1291
2180
  const quietRemainingMs = quietWindowStartedAt ? Math.max(quietMs - quietElapsedMs, 0) : quietMs;
1292
2181
  log(formatStatusLine(snapshot, quietRemainingMs));
2182
+ const actionRequiredReasons = resolveActionRequiredReasons(snapshot, { readinessMode });
2183
+ const automaticBranchRecoveryReason = automaticBranchRecoveryEnabled
2184
+ ? resolveAutomaticBranchRecoveryReason(snapshot, {
2185
+ readinessMode,
2186
+ requireExclusive: true
2187
+ })
2188
+ : null;
2189
+ const shouldAttemptRecovery = automaticBranchRecoveryEnabled
2190
+ && shouldAttemptAutomaticBranchRecovery(snapshot, { readinessMode });
2191
+ const automaticBranchRecoveryKey = automaticBranchRecoveryReason
2192
+ ? buildAutomaticBranchRecoveryKey(snapshot, automaticBranchRecoveryReason)
2193
+ : null;
2194
+ if (pendingAutomaticBranchRecoveryKey
2195
+ && pendingAutomaticBranchRecoveryKey !== automaticBranchRecoveryKey) {
2196
+ pendingAutomaticBranchRecoveryKey = null;
2197
+ }
2198
+ if (attemptedAutomaticBranchRecoveryKey
2199
+ && attemptedAutomaticBranchRecoveryKey !== automaticBranchRecoveryKey) {
2200
+ attemptedAutomaticBranchRecoveryKey = null;
2201
+ }
2202
+ if (exitOnActionRequired
2203
+ && actionRequiredReasons.length > 0
2204
+ && !shouldAttemptRecovery) {
2205
+ const details = actionRequiredReasons.join(', ');
2206
+ throw new PrWatchMergeExitError(`${isReviewMode ? 'Action required before review handoff' : 'Action required before merge'}: ${details}${snapshot.url ? ` (${snapshot.url})` : ''}`, 2);
2207
+ }
2208
+ if (snapshot.githubRateLimit) {
2209
+ pollingHealthySinceLatestSnapshot = false;
2210
+ const sleepMs = planPollingRateLimitSleepMs(snapshot.githubRateLimit, {
2211
+ owner,
2212
+ repo,
2213
+ prNumber,
2214
+ intervalMs,
2215
+ deadline
2216
+ });
2217
+ if (sleepMs <= 0) {
2218
+ break;
2219
+ }
2220
+ log(`GitHub API fan-out is rate limited: ${formatGitHubRateLimitStatus(snapshot.githubRateLimit)} (retrying in ${formatDuration(sleepMs)}).`);
2221
+ await sleep(sleepMs);
2222
+ continue;
2223
+ }
2224
+ if (shouldAttemptRecovery
2225
+ && automaticBranchRecoveryReason
2226
+ && automaticBranchRecoveryKey
2227
+ && pendingAutomaticBranchRecoveryKey !== automaticBranchRecoveryKey
2228
+ && attemptedAutomaticBranchRecoveryKey !== automaticBranchRecoveryKey) {
2229
+ if (dryRun) {
2230
+ log(`Dry run: would attempt automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} for ${automaticBranchRecoveryReason}.`);
2231
+ }
2232
+ else {
2233
+ log(`Attempting automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} via gh pr update-branch (${automaticBranchRecoveryReason}).`);
2234
+ const updateBranchResult = await attemptUpdateBranch({
2235
+ owner,
2236
+ repo,
2237
+ prNumber
2238
+ });
2239
+ if (updateBranchResult.exitCode === 0) {
2240
+ attemptedAutomaticBranchRecoveryKey = automaticBranchRecoveryKey;
2241
+ pendingAutomaticBranchRecoveryKey = automaticBranchRecoveryKey;
2242
+ quietWindowStartedAt = null;
2243
+ quietWindowAnchorUpdatedAt = null;
2244
+ quietWindowAnchorHeadOid = null;
2245
+ lastMergeAttemptHeadOid = null;
2246
+ log(`Automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} requested for PR #${prNumber}; waiting for GitHub readiness to refresh.`);
2247
+ const remainingTimeMs = deadline - Date.now();
2248
+ if (remainingTimeMs <= 0) {
2249
+ break;
2250
+ }
2251
+ await sleep(Math.min(intervalMs, remainingTimeMs));
2252
+ continue;
2253
+ }
2254
+ const updateBranchRateLimit = resolveGitHubRateLimitStatus(updateBranchResult, {
2255
+ surface: 'rest'
2256
+ });
2257
+ if (updateBranchRateLimit) {
2258
+ const sleepMs = planPollingRateLimitSleepMs(updateBranchRateLimit, {
2259
+ owner,
2260
+ repo,
2261
+ prNumber,
2262
+ intervalMs,
2263
+ deadline
2264
+ });
2265
+ if (sleepMs <= 0) {
2266
+ break;
2267
+ }
2268
+ log(`Automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} is rate limited: ${formatGitHubRateLimitStatus(updateBranchRateLimit)} (retrying in ${formatDuration(sleepMs)}).`);
2269
+ await sleep(sleepMs);
2270
+ continue;
2271
+ }
2272
+ attemptedAutomaticBranchRecoveryKey = automaticBranchRecoveryKey;
2273
+ const details = updateBranchResult.stderr
2274
+ || updateBranchResult.stdout
2275
+ || `exit code ${updateBranchResult.exitCode}`;
2276
+ log(`Automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} failed: ${details}`);
2277
+ if (isConflictLikeBranchRecoveryFailureMessage(details)) {
2278
+ log('GitHub reported merge conflicts while attempting automatic branch recovery.');
2279
+ }
2280
+ }
2281
+ }
1293
2282
  if (exitOnActionRequired) {
1294
- const actionRequiredReasons = resolveActionRequiredReasons(snapshot);
1295
2283
  if (actionRequiredReasons.length > 0) {
2284
+ if (shouldAttemptRecovery
2285
+ && automaticBranchRecoveryKey
2286
+ && pendingAutomaticBranchRecoveryKey === automaticBranchRecoveryKey) {
2287
+ log(`Automatic ${describeAutomaticBranchRecovery(automaticBranchRecoveryReason)} is still pending; suppressing action-required exit for now.`);
2288
+ const remainingTimeMs = deadline - Date.now();
2289
+ if (remainingTimeMs <= 0) {
2290
+ break;
2291
+ }
2292
+ await sleep(Math.min(intervalMs, remainingTimeMs));
2293
+ continue;
2294
+ }
1296
2295
  const details = actionRequiredReasons.join(', ');
1297
- throw new PrWatchMergeExitError(`Action required before merge: ${details}${snapshot.url ? ` (${snapshot.url})` : ''}`, 2);
2296
+ throw new PrWatchMergeExitError(`${isReviewMode ? 'Action required before review handoff' : 'Action required before merge'}: ${details}${snapshot.url ? ` (${snapshot.url})` : ''}`, 2);
1298
2297
  }
1299
2298
  }
1300
2299
  if (snapshot.readyToMerge && quietWindowStartedAt !== null && quietElapsedMs >= quietMs) {
1301
2300
  if (!autoMerge || dryRun) {
1302
- log(dryRun
1303
- ? 'Dry run: merge conditions satisfied and quiet window elapsed.'
1304
- : 'Merge conditions satisfied and quiet window elapsed.');
2301
+ log(isReviewMode
2302
+ ? 'Review handoff conditions satisfied and quiet window elapsed.'
2303
+ : dryRun
2304
+ ? 'Dry run: merge conditions satisfied and quiet window elapsed.'
2305
+ : 'Merge conditions satisfied and quiet window elapsed.');
1305
2306
  if (snapshot.url) {
1306
- log(`Ready to merge: ${snapshot.url}`);
2307
+ log(`${isReviewMode ? 'Ready for review' : 'Ready to merge'}: ${snapshot.url}`);
1307
2308
  }
1308
2309
  return;
1309
2310
  }
@@ -1335,6 +2336,13 @@ async function runPrWatchMergeOrThrow(argv, options) {
1335
2336
  }
1336
2337
  await sleep(Math.min(intervalMs, remainingTimeMs));
1337
2338
  }
2339
+ if (shouldSucceedAfterTimeout(latestSnapshot, { readinessMode, pollingHealthy: pollingHealthySinceLatestSnapshot })) {
2340
+ log('Bounded wait expired cleanly with no remaining automated-feedback blockers.');
2341
+ if (latestSnapshot?.url) {
2342
+ log(`Ready for review: ${latestSnapshot.url}`);
2343
+ }
2344
+ return;
2345
+ }
1338
2346
  throw new PrWatchMergeExitError(`Timed out after ${timeoutMinutes} minute(s) while monitoring PR #${prNumber}.`, 3);
1339
2347
  }
1340
2348
  export async function runPrWatchMerge(argv, options = {}) {