@kbediako/codex-orchestrator 0.1.37 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +73 -291
- package/bin/codex-orchestrator.js +161 -0
- package/codex.orchestrator.json +149 -13
- package/dist/bin/codex-orchestrator.js +795 -1154
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +22 -4
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +3 -3
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +2 -2
- package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +183 -11
- package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
- package/dist/orchestrator/src/cli/coStatusCliShell.js +429 -0
- package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
- package/dist/orchestrator/src/cli/codexCliShell.js +72 -0
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +49 -11
- package/dist/orchestrator/src/cli/config/delegationConfig.js +317 -5
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +2 -3
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -13
- package/dist/orchestrator/src/cli/control/authenticatedControlRouteGate.js +69 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteComposition.js +267 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteController.js +5 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteDispatcher.js +41 -0
- package/dist/orchestrator/src/cli/control/compatibilityIssuePresenter.js +1035 -0
- package/dist/orchestrator/src/cli/control/confirmationApproveController.js +62 -0
- package/dist/orchestrator/src/cli/control/confirmationCreateController.js +69 -0
- package/dist/orchestrator/src/cli/control/confirmationIssueConsumeController.js +43 -0
- package/dist/orchestrator/src/cli/control/confirmationListController.js +22 -0
- package/dist/orchestrator/src/cli/control/confirmationValidateController.js +58 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +25 -3
- package/dist/orchestrator/src/cli/control/controlActionCancelConfirmation.js +65 -0
- package/dist/orchestrator/src/cli/control/controlActionController.js +77 -0
- package/dist/orchestrator/src/cli/control/controlActionControllerSequencing.js +161 -0
- package/dist/orchestrator/src/cli/control/controlActionExecution.js +142 -0
- package/dist/orchestrator/src/cli/control/controlActionFinalization.js +43 -0
- package/dist/orchestrator/src/cli/control/controlActionOutcome.js +60 -0
- package/dist/orchestrator/src/cli/control/controlActionPreflight.js +476 -0
- package/dist/orchestrator/src/cli/control/controlAuthenticatedRouteHandoff.js +57 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapAssembly.js +39 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapMetadataPersistence.js +16 -0
- package/dist/orchestrator/src/cli/control/controlEventTransport.js +49 -0
- package/dist/orchestrator/src/cli/control/controlExpiryLifecycle.js +102 -0
- package/dist/orchestrator/src/cli/control/controlHostOwnership.js +480 -0
- package/dist/orchestrator/src/cli/control/controlHostSupervision.js +608 -0
- package/dist/orchestrator/src/cli/control/controlOversightFacade.js +8 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadService.js +16 -0
- package/dist/orchestrator/src/cli/control/controlOversightUpdateContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlPersistenceFiles.js +6 -0
- package/dist/orchestrator/src/cli/control/controlQuestionChildResolution.js +18 -0
- package/dist/orchestrator/src/cli/control/controlRequestContext.js +42 -0
- package/dist/orchestrator/src/cli/control/controlRequestController.js +9 -0
- package/dist/orchestrator/src/cli/control/controlRequestPredispatch.js +17 -0
- package/dist/orchestrator/src/cli/control/controlRequestRouteDispatch.js +44 -0
- package/dist/orchestrator/src/cli/control/controlRuntime.js +992 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +23 -1456
- package/dist/orchestrator/src/cli/control/controlServerAuditAndErrorHelpers.js +115 -0
- package/dist/orchestrator/src/cli/control/controlServerAuthenticatedRouteBranch.js +29 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapLifecycle.js +30 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapStartSequence.js +21 -0
- package/dist/orchestrator/src/cli/control/controlServerOwnedRuntimeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicLifecycle.js +756 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicRouteHelpers.js +86 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceLifecycle.js +25 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceStartup.js +18 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestBodyHelpers.js +37 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShell.js +40 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShellBinding.js +17 -0
- package/dist/orchestrator/src/cli/control/controlServerSeedLoading.js +27 -0
- package/dist/orchestrator/src/cli/control/controlServerSeededRuntimeAssembly.js +186 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupInputPreparation.js +31 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupSequence.js +49 -0
- package/dist/orchestrator/src/cli/control/controlState.js +233 -2
- package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +1899 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeBootstrapLifecycle.js +22 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeOversightFacadeFactory.js +8 -0
- package/dist/orchestrator/src/cli/control/controlTelegramCommandController.js +49 -0
- package/dist/orchestrator/src/cli/control/controlTelegramDispatchRead.js +40 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPollingController.js +89 -0
- package/dist/orchestrator/src/cli/control/controlTelegramProjectionNotificationController.js +29 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPushState.js +63 -0
- package/dist/orchestrator/src/cli/control/controlTelegramQuestionRead.js +13 -0
- package/dist/orchestrator/src/cli/control/controlTelegramReadController.js +216 -0
- package/dist/orchestrator/src/cli/control/controlTelegramUpdateHandler.js +63 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +73 -5
- package/dist/orchestrator/src/cli/control/delegationRegisterController.js +35 -0
- package/dist/orchestrator/src/cli/control/dynamicToolBridgePolicy.js +139 -0
- package/dist/orchestrator/src/cli/control/eventsSseController.js +12 -0
- package/dist/orchestrator/src/cli/control/linearBudgetState.js +1789 -0
- package/dist/orchestrator/src/cli/control/linearDispatchSource.js +1137 -0
- package/dist/orchestrator/src/cli/control/linearGraphqlClient.js +150 -0
- package/dist/orchestrator/src/cli/control/linearRateLimit.js +102 -0
- package/dist/orchestrator/src/cli/control/linearWebhookController.js +499 -0
- package/dist/orchestrator/src/cli/control/liveLinearAdvisoryRuntime.js +70 -0
- package/dist/orchestrator/src/cli/control/observabilityApiController.js +173 -0
- package/dist/orchestrator/src/cli/control/observabilityReadModel.js +500 -0
- package/dist/orchestrator/src/cli/control/observabilitySurface.js +284 -0
- package/dist/orchestrator/src/cli/control/observabilityUpdateNotifier.js +22 -0
- package/dist/orchestrator/src/cli/control/operatorDashboardPresenter.js +252 -0
- package/dist/orchestrator/src/cli/control/providerAgentCapacity.js +70 -0
- package/dist/orchestrator/src/cli/control/providerControlHostFreshnessGauge.js +1068 -0
- package/dist/orchestrator/src/cli/control/providerIntakeState.js +473 -0
- package/dist/orchestrator/src/cli/control/providerIssueHandoff.js +6811 -0
- package/dist/orchestrator/src/cli/control/providerIssueObservability.js +1348 -0
- package/dist/orchestrator/src/cli/control/providerIssueRetryQueue.js +84 -0
- package/dist/orchestrator/src/cli/control/providerLinearRuntimeProof.js +588 -0
- package/dist/orchestrator/src/cli/control/providerLinearScreenshotProof.js +473 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkerTruth.js +383 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowAudit.js +254 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowFacade.js +5573 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowStates.js +115 -0
- package/dist/orchestrator/src/cli/control/providerMergeCloseout.js +1868 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilot.js +1580 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLifecycle.js +154 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLocalRolloutExecution.js +1006 -0
- package/dist/orchestrator/src/cli/control/providerPollingHealth.js +435 -0
- package/dist/orchestrator/src/cli/control/providerTerminalCleanup.js +516 -0
- package/dist/orchestrator/src/cli/control/providerWorkerHosts.js +191 -0
- package/dist/orchestrator/src/cli/control/providerWorkflowConfigStore.js +515 -0
- package/dist/orchestrator/src/cli/control/questionChildResolutionAdapter.js +361 -0
- package/dist/orchestrator/src/cli/control/questionQueueController.js +181 -0
- package/dist/orchestrator/src/cli/control/questionReadRetryDeduplication.js +9 -0
- package/dist/orchestrator/src/cli/control/questionReadSequence.js +10 -0
- package/dist/orchestrator/src/cli/control/securityViolationController.js +27 -0
- package/dist/orchestrator/src/cli/control/selectedRunProjection.js +1838 -0
- package/dist/orchestrator/src/cli/control/telegramOversightApiClient.js +48 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridge.js +180 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeProjectionDeliveryQueue.js +25 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeRuntimeLifecycle.js +45 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeStateStore.js +77 -0
- package/dist/orchestrator/src/cli/control/telegramOversightControlActionApiClient.js +45 -0
- package/dist/orchestrator/src/cli/control/trackerDispatchPilot.js +439 -0
- package/dist/orchestrator/src/cli/control/uiDataController.js +34 -0
- package/dist/orchestrator/src/cli/control/uiSessionController.js +100 -0
- package/dist/orchestrator/src/cli/controlHostCliShell.js +860 -0
- package/dist/orchestrator/src/cli/controlHostFreshnessGaugeCliShell.js +129 -0
- package/dist/orchestrator/src/cli/controlHostSupervisionCliShell.js +2127 -0
- package/dist/orchestrator/src/cli/delegationCliShell.js +62 -0
- package/dist/orchestrator/src/cli/delegationServer.js +567 -678
- package/dist/orchestrator/src/cli/delegationServerCliShell.js +52 -0
- package/dist/orchestrator/src/cli/delegationServerQuestionFlowShell.js +228 -0
- package/dist/orchestrator/src/cli/delegationServerToolDispatchShell.js +411 -0
- package/dist/orchestrator/src/cli/delegationServerTransport.js +274 -0
- package/dist/orchestrator/src/cli/delegationSetup.js +51 -171
- package/dist/orchestrator/src/cli/devtoolsCliShell.js +34 -0
- package/dist/orchestrator/src/cli/doctor.js +542 -122
- package/dist/orchestrator/src/cli/doctorCliRequestShell.js +72 -0
- package/dist/orchestrator/src/cli/doctorCliShell.js +138 -0
- package/dist/orchestrator/src/cli/doctorUsage.js +136 -16
- package/dist/orchestrator/src/cli/exec/experience.js +16 -2
- package/dist/orchestrator/src/cli/exec/summary.js +3 -0
- package/dist/orchestrator/src/cli/execCliShell.js +51 -0
- package/dist/orchestrator/src/cli/flowCliRequestShell.js +44 -0
- package/dist/orchestrator/src/cli/flowCliShell.js +239 -0
- package/dist/orchestrator/src/cli/frontendTestCliRequestShell.js +80 -0
- package/dist/orchestrator/src/cli/frontendTestCliShell.js +41 -0
- package/dist/orchestrator/src/cli/init.js +1 -0
- package/dist/orchestrator/src/cli/initCliShell.js +50 -0
- package/dist/orchestrator/src/cli/linearCliShell.js +1200 -0
- package/dist/orchestrator/src/cli/mcpEnableCliShell.js +132 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +3 -2
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +56 -0
- package/dist/orchestrator/src/cli/orchestrator.js +66 -1376
- package/dist/orchestrator/src/cli/planCliShell.js +19 -0
- package/dist/orchestrator/src/cli/prCliShell.js +41 -0
- package/dist/orchestrator/src/cli/providerLinearChildLanePhaseContract.js +204 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +1772 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneShell.js +2420 -0
- package/dist/orchestrator/src/cli/providerLinearChildStreamShell.js +385 -0
- package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +5738 -0
- package/dist/orchestrator/src/cli/resumeCliShell.js +14 -0
- package/dist/orchestrator/src/cli/reviewCliLaunchShell.js +72 -0
- package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
- package/dist/orchestrator/src/cli/rlm/context.js +94 -7
- package/dist/orchestrator/src/cli/rlm/rlmCodexRuntimeShell.js +546 -0
- package/dist/orchestrator/src/cli/rlm/symbolic.js +4 -2
- package/dist/orchestrator/src/cli/rlmCliRequestShell.js +42 -0
- package/dist/orchestrator/src/cli/rlmCompletionCliShell.js +46 -0
- package/dist/orchestrator/src/cli/rlmLaunchCliShell.js +51 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +83 -523
- package/dist/orchestrator/src/cli/run/blockMemory.js +500 -0
- package/dist/orchestrator/src/cli/run/manifest.js +410 -73
- package/dist/orchestrator/src/cli/run/manifestPersister.js +45 -14
- package/dist/orchestrator/src/cli/run/runMemoryController.js +216 -0
- package/dist/orchestrator/src/cli/run/source0.js +690 -0
- package/dist/orchestrator/src/cli/run/workspacePath.js +101 -0
- package/dist/orchestrator/src/cli/runtime/mode.js +2 -1
- package/dist/orchestrator/src/cli/runtime/provider.js +39 -2
- package/dist/orchestrator/src/cli/selfCheckCliShell.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +668 -18
- package/dist/orchestrator/src/cli/services/execRuntime.js +66 -1
- package/dist/orchestrator/src/cli/services/orchestratorAutoScoutEvidenceRecorder.js +71 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudBranchResolution.js +8 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudEnvironmentResolution.js +22 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudExecutionLifecycleShell.js +39 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudPromptBuilder.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteFallbackContract.js +45 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteShell.js +36 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudTargetExecutor.js +277 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycle.js +98 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycleShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionLifecycle.js +112 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionModePolicy.js +27 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteAdapterShell.js +59 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteDecisionShell.js +57 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteState.js +21 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouter.js +2 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalPipelineExecutor.js +149 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalRouteShell.js +63 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanTargetTracker.js +16 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumePreparationShell.js +84 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumeTokenValidation.js +15 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleCompletion.js +31 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleExecutionRegistration.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleOrchestrationShell.js +83 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleTaskManagerShell.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRuntimeManifestMutation.js +20 -0
- package/dist/orchestrator/src/cli/services/orchestratorStartPreparationShell.js +56 -0
- package/dist/orchestrator/src/cli/services/orchestratorStatusShell.js +70 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +7 -3
- package/dist/orchestrator/src/cli/services/plannerMemory.js +119 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -3
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +9 -0
- package/dist/orchestrator/src/cli/setupBootstrapShell.js +114 -0
- package/dist/orchestrator/src/cli/setupCliShell.js +51 -0
- package/dist/orchestrator/src/cli/skillsCliShell.js +56 -0
- package/dist/orchestrator/src/cli/startCliRequestShell.js +53 -0
- package/dist/orchestrator/src/cli/startCliShell.js +68 -0
- package/dist/orchestrator/src/cli/statusCliShell.js +22 -0
- package/dist/orchestrator/src/cli/utils/authProvenanceFingerprint.js +27 -0
- package/dist/orchestrator/src/cli/utils/cloudPreflight.js +83 -1
- package/dist/orchestrator/src/cli/utils/delegationConfigParser.js +250 -0
- package/dist/orchestrator/src/cli/utils/delegationMcpHealth.js +1382 -0
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -54
- package/dist/orchestrator/src/cli/utils/mcpServerEntry.js +53 -0
- package/dist/orchestrator/src/cli/utils/packageProgramResolver.js +151 -0
- package/dist/orchestrator/src/cli/utils/providerOverrideEnv.js +71 -0
- package/dist/orchestrator/src/cli/utils/trailingJsonObject.js +59 -0
- package/dist/orchestrator/src/learning/crystalizer.js +2 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +233 -49
- package/dist/orchestrator/src/persistence/TaskStateStore.js +6 -6
- package/dist/orchestrator/src/persistence/lockFile.js +70 -4
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +39 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +3 -2
- package/dist/orchestrator/src/utils/atomicWrite.js +17 -2
- package/dist/packages/orchestrator/src/exec/unified-exec.js +99 -6
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +150 -19
- package/dist/packages/sdk-node/src/orchestrator.js +137 -13
- package/dist/packages/shared/config/designConfig.js +8 -1
- package/dist/packages/shared/streams/stdio.js +1 -1
- package/dist/scripts/design/pipeline/permit.js +15 -0
- package/dist/scripts/lib/docs-catalog.js +365 -0
- package/dist/scripts/lib/docs-helpers.js +87 -5
- package/dist/scripts/lib/pr-watch-merge.js +1088 -80
- package/dist/scripts/lib/provider-run-contract.js +26 -0
- package/dist/scripts/lib/review-command-intent-classification.js +532 -0
- package/dist/scripts/lib/review-command-probe-classification.js +385 -0
- package/dist/scripts/lib/review-execution-boundary-preflight.js +279 -0
- package/dist/scripts/lib/review-execution-runtime.js +753 -0
- package/dist/scripts/lib/review-execution-state.js +1144 -0
- package/dist/scripts/lib/review-execution-telemetry.js +215 -0
- package/dist/scripts/lib/review-inspection-target-parsing.js +78 -0
- package/dist/scripts/lib/review-launch-attempt.js +601 -0
- package/dist/scripts/lib/review-meta-surface-boundary-analysis.js +300 -0
- package/dist/scripts/lib/review-meta-surface-normalization.js +746 -0
- package/dist/scripts/lib/review-non-interactive-handoff.js +61 -0
- package/dist/scripts/lib/review-prompt-context.js +376 -0
- package/dist/scripts/lib/review-scope-advisory.js +286 -0
- package/dist/scripts/lib/review-scope-paths.js +123 -0
- package/dist/scripts/lib/review-shell-command-parser.js +389 -0
- package/dist/scripts/lib/review-shell-env-interpreter.js +340 -0
- package/dist/scripts/lib/run-manifests.js +192 -36
- package/dist/scripts/lib/spark-policy-classifier.js +593 -0
- package/dist/scripts/run-review.js +507 -1777
- package/docs/public/downstream-setup.md +106 -0
- package/docs/public/provider-onboarding.md +173 -0
- package/package.json +30 -11
- package/plugins/codex-orchestrator/.codex-plugin/plugin.json +30 -0
- package/plugins/codex-orchestrator/.mcp.json +13 -0
- package/plugins/codex-orchestrator/launcher.mjs +359 -0
- package/schemas/manifest.json +395 -0
- package/skills/chrome-devtools/SKILL.md +1 -1
- package/skills/codex-orchestrator/SKILL.md +83 -0
- package/skills/collab-subagents-first/SKILL.md +2 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +24 -11
- package/skills/delegation-usage/SKILL.md +20 -13
- package/skills/land/SKILL.md +77 -0
- package/skills/linear/SKILL.md +255 -0
- package/skills/release/SKILL.md +47 -3
- package/skills/standalone-review/SKILL.md +6 -1
- package/templates/README.md +4 -2
- package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
- package/templates/codex/.codex/agents/explorer-fast.toml +1 -0
- package/templates/codex/.codex/agents/worker-complex.toml +1 -1
- package/templates/codex/.codex/config.toml +3 -4
- package/templates/codex/.codex/providers/README.md +13 -0
- package/templates/codex/.codex/providers/control.example.json +18 -0
- package/templates/codex/.codex/providers/provider.env.example +15 -0
- package/templates/codex/AGENTS.md +12 -7
- package/templates/codex/mcp-client.json +5 -1
- package/docs/README.md +0 -307
- 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
|
+
}
|