@kbediako/codex-orchestrator 0.1.37 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +73 -291
- package/bin/codex-orchestrator.js +161 -0
- package/codex.orchestrator.json +149 -13
- package/dist/bin/codex-orchestrator.js +795 -1154
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +22 -4
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +3 -3
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +2 -2
- package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +183 -11
- package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
- package/dist/orchestrator/src/cli/coStatusCliShell.js +429 -0
- package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
- package/dist/orchestrator/src/cli/codexCliShell.js +72 -0
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +49 -11
- package/dist/orchestrator/src/cli/config/delegationConfig.js +317 -5
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +2 -3
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -13
- package/dist/orchestrator/src/cli/control/authenticatedControlRouteGate.js +69 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteComposition.js +267 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteController.js +5 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteDispatcher.js +41 -0
- package/dist/orchestrator/src/cli/control/compatibilityIssuePresenter.js +1035 -0
- package/dist/orchestrator/src/cli/control/confirmationApproveController.js +62 -0
- package/dist/orchestrator/src/cli/control/confirmationCreateController.js +69 -0
- package/dist/orchestrator/src/cli/control/confirmationIssueConsumeController.js +43 -0
- package/dist/orchestrator/src/cli/control/confirmationListController.js +22 -0
- package/dist/orchestrator/src/cli/control/confirmationValidateController.js +58 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +25 -3
- package/dist/orchestrator/src/cli/control/controlActionCancelConfirmation.js +65 -0
- package/dist/orchestrator/src/cli/control/controlActionController.js +77 -0
- package/dist/orchestrator/src/cli/control/controlActionControllerSequencing.js +161 -0
- package/dist/orchestrator/src/cli/control/controlActionExecution.js +142 -0
- package/dist/orchestrator/src/cli/control/controlActionFinalization.js +43 -0
- package/dist/orchestrator/src/cli/control/controlActionOutcome.js +60 -0
- package/dist/orchestrator/src/cli/control/controlActionPreflight.js +476 -0
- package/dist/orchestrator/src/cli/control/controlAuthenticatedRouteHandoff.js +57 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapAssembly.js +39 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapMetadataPersistence.js +16 -0
- package/dist/orchestrator/src/cli/control/controlEventTransport.js +49 -0
- package/dist/orchestrator/src/cli/control/controlExpiryLifecycle.js +102 -0
- package/dist/orchestrator/src/cli/control/controlHostOwnership.js +480 -0
- package/dist/orchestrator/src/cli/control/controlHostSupervision.js +608 -0
- package/dist/orchestrator/src/cli/control/controlOversightFacade.js +8 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadService.js +16 -0
- package/dist/orchestrator/src/cli/control/controlOversightUpdateContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlPersistenceFiles.js +6 -0
- package/dist/orchestrator/src/cli/control/controlQuestionChildResolution.js +18 -0
- package/dist/orchestrator/src/cli/control/controlRequestContext.js +42 -0
- package/dist/orchestrator/src/cli/control/controlRequestController.js +9 -0
- package/dist/orchestrator/src/cli/control/controlRequestPredispatch.js +17 -0
- package/dist/orchestrator/src/cli/control/controlRequestRouteDispatch.js +44 -0
- package/dist/orchestrator/src/cli/control/controlRuntime.js +992 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +23 -1456
- package/dist/orchestrator/src/cli/control/controlServerAuditAndErrorHelpers.js +115 -0
- package/dist/orchestrator/src/cli/control/controlServerAuthenticatedRouteBranch.js +29 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapLifecycle.js +30 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapStartSequence.js +21 -0
- package/dist/orchestrator/src/cli/control/controlServerOwnedRuntimeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicLifecycle.js +756 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicRouteHelpers.js +86 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceLifecycle.js +25 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceStartup.js +18 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestBodyHelpers.js +37 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShell.js +40 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShellBinding.js +17 -0
- package/dist/orchestrator/src/cli/control/controlServerSeedLoading.js +27 -0
- package/dist/orchestrator/src/cli/control/controlServerSeededRuntimeAssembly.js +186 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupInputPreparation.js +31 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupSequence.js +49 -0
- package/dist/orchestrator/src/cli/control/controlState.js +233 -2
- package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +1899 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeBootstrapLifecycle.js +22 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeOversightFacadeFactory.js +8 -0
- package/dist/orchestrator/src/cli/control/controlTelegramCommandController.js +49 -0
- package/dist/orchestrator/src/cli/control/controlTelegramDispatchRead.js +40 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPollingController.js +89 -0
- package/dist/orchestrator/src/cli/control/controlTelegramProjectionNotificationController.js +29 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPushState.js +63 -0
- package/dist/orchestrator/src/cli/control/controlTelegramQuestionRead.js +13 -0
- package/dist/orchestrator/src/cli/control/controlTelegramReadController.js +216 -0
- package/dist/orchestrator/src/cli/control/controlTelegramUpdateHandler.js +63 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +73 -5
- package/dist/orchestrator/src/cli/control/delegationRegisterController.js +35 -0
- package/dist/orchestrator/src/cli/control/dynamicToolBridgePolicy.js +139 -0
- package/dist/orchestrator/src/cli/control/eventsSseController.js +12 -0
- package/dist/orchestrator/src/cli/control/linearBudgetState.js +1789 -0
- package/dist/orchestrator/src/cli/control/linearDispatchSource.js +1137 -0
- package/dist/orchestrator/src/cli/control/linearGraphqlClient.js +150 -0
- package/dist/orchestrator/src/cli/control/linearRateLimit.js +102 -0
- package/dist/orchestrator/src/cli/control/linearWebhookController.js +499 -0
- package/dist/orchestrator/src/cli/control/liveLinearAdvisoryRuntime.js +70 -0
- package/dist/orchestrator/src/cli/control/observabilityApiController.js +173 -0
- package/dist/orchestrator/src/cli/control/observabilityReadModel.js +500 -0
- package/dist/orchestrator/src/cli/control/observabilitySurface.js +284 -0
- package/dist/orchestrator/src/cli/control/observabilityUpdateNotifier.js +22 -0
- package/dist/orchestrator/src/cli/control/operatorDashboardPresenter.js +252 -0
- package/dist/orchestrator/src/cli/control/providerAgentCapacity.js +70 -0
- package/dist/orchestrator/src/cli/control/providerControlHostFreshnessGauge.js +1068 -0
- package/dist/orchestrator/src/cli/control/providerIntakeState.js +473 -0
- package/dist/orchestrator/src/cli/control/providerIssueHandoff.js +6811 -0
- package/dist/orchestrator/src/cli/control/providerIssueObservability.js +1348 -0
- package/dist/orchestrator/src/cli/control/providerIssueRetryQueue.js +84 -0
- package/dist/orchestrator/src/cli/control/providerLinearRuntimeProof.js +588 -0
- package/dist/orchestrator/src/cli/control/providerLinearScreenshotProof.js +473 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkerTruth.js +383 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowAudit.js +254 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowFacade.js +5573 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowStates.js +115 -0
- package/dist/orchestrator/src/cli/control/providerMergeCloseout.js +1868 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilot.js +1580 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLifecycle.js +154 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLocalRolloutExecution.js +1006 -0
- package/dist/orchestrator/src/cli/control/providerPollingHealth.js +435 -0
- package/dist/orchestrator/src/cli/control/providerTerminalCleanup.js +516 -0
- package/dist/orchestrator/src/cli/control/providerWorkerHosts.js +191 -0
- package/dist/orchestrator/src/cli/control/providerWorkflowConfigStore.js +515 -0
- package/dist/orchestrator/src/cli/control/questionChildResolutionAdapter.js +361 -0
- package/dist/orchestrator/src/cli/control/questionQueueController.js +181 -0
- package/dist/orchestrator/src/cli/control/questionReadRetryDeduplication.js +9 -0
- package/dist/orchestrator/src/cli/control/questionReadSequence.js +10 -0
- package/dist/orchestrator/src/cli/control/securityViolationController.js +27 -0
- package/dist/orchestrator/src/cli/control/selectedRunProjection.js +1838 -0
- package/dist/orchestrator/src/cli/control/telegramOversightApiClient.js +48 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridge.js +180 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeProjectionDeliveryQueue.js +25 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeRuntimeLifecycle.js +45 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeStateStore.js +77 -0
- package/dist/orchestrator/src/cli/control/telegramOversightControlActionApiClient.js +45 -0
- package/dist/orchestrator/src/cli/control/trackerDispatchPilot.js +439 -0
- package/dist/orchestrator/src/cli/control/uiDataController.js +34 -0
- package/dist/orchestrator/src/cli/control/uiSessionController.js +100 -0
- package/dist/orchestrator/src/cli/controlHostCliShell.js +860 -0
- package/dist/orchestrator/src/cli/controlHostFreshnessGaugeCliShell.js +129 -0
- package/dist/orchestrator/src/cli/controlHostSupervisionCliShell.js +2127 -0
- package/dist/orchestrator/src/cli/delegationCliShell.js +62 -0
- package/dist/orchestrator/src/cli/delegationServer.js +567 -678
- package/dist/orchestrator/src/cli/delegationServerCliShell.js +52 -0
- package/dist/orchestrator/src/cli/delegationServerQuestionFlowShell.js +228 -0
- package/dist/orchestrator/src/cli/delegationServerToolDispatchShell.js +411 -0
- package/dist/orchestrator/src/cli/delegationServerTransport.js +274 -0
- package/dist/orchestrator/src/cli/delegationSetup.js +51 -171
- package/dist/orchestrator/src/cli/devtoolsCliShell.js +34 -0
- package/dist/orchestrator/src/cli/doctor.js +542 -122
- package/dist/orchestrator/src/cli/doctorCliRequestShell.js +72 -0
- package/dist/orchestrator/src/cli/doctorCliShell.js +138 -0
- package/dist/orchestrator/src/cli/doctorUsage.js +136 -16
- package/dist/orchestrator/src/cli/exec/experience.js +16 -2
- package/dist/orchestrator/src/cli/exec/summary.js +3 -0
- package/dist/orchestrator/src/cli/execCliShell.js +51 -0
- package/dist/orchestrator/src/cli/flowCliRequestShell.js +44 -0
- package/dist/orchestrator/src/cli/flowCliShell.js +239 -0
- package/dist/orchestrator/src/cli/frontendTestCliRequestShell.js +80 -0
- package/dist/orchestrator/src/cli/frontendTestCliShell.js +41 -0
- package/dist/orchestrator/src/cli/init.js +1 -0
- package/dist/orchestrator/src/cli/initCliShell.js +50 -0
- package/dist/orchestrator/src/cli/linearCliShell.js +1200 -0
- package/dist/orchestrator/src/cli/mcpEnableCliShell.js +132 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +3 -2
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +56 -0
- package/dist/orchestrator/src/cli/orchestrator.js +66 -1376
- package/dist/orchestrator/src/cli/planCliShell.js +19 -0
- package/dist/orchestrator/src/cli/prCliShell.js +41 -0
- package/dist/orchestrator/src/cli/providerLinearChildLanePhaseContract.js +204 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +1772 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneShell.js +2420 -0
- package/dist/orchestrator/src/cli/providerLinearChildStreamShell.js +385 -0
- package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +5738 -0
- package/dist/orchestrator/src/cli/resumeCliShell.js +14 -0
- package/dist/orchestrator/src/cli/reviewCliLaunchShell.js +72 -0
- package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
- package/dist/orchestrator/src/cli/rlm/context.js +94 -7
- package/dist/orchestrator/src/cli/rlm/rlmCodexRuntimeShell.js +546 -0
- package/dist/orchestrator/src/cli/rlm/symbolic.js +4 -2
- package/dist/orchestrator/src/cli/rlmCliRequestShell.js +42 -0
- package/dist/orchestrator/src/cli/rlmCompletionCliShell.js +46 -0
- package/dist/orchestrator/src/cli/rlmLaunchCliShell.js +51 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +83 -523
- package/dist/orchestrator/src/cli/run/blockMemory.js +500 -0
- package/dist/orchestrator/src/cli/run/manifest.js +410 -73
- package/dist/orchestrator/src/cli/run/manifestPersister.js +45 -14
- package/dist/orchestrator/src/cli/run/runMemoryController.js +216 -0
- package/dist/orchestrator/src/cli/run/source0.js +690 -0
- package/dist/orchestrator/src/cli/run/workspacePath.js +101 -0
- package/dist/orchestrator/src/cli/runtime/mode.js +2 -1
- package/dist/orchestrator/src/cli/runtime/provider.js +39 -2
- package/dist/orchestrator/src/cli/selfCheckCliShell.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +668 -18
- package/dist/orchestrator/src/cli/services/execRuntime.js +66 -1
- package/dist/orchestrator/src/cli/services/orchestratorAutoScoutEvidenceRecorder.js +71 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudBranchResolution.js +8 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudEnvironmentResolution.js +22 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudExecutionLifecycleShell.js +39 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudPromptBuilder.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteFallbackContract.js +45 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteShell.js +36 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudTargetExecutor.js +277 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycle.js +98 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycleShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionLifecycle.js +112 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionModePolicy.js +27 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteAdapterShell.js +59 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteDecisionShell.js +57 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteState.js +21 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouter.js +2 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalPipelineExecutor.js +149 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalRouteShell.js +63 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanTargetTracker.js +16 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumePreparationShell.js +84 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumeTokenValidation.js +15 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleCompletion.js +31 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleExecutionRegistration.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleOrchestrationShell.js +83 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleTaskManagerShell.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRuntimeManifestMutation.js +20 -0
- package/dist/orchestrator/src/cli/services/orchestratorStartPreparationShell.js +56 -0
- package/dist/orchestrator/src/cli/services/orchestratorStatusShell.js +70 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +7 -3
- package/dist/orchestrator/src/cli/services/plannerMemory.js +119 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -3
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +9 -0
- package/dist/orchestrator/src/cli/setupBootstrapShell.js +114 -0
- package/dist/orchestrator/src/cli/setupCliShell.js +51 -0
- package/dist/orchestrator/src/cli/skillsCliShell.js +56 -0
- package/dist/orchestrator/src/cli/startCliRequestShell.js +53 -0
- package/dist/orchestrator/src/cli/startCliShell.js +68 -0
- package/dist/orchestrator/src/cli/statusCliShell.js +22 -0
- package/dist/orchestrator/src/cli/utils/authProvenanceFingerprint.js +27 -0
- package/dist/orchestrator/src/cli/utils/cloudPreflight.js +83 -1
- package/dist/orchestrator/src/cli/utils/delegationConfigParser.js +250 -0
- package/dist/orchestrator/src/cli/utils/delegationMcpHealth.js +1382 -0
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -54
- package/dist/orchestrator/src/cli/utils/mcpServerEntry.js +53 -0
- package/dist/orchestrator/src/cli/utils/packageProgramResolver.js +151 -0
- package/dist/orchestrator/src/cli/utils/providerOverrideEnv.js +71 -0
- package/dist/orchestrator/src/cli/utils/trailingJsonObject.js +59 -0
- package/dist/orchestrator/src/learning/crystalizer.js +2 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +233 -49
- package/dist/orchestrator/src/persistence/TaskStateStore.js +6 -6
- package/dist/orchestrator/src/persistence/lockFile.js +70 -4
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +39 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +3 -2
- package/dist/orchestrator/src/utils/atomicWrite.js +17 -2
- package/dist/packages/orchestrator/src/exec/unified-exec.js +99 -6
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +150 -19
- package/dist/packages/sdk-node/src/orchestrator.js +137 -13
- package/dist/packages/shared/config/designConfig.js +8 -1
- package/dist/packages/shared/streams/stdio.js +1 -1
- package/dist/scripts/design/pipeline/permit.js +15 -0
- package/dist/scripts/lib/docs-catalog.js +365 -0
- package/dist/scripts/lib/docs-helpers.js +87 -5
- package/dist/scripts/lib/pr-watch-merge.js +1088 -80
- package/dist/scripts/lib/provider-run-contract.js +26 -0
- package/dist/scripts/lib/review-command-intent-classification.js +532 -0
- package/dist/scripts/lib/review-command-probe-classification.js +385 -0
- package/dist/scripts/lib/review-execution-boundary-preflight.js +279 -0
- package/dist/scripts/lib/review-execution-runtime.js +753 -0
- package/dist/scripts/lib/review-execution-state.js +1144 -0
- package/dist/scripts/lib/review-execution-telemetry.js +215 -0
- package/dist/scripts/lib/review-inspection-target-parsing.js +78 -0
- package/dist/scripts/lib/review-launch-attempt.js +601 -0
- package/dist/scripts/lib/review-meta-surface-boundary-analysis.js +300 -0
- package/dist/scripts/lib/review-meta-surface-normalization.js +746 -0
- package/dist/scripts/lib/review-non-interactive-handoff.js +61 -0
- package/dist/scripts/lib/review-prompt-context.js +376 -0
- package/dist/scripts/lib/review-scope-advisory.js +286 -0
- package/dist/scripts/lib/review-scope-paths.js +123 -0
- package/dist/scripts/lib/review-shell-command-parser.js +389 -0
- package/dist/scripts/lib/review-shell-env-interpreter.js +340 -0
- package/dist/scripts/lib/run-manifests.js +192 -36
- package/dist/scripts/lib/spark-policy-classifier.js +593 -0
- package/dist/scripts/run-review.js +507 -1777
- package/docs/public/downstream-setup.md +106 -0
- package/docs/public/provider-onboarding.md +173 -0
- package/package.json +30 -11
- package/plugins/codex-orchestrator/.codex-plugin/plugin.json +30 -0
- package/plugins/codex-orchestrator/.mcp.json +13 -0
- package/plugins/codex-orchestrator/launcher.mjs +359 -0
- package/schemas/manifest.json +395 -0
- package/skills/chrome-devtools/SKILL.md +1 -1
- package/skills/codex-orchestrator/SKILL.md +83 -0
- package/skills/collab-subagents-first/SKILL.md +2 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +24 -11
- package/skills/delegation-usage/SKILL.md +20 -13
- package/skills/land/SKILL.md +77 -0
- package/skills/linear/SKILL.md +255 -0
- package/skills/release/SKILL.md +47 -3
- package/skills/standalone-review/SKILL.md +6 -1
- package/templates/README.md +4 -2
- package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
- package/templates/codex/.codex/agents/explorer-fast.toml +1 -0
- package/templates/codex/.codex/agents/worker-complex.toml +1 -1
- package/templates/codex/.codex/config.toml +3 -4
- package/templates/codex/.codex/providers/README.md +13 -0
- package/templates/codex/.codex/providers/control.example.json +18 -0
- package/templates/codex/.codex/providers/provider.env.example +15 -0
- package/templates/codex/AGENTS.md +12 -7
- package/templates/codex/mcp-client.json +5 -1
- package/docs/README.md +0 -307
- package/docs/assets/setup.gif +0 -0
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
6
|
+
import { transitionProviderLinearIssueState } from './providerLinearWorkflowFacade.js';
|
|
7
|
+
import { isLiveLinearTrackedIssueOwnedByCurrentViewerOrUnassigned, sortLiveLinearTrackedIssuesForDispatch } from './linearDispatchSource.js';
|
|
8
|
+
import { classifyProviderLinearWorkflowState, isProviderLinearTrackedIssueMutable, normalizeProviderLinearWorkflowState, providerLinearTodoBlockedByNonTerminal } from './providerLinearWorkflowStates.js';
|
|
9
|
+
import { cloneProviderOperatorAutopilotLifecycleRecord } from './providerOperatorAutopilotLifecycle.js';
|
|
10
|
+
import { cloneLocalRolloutExecutionAttempt, executeProviderOperatorAutopilotLocalRolloutActions, resolveEnabledLocalRolloutExecutionActionIds, resolveProviderOperatorAutopilotLocalRolloutExecutionConfig } from './providerOperatorAutopilotLocalRolloutExecution.js';
|
|
11
|
+
export const PROVIDER_OPERATOR_AUTOPILOT_AUDIT_FILENAME = 'provider-operator-autopilot.jsonl';
|
|
12
|
+
export const DEFAULT_PROVIDER_OPERATOR_AUTOPILOT_PENDING_SUMMARY = 'Merge closeout completed; local rollout follow-up may still be required.';
|
|
13
|
+
const DEFAULT_BACKLOG_STATE_NAME = 'Backlog';
|
|
14
|
+
const DEFAULT_READY_STATE_NAME = 'Ready';
|
|
15
|
+
const DEFAULT_REWORK_STATE_NAME = 'Rework';
|
|
16
|
+
const BLOCKED_STATE_NAME = 'blocked';
|
|
17
|
+
const CANONICAL_OWNER_MARKER_PREFIX = 'codex-orchestrator:canonical-owner-key=';
|
|
18
|
+
const SUPERSEDED_CANONICAL_OWNER_MARKER_PREFIX = 'codex-orchestrator:superseded-canonical-owner-key=';
|
|
19
|
+
const TERMINAL_BLOCKER_ADVISORY_CANONICAL_OWNER_KEY = 'blocked-terminal-blocker-cleanup-advisory';
|
|
20
|
+
const TERMINAL_BLOCKER_ADVISORY_CANONICAL_OWNER_MARKERS = new Set([
|
|
21
|
+
`${CANONICAL_OWNER_MARKER_PREFIX}${TERMINAL_BLOCKER_ADVISORY_CANONICAL_OWNER_KEY}`,
|
|
22
|
+
`${SUPERSEDED_CANONICAL_OWNER_MARKER_PREFIX}${TERMINAL_BLOCKER_ADVISORY_CANONICAL_OWNER_KEY}`
|
|
23
|
+
]);
|
|
24
|
+
const DEFAULT_BACKLOG_PROMOTION_SNAPSHOT_MAX_UNTRACKED_CYCLES = 3;
|
|
25
|
+
const DEFAULT_BACKLOG_PROMOTION_SNAPSHOT_TERMINAL_STATE_TYPES = [
|
|
26
|
+
'completed',
|
|
27
|
+
'canceled'
|
|
28
|
+
];
|
|
29
|
+
const DEFAULT_REWORK_EXCLUDED_ACTION_REQUIRED_REASONS = [
|
|
30
|
+
'draft',
|
|
31
|
+
'label:do-not-merge',
|
|
32
|
+
'review=REVIEW_REQUIRED',
|
|
33
|
+
'required_checks_query_failed'
|
|
34
|
+
];
|
|
35
|
+
const BACKLOG_PROMOTION_BLOCKING_CLAIM_STATES = new Set([
|
|
36
|
+
'accepted',
|
|
37
|
+
'starting',
|
|
38
|
+
'running',
|
|
39
|
+
'resuming',
|
|
40
|
+
'resumable'
|
|
41
|
+
]);
|
|
42
|
+
const FOLLOW_UP_PACKET_PREFIX_PATTERN = /Follow-up packet prefix:\s*`?(linear-[a-z0-9-]+)/iu;
|
|
43
|
+
const REVIEW_HANDOFF_REWORK_ELIGIBLE_CLAIM_STATES = new Set(['handoff_failed']);
|
|
44
|
+
export function resolveProviderOperatorAutopilotAuditPath(runDir) {
|
|
45
|
+
return join(runDir, PROVIDER_OPERATOR_AUTOPILOT_AUDIT_FILENAME);
|
|
46
|
+
}
|
|
47
|
+
export function resolveProviderOperatorAutopilotConfig(value) {
|
|
48
|
+
const record = asRecord(value);
|
|
49
|
+
const operatorAutopilot = asRecord(record?.operator_autopilot ?? record?.operatorAutopilot);
|
|
50
|
+
const enabled = readBoolean(operatorAutopilot, 'enabled') ?? false;
|
|
51
|
+
const backlogPromotion = asRecord(operatorAutopilot?.backlog_promotion ?? operatorAutopilot?.backlogPromotion);
|
|
52
|
+
const snapshotRetention = asRecord(backlogPromotion?.snapshot_retention ?? backlogPromotion?.snapshotRetention);
|
|
53
|
+
const reviewHandoffRework = asRecord(operatorAutopilot?.review_handoff_rework ?? operatorAutopilot?.reviewHandoffRework);
|
|
54
|
+
const postMergeRollout = asRecord(operatorAutopilot?.post_merge_rollout ?? operatorAutopilot?.postMergeRollout);
|
|
55
|
+
return {
|
|
56
|
+
enabled,
|
|
57
|
+
backlog_promotion: {
|
|
58
|
+
enabled: readBoolean(backlogPromotion, 'enabled') ?? enabled,
|
|
59
|
+
state_name: readNonEmptyString(backlogPromotion, 'state_name', 'stateName') ??
|
|
60
|
+
DEFAULT_BACKLOG_STATE_NAME,
|
|
61
|
+
target_state_name: readNonEmptyString(backlogPromotion, 'target_state_name', 'targetStateName') ??
|
|
62
|
+
DEFAULT_READY_STATE_NAME,
|
|
63
|
+
snapshot_retention: {
|
|
64
|
+
// Keep at least one missing-page cycle before pruning so CO-216 manual
|
|
65
|
+
// demotion suppression cannot be erased by a single temporary omission.
|
|
66
|
+
max_untracked_cycles: Math.max(2, readPositiveInteger(snapshotRetention, 'max_untracked_cycles', 'maxUntrackedCycles') ?? DEFAULT_BACKLOG_PROMOTION_SNAPSHOT_MAX_UNTRACKED_CYCLES),
|
|
67
|
+
terminal_state_types: normalizeStringArray(readStringArray(snapshotRetention, 'terminal_state_types', 'terminalStateTypes') ?? [...DEFAULT_BACKLOG_PROMOTION_SNAPSHOT_TERMINAL_STATE_TYPES])
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
review_handoff_rework: {
|
|
71
|
+
enabled: readBoolean(reviewHandoffRework, 'enabled') ?? enabled,
|
|
72
|
+
target_state_name: readNonEmptyString(reviewHandoffRework, 'target_state_name', 'targetStateName') ??
|
|
73
|
+
DEFAULT_REWORK_STATE_NAME,
|
|
74
|
+
excluded_action_required_reasons: readStringArray(reviewHandoffRework, 'excluded_action_required_reasons', 'excludedActionRequiredReasons') ?? [...DEFAULT_REWORK_EXCLUDED_ACTION_REQUIRED_REASONS]
|
|
75
|
+
},
|
|
76
|
+
post_merge_rollout: {
|
|
77
|
+
enabled: readBoolean(postMergeRollout, 'enabled') ?? enabled,
|
|
78
|
+
summary: readNonEmptyString(postMergeRollout, 'summary') ??
|
|
79
|
+
DEFAULT_PROVIDER_OPERATOR_AUTOPILOT_PENDING_SUMMARY,
|
|
80
|
+
execution: resolveProviderOperatorAutopilotLocalRolloutExecutionConfig(postMergeRollout)
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function appendProviderOperatorAutopilotAuditResult(auditPath, result) {
|
|
85
|
+
await mkdir(dirname(auditPath), { recursive: true });
|
|
86
|
+
await appendFile(auditPath, `${JSON.stringify(result)}\n`, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
export function areProviderOperatorAutopilotResultsMeaningfullyEqual(left, right) {
|
|
89
|
+
return JSON.stringify(normalizeComparableResult(left)) === JSON.stringify(normalizeComparableResult(right));
|
|
90
|
+
}
|
|
91
|
+
export async function runProviderOperatorAutopilot(input, deps = {}) {
|
|
92
|
+
const now = deps.now ?? isoTimestamp;
|
|
93
|
+
const transitionIssueState = deps.transition_issue_state ?? transitionProviderLinearIssueState;
|
|
94
|
+
const recordedAt = now();
|
|
95
|
+
if (!input.config.enabled) {
|
|
96
|
+
return {
|
|
97
|
+
recorded_at: recordedAt,
|
|
98
|
+
status: 'disabled',
|
|
99
|
+
summary: 'Operator autopilot is disabled.',
|
|
100
|
+
error: null,
|
|
101
|
+
actions: [],
|
|
102
|
+
holds: [],
|
|
103
|
+
pending_actions: [],
|
|
104
|
+
terminal_blocker_advisories: [],
|
|
105
|
+
resolved_actions: [],
|
|
106
|
+
lifecycle_records: [],
|
|
107
|
+
local_rollout_execution_attempts: [],
|
|
108
|
+
backlog_promotion_snapshots: [],
|
|
109
|
+
backlog_promotion_snapshot_retention_records: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const sortedTrackedIssues = sortLiveLinearTrackedIssuesForDispatch(input.tracked_issues);
|
|
113
|
+
const trackedIssuesById = new Map(sortedTrackedIssues.map((issue) => [issue.id, issue]));
|
|
114
|
+
const orderByIssueId = new Map(sortedTrackedIssues.map((issue, index) => [issue.id, index]));
|
|
115
|
+
const claimsByIssueId = new Map(input.claims.map((claim) => [claim.issue_id, claim]));
|
|
116
|
+
const actions = [];
|
|
117
|
+
const holds = [];
|
|
118
|
+
const terminalBlockerAdvisories = collectTerminalBlockerAdvisories(sortedTrackedIssues);
|
|
119
|
+
const pendingActions = collectPendingActions({
|
|
120
|
+
claims: input.claims,
|
|
121
|
+
config: input.config
|
|
122
|
+
});
|
|
123
|
+
const lifecycleRecords = (input.lifecycle_records ?? []).map(cloneProviderOperatorAutopilotLifecycleRecord);
|
|
124
|
+
const resolveBacklogPromotionSnapshotState = () => resolveNextBacklogPromotionSnapshots({
|
|
125
|
+
previousResult: input.previous_result ?? null,
|
|
126
|
+
trackedIssuesById,
|
|
127
|
+
actions,
|
|
128
|
+
holds,
|
|
129
|
+
targetStateName: input.config.backlog_promotion.target_state_name,
|
|
130
|
+
backlogStateName: input.config.backlog_promotion.state_name,
|
|
131
|
+
retentionConfig: input.config.backlog_promotion.snapshot_retention,
|
|
132
|
+
evaluatedAt: recordedAt
|
|
133
|
+
});
|
|
134
|
+
const buildResultWithBacklogPromotionSnapshotState = (result) => {
|
|
135
|
+
const snapshotState = resolveBacklogPromotionSnapshotState();
|
|
136
|
+
return {
|
|
137
|
+
...result,
|
|
138
|
+
backlog_promotion_snapshots: snapshotState.snapshots,
|
|
139
|
+
backlog_promotion_snapshot_retention_records: snapshotState.retention_records
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const reviewHandoffOutcome = await maybeRunReviewHandoffRework({
|
|
143
|
+
claims: input.claims,
|
|
144
|
+
trackedIssuesById,
|
|
145
|
+
orderByIssueId,
|
|
146
|
+
config: input.config,
|
|
147
|
+
recordedAt,
|
|
148
|
+
sourceSetup: input.source_setup ?? null,
|
|
149
|
+
env: input.env ?? process.env,
|
|
150
|
+
transitionIssueState
|
|
151
|
+
});
|
|
152
|
+
if (reviewHandoffOutcome.failed) {
|
|
153
|
+
const effectiveLocalRolloutActions = resolveEffectiveLocalRolloutActions({
|
|
154
|
+
pendingActions,
|
|
155
|
+
postMergeRolloutEnabled: input.config.post_merge_rollout.enabled,
|
|
156
|
+
lifecycleRecords
|
|
157
|
+
});
|
|
158
|
+
return buildResultWithBacklogPromotionSnapshotState({
|
|
159
|
+
recorded_at: recordedAt,
|
|
160
|
+
status: 'failed',
|
|
161
|
+
summary: reviewHandoffOutcome.summary,
|
|
162
|
+
error: reviewHandoffOutcome.error,
|
|
163
|
+
actions: [],
|
|
164
|
+
holds,
|
|
165
|
+
pending_actions: effectiveLocalRolloutActions.pending_actions,
|
|
166
|
+
terminal_blocker_advisories: terminalBlockerAdvisories,
|
|
167
|
+
resolved_actions: effectiveLocalRolloutActions.resolved_actions,
|
|
168
|
+
lifecycle_records: effectiveLocalRolloutActions.lifecycle_records,
|
|
169
|
+
local_rollout_execution_attempts: cloneLocalRolloutExecutionAttempts(input.local_rollout_execution_attempts)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (reviewHandoffOutcome.action) {
|
|
173
|
+
actions.push(reviewHandoffOutcome.action);
|
|
174
|
+
}
|
|
175
|
+
if (reviewHandoffOutcome.hold) {
|
|
176
|
+
holds.push(reviewHandoffOutcome.hold);
|
|
177
|
+
}
|
|
178
|
+
if (actions.length === 0) {
|
|
179
|
+
const backlogOutcome = await maybeRunBacklogPromotion({
|
|
180
|
+
sortedTrackedIssues,
|
|
181
|
+
claimsByIssueId,
|
|
182
|
+
config: input.config,
|
|
183
|
+
recordedAt,
|
|
184
|
+
previousResult: input.previous_result ?? null,
|
|
185
|
+
sourceSetup: input.source_setup ?? null,
|
|
186
|
+
env: input.env ?? process.env,
|
|
187
|
+
repoRoot: input.repo_root ?? process.cwd(),
|
|
188
|
+
transitionIssueState
|
|
189
|
+
});
|
|
190
|
+
if (backlogOutcome.failed) {
|
|
191
|
+
const effectiveLocalRolloutActions = resolveEffectiveLocalRolloutActions({
|
|
192
|
+
pendingActions,
|
|
193
|
+
postMergeRolloutEnabled: input.config.post_merge_rollout.enabled,
|
|
194
|
+
lifecycleRecords
|
|
195
|
+
});
|
|
196
|
+
return buildResultWithBacklogPromotionSnapshotState({
|
|
197
|
+
recorded_at: recordedAt,
|
|
198
|
+
status: 'failed',
|
|
199
|
+
summary: backlogOutcome.summary,
|
|
200
|
+
error: backlogOutcome.error,
|
|
201
|
+
actions,
|
|
202
|
+
holds,
|
|
203
|
+
pending_actions: effectiveLocalRolloutActions.pending_actions,
|
|
204
|
+
terminal_blocker_advisories: terminalBlockerAdvisories,
|
|
205
|
+
resolved_actions: effectiveLocalRolloutActions.resolved_actions,
|
|
206
|
+
lifecycle_records: effectiveLocalRolloutActions.lifecycle_records,
|
|
207
|
+
local_rollout_execution_attempts: cloneLocalRolloutExecutionAttempts(input.local_rollout_execution_attempts)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (backlogOutcome.action) {
|
|
211
|
+
actions.push(backlogOutcome.action);
|
|
212
|
+
}
|
|
213
|
+
if (backlogOutcome.hold) {
|
|
214
|
+
holds.push(backlogOutcome.hold);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let effectiveLocalRolloutActions = resolveEffectiveLocalRolloutActions({
|
|
218
|
+
pendingActions,
|
|
219
|
+
postMergeRolloutEnabled: input.config.post_merge_rollout.enabled,
|
|
220
|
+
lifecycleRecords
|
|
221
|
+
});
|
|
222
|
+
let localRolloutExecutionAttempts = cloneLocalRolloutExecutionAttempts(input.local_rollout_execution_attempts);
|
|
223
|
+
if (input.config.post_merge_rollout.execution.enabled &&
|
|
224
|
+
effectiveLocalRolloutActions.pending_actions.length > 0) {
|
|
225
|
+
if (!input.repo_root) {
|
|
226
|
+
return buildResultWithBacklogPromotionSnapshotState({
|
|
227
|
+
recorded_at: recordedAt,
|
|
228
|
+
status: 'failed',
|
|
229
|
+
summary: 'Local rollout execution is enabled but repo_root was not provided.',
|
|
230
|
+
error: 'missing_repo_root',
|
|
231
|
+
actions,
|
|
232
|
+
holds,
|
|
233
|
+
pending_actions: effectiveLocalRolloutActions.pending_actions,
|
|
234
|
+
terminal_blocker_advisories: terminalBlockerAdvisories,
|
|
235
|
+
resolved_actions: effectiveLocalRolloutActions.resolved_actions,
|
|
236
|
+
lifecycle_records: effectiveLocalRolloutActions.lifecycle_records,
|
|
237
|
+
local_rollout_execution_attempts: localRolloutExecutionAttempts
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const executionOutcome = await executeProviderOperatorAutopilotLocalRolloutActions({
|
|
241
|
+
pendingActions: effectiveLocalRolloutActions.pending_actions,
|
|
242
|
+
config: input.config.post_merge_rollout.execution,
|
|
243
|
+
repoRoot: input.repo_root,
|
|
244
|
+
priorAttempts: localRolloutExecutionAttempts
|
|
245
|
+
}, {
|
|
246
|
+
now,
|
|
247
|
+
runCommand: deps.run_local_rollout_command,
|
|
248
|
+
appendExecutionAttempt: deps.append_local_rollout_execution_attempt,
|
|
249
|
+
appendLifecycleRecord: deps.append_local_rollout_lifecycle_record
|
|
250
|
+
});
|
|
251
|
+
localRolloutExecutionAttempts = executionOutcome.attempts.map(cloneLocalRolloutExecutionAttempt);
|
|
252
|
+
if (executionOutcome.lifecycle_records.length > 0) {
|
|
253
|
+
lifecycleRecords.push(...executionOutcome.lifecycle_records.map(cloneProviderOperatorAutopilotLifecycleRecord));
|
|
254
|
+
effectiveLocalRolloutActions = resolveEffectiveLocalRolloutActions({
|
|
255
|
+
pendingActions,
|
|
256
|
+
postMergeRolloutEnabled: input.config.post_merge_rollout.enabled,
|
|
257
|
+
lifecycleRecords
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const hasTransitionedAction = actions.some((action) => action.transition.status === 'transitioned');
|
|
262
|
+
const status = hasTransitionedAction ||
|
|
263
|
+
effectiveLocalRolloutActions.pending_actions.length > 0 ||
|
|
264
|
+
effectiveLocalRolloutActions.resolved_actions.length > 0 ||
|
|
265
|
+
terminalBlockerAdvisories.length > 0
|
|
266
|
+
? 'acted'
|
|
267
|
+
: 'noop';
|
|
268
|
+
return buildResultWithBacklogPromotionSnapshotState({
|
|
269
|
+
recorded_at: recordedAt,
|
|
270
|
+
status,
|
|
271
|
+
summary: summarizeOperatorAutopilotResult({
|
|
272
|
+
actions,
|
|
273
|
+
holds,
|
|
274
|
+
pendingActions: effectiveLocalRolloutActions.pending_actions,
|
|
275
|
+
resolvedActions: effectiveLocalRolloutActions.resolved_actions,
|
|
276
|
+
terminalBlockerAdvisories
|
|
277
|
+
}),
|
|
278
|
+
error: null,
|
|
279
|
+
actions,
|
|
280
|
+
holds,
|
|
281
|
+
pending_actions: effectiveLocalRolloutActions.pending_actions,
|
|
282
|
+
terminal_blocker_advisories: terminalBlockerAdvisories,
|
|
283
|
+
resolved_actions: effectiveLocalRolloutActions.resolved_actions,
|
|
284
|
+
lifecycle_records: effectiveLocalRolloutActions.lifecycle_records,
|
|
285
|
+
local_rollout_execution_attempts: localRolloutExecutionAttempts
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async function maybeRunBacklogPromotion(input) {
|
|
289
|
+
if (!input.config.backlog_promotion.enabled) {
|
|
290
|
+
return { action: null, hold: null, failed: false, summary: '', error: null };
|
|
291
|
+
}
|
|
292
|
+
const backlogState = normalizeProviderLinearWorkflowState(input.config.backlog_promotion.state_name);
|
|
293
|
+
const candidateIndex = input.sortedTrackedIssues.findIndex((issue) => normalizeProviderLinearWorkflowState(issue.state) === backlogState &&
|
|
294
|
+
isProviderLinearTrackedIssueMutable(issue));
|
|
295
|
+
const candidate = candidateIndex >= 0 ? input.sortedTrackedIssues[candidateIndex] : null;
|
|
296
|
+
if (!candidate) {
|
|
297
|
+
return { action: null, hold: null, failed: false, summary: '', error: null };
|
|
298
|
+
}
|
|
299
|
+
const higherRankedBlockedQueueLane = candidateIndex > 0
|
|
300
|
+
? input.sortedTrackedIssues.slice(0, candidateIndex).find((issue) => {
|
|
301
|
+
const workflowState = classifyProviderLinearWorkflowState(issue);
|
|
302
|
+
return (isProviderLinearTrackedIssueMutable(issue) &&
|
|
303
|
+
workflowState.isTodo &&
|
|
304
|
+
providerLinearTodoBlockedByNonTerminal(issue.blocked_by));
|
|
305
|
+
}) ?? null
|
|
306
|
+
: null;
|
|
307
|
+
if (higherRankedBlockedQueueLane) {
|
|
308
|
+
return {
|
|
309
|
+
action: null,
|
|
310
|
+
hold: buildAutopilotHoldRecord({
|
|
311
|
+
kind: 'backlog_promotion',
|
|
312
|
+
issue: candidate,
|
|
313
|
+
reason: 'backlog_head_blocked_by_higher_ranked_lane',
|
|
314
|
+
summary: `Backlog head ${candidate.identifier} remains parked because higher-ranked queue lane ${higherRankedBlockedQueueLane.identifier} is still blocked by non-terminal work: ${formatBlockedBy(higherRankedBlockedQueueLane.blocked_by)}.`,
|
|
315
|
+
actionRequiredReasons: []
|
|
316
|
+
}),
|
|
317
|
+
failed: false,
|
|
318
|
+
summary: '',
|
|
319
|
+
error: null
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const existingClaim = input.claimsByIssueId.get(candidate.id) ?? null;
|
|
323
|
+
if (existingClaim && isBacklogPromotionBlockedByExistingClaimState(existingClaim.state)) {
|
|
324
|
+
return {
|
|
325
|
+
action: null,
|
|
326
|
+
hold: buildAutopilotHoldRecord({
|
|
327
|
+
kind: 'backlog_promotion',
|
|
328
|
+
issue: candidate,
|
|
329
|
+
reason: 'backlog_head_already_claimed',
|
|
330
|
+
summary: `Backlog head ${candidate.identifier} remains parked because an intake claim is already present (${existingClaim.state}).`,
|
|
331
|
+
actionRequiredReasons: []
|
|
332
|
+
}),
|
|
333
|
+
failed: false,
|
|
334
|
+
summary: '',
|
|
335
|
+
error: null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (!isLiveLinearTrackedIssueOwnedByCurrentViewerOrUnassigned(candidate)) {
|
|
339
|
+
return {
|
|
340
|
+
action: null,
|
|
341
|
+
hold: buildAutopilotHoldRecord({
|
|
342
|
+
kind: 'backlog_promotion',
|
|
343
|
+
issue: candidate,
|
|
344
|
+
reason: 'backlog_head_owned_by_other_operator',
|
|
345
|
+
summary: `Backlog head ${candidate.identifier} remains parked because it is assigned to another operator.`,
|
|
346
|
+
actionRequiredReasons: []
|
|
347
|
+
}),
|
|
348
|
+
failed: false,
|
|
349
|
+
summary: '',
|
|
350
|
+
error: null
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (providerLinearTodoBlockedByNonTerminal(candidate.blocked_by)) {
|
|
354
|
+
return {
|
|
355
|
+
action: null,
|
|
356
|
+
hold: buildAutopilotHoldRecord({
|
|
357
|
+
kind: 'backlog_promotion',
|
|
358
|
+
issue: candidate,
|
|
359
|
+
reason: 'backlog_head_blocked_by_non_terminal',
|
|
360
|
+
summary: `Backlog head ${candidate.identifier} remains parked because it is blocked by non-terminal work: ${formatBlockedBy(candidate.blocked_by)}.`,
|
|
361
|
+
actionRequiredReasons: []
|
|
362
|
+
}),
|
|
363
|
+
failed: false,
|
|
364
|
+
summary: '',
|
|
365
|
+
error: null
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (await isTraceabilityPendingFollowUpIssue(candidate, input.repoRoot)) {
|
|
369
|
+
return {
|
|
370
|
+
action: null,
|
|
371
|
+
hold: buildAutopilotHoldRecord({
|
|
372
|
+
kind: 'backlog_promotion',
|
|
373
|
+
issue: candidate,
|
|
374
|
+
reason: 'backlog_head_follow_up_traceability_pending',
|
|
375
|
+
summary: `Backlog head ${candidate.identifier} remains parked because follow-up traceability requires packet and registry mirror setup before leaving Backlog.`,
|
|
376
|
+
actionRequiredReasons: []
|
|
377
|
+
}),
|
|
378
|
+
failed: false,
|
|
379
|
+
summary: '',
|
|
380
|
+
error: null
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const previousBacklogPromotion = resolvePreviousBacklogPromotionSnapshot({
|
|
384
|
+
previousResult: input.previousResult,
|
|
385
|
+
issueId: candidate.id,
|
|
386
|
+
targetStateName: input.config.backlog_promotion.target_state_name
|
|
387
|
+
});
|
|
388
|
+
const explicitBacklogDemotion = resolveExplicitBacklogDemotionHold({
|
|
389
|
+
candidate,
|
|
390
|
+
previousBacklogPromotion,
|
|
391
|
+
backlogStateName: input.config.backlog_promotion.state_name,
|
|
392
|
+
targetStateName: input.config.backlog_promotion.target_state_name
|
|
393
|
+
});
|
|
394
|
+
if (explicitBacklogDemotion) {
|
|
395
|
+
return {
|
|
396
|
+
action: null,
|
|
397
|
+
hold: buildAutopilotHoldRecord({
|
|
398
|
+
kind: 'backlog_promotion',
|
|
399
|
+
issue: candidate,
|
|
400
|
+
reason: 'backlog_head_manual_demotion_unacknowledged',
|
|
401
|
+
summary: explicitBacklogDemotion.summary,
|
|
402
|
+
promotionAttemptedAt: explicitBacklogDemotion.promotion_attempted_at,
|
|
403
|
+
promotionIssueUpdatedAt: explicitBacklogDemotion.promotion_issue_updated_at,
|
|
404
|
+
forcePathUsed: explicitBacklogDemotion.force_path_used,
|
|
405
|
+
actionRequiredReasons: []
|
|
406
|
+
}),
|
|
407
|
+
failed: false,
|
|
408
|
+
summary: '',
|
|
409
|
+
error: null
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const transition = await input.transitionIssueState({
|
|
413
|
+
issueId: candidate.id,
|
|
414
|
+
stateName: input.config.backlog_promotion.target_state_name,
|
|
415
|
+
expectedStateName: candidate.state,
|
|
416
|
+
expectedStateType: candidate.state_type,
|
|
417
|
+
expectedUpdatedAt: candidate.updated_at,
|
|
418
|
+
sourceSetup: input.sourceSetup,
|
|
419
|
+
env: input.env
|
|
420
|
+
});
|
|
421
|
+
const transitionRecord = mapTransitionRecord({
|
|
422
|
+
transition,
|
|
423
|
+
attemptedAt: input.recordedAt,
|
|
424
|
+
previousState: candidate.state,
|
|
425
|
+
previousStateType: candidate.state_type,
|
|
426
|
+
previousUpdatedAt: candidate.updated_at,
|
|
427
|
+
targetStateName: input.config.backlog_promotion.target_state_name
|
|
428
|
+
});
|
|
429
|
+
if (!transition.ok) {
|
|
430
|
+
return {
|
|
431
|
+
action: null,
|
|
432
|
+
hold: null,
|
|
433
|
+
failed: true,
|
|
434
|
+
summary: `Backlog head ${candidate.identifier} could not transition to ${input.config.backlog_promotion.target_state_name}.`,
|
|
435
|
+
error: transitionRecord.error
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (transition.action === 'noop') {
|
|
439
|
+
return {
|
|
440
|
+
action: {
|
|
441
|
+
kind: 'backlog_promotion',
|
|
442
|
+
issue_id: candidate.id,
|
|
443
|
+
issue_identifier: candidate.identifier,
|
|
444
|
+
reason: 'backlog_head_already_promoted',
|
|
445
|
+
summary: `Backlog head ${candidate.identifier} already reflected ${input.config.backlog_promotion.target_state_name} when autopilot evaluated it.`,
|
|
446
|
+
transition: transitionRecord,
|
|
447
|
+
action_required_reasons: []
|
|
448
|
+
},
|
|
449
|
+
hold: null,
|
|
450
|
+
failed: false,
|
|
451
|
+
summary: '',
|
|
452
|
+
error: null
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
action: {
|
|
457
|
+
kind: 'backlog_promotion',
|
|
458
|
+
issue_id: candidate.id,
|
|
459
|
+
issue_identifier: candidate.identifier,
|
|
460
|
+
reason: 'backlog_head_promoted',
|
|
461
|
+
summary: `Promoted backlog head ${candidate.identifier} to ${input.config.backlog_promotion.target_state_name}.`,
|
|
462
|
+
transition: transitionRecord,
|
|
463
|
+
action_required_reasons: []
|
|
464
|
+
},
|
|
465
|
+
hold: null,
|
|
466
|
+
failed: false,
|
|
467
|
+
summary: '',
|
|
468
|
+
error: null
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function maybeRunReviewHandoffRework(input) {
|
|
472
|
+
if (!input.config.review_handoff_rework.enabled) {
|
|
473
|
+
return { action: null, hold: null, failed: false, summary: '', error: null };
|
|
474
|
+
}
|
|
475
|
+
const candidates = input.claims
|
|
476
|
+
.flatMap((claim) => {
|
|
477
|
+
const trackedIssue = input.trackedIssuesById.get(claim.issue_id) ?? null;
|
|
478
|
+
if (!trackedIssue || !classifyProviderLinearWorkflowState(trackedIssue).isHandoff) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
if (!isReviewHandoffReworkClaimEligible({ claim, trackedIssue })) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
const reviewPromotion = claim.review_promotion ?? null;
|
|
485
|
+
if (reviewPromotion?.status !== 'action_required') {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
const actionRequiredReasons = resolveReviewHandoffActionRequiredReasons(reviewPromotion);
|
|
489
|
+
const authorActionReasons = actionRequiredReasons.filter((reason) => isAuthorActionRequiredReason(reason, input.config.review_handoff_rework.excluded_action_required_reasons));
|
|
490
|
+
return [{
|
|
491
|
+
claim,
|
|
492
|
+
trackedIssue,
|
|
493
|
+
sortOrder: input.orderByIssueId.get(claim.issue_id) ?? Number.MAX_SAFE_INTEGER,
|
|
494
|
+
actionRequiredReasons,
|
|
495
|
+
authorActionReasons
|
|
496
|
+
}];
|
|
497
|
+
})
|
|
498
|
+
.sort((left, right) => {
|
|
499
|
+
if (left.sortOrder !== right.sortOrder) {
|
|
500
|
+
return left.sortOrder - right.sortOrder;
|
|
501
|
+
}
|
|
502
|
+
const leftKey = left.trackedIssue.identifier ?? left.claim.issue_id;
|
|
503
|
+
const rightKey = right.trackedIssue.identifier ?? right.claim.issue_id;
|
|
504
|
+
return leftKey.localeCompare(rightKey);
|
|
505
|
+
});
|
|
506
|
+
const parkedCandidate = candidates[0] ?? null;
|
|
507
|
+
if (!parkedCandidate) {
|
|
508
|
+
return { action: null, hold: null, failed: false, summary: '', error: null };
|
|
509
|
+
}
|
|
510
|
+
const candidate = candidates.find((candidateEntry) => candidateEntry.authorActionReasons.length > 0) ??
|
|
511
|
+
parkedCandidate;
|
|
512
|
+
const actionRequiredReasons = [...candidate.actionRequiredReasons];
|
|
513
|
+
const authorActionReasons = [...candidate.authorActionReasons];
|
|
514
|
+
if (authorActionReasons.length === 0) {
|
|
515
|
+
return {
|
|
516
|
+
action: null,
|
|
517
|
+
hold: buildAutopilotHoldRecord({
|
|
518
|
+
kind: 'review_handoff_rework',
|
|
519
|
+
issue: candidate.trackedIssue,
|
|
520
|
+
reason: 'review_handoff_non_author_action_required',
|
|
521
|
+
summary: `Review handoff ${candidate.trackedIssue.identifier} remains parked because the current blockers are not author-action-required: ${formatReasonList(actionRequiredReasons)}.`,
|
|
522
|
+
actionRequiredReasons: [...actionRequiredReasons]
|
|
523
|
+
}),
|
|
524
|
+
failed: false,
|
|
525
|
+
summary: '',
|
|
526
|
+
error: null
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const transition = await input.transitionIssueState({
|
|
530
|
+
issueId: candidate.trackedIssue.id,
|
|
531
|
+
stateName: input.config.review_handoff_rework.target_state_name,
|
|
532
|
+
expectedStateName: candidate.trackedIssue.state,
|
|
533
|
+
expectedStateType: candidate.trackedIssue.state_type,
|
|
534
|
+
expectedUpdatedAt: candidate.trackedIssue.updated_at,
|
|
535
|
+
sourceSetup: input.sourceSetup,
|
|
536
|
+
env: input.env
|
|
537
|
+
});
|
|
538
|
+
const transitionRecord = mapTransitionRecord({
|
|
539
|
+
transition,
|
|
540
|
+
attemptedAt: input.recordedAt,
|
|
541
|
+
previousState: candidate.trackedIssue.state,
|
|
542
|
+
previousStateType: candidate.trackedIssue.state_type,
|
|
543
|
+
previousUpdatedAt: candidate.trackedIssue.updated_at,
|
|
544
|
+
targetStateName: input.config.review_handoff_rework.target_state_name
|
|
545
|
+
});
|
|
546
|
+
if (!transition.ok) {
|
|
547
|
+
return {
|
|
548
|
+
action: null,
|
|
549
|
+
hold: null,
|
|
550
|
+
failed: true,
|
|
551
|
+
summary: `Review handoff ${candidate.trackedIssue.identifier} could not transition to ${input.config.review_handoff_rework.target_state_name}.`,
|
|
552
|
+
error: transitionRecord.error
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (transition.action === 'noop') {
|
|
556
|
+
return {
|
|
557
|
+
action: {
|
|
558
|
+
kind: 'review_handoff_rework',
|
|
559
|
+
issue_id: candidate.trackedIssue.id,
|
|
560
|
+
issue_identifier: candidate.trackedIssue.identifier,
|
|
561
|
+
reason: 'author_action_required_rework_already_applied',
|
|
562
|
+
summary: `Review handoff ${candidate.trackedIssue.identifier} already reflected ${input.config.review_handoff_rework.target_state_name} when autopilot evaluated author-action-required blockers: ${authorActionReasons.join(', ')}.`,
|
|
563
|
+
transition: transitionRecord,
|
|
564
|
+
action_required_reasons: [...authorActionReasons]
|
|
565
|
+
},
|
|
566
|
+
hold: null,
|
|
567
|
+
failed: false,
|
|
568
|
+
summary: '',
|
|
569
|
+
error: null
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
action: {
|
|
574
|
+
kind: 'review_handoff_rework',
|
|
575
|
+
issue_id: candidate.trackedIssue.id,
|
|
576
|
+
issue_identifier: candidate.trackedIssue.identifier,
|
|
577
|
+
reason: 'author_action_required_rework',
|
|
578
|
+
summary: `Moved review handoff ${candidate.trackedIssue.identifier} to ${input.config.review_handoff_rework.target_state_name} because author action is required: ${authorActionReasons.join(', ')}.`,
|
|
579
|
+
transition: transitionRecord,
|
|
580
|
+
action_required_reasons: [...authorActionReasons]
|
|
581
|
+
},
|
|
582
|
+
hold: null,
|
|
583
|
+
failed: false,
|
|
584
|
+
summary: '',
|
|
585
|
+
error: null
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function isReviewHandoffReworkClaimEligible(input) {
|
|
589
|
+
return (REVIEW_HANDOFF_REWORK_ELIGIBLE_CLAIM_STATES.has(input.claim.state) &&
|
|
590
|
+
isLiveLinearTrackedIssueOwnedByCurrentViewerOrUnassigned(input.trackedIssue));
|
|
591
|
+
}
|
|
592
|
+
function collectPendingActions(input) {
|
|
593
|
+
if (!input.config.post_merge_rollout.enabled) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
const pendingByIssueId = new Map();
|
|
597
|
+
for (const claim of input.claims) {
|
|
598
|
+
const mergeCloseout = claim.merge_closeout ?? null;
|
|
599
|
+
if (!mergeCloseout || mergeCloseout.status !== 'merged') {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const nextAction = buildLocalRolloutPendingAction({
|
|
603
|
+
claim,
|
|
604
|
+
summary: input.config.post_merge_rollout.summary,
|
|
605
|
+
executableActionIds: resolveEnabledLocalRolloutExecutionActionIds(input.config.post_merge_rollout.execution)
|
|
606
|
+
});
|
|
607
|
+
const existingAction = pendingByIssueId.get(claim.issue_id) ?? null;
|
|
608
|
+
if (existingAction &&
|
|
609
|
+
compareNullableIsoTimestamp(existingAction.merge_closeout_recorded_at, nextAction.merge_closeout_recorded_at) >= 0) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
pendingByIssueId.set(claim.issue_id, nextAction);
|
|
613
|
+
}
|
|
614
|
+
return [...pendingByIssueId.values()].sort((left, right) => (left.issue_identifier ?? left.issue_id).localeCompare(right.issue_identifier ?? right.issue_id));
|
|
615
|
+
}
|
|
616
|
+
function buildLocalRolloutPendingAction(input) {
|
|
617
|
+
const mergeCloseout = input.claim.merge_closeout;
|
|
618
|
+
if (!mergeCloseout || mergeCloseout.status !== 'merged') {
|
|
619
|
+
throw new Error('Cannot build local rollout pending action without merged closeout truth.');
|
|
620
|
+
}
|
|
621
|
+
const issueIdentifier = input.claim.issue_identifier ?? mergeCloseout.issue_identifier ?? null;
|
|
622
|
+
return {
|
|
623
|
+
kind: 'local_rollout',
|
|
624
|
+
action_instance_id: buildLocalRolloutActionInstanceId({
|
|
625
|
+
claim: input.claim,
|
|
626
|
+
mergeCloseout
|
|
627
|
+
}),
|
|
628
|
+
issue_id: input.claim.issue_id,
|
|
629
|
+
issue_identifier: issueIdentifier,
|
|
630
|
+
summary: `${input.summary} Merge closeout reason=${mergeCloseout.reason}; shared_root=${mergeCloseout.shared_root?.status ?? 'unknown'}; linear_transition=${mergeCloseout.linear_transition?.status ?? 'unknown'}.`,
|
|
631
|
+
merge_closeout_recorded_at: mergeCloseout.recorded_at,
|
|
632
|
+
merge_closeout_reason: mergeCloseout.reason,
|
|
633
|
+
shared_root_status: mergeCloseout.shared_root?.status ?? null,
|
|
634
|
+
linear_transition_status: mergeCloseout.linear_transition?.status ?? null,
|
|
635
|
+
executable_action_ids: [...input.executableActionIds],
|
|
636
|
+
lifecycle_state: 'pending',
|
|
637
|
+
lifecycle_actor: null,
|
|
638
|
+
lifecycle_reason: null,
|
|
639
|
+
lifecycle_recorded_at: null
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function buildLocalRolloutActionInstanceId(input) {
|
|
643
|
+
const identity = {
|
|
644
|
+
kind: 'local_rollout',
|
|
645
|
+
issue_id: input.claim.issue_id,
|
|
646
|
+
pr_url: input.mergeCloseout.pr?.url ?? null,
|
|
647
|
+
pr_number: input.mergeCloseout.pr?.number ?? null,
|
|
648
|
+
snapshot_merged_at: input.mergeCloseout.snapshot?.merged_at ?? null,
|
|
649
|
+
snapshot_head_oid: input.mergeCloseout.snapshot?.head_oid ?? null
|
|
650
|
+
};
|
|
651
|
+
const digest = createHash('sha256')
|
|
652
|
+
.update(JSON.stringify(identity))
|
|
653
|
+
.digest('hex')
|
|
654
|
+
.slice(0, 24);
|
|
655
|
+
return `local_rollout:${digest}`;
|
|
656
|
+
}
|
|
657
|
+
function groupLifecycleRecordsByActionInstanceId(records) {
|
|
658
|
+
const grouped = new Map();
|
|
659
|
+
for (const record of records) {
|
|
660
|
+
const current = grouped.get(record.action_instance_id) ?? [];
|
|
661
|
+
current.push(cloneProviderOperatorAutopilotLifecycleRecord(record));
|
|
662
|
+
grouped.set(record.action_instance_id, current);
|
|
663
|
+
}
|
|
664
|
+
for (const [actionInstanceId, actionRecords] of grouped) {
|
|
665
|
+
grouped.set(actionInstanceId, actionRecords.sort((left, right) => compareNullableIsoTimestamp(left.recorded_at, right.recorded_at)));
|
|
666
|
+
}
|
|
667
|
+
return grouped;
|
|
668
|
+
}
|
|
669
|
+
function compareNullableIsoTimestamp(left, right) {
|
|
670
|
+
if (left === right) {
|
|
671
|
+
return 0;
|
|
672
|
+
}
|
|
673
|
+
if (!left) {
|
|
674
|
+
return -1;
|
|
675
|
+
}
|
|
676
|
+
if (!right) {
|
|
677
|
+
return 1;
|
|
678
|
+
}
|
|
679
|
+
const leftTime = Date.parse(left);
|
|
680
|
+
const rightTime = Date.parse(right);
|
|
681
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
|
682
|
+
return leftTime - rightTime;
|
|
683
|
+
}
|
|
684
|
+
return left.localeCompare(right);
|
|
685
|
+
}
|
|
686
|
+
export function resolveEffectiveLocalRolloutActions(input) {
|
|
687
|
+
if (!input.postMergeRolloutEnabled) {
|
|
688
|
+
return { pending_actions: [], resolved_actions: [], lifecycle_records: [] };
|
|
689
|
+
}
|
|
690
|
+
const recordsByActionInstanceId = groupLifecycleRecordsByActionInstanceId(input.lifecycleRecords);
|
|
691
|
+
const pendingActionInstanceIds = new Set(input.pendingActions.map((action) => action.action_instance_id));
|
|
692
|
+
const pendingActions = [];
|
|
693
|
+
const resolvedActions = [];
|
|
694
|
+
for (const action of input.pendingActions) {
|
|
695
|
+
const lifecycleRecords = recordsByActionInstanceId.get(action.action_instance_id)?.map(cloneProviderOperatorAutopilotLifecycleRecord) ?? [];
|
|
696
|
+
const latestTerminalRecord = lifecycleRecords.filter(isTerminalLifecycleRecord).at(-1) ?? null;
|
|
697
|
+
if (latestTerminalRecord) {
|
|
698
|
+
resolvedActions.push({
|
|
699
|
+
...clonePendingActionRecord(action),
|
|
700
|
+
lifecycle_state: latestTerminalRecord.state,
|
|
701
|
+
lifecycle_actor: latestTerminalRecord.actor,
|
|
702
|
+
lifecycle_reason: latestTerminalRecord.reason,
|
|
703
|
+
lifecycle_recorded_at: latestTerminalRecord.recorded_at
|
|
704
|
+
});
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const latestRecord = lifecycleRecords.at(-1) ?? null;
|
|
708
|
+
if (latestRecord?.state === 'acknowledged') {
|
|
709
|
+
pendingActions.push({
|
|
710
|
+
...clonePendingActionRecord(action),
|
|
711
|
+
lifecycle_state: 'acknowledged',
|
|
712
|
+
lifecycle_actor: latestRecord.actor,
|
|
713
|
+
lifecycle_reason: latestRecord.reason,
|
|
714
|
+
lifecycle_recorded_at: latestRecord.recorded_at
|
|
715
|
+
});
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
pendingActions.push(clonePendingActionRecord(action));
|
|
719
|
+
}
|
|
720
|
+
const matchedLifecycleRecords = input.lifecycleRecords
|
|
721
|
+
.filter((record) => pendingActionInstanceIds.has(record.action_instance_id))
|
|
722
|
+
.map(cloneProviderOperatorAutopilotLifecycleRecord);
|
|
723
|
+
return {
|
|
724
|
+
pending_actions: pendingActions,
|
|
725
|
+
resolved_actions: resolvedActions,
|
|
726
|
+
lifecycle_records: matchedLifecycleRecords
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function isTerminalLifecycleRecord(record) {
|
|
730
|
+
return record.state === 'cleared' || record.state === 'dismissed';
|
|
731
|
+
}
|
|
732
|
+
function collectTerminalBlockerAdvisories(trackedIssues) {
|
|
733
|
+
return trackedIssues
|
|
734
|
+
.flatMap((issue) => {
|
|
735
|
+
if (normalizeProviderLinearWorkflowState(issue.state) !== BLOCKED_STATE_NAME) {
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
if (!isProviderLinearTrackedIssueMutable(issue)) {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
if (issue.blocked_by_truncated === true) {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
const blockers = issue.blocked_by ?? [];
|
|
745
|
+
if (blockers.length === 0 || !blockers.every(isTerminalBlocker)) {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
const canonicalOwnerHints = resolveCanonicalOwnerHints(issue);
|
|
749
|
+
const duplicateHints = resolveDuplicateHints(issue);
|
|
750
|
+
if (canonicalOwnerHints.length === 0 &&
|
|
751
|
+
duplicateHints.length === 0 &&
|
|
752
|
+
hasExternalPrBlockerHint(issue)) {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
const recommendedAction = duplicateHints.length > 0 || canonicalOwnerHints.length > 0
|
|
756
|
+
? 'duplicate_cleanup'
|
|
757
|
+
: 'ready_to_unblock';
|
|
758
|
+
const advisory = {
|
|
759
|
+
kind: 'terminal_blocker_cleanup',
|
|
760
|
+
issue_id: issue.id,
|
|
761
|
+
issue_identifier: issue.identifier,
|
|
762
|
+
issue_state: issue.state,
|
|
763
|
+
issue_state_type: issue.state_type,
|
|
764
|
+
issue_updated_at: issue.updated_at,
|
|
765
|
+
blockers: blockers.map((blocker) => ({
|
|
766
|
+
id: blocker.id,
|
|
767
|
+
identifier: blocker.identifier,
|
|
768
|
+
state: blocker.state,
|
|
769
|
+
state_type: blocker.state_type
|
|
770
|
+
})),
|
|
771
|
+
canonical_owner_hints: canonicalOwnerHints,
|
|
772
|
+
duplicate_hints: duplicateHints,
|
|
773
|
+
recommended_action: recommendedAction,
|
|
774
|
+
summary: buildTerminalBlockerAdvisorySummary({
|
|
775
|
+
issue,
|
|
776
|
+
blockers,
|
|
777
|
+
canonicalOwnerHints,
|
|
778
|
+
duplicateHints,
|
|
779
|
+
recommendedAction,
|
|
780
|
+
relationsTruncated: issue.relations_truncated === true
|
|
781
|
+
})
|
|
782
|
+
};
|
|
783
|
+
return [advisory];
|
|
784
|
+
})
|
|
785
|
+
.sort((left, right) => (left.issue_identifier ?? left.issue_id).localeCompare(right.issue_identifier ?? right.issue_id));
|
|
786
|
+
}
|
|
787
|
+
function isTerminalBlocker(blocker) {
|
|
788
|
+
return !providerLinearTodoBlockedByNonTerminal([blocker]);
|
|
789
|
+
}
|
|
790
|
+
function hasExternalPrBlockerHint(issue) {
|
|
791
|
+
const descriptionHint = classifyCurrentExternalPrBlockerText(normalizeOptionalString(issue.description) ?? '');
|
|
792
|
+
if (descriptionHint) {
|
|
793
|
+
return descriptionHint === 'blocked';
|
|
794
|
+
}
|
|
795
|
+
const activityHint = resolveLatestTrackedActivityExternalPrHint(issue.recent_activity ?? []);
|
|
796
|
+
return activityHint === 'blocked';
|
|
797
|
+
}
|
|
798
|
+
function resolveLatestTrackedActivityExternalPrHint(recentActivity) {
|
|
799
|
+
let latestHint = null;
|
|
800
|
+
let latestCreatedAtMs = Number.NEGATIVE_INFINITY;
|
|
801
|
+
let latestOrder = -1;
|
|
802
|
+
recentActivity.forEach((entry, order) => {
|
|
803
|
+
const summary = normalizeOptionalString(entry.summary);
|
|
804
|
+
if (!summary) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const hint = classifyCurrentExternalPrBlockerText(summary);
|
|
808
|
+
if (!hint) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const candidateMs = Date.parse(entry.created_at ?? '');
|
|
812
|
+
const createdAtMs = Number.isFinite(candidateMs) ? candidateMs : Number.NEGATIVE_INFINITY;
|
|
813
|
+
if (!latestHint ||
|
|
814
|
+
createdAtMs > latestCreatedAtMs ||
|
|
815
|
+
(createdAtMs === latestCreatedAtMs && order >= latestOrder)) {
|
|
816
|
+
latestHint = hint;
|
|
817
|
+
latestCreatedAtMs = createdAtMs;
|
|
818
|
+
latestOrder = order;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
return latestHint;
|
|
822
|
+
}
|
|
823
|
+
function classifyCurrentExternalPrBlockerText(value) {
|
|
824
|
+
const segments = splitExternalPrHintSegments(value);
|
|
825
|
+
let latestHint = null;
|
|
826
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
827
|
+
if (!EXTERNAL_PR_REFERENCE_PATTERN.test(segments[index] ?? '')) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
let endIndex = index + 1;
|
|
831
|
+
while (endIndex < segments.length &&
|
|
832
|
+
!EXTERNAL_PR_REFERENCE_PATTERN.test(segments[endIndex] ?? '')) {
|
|
833
|
+
endIndex += 1;
|
|
834
|
+
}
|
|
835
|
+
const hint = classifyExternalPrHintBlock(segments.slice(index, endIndex).join(' '));
|
|
836
|
+
if (hint) {
|
|
837
|
+
latestHint = hint;
|
|
838
|
+
}
|
|
839
|
+
index = endIndex - 1;
|
|
840
|
+
}
|
|
841
|
+
return latestHint;
|
|
842
|
+
}
|
|
843
|
+
const MARKDOWN_LINK_DOT_PLACEHOLDER = '__co_pr_link_dot__';
|
|
844
|
+
function splitExternalPrHintSegments(value) {
|
|
845
|
+
return value
|
|
846
|
+
.replace(/\]\(([^)]*)\)/gu, (_match, target) => `](${target.replaceAll('.', MARKDOWN_LINK_DOT_PLACEHOLDER)})`)
|
|
847
|
+
.split(/[\n.;]+/u)
|
|
848
|
+
.map((segment) => segment.replaceAll(MARKDOWN_LINK_DOT_PLACEHOLDER, '.').trim())
|
|
849
|
+
.filter((segment) => segment.length > 0);
|
|
850
|
+
}
|
|
851
|
+
const EXTERNAL_PR_REFERENCE_PATTERN = /\b(?:pr|pull request)\b(?:\s|`|\[|\]|\(|\))*#?\d+\b/iu;
|
|
852
|
+
const EXTERNAL_PR_REFERENCE_PATTERN_GLOBAL = /\b(?:pr|pull request)\b(?:\s|`|\[|\]|\(|\))*#?\d+\b/giu;
|
|
853
|
+
const EXTERNAL_PR_MERGE_BLOCKER_PATTERN = /\bclosed\s+unmerged\b|\bunmerged\b|\bnot\s+(?:yet\s+)?merged\b|\bneeds?\s+(?:to\s+)?be\s+merged\b|\bmerge\s+pending\b|\bpending\s+merge\b/giu;
|
|
854
|
+
function classifyExternalPrHintBlock(segment) {
|
|
855
|
+
let sawResolved = false;
|
|
856
|
+
for (const prSegment of splitExternalPrReferenceSegments(segment)) {
|
|
857
|
+
const hint = classifyExternalPrHintSegment(prSegment);
|
|
858
|
+
if (hint === 'blocked') {
|
|
859
|
+
return 'blocked';
|
|
860
|
+
}
|
|
861
|
+
if (hint === 'resolved') {
|
|
862
|
+
sawResolved = true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return sawResolved ? 'resolved' : null;
|
|
866
|
+
}
|
|
867
|
+
function splitExternalPrReferenceSegments(segment) {
|
|
868
|
+
const matches = [...segment.matchAll(EXTERNAL_PR_REFERENCE_PATTERN_GLOBAL)];
|
|
869
|
+
if (matches.length <= 1) {
|
|
870
|
+
return [segment];
|
|
871
|
+
}
|
|
872
|
+
return matches
|
|
873
|
+
.map((match, index) => {
|
|
874
|
+
const start = index === 0 ? 0 : (match.index ?? 0);
|
|
875
|
+
const end = matches[index + 1]?.index ?? segment.length;
|
|
876
|
+
return segment.slice(start, end).trim();
|
|
877
|
+
})
|
|
878
|
+
.filter((entry) => entry.length > 0);
|
|
879
|
+
}
|
|
880
|
+
function classifyExternalPrHintSegment(segment) {
|
|
881
|
+
if (!EXTERNAL_PR_REFERENCE_PATTERN.test(segment)) {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
const resolvedSignals = collectExternalPrHintSignals(segment, /\b(?:no longer|not|isn't|is not)\s+(?:block(?:ed|er|ing)?|pending|failing|dirty|draft)\b|\bchecks?\s+(?:passed|passing|green|clean)\b|\bunblocked\b|\bclosed\b(?!\s+unmerged\b)/giu, 'resolved');
|
|
885
|
+
const mergeBlockerSignals = collectExternalPrHintSignals(segment, EXTERNAL_PR_MERGE_BLOCKER_PATTERN, 'blocked', resolvedSignals);
|
|
886
|
+
if (mergeBlockerSignals.length > 0) {
|
|
887
|
+
return 'blocked';
|
|
888
|
+
}
|
|
889
|
+
const blockedSignals = collectExternalPrHintSignals(segment, /\b(?:block(?:ed|er|ing)?|wait(?:ing)?\s+(?:on|for)|pending|draft|dirty|fail(?:ed|ing)?|checks?\s+(?:fail(?:ed|ing)?|pending|red)|red\s+checks?)\b/giu, 'blocked', resolvedSignals);
|
|
890
|
+
const latestSignal = [...resolvedSignals, ...blockedSignals].sort((left, right) => left.index === right.index ? left.priority - right.priority : left.index - right.index).at(-1);
|
|
891
|
+
return latestSignal?.kind ?? null;
|
|
892
|
+
}
|
|
893
|
+
function collectExternalPrHintSignals(segment, pattern, kind, ignoredSpans = []) {
|
|
894
|
+
const signals = [];
|
|
895
|
+
for (const match of segment.matchAll(pattern)) {
|
|
896
|
+
const matched = match[0];
|
|
897
|
+
const start = match.index ?? -1;
|
|
898
|
+
if (start < 0 || matched.length === 0) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const end = start + matched.length;
|
|
902
|
+
if (ignoredSpans.some((span) => start < span.end && end > span.start)) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
signals.push({
|
|
906
|
+
kind,
|
|
907
|
+
index: start,
|
|
908
|
+
priority: kind === 'blocked' ? 1 : 0,
|
|
909
|
+
start,
|
|
910
|
+
end
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return signals;
|
|
914
|
+
}
|
|
915
|
+
function resolveCanonicalOwnerHints(issue) {
|
|
916
|
+
const description = normalizeOptionalString(issue.description);
|
|
917
|
+
if (!description) {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
const hints = new Set();
|
|
921
|
+
for (const markerPrefix of [
|
|
922
|
+
CANONICAL_OWNER_MARKER_PREFIX,
|
|
923
|
+
SUPERSEDED_CANONICAL_OWNER_MARKER_PREFIX
|
|
924
|
+
]) {
|
|
925
|
+
let cursor = 0;
|
|
926
|
+
while (cursor < description.length) {
|
|
927
|
+
const markerIndex = description.indexOf(markerPrefix, cursor);
|
|
928
|
+
if (markerIndex < 0) {
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
const markerStart = markerIndex;
|
|
932
|
+
let markerEnd = markerIndex + markerPrefix.length;
|
|
933
|
+
while (markerEnd < description.length &&
|
|
934
|
+
!/[\s`'")\]}]/u.test(description[markerEnd])) {
|
|
935
|
+
markerEnd += 1;
|
|
936
|
+
}
|
|
937
|
+
const marker = description.slice(markerStart, markerEnd);
|
|
938
|
+
if (TERMINAL_BLOCKER_ADVISORY_CANONICAL_OWNER_MARKERS.has(marker)) {
|
|
939
|
+
hints.add(marker);
|
|
940
|
+
}
|
|
941
|
+
cursor = markerEnd + 1;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return [...hints].sort();
|
|
945
|
+
}
|
|
946
|
+
function resolveDuplicateHints(issue) {
|
|
947
|
+
const hints = new Set();
|
|
948
|
+
for (const relation of issue.relations ?? []) {
|
|
949
|
+
const normalizedType = normalizeProviderLinearWorkflowState(relation.type);
|
|
950
|
+
if (normalizedType !== 'duplicate' &&
|
|
951
|
+
normalizedType !== 'duplicates' &&
|
|
952
|
+
normalizedType !== 'duplicated by' &&
|
|
953
|
+
normalizedType !== 'duplicate of') {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const identifier = relation.issue.identifier ?? relation.issue.id ?? 'unknown';
|
|
957
|
+
const state = relation.issue.state ?? relation.issue.state_type ?? 'unknown';
|
|
958
|
+
hints.add(`${relation.direction}:${relation.type ?? 'unknown'}:${identifier}:${state}`);
|
|
959
|
+
}
|
|
960
|
+
return [...hints].sort();
|
|
961
|
+
}
|
|
962
|
+
function buildTerminalBlockerAdvisorySummary(input) {
|
|
963
|
+
const issueIdentifier = input.issue.identifier ?? input.issue.id;
|
|
964
|
+
const action = input.recommendedAction === 'duplicate_cleanup'
|
|
965
|
+
? 'duplicate-cleanup candidate'
|
|
966
|
+
: 'ready-to-unblock candidate';
|
|
967
|
+
const hintParts = [
|
|
968
|
+
input.duplicateHints.length > 0
|
|
969
|
+
? `duplicate hints=${input.duplicateHints.join(', ')}`
|
|
970
|
+
: null,
|
|
971
|
+
input.canonicalOwnerHints.length > 0
|
|
972
|
+
? `canonical owner hints=${input.canonicalOwnerHints.join(', ')}`
|
|
973
|
+
: null,
|
|
974
|
+
input.relationsTruncated
|
|
975
|
+
? 'relation evidence may be truncated before duplicate hints are exhausted'
|
|
976
|
+
: null
|
|
977
|
+
].filter((part) => part !== null);
|
|
978
|
+
const hintSuffix = hintParts.length > 0 ? `; ${hintParts.join('; ')}` : '';
|
|
979
|
+
return `Blocked issue ${issueIdentifier} has only terminal blockers (${formatBlockedBy(input.blockers)}); recommend ${action}${hintSuffix}.`;
|
|
980
|
+
}
|
|
981
|
+
function summarizeOperatorAutopilotResult(input) {
|
|
982
|
+
const parts = [];
|
|
983
|
+
if (input.actions.length > 0) {
|
|
984
|
+
parts.push(input.actions.map((action) => action.summary).join(' '));
|
|
985
|
+
}
|
|
986
|
+
if (input.holds.length > 0) {
|
|
987
|
+
parts.push(input.holds.map((hold) => hold.summary).join(' '));
|
|
988
|
+
}
|
|
989
|
+
if (input.pendingActions.length > 0) {
|
|
990
|
+
const acknowledgedCount = input.pendingActions.filter((pendingAction) => pendingAction.lifecycle_state === 'acknowledged').length;
|
|
991
|
+
parts.push(input.pendingActions.length === 1
|
|
992
|
+
? `Surfaced 1 ${acknowledgedCount === 1 ? 'acknowledged ' : ''}pending local rollout action (${input.pendingActions[0].issue_identifier ?? input.pendingActions[0].issue_id}).`
|
|
993
|
+
: `Surfaced ${input.pendingActions.length} pending local rollout actions.`);
|
|
994
|
+
}
|
|
995
|
+
if (input.resolvedActions.length > 0) {
|
|
996
|
+
parts.push(input.resolvedActions.length === 1
|
|
997
|
+
? `Suppressed 1 ${input.resolvedActions[0].lifecycle_state} local rollout action (${input.resolvedActions[0].issue_identifier ?? input.resolvedActions[0].issue_id}).`
|
|
998
|
+
: `Suppressed ${input.resolvedActions.length} cleared or dismissed local rollout actions.`);
|
|
999
|
+
}
|
|
1000
|
+
if (input.terminalBlockerAdvisories.length > 0) {
|
|
1001
|
+
const duplicateCleanupCount = input.terminalBlockerAdvisories.filter((advisory) => advisory.recommended_action === 'duplicate_cleanup').length;
|
|
1002
|
+
const readyToUnblockCount = input.terminalBlockerAdvisories.length - duplicateCleanupCount;
|
|
1003
|
+
const issueList = input.terminalBlockerAdvisories
|
|
1004
|
+
.map((advisory) => advisory.issue_identifier ?? advisory.issue_id)
|
|
1005
|
+
.join(', ');
|
|
1006
|
+
parts.push(`Surfaced ${input.terminalBlockerAdvisories.length} Blocked terminal-blocker advisory candidate(s): ${duplicateCleanupCount} duplicate-cleanup, ${readyToUnblockCount} ready-to-unblock (${issueList}).`);
|
|
1007
|
+
}
|
|
1008
|
+
if (parts.length === 0) {
|
|
1009
|
+
return 'Operator autopilot evaluated the current queue and found no bounded action to take.';
|
|
1010
|
+
}
|
|
1011
|
+
return parts.join(' ');
|
|
1012
|
+
}
|
|
1013
|
+
function mapTransitionRecord(input) {
|
|
1014
|
+
if (!input.transition.ok) {
|
|
1015
|
+
return {
|
|
1016
|
+
status: 'failed',
|
|
1017
|
+
attempted_at: input.attemptedAt,
|
|
1018
|
+
previous_state: input.previousState,
|
|
1019
|
+
target_state: input.targetStateName,
|
|
1020
|
+
issue_state: input.previousState,
|
|
1021
|
+
issue_state_type: input.previousStateType,
|
|
1022
|
+
issue_updated_at: input.previousUpdatedAt,
|
|
1023
|
+
force_path_used: false,
|
|
1024
|
+
error: `${input.transition.error.code}: ${input.transition.error.message}`
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
status: input.transition.action === 'noop' ? 'noop' : 'transitioned',
|
|
1029
|
+
attempted_at: input.attemptedAt,
|
|
1030
|
+
previous_state: input.transition.previous_state?.name ?? input.previousState,
|
|
1031
|
+
target_state: input.transition.target_state.name,
|
|
1032
|
+
issue_state: input.transition.issue.state?.name ?? input.previousState,
|
|
1033
|
+
issue_state_type: input.transition.issue.state?.type ?? input.previousStateType,
|
|
1034
|
+
issue_updated_at: input.transition.issue.updated_at ?? input.previousUpdatedAt,
|
|
1035
|
+
force_path_used: input.transition.transition_guard?.force ?? false,
|
|
1036
|
+
error: null
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function buildAutopilotHoldRecord(input) {
|
|
1040
|
+
return {
|
|
1041
|
+
kind: input.kind,
|
|
1042
|
+
issue_id: input.issue?.id ?? null,
|
|
1043
|
+
issue_identifier: input.issue?.identifier ?? null,
|
|
1044
|
+
issue_state: input.issue?.state ?? null,
|
|
1045
|
+
issue_state_type: input.issue?.state_type ?? null,
|
|
1046
|
+
issue_updated_at: input.issue?.updated_at ?? null,
|
|
1047
|
+
promotion_attempted_at: input.promotionAttemptedAt ?? null,
|
|
1048
|
+
promotion_issue_updated_at: input.promotionIssueUpdatedAt ?? null,
|
|
1049
|
+
force_path_used: input.forcePathUsed ?? false,
|
|
1050
|
+
reason: input.reason,
|
|
1051
|
+
summary: input.summary,
|
|
1052
|
+
action_required_reasons: [...input.actionRequiredReasons]
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function resolvePreviousBacklogPromotionSnapshot(input) {
|
|
1056
|
+
const snapshot = collectBacklogPromotionSnapshotsFromResult(input.previousResult, input.targetStateName).find((candidate) => candidate.issue_id === input.issueId);
|
|
1057
|
+
if (!snapshot) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
attempted_at: snapshot.attempted_at,
|
|
1062
|
+
issue_updated_at: snapshot.issue_updated_at,
|
|
1063
|
+
force_path_used: snapshot.force_path_used
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function resolveNextBacklogPromotionSnapshots(input) {
|
|
1067
|
+
const normalizedBacklogState = normalizeProviderLinearWorkflowState(input.backlogStateName);
|
|
1068
|
+
const normalizedTargetState = normalizeProviderLinearWorkflowState(input.targetStateName);
|
|
1069
|
+
const snapshotsByIssueId = new Map();
|
|
1070
|
+
const retentionRecords = [];
|
|
1071
|
+
const prunedPreviousSnapshotIssueIds = new Set();
|
|
1072
|
+
// Repeat the minimum defensively for persisted/legacy configs loaded before
|
|
1073
|
+
// resolveProviderOperatorAutopilotConfig normalized snapshot_retention.
|
|
1074
|
+
const maxUntrackedCycles = Math.max(2, input.retentionConfig.max_untracked_cycles);
|
|
1075
|
+
for (const snapshot of collectBacklogPromotionSnapshotsFromResult(input.previousResult, input.targetStateName)) {
|
|
1076
|
+
const issue = input.trackedIssuesById.get(snapshot.issue_id) ?? null;
|
|
1077
|
+
const currentUntrackedCycles = normalizeNonNegativeInteger(snapshot.untracked_cycles);
|
|
1078
|
+
if (!issue) {
|
|
1079
|
+
const nextUntrackedCycles = currentUntrackedCycles + 1;
|
|
1080
|
+
const shouldPrune = nextUntrackedCycles >= maxUntrackedCycles;
|
|
1081
|
+
retentionRecords.push(buildBacklogPromotionSnapshotRetentionRecord({
|
|
1082
|
+
snapshot,
|
|
1083
|
+
evaluatedAt: input.evaluatedAt,
|
|
1084
|
+
decision: shouldPrune ? 'pruned' : 'retained',
|
|
1085
|
+
reason: shouldPrune
|
|
1086
|
+
? 'stale_untracked_cycle_limit'
|
|
1087
|
+
: 'temporarily_untracked',
|
|
1088
|
+
ageMs: calculateSnapshotAgeMs(input.evaluatedAt, snapshot.attempted_at),
|
|
1089
|
+
untrackedCycles: nextUntrackedCycles,
|
|
1090
|
+
maxUntrackedCycles,
|
|
1091
|
+
issue: null,
|
|
1092
|
+
terminalStateEvidence: false
|
|
1093
|
+
}));
|
|
1094
|
+
if (!shouldPrune) {
|
|
1095
|
+
snapshotsByIssueId.set(snapshot.issue_id, {
|
|
1096
|
+
...snapshot,
|
|
1097
|
+
untracked_cycles: nextUntrackedCycles
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const issueState = normalizeProviderLinearWorkflowState(issue?.state);
|
|
1103
|
+
const terminalStateEvidence = isBacklogPromotionSnapshotTerminalIssueState(issue, input.retentionConfig.terminal_state_types);
|
|
1104
|
+
if (terminalStateEvidence) {
|
|
1105
|
+
retentionRecords.push(buildBacklogPromotionSnapshotRetentionRecord({
|
|
1106
|
+
snapshot,
|
|
1107
|
+
evaluatedAt: input.evaluatedAt,
|
|
1108
|
+
decision: 'pruned',
|
|
1109
|
+
reason: 'terminal_state',
|
|
1110
|
+
ageMs: calculateSnapshotAgeMs(input.evaluatedAt, snapshot.attempted_at),
|
|
1111
|
+
untrackedCycles: 0,
|
|
1112
|
+
maxUntrackedCycles,
|
|
1113
|
+
issue,
|
|
1114
|
+
terminalStateEvidence: true
|
|
1115
|
+
}));
|
|
1116
|
+
prunedPreviousSnapshotIssueIds.add(snapshot.issue_id);
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (!isProviderLinearTrackedIssueMutable(issue)) {
|
|
1120
|
+
retentionRecords.push(buildBacklogPromotionSnapshotRetentionRecord({
|
|
1121
|
+
snapshot,
|
|
1122
|
+
evaluatedAt: input.evaluatedAt,
|
|
1123
|
+
decision: 'pruned',
|
|
1124
|
+
reason: 'tracked_archived_or_trashed',
|
|
1125
|
+
ageMs: calculateSnapshotAgeMs(input.evaluatedAt, snapshot.attempted_at),
|
|
1126
|
+
untrackedCycles: 0,
|
|
1127
|
+
maxUntrackedCycles,
|
|
1128
|
+
issue,
|
|
1129
|
+
terminalStateEvidence: false
|
|
1130
|
+
}));
|
|
1131
|
+
prunedPreviousSnapshotIssueIds.add(snapshot.issue_id);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if ((issueState === normalizedBacklogState || issueState === normalizedTargetState)) {
|
|
1135
|
+
const nextSnapshot = {
|
|
1136
|
+
...snapshot,
|
|
1137
|
+
issue_identifier: issue.identifier ?? snapshot.issue_identifier,
|
|
1138
|
+
untracked_cycles: 0
|
|
1139
|
+
};
|
|
1140
|
+
if (currentUntrackedCycles > 0) {
|
|
1141
|
+
retentionRecords.push(buildBacklogPromotionSnapshotRetentionRecord({
|
|
1142
|
+
snapshot: nextSnapshot,
|
|
1143
|
+
evaluatedAt: input.evaluatedAt,
|
|
1144
|
+
decision: 'retained',
|
|
1145
|
+
reason: 'tracked_state_reset_untracked_cycles',
|
|
1146
|
+
ageMs: calculateSnapshotAgeMs(input.evaluatedAt, snapshot.attempted_at),
|
|
1147
|
+
untrackedCycles: 0,
|
|
1148
|
+
maxUntrackedCycles,
|
|
1149
|
+
issue,
|
|
1150
|
+
terminalStateEvidence: false
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
snapshotsByIssueId.set(snapshot.issue_id, nextSnapshot);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
retentionRecords.push(buildBacklogPromotionSnapshotRetentionRecord({
|
|
1157
|
+
snapshot,
|
|
1158
|
+
evaluatedAt: input.evaluatedAt,
|
|
1159
|
+
decision: 'pruned',
|
|
1160
|
+
reason: 'tracked_non_backlog_non_target_state',
|
|
1161
|
+
ageMs: calculateSnapshotAgeMs(input.evaluatedAt, snapshot.attempted_at),
|
|
1162
|
+
untrackedCycles: 0,
|
|
1163
|
+
maxUntrackedCycles,
|
|
1164
|
+
issue,
|
|
1165
|
+
terminalStateEvidence: false
|
|
1166
|
+
}));
|
|
1167
|
+
prunedPreviousSnapshotIssueIds.add(snapshot.issue_id);
|
|
1168
|
+
}
|
|
1169
|
+
for (const action of input.actions) {
|
|
1170
|
+
const snapshot = buildBacklogPromotionSnapshotFromAction(action, input.targetStateName);
|
|
1171
|
+
if (snapshot) {
|
|
1172
|
+
prunedPreviousSnapshotIssueIds.delete(snapshot.issue_id);
|
|
1173
|
+
snapshotsByIssueId.set(snapshot.issue_id, snapshot);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const hold of input.holds) {
|
|
1177
|
+
const snapshot = buildBacklogPromotionSnapshotFromHold(hold, input.targetStateName);
|
|
1178
|
+
if (snapshot && !prunedPreviousSnapshotIssueIds.has(snapshot.issue_id)) {
|
|
1179
|
+
snapshotsByIssueId.set(snapshot.issue_id, snapshot);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
snapshots: sortBacklogPromotionSnapshots([...snapshotsByIssueId.values()]),
|
|
1184
|
+
retention_records: sortBacklogPromotionSnapshotRetentionRecords(retentionRecords)
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function buildBacklogPromotionSnapshotRetentionRecord(input) {
|
|
1188
|
+
return {
|
|
1189
|
+
issue_id: input.snapshot.issue_id,
|
|
1190
|
+
issue_identifier: input.issue?.identifier ?? input.snapshot.issue_identifier,
|
|
1191
|
+
target_state: input.snapshot.target_state,
|
|
1192
|
+
attempted_at: input.snapshot.attempted_at,
|
|
1193
|
+
issue_updated_at: input.snapshot.issue_updated_at,
|
|
1194
|
+
evaluated_at: input.evaluatedAt,
|
|
1195
|
+
decision: input.decision,
|
|
1196
|
+
reason: input.reason,
|
|
1197
|
+
age_ms: input.ageMs,
|
|
1198
|
+
untracked_cycles: input.untrackedCycles,
|
|
1199
|
+
max_untracked_cycles: input.maxUntrackedCycles,
|
|
1200
|
+
issue_state: input.issue?.state ?? null,
|
|
1201
|
+
issue_state_type: input.issue?.state_type ?? null,
|
|
1202
|
+
issue_archived_at: input.issue?.archived_at ?? null,
|
|
1203
|
+
issue_trashed: input.issue?.trashed ?? null,
|
|
1204
|
+
issue_observed_updated_at: input.issue?.updated_at ?? null,
|
|
1205
|
+
terminal_state_evidence: input.terminalStateEvidence,
|
|
1206
|
+
force_path_used: input.snapshot.force_path_used ?? false
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function isBacklogPromotionSnapshotTerminalIssueState(issue, terminalStateTypes) {
|
|
1210
|
+
const normalizedTerminalStateTypes = terminalStateTypes
|
|
1211
|
+
.map((stateType) => normalizeProviderLinearWorkflowState(stateType))
|
|
1212
|
+
.filter((stateType) => stateType !== null);
|
|
1213
|
+
const normalizedType = normalizeProviderLinearWorkflowState(issue.state_type);
|
|
1214
|
+
if (normalizedType !== null && normalizedTerminalStateTypes.includes(normalizedType)) {
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
const normalizedState = normalizeProviderLinearWorkflowState(issue.state);
|
|
1218
|
+
if (normalizedState !== null && normalizedTerminalStateTypes.includes(normalizedState)) {
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
return classifyProviderLinearWorkflowState(issue).isTerminal;
|
|
1222
|
+
}
|
|
1223
|
+
function calculateSnapshotAgeMs(evaluatedAt, attemptedAt) {
|
|
1224
|
+
const evaluatedAtMs = Date.parse(evaluatedAt);
|
|
1225
|
+
const attemptedAtMs = Date.parse(attemptedAt);
|
|
1226
|
+
if (!Number.isFinite(evaluatedAtMs) || !Number.isFinite(attemptedAtMs)) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
return Math.max(0, evaluatedAtMs - attemptedAtMs);
|
|
1230
|
+
}
|
|
1231
|
+
function sortBacklogPromotionSnapshots(snapshots) {
|
|
1232
|
+
return [...snapshots].sort((left, right) => (left.issue_identifier ?? left.issue_id).localeCompare(right.issue_identifier ?? right.issue_id));
|
|
1233
|
+
}
|
|
1234
|
+
function sortBacklogPromotionSnapshotRetentionRecords(records) {
|
|
1235
|
+
return [...records].sort((left, right) => (left.issue_identifier ?? left.issue_id).localeCompare(right.issue_identifier ?? right.issue_id));
|
|
1236
|
+
}
|
|
1237
|
+
function collectBacklogPromotionSnapshotsFromResult(result, targetStateName) {
|
|
1238
|
+
if (!result) {
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
const snapshotsByIssueId = new Map();
|
|
1242
|
+
const prunedSnapshotKeys = collectPrunedBacklogPromotionSnapshotKeysFromResult(result, targetStateName);
|
|
1243
|
+
const normalizedTarget = normalizeProviderLinearWorkflowState(targetStateName);
|
|
1244
|
+
for (const snapshot of result.backlog_promotion_snapshots ?? []) {
|
|
1245
|
+
if (normalizedTarget !== null &&
|
|
1246
|
+
normalizeProviderLinearWorkflowState(snapshot.target_state) === normalizedTarget &&
|
|
1247
|
+
normalizeOptionalString(snapshot.issue_id) &&
|
|
1248
|
+
normalizeOptionalString(snapshot.attempted_at)) {
|
|
1249
|
+
snapshotsByIssueId.set(snapshot.issue_id, {
|
|
1250
|
+
issue_id: snapshot.issue_id,
|
|
1251
|
+
issue_identifier: snapshot.issue_identifier ?? null,
|
|
1252
|
+
target_state: snapshot.target_state,
|
|
1253
|
+
attempted_at: snapshot.attempted_at,
|
|
1254
|
+
issue_updated_at: snapshot.issue_updated_at ?? null,
|
|
1255
|
+
force_path_used: snapshot.force_path_used ?? false,
|
|
1256
|
+
untracked_cycles: normalizeNonNegativeInteger(snapshot.untracked_cycles)
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (const action of result.actions) {
|
|
1261
|
+
const snapshot = buildBacklogPromotionSnapshotFromAction(action, targetStateName);
|
|
1262
|
+
if (snapshot && !prunedSnapshotKeys.has(buildBacklogPromotionSnapshotKey(snapshot))) {
|
|
1263
|
+
snapshotsByIssueId.set(snapshot.issue_id, snapshot);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
for (const hold of result.holds) {
|
|
1267
|
+
const snapshot = buildBacklogPromotionSnapshotFromHold(hold, targetStateName);
|
|
1268
|
+
if (snapshot && !prunedSnapshotKeys.has(buildBacklogPromotionSnapshotKey(snapshot))) {
|
|
1269
|
+
snapshotsByIssueId.set(snapshot.issue_id, snapshot);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return [...snapshotsByIssueId.values()];
|
|
1273
|
+
}
|
|
1274
|
+
function collectPrunedBacklogPromotionSnapshotKeysFromResult(result, targetStateName) {
|
|
1275
|
+
const keys = new Set();
|
|
1276
|
+
const normalizedTarget = normalizeProviderLinearWorkflowState(targetStateName);
|
|
1277
|
+
for (const record of result.backlog_promotion_snapshot_retention_records ?? []) {
|
|
1278
|
+
if (record.decision === 'pruned' &&
|
|
1279
|
+
normalizedTarget !== null &&
|
|
1280
|
+
normalizeProviderLinearWorkflowState(record.target_state) === normalizedTarget &&
|
|
1281
|
+
normalizeOptionalString(record.issue_id) &&
|
|
1282
|
+
normalizeOptionalString(record.attempted_at)) {
|
|
1283
|
+
keys.add(buildBacklogPromotionSnapshotKey({
|
|
1284
|
+
issue_id: record.issue_id,
|
|
1285
|
+
target_state: record.target_state,
|
|
1286
|
+
attempted_at: record.attempted_at
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return keys;
|
|
1291
|
+
}
|
|
1292
|
+
function buildBacklogPromotionSnapshotKey(input) {
|
|
1293
|
+
const normalizedTarget = normalizeProviderLinearWorkflowState(input.target_state) ?? input.target_state;
|
|
1294
|
+
return `${input.issue_id}\u0000${normalizedTarget}\u0000${input.attempted_at}`;
|
|
1295
|
+
}
|
|
1296
|
+
function buildBacklogPromotionSnapshotFromAction(action, targetStateName) {
|
|
1297
|
+
const normalizedTarget = normalizeProviderLinearWorkflowState(targetStateName);
|
|
1298
|
+
if (action.kind !== 'backlog_promotion' ||
|
|
1299
|
+
action.reason !== 'backlog_head_promoted' ||
|
|
1300
|
+
action.transition.status !== 'transitioned' ||
|
|
1301
|
+
normalizedTarget === null ||
|
|
1302
|
+
normalizeProviderLinearWorkflowState(action.transition.target_state) !== normalizedTarget) {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
issue_id: action.issue_id,
|
|
1307
|
+
issue_identifier: action.issue_identifier,
|
|
1308
|
+
target_state: action.transition.target_state,
|
|
1309
|
+
attempted_at: action.transition.attempted_at,
|
|
1310
|
+
issue_updated_at: action.transition.issue_updated_at,
|
|
1311
|
+
force_path_used: action.transition.force_path_used ?? false,
|
|
1312
|
+
untracked_cycles: 0
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function buildBacklogPromotionSnapshotFromHold(hold, targetStateName) {
|
|
1316
|
+
if (hold.kind !== 'backlog_promotion' ||
|
|
1317
|
+
hold.reason !== 'backlog_head_manual_demotion_unacknowledged' ||
|
|
1318
|
+
!hold.issue_id ||
|
|
1319
|
+
!hold.promotion_attempted_at) {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
issue_id: hold.issue_id,
|
|
1324
|
+
issue_identifier: hold.issue_identifier,
|
|
1325
|
+
target_state: targetStateName,
|
|
1326
|
+
attempted_at: hold.promotion_attempted_at,
|
|
1327
|
+
issue_updated_at: hold.promotion_issue_updated_at ?? null,
|
|
1328
|
+
force_path_used: hold.force_path_used ?? false,
|
|
1329
|
+
untracked_cycles: 0
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function resolveExplicitBacklogDemotionHold(input) {
|
|
1333
|
+
if (!input.previousBacklogPromotion) {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
const latestActivity = resolveMostRecentTrackedActivity(input.candidate.recent_activity);
|
|
1337
|
+
if (!latestActivity) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
const stateTransition = parseTrackedIssueStateTransitionSummary(latestActivity.summary);
|
|
1341
|
+
const normalizedBacklogState = normalizeProviderLinearWorkflowState(input.backlogStateName);
|
|
1342
|
+
const normalizedTargetState = normalizeProviderLinearWorkflowState(input.targetStateName);
|
|
1343
|
+
if (!stateTransition ||
|
|
1344
|
+
normalizedBacklogState === null ||
|
|
1345
|
+
normalizedTargetState === null ||
|
|
1346
|
+
normalizeProviderLinearWorkflowState(stateTransition.fromState) !== normalizedTargetState ||
|
|
1347
|
+
normalizeProviderLinearWorkflowState(stateTransition.toState) !== normalizedBacklogState) {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
const previousPromotionTimestamp = input.previousBacklogPromotion.issue_updated_at ?? input.previousBacklogPromotion.attempted_at;
|
|
1351
|
+
const demotionTimestamp = latestActivity.created_at ?? input.candidate.updated_at;
|
|
1352
|
+
if (compareNullableIsoTimestamp(demotionTimestamp, previousPromotionTimestamp) <= 0) {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
const actorFragment = latestActivity.actor_name ? ` by ${latestActivity.actor_name}` : '';
|
|
1356
|
+
const timestampFragment = latestActivity.created_at ? ` at ${latestActivity.created_at}` : '';
|
|
1357
|
+
return {
|
|
1358
|
+
summary: `Backlog head ${input.candidate.identifier} remains parked because autopilot last promoted it at ${previousPromotionTimestamp} and the latest issue activity is an explicit ${stateTransition.fromState} -> ${stateTransition.toState} demotion${actorFragment}${timestampFragment}; wait for a newer acknowledgement update before re-promoting.`,
|
|
1359
|
+
promotion_attempted_at: input.previousBacklogPromotion.attempted_at,
|
|
1360
|
+
promotion_issue_updated_at: input.previousBacklogPromotion.issue_updated_at,
|
|
1361
|
+
force_path_used: input.previousBacklogPromotion.force_path_used
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function resolveMostRecentTrackedActivity(recentActivity) {
|
|
1365
|
+
if (recentActivity.length === 0) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
return [...recentActivity].sort((left, right) => compareNullableIsoTimestamp(right.created_at, left.created_at))[0] ?? null;
|
|
1369
|
+
}
|
|
1370
|
+
function parseTrackedIssueStateTransitionSummary(summary) {
|
|
1371
|
+
const normalized = normalizeOptionalString(summary);
|
|
1372
|
+
if (!normalized || !normalized.startsWith('State ') || !normalized.includes(' -> ')) {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
const transition = normalized.slice('State '.length);
|
|
1376
|
+
const separatorIndex = transition.indexOf(' -> ');
|
|
1377
|
+
if (separatorIndex < 0) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
const fromState = normalizeOptionalString(transition.slice(0, separatorIndex));
|
|
1381
|
+
const toState = normalizeOptionalString(transition.slice(separatorIndex + ' -> '.length));
|
|
1382
|
+
if (!fromState && !toState) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
fromState,
|
|
1387
|
+
toState
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function isAuthorActionRequiredReason(reason, excludedReasons) {
|
|
1391
|
+
if (excludedReasons.includes(reason)) {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
return (reason === 'pr_closed_unmerged' ||
|
|
1395
|
+
reason.startsWith('review=') ||
|
|
1396
|
+
reason.startsWith('merge_state=') ||
|
|
1397
|
+
reason.startsWith('unresolved_threads=') ||
|
|
1398
|
+
reason.startsWith('unacknowledged_bot_feedback=') ||
|
|
1399
|
+
reason.startsWith('required_checks_failed=') ||
|
|
1400
|
+
reason.startsWith('checks_failed='));
|
|
1401
|
+
}
|
|
1402
|
+
function resolveReviewHandoffActionRequiredReasons(reviewPromotion) {
|
|
1403
|
+
const snapshotReasons = reviewPromotion?.snapshot?.action_required_reasons ?? [];
|
|
1404
|
+
if (snapshotReasons.length > 0) {
|
|
1405
|
+
return [...snapshotReasons];
|
|
1406
|
+
}
|
|
1407
|
+
const fallbackReason = normalizeOptionalString(reviewPromotion?.reason);
|
|
1408
|
+
return fallbackReason ? [fallbackReason] : [];
|
|
1409
|
+
}
|
|
1410
|
+
function normalizeComparableResult(result) {
|
|
1411
|
+
if (!result) {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
status: result.status,
|
|
1416
|
+
summary: result.summary,
|
|
1417
|
+
error: result.error,
|
|
1418
|
+
actions: result.actions,
|
|
1419
|
+
holds: result.holds,
|
|
1420
|
+
pending_actions: result.pending_actions,
|
|
1421
|
+
terminal_blocker_advisories: result.terminal_blocker_advisories ?? [],
|
|
1422
|
+
resolved_actions: result.resolved_actions ?? [],
|
|
1423
|
+
lifecycle_records: result.lifecycle_records ?? [],
|
|
1424
|
+
local_rollout_execution_attempts: result.local_rollout_execution_attempts ?? [],
|
|
1425
|
+
backlog_promotion_snapshots: result.backlog_promotion_snapshots ?? [],
|
|
1426
|
+
backlog_promotion_snapshot_retention_records: result.backlog_promotion_snapshot_retention_records ?? []
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function cloneLocalRolloutExecutionAttempts(attempts) {
|
|
1430
|
+
return (attempts ?? []).map(cloneLocalRolloutExecutionAttempt);
|
|
1431
|
+
}
|
|
1432
|
+
function clonePendingActionRecord(record) {
|
|
1433
|
+
return {
|
|
1434
|
+
kind: record.kind,
|
|
1435
|
+
action_instance_id: record.action_instance_id,
|
|
1436
|
+
issue_id: record.issue_id,
|
|
1437
|
+
issue_identifier: record.issue_identifier,
|
|
1438
|
+
summary: record.summary,
|
|
1439
|
+
merge_closeout_recorded_at: record.merge_closeout_recorded_at,
|
|
1440
|
+
merge_closeout_reason: record.merge_closeout_reason,
|
|
1441
|
+
shared_root_status: record.shared_root_status,
|
|
1442
|
+
linear_transition_status: record.linear_transition_status,
|
|
1443
|
+
executable_action_ids: [...(record.executable_action_ids ?? [])],
|
|
1444
|
+
lifecycle_state: record.lifecycle_state,
|
|
1445
|
+
lifecycle_actor: record.lifecycle_actor,
|
|
1446
|
+
lifecycle_reason: record.lifecycle_reason,
|
|
1447
|
+
lifecycle_recorded_at: record.lifecycle_recorded_at
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function formatBlockedBy(blockedBy) {
|
|
1451
|
+
const blockers = (blockedBy ?? []).map((blocker) => {
|
|
1452
|
+
const identifier = normalizeOptionalString(blocker.identifier) ?? 'unknown';
|
|
1453
|
+
const state = normalizeOptionalString(blocker.state) ?? blocker.state_type ?? 'unknown';
|
|
1454
|
+
return `${identifier}:${state}`;
|
|
1455
|
+
});
|
|
1456
|
+
return blockers.length > 0 ? blockers.join(', ') : 'unknown blockers';
|
|
1457
|
+
}
|
|
1458
|
+
function formatReasonList(reasons) {
|
|
1459
|
+
return reasons.length > 0 ? reasons.join(', ') : 'unclassified action_required';
|
|
1460
|
+
}
|
|
1461
|
+
function isBacklogPromotionBlockedByExistingClaimState(state) {
|
|
1462
|
+
return typeof state === 'string' && BACKLOG_PROMOTION_BLOCKING_CLAIM_STATES.has(state);
|
|
1463
|
+
}
|
|
1464
|
+
async function isTraceabilityPendingFollowUpIssue(issue, repoRoot) {
|
|
1465
|
+
const description = typeof issue.description === 'string' ? issue.description : '';
|
|
1466
|
+
const followUpTaskId = description.match(FOLLOW_UP_PACKET_PREFIX_PATTERN)?.[1] ?? null;
|
|
1467
|
+
if (!followUpTaskId) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
const packetPaths = [
|
|
1471
|
+
`docs/PRD-${followUpTaskId}.md`,
|
|
1472
|
+
`docs/TECH_SPEC-${followUpTaskId}.md`,
|
|
1473
|
+
`docs/ACTION_PLAN-${followUpTaskId}.md`,
|
|
1474
|
+
`tasks/specs/${followUpTaskId}.md`,
|
|
1475
|
+
`tasks/tasks-${followUpTaskId}.md`,
|
|
1476
|
+
`.agent/task/${followUpTaskId}.md`
|
|
1477
|
+
];
|
|
1478
|
+
for (const path of packetPaths) {
|
|
1479
|
+
if (!await fileExists(join(repoRoot, path))) {
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const registryMirrorPaths = [
|
|
1484
|
+
'tasks/index.json',
|
|
1485
|
+
'docs/TASKS.md',
|
|
1486
|
+
'docs/docs-freshness-registry.json'
|
|
1487
|
+
];
|
|
1488
|
+
for (const path of registryMirrorPaths) {
|
|
1489
|
+
const content = await readTextFileIfPresent(join(repoRoot, path));
|
|
1490
|
+
if (!content?.includes(followUpTaskId)) {
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
function asRecord(value) {
|
|
1497
|
+
return value && typeof value === 'object' ? value : null;
|
|
1498
|
+
}
|
|
1499
|
+
async function fileExists(path) {
|
|
1500
|
+
try {
|
|
1501
|
+
await access(path);
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
catch {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function readTextFileIfPresent(path) {
|
|
1509
|
+
try {
|
|
1510
|
+
return await readFile(path, 'utf8');
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function readBoolean(record, ...keys) {
|
|
1517
|
+
for (const key of keys) {
|
|
1518
|
+
const value = record?.[key];
|
|
1519
|
+
if (typeof value === 'boolean') {
|
|
1520
|
+
return value;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
function readNonEmptyString(record, ...keys) {
|
|
1526
|
+
for (const key of keys) {
|
|
1527
|
+
const value = record?.[key];
|
|
1528
|
+
if (typeof value === 'string') {
|
|
1529
|
+
const trimmed = value.trim();
|
|
1530
|
+
if (trimmed.length > 0) {
|
|
1531
|
+
return trimmed;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
function readStringArray(record, ...keys) {
|
|
1538
|
+
for (const key of keys) {
|
|
1539
|
+
const value = record?.[key];
|
|
1540
|
+
if (!Array.isArray(value)) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const normalized = value
|
|
1544
|
+
.flatMap((item) => (typeof item === 'string' ? [item.trim()] : []))
|
|
1545
|
+
.filter((item) => item.length > 0);
|
|
1546
|
+
return normalized;
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
function readPositiveInteger(record, ...keys) {
|
|
1551
|
+
for (const key of keys) {
|
|
1552
|
+
const value = record?.[key];
|
|
1553
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
1554
|
+
return value;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
function normalizeNonNegativeInteger(value) {
|
|
1560
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0
|
|
1561
|
+
? value
|
|
1562
|
+
: 0;
|
|
1563
|
+
}
|
|
1564
|
+
function normalizeStringArray(values) {
|
|
1565
|
+
const normalized = values
|
|
1566
|
+
.map((value) => value.trim())
|
|
1567
|
+
.filter((value) => value.length > 0);
|
|
1568
|
+
// Empty terminal_state_types is treated as unset; disabling terminal-state
|
|
1569
|
+
// pruning is not supported because terminal evidence is the safest prune path.
|
|
1570
|
+
return normalized.length > 0
|
|
1571
|
+
? normalized
|
|
1572
|
+
: [...DEFAULT_BACKLOG_PROMOTION_SNAPSHOT_TERMINAL_STATE_TYPES];
|
|
1573
|
+
}
|
|
1574
|
+
function normalizeOptionalString(value) {
|
|
1575
|
+
if (typeof value !== 'string') {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
const trimmed = value.trim();
|
|
1579
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1580
|
+
}
|