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