@kbediako/codex-orchestrator 0.1.38 → 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 (299) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +70 -301
  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 +119 -15
  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 +667 -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 +20 -10
  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 +394 -0
  282. package/skills/collab-subagents-first/SKILL.md +1 -1
  283. package/skills/delegation-usage/DELEGATION_GUIDE.md +24 -11
  284. package/skills/delegation-usage/SKILL.md +19 -13
  285. package/skills/land/SKILL.md +77 -0
  286. package/skills/linear/SKILL.md +255 -0
  287. package/skills/release/SKILL.md +47 -3
  288. package/skills/standalone-review/SKILL.md +6 -1
  289. package/templates/README.md +4 -2
  290. package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
  291. package/templates/codex/.codex/agents/worker-complex.toml +1 -1
  292. package/templates/codex/.codex/config.toml +3 -4
  293. package/templates/codex/.codex/providers/README.md +13 -0
  294. package/templates/codex/.codex/providers/control.example.json +18 -0
  295. package/templates/codex/.codex/providers/provider.env.example +15 -0
  296. package/templates/codex/AGENTS.md +12 -7
  297. package/templates/codex/mcp-client.json +5 -1
  298. package/docs/README.md +0 -310
  299. package/docs/assets/setup.gif +0 -0
@@ -0,0 +1,1868 @@
1
+ import { execFile } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { promisify } from 'node:util';
4
+ import { classifyProviderLinearWorkflowState } from './providerLinearWorkflowStates.js';
5
+ import { getProviderLinearIssueContext, transitionProviderLinearIssueState } from './providerLinearWorkflowFacade.js';
6
+ import { isoTimestamp } from '../utils/time.js';
7
+ import { buildPrMergeArgs, buildPrUpdateBranchArgs, fetchPrStatusSnapshot, formatGitHubRateLimitStatus, isConflictLikeBranchRecoveryFailureMessage, parseGitHubRepoFromRemoteUrl, resolveAutomaticBranchRecoveryReason, resolveActionRequiredReasons, resolveGitHubRateLimitStatus, shouldAttemptAutomaticBranchRecovery } from '../../../../scripts/lib/pr-watch-merge.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const PROVIDER_MERGE_CLOSEOUT_COMMAND_TIMEOUT_MS = 15_000;
10
+ const PROVIDER_MERGE_CLOSEOUT_MERGE_METHOD = 'squash';
11
+ export async function runProviderDeterministicMergeCloseout(input, deps = {}) {
12
+ const env = input.env ?? process.env;
13
+ const now = deps.now ?? isoTimestamp;
14
+ const readIssueContext = deps.readIssueContext ?? getProviderLinearIssueContext;
15
+ const transitionIssueState = deps.transitionIssueState ?? transitionProviderLinearIssueState;
16
+ const runCommand = deps.runCommand ?? runProviderMergeCloseoutCommand;
17
+ const resolveSnapshot = deps.fetchSnapshot ?? fetchPrStatusSnapshot;
18
+ const resolveSnapshotActionRequiredReasons = deps.resolveSnapshotActionRequiredReasons ??
19
+ ((snapshot, options) => resolveActionRequiredReasons(snapshot, options));
20
+ const mode = input.mode ?? 'full';
21
+ const recordedAt = now();
22
+ const base = {
23
+ recorded_at: recordedAt,
24
+ issue_id: input.issueId,
25
+ issue_identifier: normalizeOptionalString(input.issueIdentifier),
26
+ issue_state: normalizeOptionalString(input.issueState),
27
+ issue_state_type: normalizeOptionalString(input.issueStateType),
28
+ issue_updated_at: normalizeOptionalString(input.issueUpdatedAt),
29
+ attached_pr_urls: [],
30
+ ignored_historical_pr_urls: [],
31
+ ignored_closed_unmerged_pr_urls: [],
32
+ conflicting_attached_pr_urls: [],
33
+ pr: null,
34
+ snapshot: null,
35
+ branch_recovery: null,
36
+ merge_attempt: null,
37
+ shared_root: null,
38
+ linear_transition: null,
39
+ github_rate_limit: null
40
+ };
41
+ const repoOriginResult = await runCommand({
42
+ command: 'git',
43
+ args: ['-C', input.repoRoot, 'remote', 'get-url', 'origin'],
44
+ cwd: input.repoRoot
45
+ });
46
+ if (!repoOriginResult.ok) {
47
+ return {
48
+ ...base,
49
+ status: 'merge_failed',
50
+ reason: 'shared_root_origin_unavailable',
51
+ summary: 'Shared repo origin remote could not be resolved.',
52
+ attached_pr_urls: [],
53
+ pr: null,
54
+ snapshot: null,
55
+ merge_attempt: null,
56
+ shared_root: null,
57
+ linear_transition: null
58
+ };
59
+ }
60
+ const parsedRepo = parseGitHubRepoFromRemoteUrl(repoOriginResult.stdout);
61
+ const repoKey = parsedRepo
62
+ ? `${parsedRepo.owner.toLowerCase()}/${parsedRepo.repo.toLowerCase()}`
63
+ : null;
64
+ if (!repoKey) {
65
+ return {
66
+ ...base,
67
+ status: 'merge_failed',
68
+ reason: 'shared_root_repo_unrecognized',
69
+ summary: 'Shared repo origin is not a GitHub repository URL.',
70
+ attached_pr_urls: [],
71
+ pr: null,
72
+ snapshot: null,
73
+ merge_attempt: null,
74
+ shared_root: null,
75
+ linear_transition: null
76
+ };
77
+ }
78
+ const issueContext = await readIssueContext({
79
+ issueId: input.issueId,
80
+ env,
81
+ sourceSetup: input.sourceSetup,
82
+ fallbackToCacheOnFailure: mode === 'probe-merged-recovery'
83
+ });
84
+ if (!issueContext.ok) {
85
+ return {
86
+ ...base,
87
+ status: 'merge_failed',
88
+ reason: 'linear_issue_context_failed',
89
+ summary: `Linear issue context could not be loaded (${issueContext.error.code}).`,
90
+ attached_pr_urls: [],
91
+ pr: null,
92
+ snapshot: null,
93
+ merge_attempt: null,
94
+ shared_root: null,
95
+ linear_transition: null
96
+ };
97
+ }
98
+ const attachedPrCandidates = collectAttachedGitHubPrCandidates(issueContext.issue.attachments);
99
+ const attachedPrUrls = attachedPrCandidates.map((candidate) => candidate.pr.url);
100
+ const usedCachedIssueContext = issueContext.cache_fallback_used === true;
101
+ if (mode === 'probe-merged-recovery' &&
102
+ usedCachedIssueContext &&
103
+ !isProbeRecoveryCacheContextFreshEnough({
104
+ issueState: input.issueState,
105
+ issueStateType: input.issueStateType,
106
+ issueUpdatedAt: input.issueUpdatedAt,
107
+ issueContext: issueContext.issue
108
+ })) {
109
+ return {
110
+ ...base,
111
+ attached_pr_urls: [...attachedPrUrls],
112
+ status: 'watching',
113
+ reason: 'probe_issue_context_cache_stale',
114
+ summary: 'Cached Linear issue context does not match the tracked issue metadata, so merged recovery will not transition the issue to Done until a fresh issue-context read succeeds.',
115
+ pr: null,
116
+ snapshot: null,
117
+ merge_attempt: null,
118
+ shared_root: null,
119
+ linear_transition: null
120
+ };
121
+ }
122
+ const sameRepoPrs = attachedPrCandidates.filter((candidate) => `${candidate.pr.owner.toLowerCase()}/${candidate.pr.repo.toLowerCase()}` === repoKey);
123
+ const currentIssueState = issueContext.issue.state?.name ?? base.issue_state;
124
+ const currentIssueStateType = issueContext.issue.state?.type ?? base.issue_state_type;
125
+ const currentIssueUpdatedAt = issueContext.issue.updated_at ?? base.issue_updated_at;
126
+ const currentIssueAlreadyCompleted = currentIssueStateType === 'completed';
127
+ const allowCompletedIssueRecovery = mode === 'probe-merged-recovery' && currentIssueAlreadyCompleted;
128
+ const baseWithContext = {
129
+ ...base,
130
+ issue_state: currentIssueState,
131
+ issue_state_type: currentIssueStateType,
132
+ issue_updated_at: currentIssueUpdatedAt,
133
+ attached_pr_urls: [...attachedPrUrls]
134
+ };
135
+ if (normalizeProviderMergeCloseoutIssueState(currentIssueState) !== 'merging' &&
136
+ !allowCompletedIssueRecovery) {
137
+ return {
138
+ ...baseWithContext,
139
+ status: 'action_required',
140
+ reason: 'issue_no_longer_merging',
141
+ summary: currentIssueState && currentIssueState.trim().length > 0
142
+ ? `Live Linear issue state is ${currentIssueState}, so deterministic merge closeout is not armed.`
143
+ : 'Live Linear issue state is no longer Merging, so deterministic merge closeout is not armed.'
144
+ };
145
+ }
146
+ if (sameRepoPrs.length === 0) {
147
+ return {
148
+ ...baseWithContext,
149
+ status: 'action_required',
150
+ reason: attachedPrUrls.length === 0 ? 'no_attached_pr' : 'attached_pr_repo_mismatch',
151
+ summary: attachedPrUrls.length === 0
152
+ ? 'No attached GitHub pull request is present for this Merging issue.'
153
+ : 'Attached GitHub pull requests do not match the shared repository root.'
154
+ };
155
+ }
156
+ let ignoredHistoricalPrUrls = [];
157
+ let ignoredClosedUnmergedPrUrls = [];
158
+ let conflictingAttachedPrUrls = [];
159
+ let selectionNote = null;
160
+ let pr = sameRepoPrs[0].pr;
161
+ let snapshot = null;
162
+ if (sameRepoPrs.length > 1) {
163
+ let resolution;
164
+ try {
165
+ resolution = await resolveAttachedSameRepoPullRequestCandidate({
166
+ candidates: sameRepoPrs,
167
+ mode: 'merge_closeout',
168
+ resolveSnapshot,
169
+ resolveSnapshotActionRequiredReasons
170
+ });
171
+ }
172
+ catch (error) {
173
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
174
+ if (githubRateLimit) {
175
+ return {
176
+ ...baseWithContext,
177
+ status: 'watching',
178
+ reason: 'github_rate_limited',
179
+ summary: `GitHub API budget blocked attached pull-request disambiguation during merge closeout: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`,
180
+ github_rate_limit: githubRateLimit
181
+ };
182
+ }
183
+ return {
184
+ ...baseWithContext,
185
+ status: 'merge_failed',
186
+ reason: 'snapshot_read_failed',
187
+ summary: `GitHub merge-readiness snapshot could not be loaded while disambiguating attached pull requests: ${error?.message ?? String(error)}.`
188
+ };
189
+ }
190
+ ignoredHistoricalPrUrls = resolution.ignored_historical_pr_urls;
191
+ ignoredClosedUnmergedPrUrls = resolution.ignored_closed_unmerged_pr_urls;
192
+ conflictingAttachedPrUrls = resolution.conflicting_attached_pr_urls;
193
+ selectionNote = resolution.selection_note;
194
+ if (!resolution.selected_pr) {
195
+ return {
196
+ ...baseWithContext,
197
+ ignored_historical_pr_urls: [...ignoredHistoricalPrUrls],
198
+ ignored_closed_unmerged_pr_urls: [...ignoredClosedUnmergedPrUrls],
199
+ conflicting_attached_pr_urls: [...conflictingAttachedPrUrls],
200
+ status: 'action_required',
201
+ reason: 'multiple_attached_prs',
202
+ summary: buildMultipleAttachedPrsSummary({
203
+ repoKey,
204
+ ignoredHistoricalPrUrls,
205
+ ignoredClosedUnmergedPrUrls,
206
+ conflictingAttachedPrUrls
207
+ })
208
+ };
209
+ }
210
+ pr = resolution.selected_pr;
211
+ snapshot = resolution.selected_snapshot;
212
+ }
213
+ const baseWithResolution = {
214
+ ...baseWithContext,
215
+ ignored_historical_pr_urls: [...ignoredHistoricalPrUrls],
216
+ ignored_closed_unmerged_pr_urls: [...ignoredClosedUnmergedPrUrls],
217
+ conflicting_attached_pr_urls: [...conflictingAttachedPrUrls]
218
+ };
219
+ const summarizeSelection = (summary) => selectionNote ? `${summary} ${selectionNote}` : summary;
220
+ if (!snapshot) {
221
+ try {
222
+ snapshot = await loadProviderSnapshotRecord({
223
+ owner: pr.owner,
224
+ repo: pr.repo,
225
+ prNumber: pr.number,
226
+ readinessMode: 'merge',
227
+ resolveSnapshot,
228
+ resolveSnapshotActionRequiredReasons
229
+ });
230
+ }
231
+ catch (error) {
232
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
233
+ if (githubRateLimit) {
234
+ return {
235
+ ...baseWithResolution,
236
+ pr,
237
+ status: 'watching',
238
+ reason: 'github_rate_limited',
239
+ summary: summarizeSelection(`GitHub API budget blocked merge-readiness snapshot loading: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`),
240
+ snapshot: null,
241
+ github_rate_limit: githubRateLimit
242
+ };
243
+ }
244
+ return {
245
+ ...baseWithResolution,
246
+ pr,
247
+ status: 'merge_failed',
248
+ reason: 'snapshot_read_failed',
249
+ summary: summarizeSelection(`GitHub merge-readiness snapshot could not be loaded: ${error?.message ?? String(error)}`),
250
+ snapshot: null
251
+ };
252
+ }
253
+ }
254
+ if (!snapshot) {
255
+ return {
256
+ ...baseWithResolution,
257
+ pr,
258
+ status: 'merge_failed',
259
+ reason: 'snapshot_read_failed',
260
+ summary: summarizeSelection('GitHub merge-readiness snapshot could not be loaded.'),
261
+ snapshot: null
262
+ };
263
+ }
264
+ let alreadyMerged = snapshot.merged_at !== null || snapshot.state === 'MERGED';
265
+ if (mode === 'probe-merged-recovery' && !alreadyMerged) {
266
+ return {
267
+ ...baseWithResolution,
268
+ pr,
269
+ snapshot,
270
+ status: 'watching',
271
+ reason: 'probe_pr_not_merged',
272
+ summary: summarizeSelection(`Attached PR #${pr.number} is not merged yet, so merged recovery cannot retire the rehydrated Merging claim.`)
273
+ };
274
+ }
275
+ let branchRecovery = null;
276
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
277
+ const preRecoveryOutcome = classifyPreBranchRecoverySnapshot(snapshot, pr.number, 'merge_closeout');
278
+ if (preRecoveryOutcome) {
279
+ return {
280
+ ...baseWithResolution,
281
+ pr,
282
+ snapshot,
283
+ ...preRecoveryOutcome,
284
+ summary: summarizeSelection(preRecoveryOutcome.summary)
285
+ };
286
+ }
287
+ branchRecovery = await attemptProviderBranchRecovery({
288
+ pr,
289
+ snapshot,
290
+ previousBranchRecovery: input.previousBranchRecovery ?? null,
291
+ repoRoot: input.repoRoot,
292
+ now,
293
+ runCommand
294
+ });
295
+ if (branchRecovery?.ok) {
296
+ try {
297
+ snapshot = await loadProviderSnapshotRecord({
298
+ owner: pr.owner,
299
+ repo: pr.repo,
300
+ prNumber: pr.number,
301
+ readinessMode: 'merge',
302
+ resolveSnapshot,
303
+ resolveSnapshotActionRequiredReasons
304
+ });
305
+ }
306
+ catch (error) {
307
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
308
+ if (githubRateLimit) {
309
+ return {
310
+ ...baseWithResolution,
311
+ pr,
312
+ snapshot,
313
+ branch_recovery: branchRecovery,
314
+ status: 'watching',
315
+ reason: 'github_rate_limited',
316
+ summary: summarizeSelection(`GitHub API budget blocked post-branch-recovery readiness verification: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`),
317
+ github_rate_limit: githubRateLimit
318
+ };
319
+ }
320
+ // Preserve the pre-recovery snapshot when verification cannot be reread.
321
+ }
322
+ alreadyMerged = snapshot.merged_at !== null || snapshot.state === 'MERGED';
323
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
324
+ const prePendingRecoveryOutcome = classifyPreBranchRecoverySnapshot(snapshot, pr.number, 'merge_closeout');
325
+ if (prePendingRecoveryOutcome) {
326
+ return {
327
+ ...baseWithResolution,
328
+ pr,
329
+ snapshot,
330
+ branch_recovery: branchRecovery,
331
+ ...prePendingRecoveryOutcome,
332
+ summary: summarizeSelection(prePendingRecoveryOutcome.summary)
333
+ };
334
+ }
335
+ const pendingRecovery = classifyPendingBranchRecovery({
336
+ snapshot,
337
+ recoveryAttempt: branchRecovery,
338
+ prNumber: pr.number,
339
+ mode: 'merge_closeout'
340
+ });
341
+ if (pendingRecovery) {
342
+ return {
343
+ ...baseWithResolution,
344
+ pr,
345
+ snapshot,
346
+ branch_recovery: branchRecovery,
347
+ ...pendingRecovery,
348
+ summary: summarizeSelection(pendingRecovery.summary)
349
+ };
350
+ }
351
+ const nonMergedOutcome = classifyNonMergedSnapshot(snapshot, pr.number);
352
+ return {
353
+ ...baseWithResolution,
354
+ pr,
355
+ snapshot,
356
+ branch_recovery: branchRecovery,
357
+ ...nonMergedOutcome,
358
+ summary: summarizeSelection(nonMergedOutcome.summary)
359
+ };
360
+ }
361
+ }
362
+ else if (branchRecovery?.failure_kind === 'conflict') {
363
+ const transitionAttemptedAt = now();
364
+ const transitionResult = await transitionIssueState({
365
+ issueId: input.issueId,
366
+ stateName: 'Rework',
367
+ expectedStateName: currentIssueState,
368
+ expectedStateType: currentIssueStateType,
369
+ expectedUpdatedAt: currentIssueUpdatedAt,
370
+ env,
371
+ sourceSetup: input.sourceSetup
372
+ });
373
+ const linearTransition = transitionResult.ok
374
+ ? {
375
+ status: transitionResult.action === 'noop' ? 'noop' : 'transitioned',
376
+ attempted_at: transitionAttemptedAt,
377
+ previous_state: transitionResult.previous_state?.name ?? currentIssueState ?? null,
378
+ target_state: transitionResult.target_state.name,
379
+ issue_state: transitionResult.issue.state?.name ?? null,
380
+ issue_state_type: transitionResult.issue.state?.type ?? null,
381
+ issue_updated_at: transitionResult.issue.updated_at ?? currentIssueUpdatedAt,
382
+ error: null
383
+ }
384
+ : {
385
+ status: 'failed',
386
+ attempted_at: transitionAttemptedAt,
387
+ previous_state: currentIssueState ?? null,
388
+ target_state: 'Rework',
389
+ issue_state: currentIssueState ?? null,
390
+ issue_state_type: currentIssueStateType ?? null,
391
+ issue_updated_at: currentIssueUpdatedAt,
392
+ error: `${transitionResult.error.code}: ${transitionResult.error.message}`
393
+ };
394
+ if (!transitionResult.ok) {
395
+ return {
396
+ ...baseWithResolution,
397
+ pr,
398
+ snapshot,
399
+ branch_recovery: branchRecovery,
400
+ linear_transition: linearTransition,
401
+ status: 'transition_failed',
402
+ reason: 'linear_rework_transition_failed_after_branch_recovery_conflict',
403
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} hit a merge conflict for attached PR #${pr.number}, and the Linear issue could not transition to Rework.`)
404
+ };
405
+ }
406
+ return {
407
+ ...baseWithResolution,
408
+ issue_state: linearTransition.issue_state,
409
+ issue_state_type: linearTransition.issue_state_type,
410
+ issue_updated_at: linearTransition.issue_updated_at,
411
+ pr,
412
+ snapshot,
413
+ branch_recovery: branchRecovery,
414
+ linear_transition: linearTransition,
415
+ status: 'action_required',
416
+ reason: 'branch_recovery_conflict',
417
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} hit a merge conflict for attached PR #${pr.number}; moved the issue to Rework with exact recovery metadata recorded.`)
418
+ };
419
+ }
420
+ else if (branchRecovery) {
421
+ return {
422
+ ...baseWithResolution,
423
+ pr,
424
+ snapshot,
425
+ branch_recovery: branchRecovery,
426
+ status: 'action_required',
427
+ reason: 'branch_recovery_failed',
428
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} failed for attached PR #${pr.number}; inspect the recorded gh output before merge closeout can continue.`)
429
+ };
430
+ }
431
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
432
+ const nonMergedOutcome = classifyNonMergedSnapshot(snapshot, pr.number);
433
+ return {
434
+ ...baseWithResolution,
435
+ pr,
436
+ snapshot,
437
+ ...nonMergedOutcome,
438
+ summary: summarizeSelection(nonMergedOutcome.summary)
439
+ };
440
+ }
441
+ }
442
+ const preMergeRateLimitOutcome = classifyProviderMutationRateLimitSnapshot(snapshot, pr.number, 'merge_closeout');
443
+ if (preMergeRateLimitOutcome) {
444
+ return {
445
+ ...baseWithResolution,
446
+ pr,
447
+ snapshot,
448
+ branch_recovery: branchRecovery,
449
+ ...preMergeRateLimitOutcome,
450
+ summary: summarizeSelection(preMergeRateLimitOutcome.summary)
451
+ };
452
+ }
453
+ let mergeAttempt = null;
454
+ let verificationSnapshot = snapshot;
455
+ if (!alreadyMerged) {
456
+ const mergeAttemptedAt = now();
457
+ const mergeArgs = buildPrMergeArgs({
458
+ owner: pr.owner,
459
+ repo: pr.repo,
460
+ prNumber: pr.number,
461
+ mergeMethod: PROVIDER_MERGE_CLOSEOUT_MERGE_METHOD,
462
+ deleteBranch: false,
463
+ headOid: snapshot.head_oid
464
+ });
465
+ const mergeResult = await runCommand({
466
+ command: 'gh',
467
+ args: mergeArgs,
468
+ cwd: input.repoRoot
469
+ });
470
+ mergeAttempt = {
471
+ attempted_at: mergeAttemptedAt,
472
+ command: 'gh',
473
+ args: mergeArgs,
474
+ exit_code: mergeResult.exitCode,
475
+ ok: mergeResult.ok,
476
+ stdout: normalizeCommandText(mergeResult.stdout),
477
+ stderr: normalizeCommandText(mergeResult.stderr)
478
+ };
479
+ let verificationRateLimit = null;
480
+ try {
481
+ const rawVerificationSnapshot = await resolveSnapshot({
482
+ owner: pr.owner,
483
+ repo: pr.repo,
484
+ prNumber: pr.number,
485
+ readinessMode: 'merge'
486
+ });
487
+ verificationSnapshot = mapSnapshotRecord(rawVerificationSnapshot, resolveSnapshotActionRequiredReasons(rawVerificationSnapshot, {
488
+ readinessMode: 'merge'
489
+ }));
490
+ }
491
+ catch (error) {
492
+ verificationRateLimit = resolveProviderGitHubRateLimitRecord(error);
493
+ // Preserve the pre-merge readiness snapshot when verification cannot be reread.
494
+ }
495
+ if (verificationRateLimit && mergeResult.ok === false) {
496
+ return {
497
+ ...baseWithResolution,
498
+ pr,
499
+ snapshot: verificationSnapshot,
500
+ branch_recovery: branchRecovery,
501
+ merge_attempt: mergeAttempt,
502
+ status: 'merge_failed',
503
+ reason: 'merge_command_failed',
504
+ summary: summarizeSelection(`GitHub merge command failed before the pull request was confirmed merged; post-merge verification was also blocked by GitHub API budget: ${formatProviderGitHubRateLimitSummary(verificationRateLimit)}.`),
505
+ github_rate_limit: verificationRateLimit
506
+ };
507
+ }
508
+ if (verificationRateLimit) {
509
+ return {
510
+ ...baseWithResolution,
511
+ pr,
512
+ snapshot: verificationSnapshot,
513
+ branch_recovery: branchRecovery,
514
+ merge_attempt: mergeAttempt,
515
+ status: 'watching',
516
+ reason: 'github_rate_limited',
517
+ summary: summarizeSelection(`GitHub API budget blocked post-merge verification snapshot loading: ${formatProviderGitHubRateLimitSummary(verificationRateLimit)}.`),
518
+ github_rate_limit: verificationRateLimit
519
+ };
520
+ }
521
+ if (verificationSnapshot.merged_at === null && verificationSnapshot.state !== 'MERGED') {
522
+ const verificationOutcome = classifyNonMergedSnapshot(verificationSnapshot, pr.number);
523
+ if (verificationOutcome) {
524
+ return {
525
+ ...baseWithResolution,
526
+ pr,
527
+ snapshot: verificationSnapshot,
528
+ branch_recovery: branchRecovery,
529
+ merge_attempt: mergeAttempt,
530
+ ...verificationOutcome,
531
+ summary: summarizeSelection(verificationOutcome.summary)
532
+ };
533
+ }
534
+ return {
535
+ ...baseWithResolution,
536
+ pr,
537
+ snapshot: verificationSnapshot,
538
+ branch_recovery: branchRecovery,
539
+ merge_attempt: mergeAttempt,
540
+ status: 'merge_failed',
541
+ reason: mergeResult.ok ? 'merge_not_confirmed' : 'merge_command_failed',
542
+ summary: summarizeSelection(mergeResult.ok
543
+ ? 'GitHub merge command exited successfully, but the pull request did not report a merged timestamp.'
544
+ : 'GitHub merge command failed before the pull request was confirmed merged.')
545
+ };
546
+ }
547
+ }
548
+ const sharedRoot = await reconcileSharedRootAfterMerge({
549
+ repoRoot: input.repoRoot,
550
+ now,
551
+ runCommand
552
+ });
553
+ if (sharedRoot.status === 'failed') {
554
+ return {
555
+ ...baseWithResolution,
556
+ pr,
557
+ snapshot: verificationSnapshot,
558
+ branch_recovery: branchRecovery,
559
+ merge_attempt: mergeAttempt,
560
+ shared_root: sharedRoot,
561
+ status: 'merge_failed',
562
+ reason: 'shared_root_reconciliation_failed',
563
+ summary: summarizeSelection('The pull request merged, but shared-root reconciliation failed.')
564
+ };
565
+ }
566
+ if (sharedRoot.status === 'skipped') {
567
+ return {
568
+ ...baseWithResolution,
569
+ pr,
570
+ snapshot: verificationSnapshot,
571
+ branch_recovery: branchRecovery,
572
+ merge_attempt: mergeAttempt,
573
+ shared_root: sharedRoot,
574
+ status: 'action_required',
575
+ reason: 'pending_shared_root_reconciliation',
576
+ summary: summarizeSelection(alreadyMerged
577
+ ? `Attached PR #${pr.number} was already merged; shared-root reconciliation is pending (${sharedRoot.reason}) before the Linear issue can transition to Done.`
578
+ : `Merged attached PR #${pr.number}; shared-root reconciliation is pending (${sharedRoot.reason}) before the Linear issue can transition to Done.`)
579
+ };
580
+ }
581
+ const transitionAttemptedAt = now();
582
+ const transitionResult = await transitionIssueState({
583
+ issueId: input.issueId,
584
+ stateName: 'Done',
585
+ expectedStateName: currentIssueState,
586
+ expectedStateType: currentIssueStateType,
587
+ expectedUpdatedAt: currentIssueUpdatedAt,
588
+ env,
589
+ sourceSetup: input.sourceSetup
590
+ });
591
+ const linearTransition = transitionResult.ok
592
+ ? {
593
+ status: transitionResult.action === 'noop' ? 'noop' : 'transitioned',
594
+ attempted_at: transitionAttemptedAt,
595
+ previous_state: transitionResult.previous_state?.name ?? currentIssueState ?? null,
596
+ target_state: transitionResult.target_state.name,
597
+ issue_state: transitionResult.issue.state?.name ?? null,
598
+ issue_state_type: transitionResult.issue.state?.type ?? null,
599
+ issue_updated_at: transitionResult.issue.updated_at ?? currentIssueUpdatedAt,
600
+ error: null
601
+ }
602
+ : {
603
+ status: 'failed',
604
+ attempted_at: transitionAttemptedAt,
605
+ previous_state: currentIssueState ?? null,
606
+ target_state: 'Done',
607
+ issue_state: currentIssueState ?? null,
608
+ issue_state_type: currentIssueStateType ?? null,
609
+ issue_updated_at: currentIssueUpdatedAt,
610
+ error: `${transitionResult.error.code}: ${transitionResult.error.message}`
611
+ };
612
+ if (!transitionResult.ok) {
613
+ if (alreadyMerged && transitionResult.error.code === 'linear_rate_limited') {
614
+ return {
615
+ ...baseWithResolution,
616
+ pr,
617
+ snapshot: verificationSnapshot,
618
+ branch_recovery: branchRecovery,
619
+ merge_attempt: mergeAttempt,
620
+ shared_root: sharedRoot,
621
+ linear_transition: linearTransition,
622
+ status: 'merged',
623
+ reason: 'merged_and_shared_root_reconciled_transition_deferred',
624
+ summary: summarizeSelection(`Attached PR #${pr.number} was already merged and the shared root is reconciled; local merge closeout is authoritative while the Linear Done transition is deferred by shared-budget cooldown.`)
625
+ };
626
+ }
627
+ return {
628
+ ...baseWithResolution,
629
+ pr,
630
+ snapshot: verificationSnapshot,
631
+ branch_recovery: branchRecovery,
632
+ merge_attempt: mergeAttempt,
633
+ shared_root: sharedRoot,
634
+ linear_transition: linearTransition,
635
+ status: 'transition_failed',
636
+ reason: 'linear_done_transition_failed',
637
+ summary: summarizeSelection('The pull request merged, but the Linear issue could not transition to Done.')
638
+ };
639
+ }
640
+ return {
641
+ ...baseWithResolution,
642
+ issue_state: linearTransition.issue_state,
643
+ issue_state_type: linearTransition.issue_state_type,
644
+ issue_updated_at: linearTransition.issue_updated_at,
645
+ pr,
646
+ snapshot: verificationSnapshot,
647
+ branch_recovery: branchRecovery,
648
+ merge_attempt: mergeAttempt,
649
+ shared_root: sharedRoot,
650
+ linear_transition: linearTransition,
651
+ status: 'merged',
652
+ reason: alreadyMerged
653
+ ? 'merged_and_transitioned_done_after_recovery'
654
+ : 'merged_and_transitioned_done',
655
+ summary: summarizeSelection(alreadyMerged
656
+ ? `Attached PR #${pr.number} was already merged; reconciled shared root and transitioned the Linear issue to Done.`
657
+ : `Merged attached PR #${pr.number}, reconciled shared root, and transitioned the Linear issue to Done.`)
658
+ };
659
+ }
660
+ export async function runProviderReviewHandoffPromotion(input, deps = {}) {
661
+ const env = input.env ?? process.env;
662
+ const now = deps.now ?? isoTimestamp;
663
+ const readIssueContext = deps.readIssueContext ?? getProviderLinearIssueContext;
664
+ const transitionIssueState = deps.transitionIssueState ?? transitionProviderLinearIssueState;
665
+ const runCommand = deps.runCommand ?? runProviderMergeCloseoutCommand;
666
+ const resolveSnapshot = deps.fetchSnapshot ?? fetchPrStatusSnapshot;
667
+ const resolveSnapshotActionRequiredReasons = deps.resolveSnapshotActionRequiredReasons ??
668
+ ((snapshot, options) => resolveActionRequiredReasons(snapshot, options));
669
+ const recordedAt = now();
670
+ const base = {
671
+ recorded_at: recordedAt,
672
+ issue_id: input.issueId,
673
+ issue_identifier: normalizeOptionalString(input.issueIdentifier),
674
+ issue_state: normalizeOptionalString(input.issueState),
675
+ issue_state_type: normalizeOptionalString(input.issueStateType),
676
+ issue_updated_at: normalizeOptionalString(input.issueUpdatedAt),
677
+ attached_pr_urls: [],
678
+ ignored_historical_pr_urls: [],
679
+ ignored_closed_unmerged_pr_urls: [],
680
+ ignored_cross_issue_pr_urls: [],
681
+ conflicting_attached_pr_urls: [],
682
+ pr: null,
683
+ snapshot: null,
684
+ branch_recovery: null,
685
+ linear_transition: null,
686
+ github_rate_limit: null
687
+ };
688
+ const repoOriginResult = await runCommand({
689
+ command: 'git',
690
+ args: ['-C', input.repoRoot, 'remote', 'get-url', 'origin'],
691
+ cwd: input.repoRoot
692
+ });
693
+ if (!repoOriginResult.ok) {
694
+ return {
695
+ ...base,
696
+ status: 'promotion_failed',
697
+ reason: 'shared_root_origin_unavailable',
698
+ summary: 'Shared repo origin remote could not be resolved.'
699
+ };
700
+ }
701
+ const parsedRepo = parseGitHubRepoFromRemoteUrl(repoOriginResult.stdout);
702
+ const repoKey = parsedRepo
703
+ ? `${parsedRepo.owner.toLowerCase()}/${parsedRepo.repo.toLowerCase()}`
704
+ : null;
705
+ if (!repoKey) {
706
+ return {
707
+ ...base,
708
+ status: 'promotion_failed',
709
+ reason: 'shared_root_repo_unrecognized',
710
+ summary: 'Shared repo origin is not a GitHub repository URL.'
711
+ };
712
+ }
713
+ const issueContext = await readIssueContext({
714
+ issueId: input.issueId,
715
+ env,
716
+ sourceSetup: input.sourceSetup,
717
+ fallbackToCacheOnFailure: false
718
+ });
719
+ if (!issueContext.ok) {
720
+ return {
721
+ ...base,
722
+ status: 'promotion_failed',
723
+ reason: 'linear_issue_context_failed',
724
+ summary: `Linear issue context could not be loaded (${issueContext.error.code}).`
725
+ };
726
+ }
727
+ const attachedPrCandidates = collectAttachedGitHubPrCandidates(issueContext.issue.attachments);
728
+ const attachedPrUrls = attachedPrCandidates.map((candidate) => candidate.pr.url);
729
+ const sameRepoPrs = attachedPrCandidates.filter((candidate) => `${candidate.pr.owner.toLowerCase()}/${candidate.pr.repo.toLowerCase()}` === repoKey);
730
+ const currentIssueState = issueContext.issue.state?.name ?? base.issue_state;
731
+ const currentIssueStateType = issueContext.issue.state?.type ?? base.issue_state_type;
732
+ const currentIssueUpdatedAt = issueContext.issue.updated_at ?? base.issue_updated_at;
733
+ const currentWorkflowState = classifyProviderLinearWorkflowState({
734
+ state: currentIssueState,
735
+ state_type: currentIssueStateType
736
+ });
737
+ const baseWithContext = {
738
+ ...base,
739
+ issue_state: currentIssueState,
740
+ issue_state_type: currentIssueStateType,
741
+ issue_updated_at: currentIssueUpdatedAt,
742
+ attached_pr_urls: [...attachedPrUrls]
743
+ };
744
+ if (!currentWorkflowState.isHandoff) {
745
+ return {
746
+ ...baseWithContext,
747
+ status: 'action_required',
748
+ reason: 'issue_no_longer_review_handoff',
749
+ summary: currentIssueState && currentIssueState.trim().length > 0
750
+ ? `Live Linear issue state is ${currentIssueState}, so review-handoff promotion is not armed.`
751
+ : 'Live Linear issue state is no longer a review handoff state, so review-handoff promotion is not armed.'
752
+ };
753
+ }
754
+ if (sameRepoPrs.length === 0) {
755
+ return {
756
+ ...baseWithContext,
757
+ status: 'action_required',
758
+ reason: attachedPrUrls.length === 0 ? 'no_attached_pr' : 'attached_pr_repo_mismatch',
759
+ summary: attachedPrUrls.length === 0
760
+ ? 'No attached GitHub pull request is present for this review handoff issue.'
761
+ : 'Attached GitHub pull requests do not match the shared repository root.'
762
+ };
763
+ }
764
+ let ignoredHistoricalPrUrls = [];
765
+ let ignoredClosedUnmergedPrUrls = [];
766
+ let ignoredCrossIssuePrUrls = [];
767
+ let conflictingAttachedPrUrls = [];
768
+ let selectionNote = null;
769
+ let pr = sameRepoPrs[0].pr;
770
+ let snapshot = null;
771
+ if (sameRepoPrs.length > 1) {
772
+ let resolution;
773
+ try {
774
+ resolution = await resolveAttachedSameRepoPullRequestCandidate({
775
+ candidates: sameRepoPrs,
776
+ mode: 'review_promotion',
777
+ issueIdentifier: issueContext.issue.identifier,
778
+ blockedBy: input.blockedBy ?? null,
779
+ resolveSnapshot,
780
+ resolveSnapshotActionRequiredReasons
781
+ });
782
+ }
783
+ catch (error) {
784
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
785
+ if (githubRateLimit) {
786
+ return {
787
+ ...baseWithContext,
788
+ status: 'watching',
789
+ reason: 'github_rate_limited',
790
+ summary: `GitHub API budget blocked attached pull-request disambiguation during review-handoff promotion: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`,
791
+ github_rate_limit: githubRateLimit
792
+ };
793
+ }
794
+ return {
795
+ ...baseWithContext,
796
+ status: 'promotion_failed',
797
+ reason: 'snapshot_read_failed',
798
+ summary: `GitHub merge-readiness snapshot could not be loaded while disambiguating attached pull requests: ${error?.message ?? String(error)}.`
799
+ };
800
+ }
801
+ ignoredHistoricalPrUrls = resolution.ignored_historical_pr_urls;
802
+ ignoredClosedUnmergedPrUrls = resolution.ignored_closed_unmerged_pr_urls;
803
+ ignoredCrossIssuePrUrls = resolution.ignored_cross_issue_pr_urls;
804
+ conflictingAttachedPrUrls = resolution.conflicting_attached_pr_urls;
805
+ selectionNote = resolution.selection_note;
806
+ if (!resolution.selected_pr) {
807
+ return {
808
+ ...baseWithContext,
809
+ ignored_historical_pr_urls: [...ignoredHistoricalPrUrls],
810
+ ignored_closed_unmerged_pr_urls: [...ignoredClosedUnmergedPrUrls],
811
+ ignored_cross_issue_pr_urls: [...ignoredCrossIssuePrUrls],
812
+ conflicting_attached_pr_urls: [...conflictingAttachedPrUrls],
813
+ status: 'action_required',
814
+ reason: 'multiple_attached_prs',
815
+ summary: buildMultipleAttachedPrsPromotionSummary({
816
+ repoKey,
817
+ ignoredHistoricalPrUrls,
818
+ ignoredClosedUnmergedPrUrls,
819
+ ignoredCrossIssuePrUrls,
820
+ conflictingAttachedPrUrls
821
+ })
822
+ };
823
+ }
824
+ pr = resolution.selected_pr;
825
+ snapshot = resolution.selected_snapshot;
826
+ }
827
+ const baseWithResolution = {
828
+ ...baseWithContext,
829
+ ignored_historical_pr_urls: [...ignoredHistoricalPrUrls],
830
+ ignored_closed_unmerged_pr_urls: [...ignoredClosedUnmergedPrUrls],
831
+ ignored_cross_issue_pr_urls: [...ignoredCrossIssuePrUrls],
832
+ conflicting_attached_pr_urls: [...conflictingAttachedPrUrls]
833
+ };
834
+ const summarizeSelection = (summary) => selectionNote ? `${summary} ${selectionNote}` : summary;
835
+ if (!snapshot) {
836
+ try {
837
+ snapshot = await loadProviderSnapshotRecord({
838
+ owner: pr.owner,
839
+ repo: pr.repo,
840
+ prNumber: pr.number,
841
+ readinessMode: 'merge',
842
+ resolveSnapshot,
843
+ resolveSnapshotActionRequiredReasons
844
+ });
845
+ }
846
+ catch (error) {
847
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
848
+ if (githubRateLimit) {
849
+ return {
850
+ ...baseWithResolution,
851
+ pr,
852
+ status: 'watching',
853
+ reason: 'github_rate_limited',
854
+ summary: summarizeSelection(`GitHub API budget blocked review-handoff readiness snapshot loading: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`),
855
+ snapshot: null,
856
+ github_rate_limit: githubRateLimit
857
+ };
858
+ }
859
+ return {
860
+ ...baseWithResolution,
861
+ pr,
862
+ status: 'promotion_failed',
863
+ reason: 'snapshot_read_failed',
864
+ summary: summarizeSelection(`GitHub merge-readiness snapshot could not be loaded: ${error?.message ?? String(error)}`),
865
+ snapshot: null
866
+ };
867
+ }
868
+ }
869
+ if (!snapshot) {
870
+ return {
871
+ ...baseWithResolution,
872
+ pr,
873
+ status: 'promotion_failed',
874
+ reason: 'snapshot_read_failed',
875
+ summary: summarizeSelection('GitHub merge-readiness snapshot could not be loaded.'),
876
+ snapshot: null
877
+ };
878
+ }
879
+ let alreadyMerged = isMergedPullRequestSnapshot(snapshot);
880
+ let branchRecovery = null;
881
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
882
+ const preRecoveryOutcome = classifyPreBranchRecoverySnapshot(snapshot, pr.number, 'review_promotion');
883
+ if (preRecoveryOutcome) {
884
+ return {
885
+ ...baseWithResolution,
886
+ pr,
887
+ snapshot,
888
+ ...preRecoveryOutcome,
889
+ summary: summarizeSelection(preRecoveryOutcome.summary)
890
+ };
891
+ }
892
+ branchRecovery = await attemptProviderBranchRecovery({
893
+ pr,
894
+ snapshot,
895
+ previousBranchRecovery: input.previousBranchRecovery ?? null,
896
+ repoRoot: input.repoRoot,
897
+ now,
898
+ runCommand
899
+ });
900
+ if (branchRecovery?.ok) {
901
+ try {
902
+ snapshot = await loadProviderSnapshotRecord({
903
+ owner: pr.owner,
904
+ repo: pr.repo,
905
+ prNumber: pr.number,
906
+ readinessMode: 'merge',
907
+ resolveSnapshot,
908
+ resolveSnapshotActionRequiredReasons
909
+ });
910
+ }
911
+ catch (error) {
912
+ const githubRateLimit = resolveProviderGitHubRateLimitRecord(error);
913
+ if (githubRateLimit) {
914
+ return {
915
+ ...baseWithResolution,
916
+ pr,
917
+ snapshot,
918
+ branch_recovery: branchRecovery,
919
+ status: 'watching',
920
+ reason: 'github_rate_limited',
921
+ summary: summarizeSelection(`GitHub API budget blocked post-branch-recovery review-handoff verification: ${formatProviderGitHubRateLimitSummary(githubRateLimit)}.`),
922
+ github_rate_limit: githubRateLimit
923
+ };
924
+ }
925
+ // Preserve the pre-recovery snapshot when verification cannot be reread.
926
+ }
927
+ alreadyMerged = isMergedPullRequestSnapshot(snapshot);
928
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
929
+ const prePendingRecoveryOutcome = classifyPreBranchRecoverySnapshot(snapshot, pr.number, 'review_promotion');
930
+ if (prePendingRecoveryOutcome) {
931
+ return {
932
+ ...baseWithResolution,
933
+ pr,
934
+ snapshot,
935
+ branch_recovery: branchRecovery,
936
+ ...prePendingRecoveryOutcome,
937
+ summary: summarizeSelection(prePendingRecoveryOutcome.summary)
938
+ };
939
+ }
940
+ const pendingRecovery = classifyPendingBranchRecovery({
941
+ snapshot,
942
+ recoveryAttempt: branchRecovery,
943
+ prNumber: pr.number,
944
+ mode: 'review_promotion'
945
+ });
946
+ if (pendingRecovery) {
947
+ return {
948
+ ...baseWithResolution,
949
+ pr,
950
+ snapshot,
951
+ branch_recovery: branchRecovery,
952
+ ...pendingRecovery,
953
+ summary: summarizeSelection(pendingRecovery.summary)
954
+ };
955
+ }
956
+ const promotionOutcome = classifyNonMergedReviewPromotionSnapshot(snapshot, pr.number);
957
+ return {
958
+ ...baseWithResolution,
959
+ pr,
960
+ snapshot,
961
+ branch_recovery: branchRecovery,
962
+ ...promotionOutcome,
963
+ summary: summarizeSelection(promotionOutcome.summary)
964
+ };
965
+ }
966
+ }
967
+ else if (branchRecovery?.failure_kind === 'conflict') {
968
+ const transitionAttemptedAt = now();
969
+ const transitionResult = await transitionIssueState({
970
+ issueId: input.issueId,
971
+ stateName: 'Rework',
972
+ expectedStateName: currentIssueState,
973
+ expectedStateType: currentIssueStateType,
974
+ expectedUpdatedAt: currentIssueUpdatedAt,
975
+ env,
976
+ sourceSetup: input.sourceSetup
977
+ });
978
+ const linearTransition = transitionResult.ok
979
+ ? {
980
+ status: transitionResult.action === 'noop' ? 'noop' : 'transitioned',
981
+ attempted_at: transitionAttemptedAt,
982
+ previous_state: transitionResult.previous_state?.name ?? currentIssueState ?? null,
983
+ target_state: transitionResult.target_state.name,
984
+ issue_state: transitionResult.issue.state?.name ?? null,
985
+ issue_state_type: transitionResult.issue.state?.type ?? null,
986
+ issue_updated_at: transitionResult.issue.updated_at ?? currentIssueUpdatedAt,
987
+ error: null
988
+ }
989
+ : {
990
+ status: 'failed',
991
+ attempted_at: transitionAttemptedAt,
992
+ previous_state: currentIssueState ?? null,
993
+ target_state: 'Rework',
994
+ issue_state: currentIssueState ?? null,
995
+ issue_state_type: currentIssueStateType ?? null,
996
+ issue_updated_at: currentIssueUpdatedAt,
997
+ error: `${transitionResult.error.code}: ${transitionResult.error.message}`
998
+ };
999
+ if (!transitionResult.ok) {
1000
+ return {
1001
+ ...baseWithResolution,
1002
+ pr,
1003
+ snapshot,
1004
+ branch_recovery: branchRecovery,
1005
+ linear_transition: linearTransition,
1006
+ status: 'transition_failed',
1007
+ reason: 'linear_rework_transition_failed_after_branch_recovery_conflict',
1008
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} hit a merge conflict for attached PR #${pr.number}, and the Linear issue could not transition to Rework before review handoff could continue.`)
1009
+ };
1010
+ }
1011
+ return {
1012
+ ...baseWithResolution,
1013
+ issue_state: linearTransition.issue_state,
1014
+ issue_state_type: linearTransition.issue_state_type,
1015
+ issue_updated_at: linearTransition.issue_updated_at,
1016
+ pr,
1017
+ snapshot,
1018
+ branch_recovery: branchRecovery,
1019
+ linear_transition: linearTransition,
1020
+ status: 'action_required',
1021
+ reason: 'branch_recovery_conflict',
1022
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} hit a merge conflict for attached PR #${pr.number}; moved the issue to Rework with exact recovery metadata recorded.`)
1023
+ };
1024
+ }
1025
+ else if (branchRecovery) {
1026
+ return {
1027
+ ...baseWithResolution,
1028
+ pr,
1029
+ snapshot,
1030
+ branch_recovery: branchRecovery,
1031
+ status: 'action_required',
1032
+ reason: 'branch_recovery_failed',
1033
+ summary: summarizeSelection(`Automatic ${describeProviderBranchRecoveryReason(branchRecovery.recovery_reason)} failed for attached PR #${pr.number}; inspect the recorded gh output before review-handoff promotion can continue.`)
1034
+ };
1035
+ }
1036
+ if (!alreadyMerged && !snapshot.ready_to_merge) {
1037
+ const promotionOutcome = classifyNonMergedReviewPromotionSnapshot(snapshot, pr.number);
1038
+ return {
1039
+ ...baseWithResolution,
1040
+ pr,
1041
+ snapshot,
1042
+ ...promotionOutcome,
1043
+ summary: summarizeSelection(promotionOutcome.summary)
1044
+ };
1045
+ }
1046
+ }
1047
+ const prePromotionRateLimitOutcome = classifyProviderMutationRateLimitSnapshot(snapshot, pr.number, 'review_promotion');
1048
+ if (prePromotionRateLimitOutcome) {
1049
+ return {
1050
+ ...baseWithResolution,
1051
+ pr,
1052
+ snapshot,
1053
+ branch_recovery: branchRecovery,
1054
+ ...prePromotionRateLimitOutcome,
1055
+ summary: summarizeSelection(prePromotionRateLimitOutcome.summary)
1056
+ };
1057
+ }
1058
+ const transitionAttemptedAt = now();
1059
+ const transitionResult = await transitionIssueState({
1060
+ issueId: input.issueId,
1061
+ stateName: 'Merging',
1062
+ expectedStateName: currentIssueState,
1063
+ expectedStateType: currentIssueStateType,
1064
+ expectedUpdatedAt: currentIssueUpdatedAt,
1065
+ env,
1066
+ sourceSetup: input.sourceSetup
1067
+ });
1068
+ const linearTransition = transitionResult.ok
1069
+ ? {
1070
+ status: transitionResult.action === 'noop' ? 'noop' : 'transitioned',
1071
+ attempted_at: transitionAttemptedAt,
1072
+ previous_state: transitionResult.previous_state?.name ?? currentIssueState ?? null,
1073
+ target_state: transitionResult.target_state.name,
1074
+ issue_state: transitionResult.issue.state?.name ?? null,
1075
+ issue_state_type: transitionResult.issue.state?.type ?? null,
1076
+ issue_updated_at: transitionResult.issue.updated_at ?? currentIssueUpdatedAt,
1077
+ error: null
1078
+ }
1079
+ : {
1080
+ status: 'failed',
1081
+ attempted_at: transitionAttemptedAt,
1082
+ previous_state: currentIssueState ?? null,
1083
+ target_state: 'Merging',
1084
+ issue_state: currentIssueState ?? null,
1085
+ issue_state_type: currentIssueStateType ?? null,
1086
+ issue_updated_at: currentIssueUpdatedAt,
1087
+ error: `${transitionResult.error.code}: ${transitionResult.error.message}`
1088
+ };
1089
+ if (!transitionResult.ok) {
1090
+ return {
1091
+ ...baseWithResolution,
1092
+ pr,
1093
+ snapshot,
1094
+ branch_recovery: branchRecovery,
1095
+ linear_transition: linearTransition,
1096
+ status: 'transition_failed',
1097
+ reason: 'linear_merging_transition_failed',
1098
+ summary: summarizeSelection(alreadyMerged
1099
+ ? `Attached PR #${pr.number} is already merged, but the Linear issue could not transition to Merging for deterministic closeout.`
1100
+ : `Attached PR #${pr.number} is merge-ready, but the Linear issue could not transition to Merging.`)
1101
+ };
1102
+ }
1103
+ return {
1104
+ ...baseWithResolution,
1105
+ issue_state: linearTransition.issue_state,
1106
+ issue_state_type: linearTransition.issue_state_type,
1107
+ issue_updated_at: linearTransition.issue_updated_at,
1108
+ pr,
1109
+ snapshot,
1110
+ branch_recovery: branchRecovery,
1111
+ linear_transition: linearTransition,
1112
+ status: 'promoted',
1113
+ reason: 'promoted_to_merging',
1114
+ summary: summarizeSelection(alreadyMerged
1115
+ ? `Attached PR #${pr.number} is already merged; promoted the issue from review handoff into Merging for deterministic closeout.`
1116
+ : `Promoted attached PR #${pr.number} from review handoff into Merging.`)
1117
+ };
1118
+ }
1119
+ async function reconcileSharedRootAfterMerge(input) {
1120
+ const attemptedAt = input.now();
1121
+ const beforeStatusResult = await input.runCommand({
1122
+ command: 'git',
1123
+ args: ['-C', input.repoRoot, 'status', '--short', '--branch'],
1124
+ cwd: input.repoRoot
1125
+ });
1126
+ const beforeStatus = normalizeCommandText(beforeStatusResult.stdout);
1127
+ if (!beforeStatusResult.ok) {
1128
+ return {
1129
+ status: 'failed',
1130
+ attempted_at: attemptedAt,
1131
+ before_status: beforeStatus,
1132
+ after_status: null,
1133
+ reason: 'git_status_failed'
1134
+ };
1135
+ }
1136
+ const safety = assessSharedRootMergeSafety(beforeStatus);
1137
+ if (!safety.safe) {
1138
+ return {
1139
+ status: 'skipped',
1140
+ attempted_at: attemptedAt,
1141
+ before_status: beforeStatus,
1142
+ after_status: beforeStatus,
1143
+ reason: safety.reason
1144
+ };
1145
+ }
1146
+ const fetchResult = await input.runCommand({
1147
+ command: 'git',
1148
+ args: ['-C', input.repoRoot, 'fetch', 'origin', 'refs/heads/main:refs/remotes/origin/main'],
1149
+ cwd: input.repoRoot
1150
+ });
1151
+ if (!fetchResult.ok) {
1152
+ return {
1153
+ status: 'failed',
1154
+ attempted_at: attemptedAt,
1155
+ before_status: beforeStatus,
1156
+ after_status: null,
1157
+ reason: 'shared_root_fetch_failed'
1158
+ };
1159
+ }
1160
+ const mergeResult = await input.runCommand({
1161
+ command: 'git',
1162
+ args: ['-C', input.repoRoot, 'merge', '--ff-only', 'origin/main'],
1163
+ cwd: input.repoRoot
1164
+ });
1165
+ if (!mergeResult.ok) {
1166
+ return {
1167
+ status: 'failed',
1168
+ attempted_at: attemptedAt,
1169
+ before_status: beforeStatus,
1170
+ after_status: null,
1171
+ reason: 'shared_root_fast_forward_failed'
1172
+ };
1173
+ }
1174
+ const afterStatusResult = await input.runCommand({
1175
+ command: 'git',
1176
+ args: ['-C', input.repoRoot, 'status', '--short', '--branch'],
1177
+ cwd: input.repoRoot
1178
+ });
1179
+ return {
1180
+ status: afterStatusResult.ok ? 'reconciled' : 'failed',
1181
+ attempted_at: attemptedAt,
1182
+ before_status: beforeStatus,
1183
+ after_status: normalizeCommandText(afterStatusResult.stdout),
1184
+ reason: afterStatusResult.ok ? 'shared_root_reconciled' : 'git_status_after_failed'
1185
+ };
1186
+ }
1187
+ async function runProviderMergeCloseoutCommand(input) {
1188
+ try {
1189
+ const { stdout, stderr } = await execFileAsync(input.command, input.args, {
1190
+ cwd: input.cwd,
1191
+ timeout: PROVIDER_MERGE_CLOSEOUT_COMMAND_TIMEOUT_MS
1192
+ });
1193
+ return {
1194
+ ok: true,
1195
+ exitCode: 0,
1196
+ stdout,
1197
+ stderr
1198
+ };
1199
+ }
1200
+ catch (error) {
1201
+ const execError = error;
1202
+ const timedOut = execError.killed === true && execError.signal === 'SIGTERM';
1203
+ return {
1204
+ ok: false,
1205
+ exitCode: typeof execError.code === 'number' && Number.isInteger(execError.code)
1206
+ ? execError.code
1207
+ : null,
1208
+ stdout: typeof execError.stdout === 'string' ? execError.stdout : '',
1209
+ stderr: typeof execError.stderr === 'string' && execError.stderr.length > 0
1210
+ ? execError.stderr
1211
+ : timedOut
1212
+ ? `command timed out after ${PROVIDER_MERGE_CLOSEOUT_COMMAND_TIMEOUT_MS}ms`
1213
+ : (execError.message ?? '')
1214
+ };
1215
+ }
1216
+ }
1217
+ function collectAttachedGitHubPrCandidates(attachments) {
1218
+ const candidatesByUrl = new Map();
1219
+ for (const attachment of attachments) {
1220
+ const parsed = parseGitHubPullRequestUrl(attachment?.url);
1221
+ const comparisonKey = parsed
1222
+ ? `${parsed.owner.toLowerCase()}/${parsed.repo.toLowerCase()}#${parsed.number}`
1223
+ : null;
1224
+ if (!parsed || !comparisonKey) {
1225
+ continue;
1226
+ }
1227
+ const title = normalizeOptionalString(attachment?.title);
1228
+ const existingCandidate = candidatesByUrl.get(comparisonKey);
1229
+ if (existingCandidate) {
1230
+ existingCandidate.attachment_title = title;
1231
+ continue;
1232
+ }
1233
+ candidatesByUrl.set(comparisonKey, {
1234
+ pr: parsed,
1235
+ attachment_title: title
1236
+ });
1237
+ }
1238
+ return [...candidatesByUrl.values()];
1239
+ }
1240
+ async function resolveAttachedSameRepoPullRequestCandidate(input) {
1241
+ const mode = input.mode ?? 'merge_closeout';
1242
+ const ignoredCrossIssueCandidates = mode === 'review_promotion'
1243
+ ? input.candidates.filter((candidate) => isReviewPromotionCrossIssueCandidate({
1244
+ attachmentTitle: candidate.attachment_title,
1245
+ issueIdentifier: input.issueIdentifier ?? null,
1246
+ blockedBy: input.blockedBy ?? null
1247
+ }))
1248
+ : [];
1249
+ const ignoredCrossIssuePrUrls = ignoredCrossIssueCandidates.map((candidate) => candidate.pr.url);
1250
+ const ignoredCrossIssueUrlSet = new Set(ignoredCrossIssuePrUrls);
1251
+ const inspectedCandidates = [];
1252
+ for (const candidate of input.candidates) {
1253
+ if (ignoredCrossIssueUrlSet.has(candidate.pr.url)) {
1254
+ continue;
1255
+ }
1256
+ const rawSnapshot = await input.resolveSnapshot({
1257
+ owner: candidate.pr.owner,
1258
+ repo: candidate.pr.repo,
1259
+ prNumber: candidate.pr.number,
1260
+ readinessMode: 'merge'
1261
+ });
1262
+ const snapshot = mapSnapshotRecord(rawSnapshot, input.resolveSnapshotActionRequiredReasons(rawSnapshot, {
1263
+ readinessMode: 'merge'
1264
+ }));
1265
+ if (snapshot.github_rate_limit && !isTerminalPullRequestSnapshot(snapshot)) {
1266
+ throw buildProviderGitHubRateLimitError(snapshot.github_rate_limit);
1267
+ }
1268
+ inspectedCandidates.push({
1269
+ pr: candidate.pr,
1270
+ snapshot,
1271
+ attachment_title: candidate.attachment_title
1272
+ });
1273
+ }
1274
+ const openUnmergedCandidates = inspectedCandidates.filter((candidate) => !isMergedPullRequestSnapshot(candidate.snapshot) && candidate.snapshot.state !== 'CLOSED');
1275
+ const ignoredClosedUnmergedCandidates = mode === 'merge_closeout' && openUnmergedCandidates.length === 1
1276
+ ? inspectedCandidates.filter((candidate) => isClosedUnmergedPullRequestSnapshot(candidate.snapshot))
1277
+ : inspectedCandidates.filter((candidate) => isClosedUnmergedPullRequestSnapshot(candidate.snapshot) &&
1278
+ openUnmergedCandidates.some((openCandidate) => isSnapshotStrictlyOlderThanSelection(candidate.snapshot, openCandidate.snapshot)));
1279
+ const ignoredClosedUnmergedPrUrls = ignoredClosedUnmergedCandidates.map((candidate) => candidate.pr.url);
1280
+ const ignoredClosedUnmergedUrlSet = new Set(ignoredClosedUnmergedPrUrls);
1281
+ const resolutionCandidates = inspectedCandidates.filter((candidate) => !ignoredClosedUnmergedUrlSet.has(candidate.pr.url));
1282
+ if (resolutionCandidates.length === 1) {
1283
+ const selectedCandidate = resolutionCandidates[0];
1284
+ return {
1285
+ selected_pr: selectedCandidate.pr,
1286
+ selected_snapshot: selectedCandidate.snapshot,
1287
+ ignored_historical_pr_urls: [],
1288
+ ignored_closed_unmerged_pr_urls: ignoredClosedUnmergedPrUrls,
1289
+ ignored_cross_issue_pr_urls: ignoredCrossIssuePrUrls,
1290
+ conflicting_attached_pr_urls: [],
1291
+ selection_note: buildIgnoredAttachedPrSelectionNote({
1292
+ ignoredClosedUnmergedPrUrls,
1293
+ ignoredCrossIssuePrUrls
1294
+ })
1295
+ };
1296
+ }
1297
+ const mergedCandidates = resolutionCandidates.filter((candidate) => isMergedPullRequestSnapshot(candidate.snapshot));
1298
+ const nonMergedCandidates = resolutionCandidates.filter((candidate) => !isMergedPullRequestSnapshot(candidate.snapshot));
1299
+ if (mergedCandidates.length > 0 && nonMergedCandidates.length === 1) {
1300
+ const selectedCandidate = nonMergedCandidates[0];
1301
+ const ignoredHistoricalCandidates = mergedCandidates.filter((candidate) => isSnapshotStrictlyOlderThanSelection(candidate.snapshot, selectedCandidate.snapshot));
1302
+ if (ignoredHistoricalCandidates.length === mergedCandidates.length) {
1303
+ const ignoredHistoricalPrUrls = ignoredHistoricalCandidates.map((candidate) => candidate.pr.url);
1304
+ return {
1305
+ selected_pr: selectedCandidate.pr,
1306
+ selected_snapshot: selectedCandidate.snapshot,
1307
+ ignored_historical_pr_urls: ignoredHistoricalPrUrls,
1308
+ ignored_closed_unmerged_pr_urls: ignoredClosedUnmergedPrUrls,
1309
+ ignored_cross_issue_pr_urls: ignoredCrossIssuePrUrls,
1310
+ conflicting_attached_pr_urls: [],
1311
+ selection_note: appendIgnoredAttachedPrSelectionNote(ignoredHistoricalPrUrls.length > 0
1312
+ ? `Ignored historical merged PR URLs: ${ignoredHistoricalPrUrls.join(', ')}.`
1313
+ : null, {
1314
+ ignoredClosedUnmergedPrUrls,
1315
+ ignoredCrossIssuePrUrls
1316
+ })
1317
+ };
1318
+ }
1319
+ }
1320
+ const selectedMergedCandidate = mergedCandidates.reduce((currentNewest, candidate) => !currentNewest || isSnapshotStrictlyOlderThanSelection(currentNewest.snapshot, candidate.snapshot)
1321
+ ? candidate
1322
+ : currentNewest, null);
1323
+ if (selectedMergedCandidate) {
1324
+ const otherMergedCandidates = mergedCandidates.filter((candidate) => candidate.pr.url !== selectedMergedCandidate.pr.url);
1325
+ const ignoredHistoricalMergedCandidates = otherMergedCandidates.filter((candidate) => isSnapshotStrictlyOlderThanSelection(candidate.snapshot, selectedMergedCandidate.snapshot));
1326
+ const staleUnmergedCandidates = nonMergedCandidates.filter((candidate) => isSnapshotStrictlyOlderThanSelection(candidate.snapshot, selectedMergedCandidate.snapshot));
1327
+ if (ignoredHistoricalMergedCandidates.length === otherMergedCandidates.length &&
1328
+ staleUnmergedCandidates.length === nonMergedCandidates.length) {
1329
+ const ignoredHistoricalMergedPrUrls = ignoredHistoricalMergedCandidates.map((candidate) => candidate.pr.url);
1330
+ const staleUnmergedPrUrls = staleUnmergedCandidates.map((candidate) => candidate.pr.url);
1331
+ const ignoredHistoricalPrUrls = [
1332
+ ...ignoredHistoricalMergedPrUrls,
1333
+ ...staleUnmergedPrUrls
1334
+ ];
1335
+ return {
1336
+ selected_pr: selectedMergedCandidate.pr,
1337
+ selected_snapshot: selectedMergedCandidate.snapshot,
1338
+ ignored_historical_pr_urls: ignoredHistoricalPrUrls,
1339
+ ignored_closed_unmerged_pr_urls: ignoredClosedUnmergedPrUrls,
1340
+ ignored_cross_issue_pr_urls: ignoredCrossIssuePrUrls,
1341
+ conflicting_attached_pr_urls: [],
1342
+ selection_note: appendIgnoredAttachedPrSelectionNote(`Selected already-merged PR ${selectedMergedCandidate.pr.url} because all remaining attached same-repo PR URLs are older.${ignoredHistoricalMergedPrUrls.length > 0 ? ` Ignored older merged PR URLs: ${ignoredHistoricalMergedPrUrls.join(', ')}.` : ''}${staleUnmergedPrUrls.length > 0 ? ` Older unmerged PR URLs: ${staleUnmergedPrUrls.join(', ')}.` : ''}`, {
1343
+ ignoredClosedUnmergedPrUrls,
1344
+ ignoredCrossIssuePrUrls
1345
+ })
1346
+ };
1347
+ }
1348
+ }
1349
+ const ignoredHistoricalCandidates = nonMergedCandidates.length > 0
1350
+ ? mergedCandidates.filter((candidate) => nonMergedCandidates.every((currentCandidate) => isSnapshotStrictlyOlderThanSelection(candidate.snapshot, currentCandidate.snapshot)))
1351
+ : [];
1352
+ const ignoredHistoricalPrUrls = ignoredHistoricalCandidates.map((candidate) => candidate.pr.url);
1353
+ const ignoredHistoricalUrlSet = new Set(ignoredHistoricalPrUrls);
1354
+ return {
1355
+ selected_pr: null,
1356
+ selected_snapshot: null,
1357
+ ignored_historical_pr_urls: ignoredHistoricalPrUrls,
1358
+ ignored_closed_unmerged_pr_urls: ignoredClosedUnmergedPrUrls,
1359
+ ignored_cross_issue_pr_urls: ignoredCrossIssuePrUrls,
1360
+ conflicting_attached_pr_urls: resolutionCandidates
1361
+ .filter((candidate) => !ignoredHistoricalUrlSet.has(candidate.pr.url))
1362
+ .map((candidate) => candidate.pr.url),
1363
+ selection_note: null
1364
+ };
1365
+ }
1366
+ function buildMultipleAttachedPrsSummary(input) {
1367
+ const ignoredSummary = buildIgnoredAttachedPrSummary({
1368
+ ignoredHistoricalPrUrls: input.ignoredHistoricalPrUrls,
1369
+ ignoredClosedUnmergedPrUrls: input.ignoredClosedUnmergedPrUrls,
1370
+ ignoredCrossIssuePrUrls: []
1371
+ });
1372
+ if (input.conflictingAttachedPrUrls.length === 0) {
1373
+ return `Attached GitHub pull requests match ${input.repoKey}, but no current merge candidate remains after bounded filtering; merge closeout is not armed.${ignoredSummary}`;
1374
+ }
1375
+ return `Multiple attached GitHub pull requests match ${input.repoKey}; conflicting attached PR URLs still require deterministic disambiguation: ${input.conflictingAttachedPrUrls.join(', ')}.${ignoredSummary}`;
1376
+ }
1377
+ function buildMultipleAttachedPrsPromotionSummary(input) {
1378
+ const ignoredSummary = buildIgnoredAttachedPrSummary({
1379
+ ignoredHistoricalPrUrls: input.ignoredHistoricalPrUrls,
1380
+ ignoredClosedUnmergedPrUrls: input.ignoredClosedUnmergedPrUrls,
1381
+ ignoredCrossIssuePrUrls: input.ignoredCrossIssuePrUrls
1382
+ });
1383
+ if (input.conflictingAttachedPrUrls.length === 0) {
1384
+ return `Attached GitHub pull requests match ${input.repoKey}, but no current review-handoff promotion candidate remains after bounded filtering.${ignoredSummary}`;
1385
+ }
1386
+ return `Multiple attached GitHub pull requests match ${input.repoKey}; conflicting current-candidate PR URLs still require deterministic disambiguation before review-handoff promotion can continue: ${input.conflictingAttachedPrUrls.join(', ')}.${ignoredSummary}`;
1387
+ }
1388
+ async function loadProviderSnapshotRecord(input) {
1389
+ const rawSnapshot = await input.resolveSnapshot({
1390
+ owner: input.owner,
1391
+ repo: input.repo,
1392
+ prNumber: input.prNumber,
1393
+ readinessMode: input.readinessMode
1394
+ });
1395
+ const actionRequiredReasons = input.resolveSnapshotActionRequiredReasons(rawSnapshot, {
1396
+ readinessMode: input.readinessMode
1397
+ });
1398
+ return mapSnapshotRecord(rawSnapshot, actionRequiredReasons);
1399
+ }
1400
+ function describeProviderBranchRecoveryReason(reason) {
1401
+ if (reason === 'merge_state=DIRTY') {
1402
+ return 'conflict recovery';
1403
+ }
1404
+ return 'branch refresh';
1405
+ }
1406
+ function doesProviderBranchRecoveryMatchPullRequest(recovery, pr) {
1407
+ if (!recovery || recovery.command !== 'gh') {
1408
+ return false;
1409
+ }
1410
+ const expectedRepo = `${pr.owner}/${pr.repo}`;
1411
+ return (recovery.args[0] === 'pr'
1412
+ && recovery.args[1] === 'update-branch'
1413
+ && recovery.args[2] === String(pr.number)
1414
+ && recovery.args.includes(expectedRepo));
1415
+ }
1416
+ async function attemptProviderBranchRecovery(input) {
1417
+ const recoveryReason = resolveAutomaticBranchRecoveryReason(input.snapshot, {
1418
+ requireExclusive: true
1419
+ });
1420
+ if (!recoveryReason
1421
+ || !shouldAttemptAutomaticBranchRecovery(input.snapshot)) {
1422
+ return null;
1423
+ }
1424
+ const previousBranchRecovery = input.previousBranchRecovery ?? null;
1425
+ if (previousBranchRecovery?.ok === true
1426
+ && previousBranchRecovery.failure_kind === null
1427
+ && doesProviderBranchRecoveryMatchPullRequest(previousBranchRecovery, input.pr)
1428
+ && previousBranchRecovery.head_oid === input.snapshot.head_oid
1429
+ && previousBranchRecovery.recovery_reason === recoveryReason) {
1430
+ return previousBranchRecovery;
1431
+ }
1432
+ const attemptedAt = input.now();
1433
+ const args = buildPrUpdateBranchArgs({
1434
+ owner: input.pr.owner,
1435
+ repo: input.pr.repo,
1436
+ prNumber: input.pr.number
1437
+ });
1438
+ const result = await input.runCommand({
1439
+ command: 'gh',
1440
+ args,
1441
+ cwd: input.repoRoot
1442
+ });
1443
+ const details = normalizeCommandText(result.stderr) ?? normalizeCommandText(result.stdout);
1444
+ return {
1445
+ attempted_at: attemptedAt,
1446
+ head_oid: input.snapshot.head_oid,
1447
+ recovery_reason: recoveryReason,
1448
+ command: 'gh',
1449
+ args,
1450
+ exit_code: result.exitCode,
1451
+ ok: result.ok,
1452
+ stdout: normalizeCommandText(result.stdout),
1453
+ stderr: normalizeCommandText(result.stderr),
1454
+ failure_kind: result.ok
1455
+ ? null
1456
+ : isConflictLikeBranchRecoveryFailureMessage(details) ? 'conflict' : 'other'
1457
+ };
1458
+ }
1459
+ function classifyPreBranchRecoverySnapshot(snapshot, prNumber, mode) {
1460
+ if (snapshot.state === 'CLOSED') {
1461
+ return {
1462
+ status: 'action_required',
1463
+ reason: 'pr_closed_unmerged',
1464
+ summary: mode === 'review_promotion'
1465
+ ? `Attached PR #${prNumber} is closed without merging; reopen it or attach a replacement PR before review-handoff promotion can continue.`
1466
+ : `Attached PR #${prNumber} is closed without merging; reopen it or attach a replacement PR.`
1467
+ };
1468
+ }
1469
+ if (snapshot.action_required_reasons.length > 0
1470
+ && !shouldAttemptAutomaticBranchRecovery(snapshot)) {
1471
+ return {
1472
+ status: 'action_required',
1473
+ reason: snapshot.action_required_reasons[0] ??
1474
+ (mode === 'review_promotion'
1475
+ ? 'review_handoff_promotion_blocked'
1476
+ : 'merge_action_required'),
1477
+ summary: mode === 'review_promotion'
1478
+ ? `Review-handoff promotion is blocked by: ${snapshot.action_required_reasons.join(', ')}.`
1479
+ : `Merge closeout is blocked by: ${snapshot.action_required_reasons.join(', ')}.`
1480
+ };
1481
+ }
1482
+ if (snapshot.github_rate_limit) {
1483
+ return {
1484
+ status: 'watching',
1485
+ reason: 'github_rate_limited',
1486
+ summary: mode === 'review_promotion'
1487
+ ? `Review-handoff promotion is waiting for GitHub API budget recovery before rereading PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`
1488
+ : `Merge closeout is waiting for GitHub API budget recovery before rereading PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`,
1489
+ github_rate_limit: snapshot.github_rate_limit
1490
+ };
1491
+ }
1492
+ return null;
1493
+ }
1494
+ function classifyProviderMutationRateLimitSnapshot(snapshot, prNumber, mode) {
1495
+ if (!snapshot.github_rate_limit || isMergedPullRequestSnapshot(snapshot)) {
1496
+ return null;
1497
+ }
1498
+ return {
1499
+ status: 'watching',
1500
+ reason: 'github_rate_limited',
1501
+ summary: mode === 'review_promotion'
1502
+ ? `Review-handoff promotion is waiting for GitHub API budget recovery before mutating PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`
1503
+ : `Merge closeout is waiting for GitHub API budget recovery before mutating PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`,
1504
+ github_rate_limit: snapshot.github_rate_limit
1505
+ };
1506
+ }
1507
+ function classifyPendingBranchRecovery(input) {
1508
+ const pendingReason = resolveAutomaticBranchRecoveryReason(input.snapshot, {
1509
+ requireExclusive: true
1510
+ });
1511
+ if (pendingReason !== input.recoveryAttempt.recovery_reason
1512
+ || !shouldAttemptAutomaticBranchRecovery(input.snapshot)) {
1513
+ return null;
1514
+ }
1515
+ const action = describeProviderBranchRecoveryReason(input.recoveryAttempt.recovery_reason);
1516
+ return {
1517
+ status: 'watching',
1518
+ reason: 'branch_refresh_requested',
1519
+ summary: input.mode === 'review_promotion'
1520
+ ? `Requested automatic ${action} for attached PR #${input.prNumber}; waiting for GitHub to recompute review-handoff readiness.`
1521
+ : `Requested automatic ${action} for attached PR #${input.prNumber}; waiting for GitHub to recompute merge readiness.`
1522
+ };
1523
+ }
1524
+ function classifyNonMergedSnapshot(snapshot, prNumber) {
1525
+ if (snapshot.state === 'CLOSED') {
1526
+ return {
1527
+ status: 'action_required',
1528
+ reason: 'pr_closed_unmerged',
1529
+ summary: `Attached PR #${prNumber} is closed without merging; reopen it or attach a replacement PR.`
1530
+ };
1531
+ }
1532
+ if (snapshot.action_required_reasons.length > 0) {
1533
+ return {
1534
+ status: 'action_required',
1535
+ reason: snapshot.action_required_reasons[0] ?? 'merge_action_required',
1536
+ summary: `Merge closeout is blocked by: ${snapshot.action_required_reasons.join(', ')}.`
1537
+ };
1538
+ }
1539
+ if (snapshot.github_rate_limit) {
1540
+ return {
1541
+ status: 'watching',
1542
+ reason: 'github_rate_limited',
1543
+ summary: `Merge closeout is waiting for GitHub API budget recovery before rereading PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`,
1544
+ github_rate_limit: snapshot.github_rate_limit
1545
+ };
1546
+ }
1547
+ if (snapshot.gate_reasons.length > 0 || !snapshot.ready_to_merge) {
1548
+ return {
1549
+ status: 'watching',
1550
+ reason: snapshot.gate_reasons[0] ?? 'waiting_for_merge_ready',
1551
+ summary: snapshot.gate_reasons.length > 0
1552
+ ? `Merge closeout is waiting for readiness gates to clear: ${snapshot.gate_reasons.join(', ')}.`
1553
+ : 'Merge closeout is waiting for the attached pull request to become merge-ready.'
1554
+ };
1555
+ }
1556
+ return null;
1557
+ }
1558
+ function classifyNonMergedReviewPromotionSnapshot(snapshot, prNumber) {
1559
+ if (snapshot.state === 'CLOSED') {
1560
+ return {
1561
+ status: 'action_required',
1562
+ reason: 'pr_closed_unmerged',
1563
+ summary: `Attached PR #${prNumber} is closed without merging; reopen it or attach a replacement PR before review-handoff promotion can continue.`
1564
+ };
1565
+ }
1566
+ if (snapshot.action_required_reasons.length > 0) {
1567
+ return {
1568
+ status: 'action_required',
1569
+ reason: snapshot.action_required_reasons[0] ?? 'review_handoff_promotion_blocked',
1570
+ summary: `Review-handoff promotion is blocked by: ${snapshot.action_required_reasons.join(', ')}.`
1571
+ };
1572
+ }
1573
+ if (snapshot.github_rate_limit) {
1574
+ return {
1575
+ status: 'watching',
1576
+ reason: 'github_rate_limited',
1577
+ summary: `Review-handoff promotion is waiting for GitHub API budget recovery before rereading PR #${prNumber}: ${formatProviderGitHubRateLimitSummary(snapshot.github_rate_limit)}.`,
1578
+ github_rate_limit: snapshot.github_rate_limit
1579
+ };
1580
+ }
1581
+ if (snapshot.gate_reasons.length > 0 || !snapshot.ready_to_merge) {
1582
+ return {
1583
+ status: 'watching',
1584
+ reason: snapshot.gate_reasons[0] ?? 'waiting_for_merge_ready',
1585
+ summary: snapshot.gate_reasons.length > 0
1586
+ ? `Review-handoff promotion is waiting for readiness gates to clear: ${snapshot.gate_reasons.join(', ')}.`
1587
+ : 'Review-handoff promotion is waiting for the attached pull request to become merge-ready.'
1588
+ };
1589
+ }
1590
+ return null;
1591
+ }
1592
+ function isMergedPullRequestSnapshot(snapshot) {
1593
+ return snapshot.merged_at !== null || snapshot.state === 'MERGED';
1594
+ }
1595
+ function isClosedUnmergedPullRequestSnapshot(snapshot) {
1596
+ return snapshot.state === 'CLOSED' && !isMergedPullRequestSnapshot(snapshot);
1597
+ }
1598
+ function isTerminalPullRequestSnapshot(snapshot) {
1599
+ return isMergedPullRequestSnapshot(snapshot) || snapshot.state === 'CLOSED';
1600
+ }
1601
+ function isSnapshotStrictlyOlderThanSelection(candidate, selected) {
1602
+ const candidateTimestamp = resolveSnapshotDisambiguationTimestamp(candidate);
1603
+ const selectedTimestamp = resolveSnapshotDisambiguationTimestamp(selected);
1604
+ if (candidateTimestamp === null || selectedTimestamp === null) {
1605
+ return false;
1606
+ }
1607
+ return candidateTimestamp < selectedTimestamp;
1608
+ }
1609
+ function buildIgnoredAttachedPrSummary(input) {
1610
+ const parts = [];
1611
+ if (input.ignoredHistoricalPrUrls.length > 0) {
1612
+ parts.push(`Ignored historical merged PR URLs: ${input.ignoredHistoricalPrUrls.join(', ')}.`);
1613
+ }
1614
+ if (input.ignoredClosedUnmergedPrUrls.length > 0) {
1615
+ parts.push(`Ignored closed prior-attempt PR URLs: ${input.ignoredClosedUnmergedPrUrls.join(', ')}.`);
1616
+ }
1617
+ if (input.ignoredCrossIssuePrUrls.length > 0) {
1618
+ parts.push(`Ignored cross-issue PR URLs: ${input.ignoredCrossIssuePrUrls.join(', ')}.`);
1619
+ }
1620
+ return parts.length > 0 ? ` ${parts.join(' ')}` : '';
1621
+ }
1622
+ function buildIgnoredAttachedPrSelectionNote(input) {
1623
+ return appendIgnoredAttachedPrSelectionNote(null, input);
1624
+ }
1625
+ function appendIgnoredAttachedPrSelectionNote(baseNote, input) {
1626
+ const parts = [];
1627
+ if (typeof baseNote === 'string' && baseNote.trim().length > 0) {
1628
+ parts.push(baseNote.trim());
1629
+ }
1630
+ if (input.ignoredClosedUnmergedPrUrls.length > 0) {
1631
+ parts.push(`Ignored closed prior-attempt PR URLs: ${input.ignoredClosedUnmergedPrUrls.join(', ')}.`);
1632
+ }
1633
+ if (input.ignoredCrossIssuePrUrls.length > 0) {
1634
+ parts.push(`Ignored cross-issue PR URLs: ${input.ignoredCrossIssuePrUrls.join(', ')}.`);
1635
+ }
1636
+ return parts.length > 0 ? parts.join(' ') : null;
1637
+ }
1638
+ function isReviewPromotionCrossIssueCandidate(input) {
1639
+ const currentIssueIdentifier = normalizeIssueIdentifierToken(input.issueIdentifier);
1640
+ if (!currentIssueIdentifier) {
1641
+ return false;
1642
+ }
1643
+ const leadingIssueIdentifier = extractLeadingIssueIdentifier(input.attachmentTitle);
1644
+ if (!leadingIssueIdentifier || leadingIssueIdentifier === currentIssueIdentifier) {
1645
+ return false;
1646
+ }
1647
+ const title = normalizeOptionalString(input.attachmentTitle) ?? '';
1648
+ const blockedIssueIdentifiers = new Set((input.blockedBy ?? [])
1649
+ .map((blocker) => normalizeIssueIdentifierToken(blocker.identifier))
1650
+ .filter((value) => value !== null));
1651
+ if (blockedIssueIdentifiers.has(leadingIssueIdentifier) &&
1652
+ hasReviewPromotionBlockerWording(title)) {
1653
+ return true;
1654
+ }
1655
+ return /\bfollow(?:-| )up\b/i.test(title);
1656
+ }
1657
+ function extractLeadingIssueIdentifier(value) {
1658
+ if (typeof value !== 'string') {
1659
+ return null;
1660
+ }
1661
+ const match = /^\s*[[(]?([A-Z][A-Z0-9]+-\d+)\b/i.exec(value);
1662
+ return normalizeIssueIdentifierToken(match?.[1] ?? null);
1663
+ }
1664
+ function hasReviewPromotionBlockerWording(value) {
1665
+ return /\bblock(?:er|ed|ing)?\b/i.test(value);
1666
+ }
1667
+ function normalizeIssueIdentifierToken(value) {
1668
+ if (typeof value !== 'string') {
1669
+ return null;
1670
+ }
1671
+ const trimmed = value.trim().toUpperCase();
1672
+ return /^[A-Z][A-Z0-9]+-\d+$/.test(trimmed) ? trimmed : null;
1673
+ }
1674
+ function resolveSnapshotDisambiguationTimestamp(snapshot) {
1675
+ const preferredTimestamp = isMergedPullRequestSnapshot(snapshot)
1676
+ ? snapshot.merged_at ?? snapshot.updated_at
1677
+ : snapshot.updated_at;
1678
+ const parsed = Date.parse(preferredTimestamp ?? '');
1679
+ return Number.isFinite(parsed) ? parsed : null;
1680
+ }
1681
+ function mapSnapshotRecord(snapshot, actionRequiredReasons) {
1682
+ const snapshotRecord = readRecord(snapshot);
1683
+ const checks = readRecord(snapshot.checks);
1684
+ const requiredChecks = readRecord(snapshot.requiredChecks);
1685
+ const githubRateLimit = mapProviderGitHubRateLimit(readRecord(snapshotRecord?.githubRateLimit) ?? readRecord(snapshotRecord?.github_rate_limit));
1686
+ return {
1687
+ state: normalizeOptionalString(snapshot.state),
1688
+ review_decision: normalizeOptionalString(snapshot.reviewDecision),
1689
+ merge_state_status: normalizeOptionalString(snapshot.mergeStateStatus),
1690
+ ready_to_merge: snapshot.readyToMerge === true,
1691
+ gate_reasons: readStringArray(snapshot.gateReasons),
1692
+ action_required_reasons: [...actionRequiredReasons],
1693
+ unresolved_thread_count: normalizeOptionalNumber(snapshot.unresolvedThreadCount),
1694
+ checks_pending: readArrayLength(checks?.pending),
1695
+ checks_failed: readArrayLength(checks?.failed),
1696
+ required_checks_pending: readArrayLength(requiredChecks?.pending),
1697
+ required_checks_failed: readArrayLength(requiredChecks?.failed),
1698
+ updated_at: normalizeOptionalString(snapshot.updatedAt),
1699
+ merged_at: normalizeOptionalString(snapshot.mergedAt),
1700
+ head_oid: normalizeOptionalString(snapshot.headOid),
1701
+ github_rate_limit: githubRateLimit
1702
+ };
1703
+ }
1704
+ function resolveProviderGitHubRateLimitRecord(input) {
1705
+ const inputRecord = readRecord(input);
1706
+ const embedded = inputRecord?.githubRateLimit ?? inputRecord?.github_rate_limit;
1707
+ const embeddedRateLimit = mapProviderGitHubRateLimit(embedded);
1708
+ if (embeddedRateLimit) {
1709
+ return embeddedRateLimit;
1710
+ }
1711
+ return mapProviderGitHubRateLimit(resolveGitHubRateLimitStatus(input));
1712
+ }
1713
+ function mapProviderGitHubRateLimit(input) {
1714
+ const rateLimit = readRecord(input);
1715
+ if (!rateLimit || rateLimit.kind !== 'github_rate_limited') {
1716
+ return null;
1717
+ }
1718
+ return {
1719
+ kind: 'github_rate_limited',
1720
+ surface: normalizeOptionalString(rateLimit.surface) ?? 'unknown',
1721
+ limit_type: normalizeOptionalString(rateLimit.limit_type) ?? 'unknown',
1722
+ status: normalizeOptionalNumber(rateLimit.status),
1723
+ reset_at: normalizeOptionalString(rateLimit.reset_at),
1724
+ retry_after_seconds: normalizeOptionalNumber(rateLimit.retry_after_seconds),
1725
+ retry_at: normalizeOptionalString(rateLimit.retry_at),
1726
+ message: normalizeOptionalString(rateLimit.message)
1727
+ };
1728
+ }
1729
+ function formatProviderGitHubRateLimitSummary(rateLimit) {
1730
+ return formatGitHubRateLimitStatus(rateLimit);
1731
+ }
1732
+ function buildProviderGitHubRateLimitError(rateLimit) {
1733
+ const error = new Error(formatProviderGitHubRateLimitSummary(rateLimit));
1734
+ error.githubRateLimit = rateLimit;
1735
+ return error;
1736
+ }
1737
+ function assessSharedRootMergeSafety(statusOutput) {
1738
+ const lines = (statusOutput ?? '').split(/\r?\n/u).filter((line) => line.length > 0);
1739
+ const branchHeader = lines[0]?.trim() ?? '';
1740
+ if (!branchHeader.startsWith('## ')) {
1741
+ return {
1742
+ safe: false,
1743
+ reason: 'shared_root_status_header_missing'
1744
+ };
1745
+ }
1746
+ if (!/^## main(?:$|\.{3})/u.test(branchHeader)) {
1747
+ return {
1748
+ safe: false,
1749
+ reason: 'shared_root_not_on_main'
1750
+ };
1751
+ }
1752
+ const trackingStatusMatch = branchHeader.match(/\[([^\]]+)\]\s*$/u);
1753
+ if (trackingStatusMatch) {
1754
+ const trackingParts = trackingStatusMatch[1]
1755
+ .split(',')
1756
+ .map((part) => part.trim().toLowerCase())
1757
+ .filter((part) => part.length > 0);
1758
+ const fastForwardSafe = trackingParts.every((part) => /^behind \d+$/u.test(part));
1759
+ if (!fastForwardSafe) {
1760
+ return {
1761
+ safe: false,
1762
+ reason: 'shared_root_not_ff_only_safe'
1763
+ };
1764
+ }
1765
+ }
1766
+ if (lines.length > 1) {
1767
+ return {
1768
+ safe: false,
1769
+ reason: 'shared_root_dirty'
1770
+ };
1771
+ }
1772
+ return {
1773
+ safe: true,
1774
+ reason: 'shared_root_clean_main'
1775
+ };
1776
+ }
1777
+ function parseGitHubPullRequestUrl(value) {
1778
+ const normalized = normalizeOptionalString(value);
1779
+ if (!normalized) {
1780
+ return null;
1781
+ }
1782
+ let parsed;
1783
+ try {
1784
+ parsed = new URL(normalized);
1785
+ }
1786
+ catch {
1787
+ return null;
1788
+ }
1789
+ const hostname = parsed.hostname.toLowerCase();
1790
+ if (hostname !== 'github.com' && hostname !== 'www.github.com') {
1791
+ return null;
1792
+ }
1793
+ const segments = parsed.pathname.split('/').filter(Boolean);
1794
+ const owner = normalizeOptionalString(segments[0] ?? null);
1795
+ const repo = normalizeOptionalString(segments[1] ?? null);
1796
+ const resource = normalizeOptionalString(segments[2] ?? null)?.toLowerCase() ?? null;
1797
+ const number = normalizePrNumber(segments[3] ?? null);
1798
+ if (!owner || !repo || resource !== 'pull' || number === null) {
1799
+ return null;
1800
+ }
1801
+ return {
1802
+ url: `https://github.com/${owner}/${repo}/pull/${number}`,
1803
+ owner,
1804
+ repo,
1805
+ number
1806
+ };
1807
+ }
1808
+ function normalizePrNumber(value) {
1809
+ const normalized = normalizeOptionalString(value);
1810
+ if (!normalized || !/^\d+$/u.test(normalized)) {
1811
+ return null;
1812
+ }
1813
+ const parsed = Number.parseInt(normalized, 10);
1814
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
1815
+ }
1816
+ function normalizeOptionalString(value) {
1817
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
1818
+ }
1819
+ function normalizeProviderMergeCloseoutIssueState(value) {
1820
+ const normalized = normalizeOptionalString(value);
1821
+ return normalized ? normalized.toLowerCase() : null;
1822
+ }
1823
+ function isProbeRecoveryCacheContextFreshEnough(input) {
1824
+ const expectedState = normalizeProviderMergeCloseoutIssueState(input.issueState);
1825
+ const cachedState = normalizeProviderMergeCloseoutIssueState(input.issueContext.state?.name ?? null);
1826
+ if (expectedState !== null && cachedState !== expectedState) {
1827
+ return false;
1828
+ }
1829
+ const expectedStateType = normalizeOptionalString(input.issueStateType);
1830
+ const cachedStateType = normalizeOptionalString(input.issueContext.state?.type ?? null);
1831
+ if (expectedStateType !== null && cachedStateType !== expectedStateType) {
1832
+ return false;
1833
+ }
1834
+ const expectedUpdatedAt = normalizeOptionalString(input.issueUpdatedAt);
1835
+ if (expectedUpdatedAt === null) {
1836
+ return true;
1837
+ }
1838
+ const cachedUpdatedAt = normalizeOptionalString(input.issueContext.updated_at);
1839
+ if (cachedUpdatedAt === null) {
1840
+ return false;
1841
+ }
1842
+ const expectedUpdatedAtMs = Date.parse(expectedUpdatedAt);
1843
+ const cachedUpdatedAtMs = Date.parse(cachedUpdatedAt);
1844
+ if (Number.isFinite(expectedUpdatedAtMs) && Number.isFinite(cachedUpdatedAtMs)) {
1845
+ return cachedUpdatedAtMs >= expectedUpdatedAtMs;
1846
+ }
1847
+ return cachedUpdatedAt === expectedUpdatedAt;
1848
+ }
1849
+ function normalizeOptionalNumber(value) {
1850
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
1851
+ }
1852
+ function normalizeCommandText(value) {
1853
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
1854
+ }
1855
+ function readRecord(value) {
1856
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1857
+ ? value
1858
+ : null;
1859
+ }
1860
+ function readArrayLength(value) {
1861
+ return Array.isArray(value) ? value.length : null;
1862
+ }
1863
+ function readStringArray(value) {
1864
+ if (!Array.isArray(value)) {
1865
+ return [];
1866
+ }
1867
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
1868
+ }