@kbediako/codex-orchestrator 0.1.38 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +70 -301
- 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 +119 -15
- 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 +667 -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 +20 -10
- 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 +394 -0
- package/skills/collab-subagents-first/SKILL.md +1 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +24 -11
- package/skills/delegation-usage/SKILL.md +19 -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/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 -310
- package/docs/assets/setup.gif +0 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { getProviderLinearIssueContext } from './providerLinearWorkflowFacade.js';
|
|
6
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export const DEFAULT_PROVIDER_TERMINAL_CLEANUP_COMMENT_TEMPLATE = 'Closing because the Linear issue for branch {{branch}} entered a terminal state without merge.';
|
|
9
|
+
const PROVIDER_TERMINAL_CLEANUP_COMMAND_TIMEOUT_MS = 5_000;
|
|
10
|
+
const PROVIDER_TERMINAL_CLEANUP_TOTAL_BUDGET_MS = 15_000;
|
|
11
|
+
const PROVIDER_TERMINAL_CLEANUP_MAX_ATTACHED_PRS = 20;
|
|
12
|
+
export function resolveProviderTerminalCleanupConfig(value) {
|
|
13
|
+
const record = asRecord(value);
|
|
14
|
+
const terminalCleanup = asRecord(record?.terminal_cleanup ?? record?.terminalCleanup);
|
|
15
|
+
const enabled = readBoolean(terminalCleanup, 'enabled') ?? false;
|
|
16
|
+
const closeAttachedPr = asRecord(terminalCleanup?.close_attached_pr ?? terminalCleanup?.closeAttachedPr);
|
|
17
|
+
return {
|
|
18
|
+
enabled,
|
|
19
|
+
closeAttachedPr: {
|
|
20
|
+
enabled: readBoolean(closeAttachedPr, 'enabled') ?? enabled,
|
|
21
|
+
commentTemplate: readNonEmptyString(closeAttachedPr, 'comment_template', 'commentTemplate') ??
|
|
22
|
+
DEFAULT_PROVIDER_TERMINAL_CLEANUP_COMMENT_TEMPLATE
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function runProviderTerminalCleanup(input, deps = {}) {
|
|
27
|
+
const attemptedAt = deps.now?.() ?? isoTimestamp();
|
|
28
|
+
const env = input.env ?? process.env;
|
|
29
|
+
const readIssueContext = deps.readIssueContext ?? getProviderLinearIssueContext;
|
|
30
|
+
const runCommand = deps.runCommand ?? runProviderTerminalCleanupCommand;
|
|
31
|
+
const issueIdentifier = normalizeOptionalString(input.issueIdentifier);
|
|
32
|
+
const nowMs = deps.nowMs ?? Date.now;
|
|
33
|
+
if (!input.config.enabled) {
|
|
34
|
+
return buildResult({
|
|
35
|
+
attemptedAt,
|
|
36
|
+
issueId: input.issueId,
|
|
37
|
+
issueIdentifier,
|
|
38
|
+
workspacePath: input.workspacePath,
|
|
39
|
+
status: 'disabled',
|
|
40
|
+
summary: 'Terminal cleanup hook is disabled.',
|
|
41
|
+
branch: null
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!input.config.closeAttachedPr.enabled) {
|
|
45
|
+
return buildResult({
|
|
46
|
+
attemptedAt,
|
|
47
|
+
issueId: input.issueId,
|
|
48
|
+
issueIdentifier,
|
|
49
|
+
workspacePath: input.workspacePath,
|
|
50
|
+
status: 'disabled',
|
|
51
|
+
summary: 'Attached PR auto-close is disabled for terminal cleanup.',
|
|
52
|
+
branch: null
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!(await workspaceExists(input.workspacePath))) {
|
|
56
|
+
return buildResult({
|
|
57
|
+
attemptedAt,
|
|
58
|
+
issueId: input.issueId,
|
|
59
|
+
issueIdentifier,
|
|
60
|
+
workspacePath: input.workspacePath,
|
|
61
|
+
status: 'noop',
|
|
62
|
+
summary: 'Workspace was already absent before terminal cleanup.',
|
|
63
|
+
branch: null
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const branchResult = await runCommand({
|
|
67
|
+
command: 'git',
|
|
68
|
+
args: ['-C', input.workspacePath, 'branch', '--show-current'],
|
|
69
|
+
cwd: input.workspacePath
|
|
70
|
+
});
|
|
71
|
+
if (!branchResult.ok) {
|
|
72
|
+
return buildResult({
|
|
73
|
+
attemptedAt,
|
|
74
|
+
issueId: input.issueId,
|
|
75
|
+
issueIdentifier,
|
|
76
|
+
workspacePath: input.workspacePath,
|
|
77
|
+
status: 'failed',
|
|
78
|
+
summary: 'Terminal cleanup could not determine the workspace branch.',
|
|
79
|
+
error: formatCommandFailure('git', branchResult),
|
|
80
|
+
branch: null
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const branch = normalizeOptionalString(branchResult.stdout);
|
|
84
|
+
const headResult = await runCommand({
|
|
85
|
+
command: 'git',
|
|
86
|
+
args: ['-C', input.workspacePath, 'rev-parse', 'HEAD'],
|
|
87
|
+
cwd: input.workspacePath
|
|
88
|
+
});
|
|
89
|
+
if (!headResult.ok) {
|
|
90
|
+
return buildResult({
|
|
91
|
+
attemptedAt,
|
|
92
|
+
issueId: input.issueId,
|
|
93
|
+
issueIdentifier,
|
|
94
|
+
workspacePath: input.workspacePath,
|
|
95
|
+
status: 'failed',
|
|
96
|
+
summary: branch === null
|
|
97
|
+
? 'Terminal cleanup could not determine the detached workspace HEAD.'
|
|
98
|
+
: 'Terminal cleanup could not determine the workspace HEAD.',
|
|
99
|
+
error: formatCommandFailure('git', headResult),
|
|
100
|
+
branch
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const workspaceHeadOid = normalizeOptionalString(headResult.stdout);
|
|
104
|
+
if (!workspaceHeadOid) {
|
|
105
|
+
if (!branch) {
|
|
106
|
+
return buildResult({
|
|
107
|
+
attemptedAt,
|
|
108
|
+
issueId: input.issueId,
|
|
109
|
+
issueIdentifier,
|
|
110
|
+
workspacePath: input.workspacePath,
|
|
111
|
+
status: 'noop',
|
|
112
|
+
summary: 'Workspace is detached or has no current branch; skipping attached PR cleanup.',
|
|
113
|
+
branch: null
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return buildResult({
|
|
117
|
+
attemptedAt,
|
|
118
|
+
issueId: input.issueId,
|
|
119
|
+
issueIdentifier,
|
|
120
|
+
workspacePath: input.workspacePath,
|
|
121
|
+
status: 'failed',
|
|
122
|
+
summary: 'Terminal cleanup could not determine the workspace HEAD.',
|
|
123
|
+
branch
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const workspaceTargetLabel = formatWorkspaceTargetLabel(branch, workspaceHeadOid);
|
|
127
|
+
const originUrlResult = await runCommand({
|
|
128
|
+
command: 'git',
|
|
129
|
+
args: ['-C', input.workspacePath, 'remote', 'get-url', 'origin'],
|
|
130
|
+
cwd: input.workspacePath
|
|
131
|
+
});
|
|
132
|
+
if (!originUrlResult.ok) {
|
|
133
|
+
return buildResult({
|
|
134
|
+
attemptedAt,
|
|
135
|
+
issueId: input.issueId,
|
|
136
|
+
issueIdentifier,
|
|
137
|
+
workspacePath: input.workspacePath,
|
|
138
|
+
status: 'failed',
|
|
139
|
+
summary: 'Terminal cleanup could not determine the workspace origin remote.',
|
|
140
|
+
error: formatCommandFailure('git', originUrlResult),
|
|
141
|
+
branch
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const workspaceRepoKey = parseGitHubRepositoryUrl(originUrlResult.stdout);
|
|
145
|
+
if (!workspaceRepoKey) {
|
|
146
|
+
return buildResult({
|
|
147
|
+
attemptedAt,
|
|
148
|
+
issueId: input.issueId,
|
|
149
|
+
issueIdentifier,
|
|
150
|
+
workspacePath: input.workspacePath,
|
|
151
|
+
status: 'failed',
|
|
152
|
+
summary: 'Terminal cleanup could not determine the workspace GitHub repository.',
|
|
153
|
+
error: JSON.stringify(originUrlResult.stdout.trim()),
|
|
154
|
+
branch
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const issueContext = await readIssueContext({
|
|
158
|
+
issueId: input.issueId,
|
|
159
|
+
env
|
|
160
|
+
});
|
|
161
|
+
if (!issueContext.ok) {
|
|
162
|
+
return buildResult({
|
|
163
|
+
attemptedAt,
|
|
164
|
+
issueId: input.issueId,
|
|
165
|
+
issueIdentifier,
|
|
166
|
+
workspacePath: input.workspacePath,
|
|
167
|
+
status: 'failed',
|
|
168
|
+
summary: `Terminal cleanup could not load Linear issue context for ${input.issueId}.`,
|
|
169
|
+
error: `${issueContext.error.code}: ${issueContext.error.message}`,
|
|
170
|
+
branch
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const attachedPrUrls = collectAttachedGitHubPrUrls(issueContext.issue.attachments);
|
|
174
|
+
if (attachedPrUrls.length === 0) {
|
|
175
|
+
return buildResult({
|
|
176
|
+
attemptedAt,
|
|
177
|
+
issueId: input.issueId,
|
|
178
|
+
issueIdentifier,
|
|
179
|
+
workspacePath: input.workspacePath,
|
|
180
|
+
status: 'noop',
|
|
181
|
+
summary: `No attached GitHub PRs were present for ${workspaceTargetLabel}.`,
|
|
182
|
+
branch
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const matchingOpenPrUrls = [];
|
|
186
|
+
const closedPrUrls = [];
|
|
187
|
+
const errors = [];
|
|
188
|
+
let resolvedBranch = branch;
|
|
189
|
+
const cleanupDeadlineMs = nowMs() + PROVIDER_TERMINAL_CLEANUP_TOTAL_BUDGET_MS;
|
|
190
|
+
const sameRepoAttachedPrUrls = attachedPrUrls.filter((attachedPrUrl) => parseGitHubPullRequestUrl(attachedPrUrl)?.repoKey === workspaceRepoKey);
|
|
191
|
+
const attachedPrUrlsToProcess = sameRepoAttachedPrUrls.slice(0, PROVIDER_TERMINAL_CLEANUP_MAX_ATTACHED_PRS);
|
|
192
|
+
if (sameRepoAttachedPrUrls.length > attachedPrUrlsToProcess.length) {
|
|
193
|
+
errors.push(`skipped ${sameRepoAttachedPrUrls.length - attachedPrUrlsToProcess.length} attached PR(s) beyond cleanup cap of ${PROVIDER_TERMINAL_CLEANUP_MAX_ATTACHED_PRS}`);
|
|
194
|
+
}
|
|
195
|
+
let processedAttachedPrCount = 0;
|
|
196
|
+
for (const attachedPrUrl of attachedPrUrlsToProcess) {
|
|
197
|
+
if (nowMs() >= cleanupDeadlineMs) {
|
|
198
|
+
errors.push(`cleanup budget exceeded after processing ${processedAttachedPrCount} attached PR(s)`);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
processedAttachedPrCount += 1;
|
|
202
|
+
const prView = await runCommand({
|
|
203
|
+
command: 'gh',
|
|
204
|
+
args: [
|
|
205
|
+
'pr',
|
|
206
|
+
'view',
|
|
207
|
+
attachedPrUrl,
|
|
208
|
+
'--json',
|
|
209
|
+
'state,headRefName,headRefOid,url,headRepository,headRepositoryOwner,isCrossRepository'
|
|
210
|
+
],
|
|
211
|
+
cwd: input.workspacePath
|
|
212
|
+
});
|
|
213
|
+
if (!prView.ok) {
|
|
214
|
+
errors.push(`gh pr view ${attachedPrUrl}: ${formatCommandFailure('gh', prView)}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const prDetails = parsePrViewResponse(prView.stdout);
|
|
218
|
+
if (!prDetails) {
|
|
219
|
+
errors.push(`gh pr view ${attachedPrUrl}: invalid JSON response`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const sameBaseRepository = prDetails.repoKey === workspaceRepoKey;
|
|
223
|
+
const sameHeadRepository = prDetails.headRepoKey === workspaceRepoKey;
|
|
224
|
+
const matchesBranch = branch !== null &&
|
|
225
|
+
prDetails.headRefName === branch &&
|
|
226
|
+
prDetails.headRefOid === workspaceHeadOid;
|
|
227
|
+
const matchesHead = branch === null &&
|
|
228
|
+
workspaceHeadOid !== null &&
|
|
229
|
+
prDetails.headRefOid !== null &&
|
|
230
|
+
prDetails.headRefOid === workspaceHeadOid;
|
|
231
|
+
if (prDetails.state !== 'OPEN' ||
|
|
232
|
+
!sameBaseRepository ||
|
|
233
|
+
!sameHeadRepository ||
|
|
234
|
+
(!matchesBranch && !matchesHead)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const closingBranch = prDetails.headRefName ??
|
|
238
|
+
branch ??
|
|
239
|
+
resolvedBranch ??
|
|
240
|
+
`HEAD ${shortOid(workspaceHeadOid)}`;
|
|
241
|
+
resolvedBranch ??= prDetails.headRefName;
|
|
242
|
+
matchingOpenPrUrls.push(prDetails.url ?? attachedPrUrl);
|
|
243
|
+
if (nowMs() >= cleanupDeadlineMs) {
|
|
244
|
+
errors.push(`cleanup budget exceeded before closing ${attachedPrUrl}`);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
const prClose = await runCommand({
|
|
248
|
+
command: 'gh',
|
|
249
|
+
args: [
|
|
250
|
+
'pr',
|
|
251
|
+
'close',
|
|
252
|
+
attachedPrUrl,
|
|
253
|
+
'--comment',
|
|
254
|
+
renderClosingComment(input.config.closeAttachedPr.commentTemplate, closingBranch)
|
|
255
|
+
],
|
|
256
|
+
cwd: input.workspacePath
|
|
257
|
+
});
|
|
258
|
+
if (!prClose.ok) {
|
|
259
|
+
errors.push(`gh pr close ${attachedPrUrl}: ${formatCommandFailure('gh', prClose)}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
closedPrUrls.push(prDetails.url ?? attachedPrUrl);
|
|
263
|
+
}
|
|
264
|
+
if (errors.length > 0) {
|
|
265
|
+
return buildResult({
|
|
266
|
+
attemptedAt,
|
|
267
|
+
issueId: input.issueId,
|
|
268
|
+
issueIdentifier,
|
|
269
|
+
workspacePath: input.workspacePath,
|
|
270
|
+
status: 'failed',
|
|
271
|
+
summary: matchingOpenPrUrls.length === 0
|
|
272
|
+
? `Terminal cleanup encountered ${errors.length} attached PR error(s) for ${workspaceTargetLabel}.`
|
|
273
|
+
: `Terminal cleanup closed ${closedPrUrls.length} of ${matchingOpenPrUrls.length} matching attached PR(s) for ${formatWorkspaceTargetLabel(resolvedBranch, workspaceHeadOid)} and encountered ${errors.length} error(s).`,
|
|
274
|
+
error: errors.join(' | '),
|
|
275
|
+
branch: resolvedBranch,
|
|
276
|
+
attachedPrUrls,
|
|
277
|
+
matchingOpenPrUrls,
|
|
278
|
+
closedPrUrls
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (matchingOpenPrUrls.length === 0) {
|
|
282
|
+
return buildResult({
|
|
283
|
+
attemptedAt,
|
|
284
|
+
issueId: input.issueId,
|
|
285
|
+
issueIdentifier,
|
|
286
|
+
workspacePath: input.workspacePath,
|
|
287
|
+
status: 'noop',
|
|
288
|
+
summary: `No attached open PR matched ${workspaceTargetLabel}.`,
|
|
289
|
+
branch: resolvedBranch,
|
|
290
|
+
attachedPrUrls
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return buildResult({
|
|
294
|
+
attemptedAt,
|
|
295
|
+
issueId: input.issueId,
|
|
296
|
+
issueIdentifier,
|
|
297
|
+
workspacePath: input.workspacePath,
|
|
298
|
+
status: 'succeeded',
|
|
299
|
+
summary: `Closed ${closedPrUrls.length} attached PR(s) for ${formatWorkspaceTargetLabel(resolvedBranch, workspaceHeadOid)}.`,
|
|
300
|
+
branch: resolvedBranch,
|
|
301
|
+
attachedPrUrls,
|
|
302
|
+
matchingOpenPrUrls,
|
|
303
|
+
closedPrUrls
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async function runProviderTerminalCleanupCommand(input) {
|
|
307
|
+
try {
|
|
308
|
+
const { stdout, stderr } = await execFileAsync(input.command, input.args, {
|
|
309
|
+
cwd: input.cwd,
|
|
310
|
+
timeout: PROVIDER_TERMINAL_CLEANUP_COMMAND_TIMEOUT_MS
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
exitCode: 0,
|
|
315
|
+
stdout,
|
|
316
|
+
stderr
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const execError = error;
|
|
321
|
+
const timedOut = execError.killed === true && execError.signal === 'SIGTERM';
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
exitCode: typeof execError.code === 'number' && Number.isInteger(execError.code)
|
|
325
|
+
? execError.code
|
|
326
|
+
: null,
|
|
327
|
+
stdout: typeof execError.stdout === 'string' ? execError.stdout : '',
|
|
328
|
+
stderr: typeof execError.stderr === 'string' && execError.stderr.length > 0
|
|
329
|
+
? execError.stderr
|
|
330
|
+
: timedOut
|
|
331
|
+
? `command timed out after ${PROVIDER_TERMINAL_CLEANUP_COMMAND_TIMEOUT_MS}ms`
|
|
332
|
+
: (execError.message ?? ''),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function buildResult(input) {
|
|
337
|
+
return {
|
|
338
|
+
attemptedAt: input.attemptedAt,
|
|
339
|
+
status: input.status,
|
|
340
|
+
summary: input.summary,
|
|
341
|
+
error: input.error ?? null,
|
|
342
|
+
issueId: input.issueId,
|
|
343
|
+
issueIdentifier: input.issueIdentifier,
|
|
344
|
+
workspacePath: input.workspacePath,
|
|
345
|
+
branch: input.branch,
|
|
346
|
+
attachedPrUrls: [...(input.attachedPrUrls ?? [])],
|
|
347
|
+
matchingOpenPrUrls: [...(input.matchingOpenPrUrls ?? [])],
|
|
348
|
+
closedPrUrls: [...(input.closedPrUrls ?? [])]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function collectAttachedGitHubPrUrls(attachments) {
|
|
352
|
+
const seen = new Set();
|
|
353
|
+
const urls = [];
|
|
354
|
+
for (const attachment of attachments) {
|
|
355
|
+
const parsed = parseGitHubPullRequestUrl(attachment.url);
|
|
356
|
+
if (!parsed || seen.has(parsed.canonicalUrl)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
seen.add(parsed.canonicalUrl);
|
|
360
|
+
urls.push(parsed.canonicalUrl);
|
|
361
|
+
}
|
|
362
|
+
return urls;
|
|
363
|
+
}
|
|
364
|
+
function parsePrViewResponse(value) {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(value);
|
|
367
|
+
const parsedUrl = parseGitHubPullRequestUrl(typeof parsed.url === 'string' ? parsed.url : null);
|
|
368
|
+
const headRepository = asRecord(parsed.headRepository);
|
|
369
|
+
const headRepositoryOwner = asRecord(parsed.headRepositoryOwner);
|
|
370
|
+
const headRepositoryName = normalizeOptionalString(headRepository?.name);
|
|
371
|
+
const headRepositoryOwnerLogin = normalizeOptionalString(headRepositoryOwner?.login);
|
|
372
|
+
const isCrossRepository = typeof parsed.isCrossRepository === 'boolean' ? parsed.isCrossRepository : null;
|
|
373
|
+
const headRepoKey = headRepositoryName && headRepositoryOwnerLogin
|
|
374
|
+
? `${headRepositoryOwnerLogin.toLowerCase()}/${headRepositoryName.toLowerCase()}`
|
|
375
|
+
: isCrossRepository === false
|
|
376
|
+
? parsedUrl?.repoKey ?? null
|
|
377
|
+
: null;
|
|
378
|
+
return {
|
|
379
|
+
state: normalizeOptionalString(typeof parsed.state === 'string' ? parsed.state : null)?.toUpperCase() ?? null,
|
|
380
|
+
headRefName: normalizeOptionalString(typeof parsed.headRefName === 'string' ? parsed.headRefName : null),
|
|
381
|
+
headRefOid: normalizeOptionalString(typeof parsed.headRefOid === 'string' ? parsed.headRefOid : null),
|
|
382
|
+
url: parsedUrl?.canonicalUrl ?? null,
|
|
383
|
+
repoKey: parsedUrl?.repoKey ?? null,
|
|
384
|
+
headRepoKey
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function renderClosingComment(template, branch) {
|
|
392
|
+
return template.replaceAll('{{branch}}', branch);
|
|
393
|
+
}
|
|
394
|
+
function formatCommandFailure(command, result) {
|
|
395
|
+
const parts = [`${command} exited ${result.exitCode ?? 'unknown'}`];
|
|
396
|
+
const stderr = result.stderr.trim();
|
|
397
|
+
const stdout = result.stdout.trim();
|
|
398
|
+
if (stderr) {
|
|
399
|
+
parts.push(`stderr=${JSON.stringify(stderr)}`);
|
|
400
|
+
}
|
|
401
|
+
else if (stdout) {
|
|
402
|
+
parts.push(`stdout=${JSON.stringify(stdout)}`);
|
|
403
|
+
}
|
|
404
|
+
return parts.join(' ');
|
|
405
|
+
}
|
|
406
|
+
async function workspaceExists(workspacePath) {
|
|
407
|
+
try {
|
|
408
|
+
await access(workspacePath);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function formatWorkspaceTargetLabel(branch, headOid) {
|
|
416
|
+
if (branch) {
|
|
417
|
+
return `branch ${branch}`;
|
|
418
|
+
}
|
|
419
|
+
if (headOid) {
|
|
420
|
+
return `detached HEAD ${shortOid(headOid)}`;
|
|
421
|
+
}
|
|
422
|
+
return 'the workspace target';
|
|
423
|
+
}
|
|
424
|
+
function shortOid(value) {
|
|
425
|
+
const normalized = normalizeOptionalString(value);
|
|
426
|
+
return normalized ? normalized.slice(0, 12) : 'unknown';
|
|
427
|
+
}
|
|
428
|
+
function parseGitHubPullRequestUrl(value) {
|
|
429
|
+
const normalized = normalizeOptionalString(value);
|
|
430
|
+
if (!normalized) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
let parsed;
|
|
434
|
+
try {
|
|
435
|
+
parsed = new URL(normalized);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
441
|
+
if (hostname !== 'github.com' && hostname !== 'www.github.com') {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
445
|
+
const owner = normalizeOptionalString(segments[0] ?? null);
|
|
446
|
+
const repo = normalizeOptionalString(segments[1] ?? null);
|
|
447
|
+
const resource = normalizeOptionalString(segments[2] ?? null)?.toLowerCase() ?? null;
|
|
448
|
+
const pullNumber = normalizePullRequestNumber(segments[3] ?? null);
|
|
449
|
+
if (!owner || !repo || resource !== 'pull' || !pullNumber) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
canonicalUrl: `https://github.com/${owner}/${repo}/pull/${pullNumber}`,
|
|
454
|
+
repoKey: `${owner.toLowerCase()}/${repo.toLowerCase()}`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function parseGitHubRepositoryUrl(value) {
|
|
458
|
+
const normalized = normalizeOptionalString(value);
|
|
459
|
+
if (!normalized) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const scpMatch = normalized.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
463
|
+
if (scpMatch) {
|
|
464
|
+
return `${scpMatch[1].toLowerCase()}/${scpMatch[2].toLowerCase()}`;
|
|
465
|
+
}
|
|
466
|
+
let parsed;
|
|
467
|
+
try {
|
|
468
|
+
parsed = new URL(normalized);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
474
|
+
if (hostname !== 'github.com' && hostname !== 'www.github.com') {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
478
|
+
const owner = normalizeOptionalString(segments[0] ?? null);
|
|
479
|
+
const repo = normalizeOptionalString((segments[1] ?? '').replace(/\.git$/iu, ''));
|
|
480
|
+
if (!owner || !repo || segments.length !== 2) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return `${owner.toLowerCase()}/${repo.toLowerCase()}`;
|
|
484
|
+
}
|
|
485
|
+
function normalizePullRequestNumber(value) {
|
|
486
|
+
const normalized = normalizeOptionalString(value);
|
|
487
|
+
if (!normalized || !/^\d+$/u.test(normalized)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return String(Number.parseInt(normalized, 10));
|
|
491
|
+
}
|
|
492
|
+
function asRecord(value) {
|
|
493
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function readBoolean(record, key) {
|
|
499
|
+
const value = record?.[key];
|
|
500
|
+
if (typeof value === 'boolean') {
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function readNonEmptyString(record, ...keys) {
|
|
506
|
+
for (const key of keys) {
|
|
507
|
+
const value = normalizeOptionalString(record?.[key]);
|
|
508
|
+
if (value) {
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function normalizeOptionalString(value) {
|
|
515
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
516
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const PROVIDER_WORKER_HOST_ENV_KEY = 'CODEX_ORCHESTRATOR_PROVIDER_WORKER_HOST';
|
|
2
|
+
const DEFAULT_PROVIDER_WORKER_HOST_MAX_CONCURRENT_AGENTS = 1;
|
|
3
|
+
const ACTIVE_PROVIDER_WORKER_HOST_CLAIM_STATES = new Set(['starting', 'running', 'resuming']);
|
|
4
|
+
export function resolveProviderWorkerHostConfig(metadata) {
|
|
5
|
+
const record = asRecord(metadata);
|
|
6
|
+
const workerHostsRecord = asRecord(record?.worker_hosts ?? record?.workerHosts);
|
|
7
|
+
if (workerHostsRecord && workerHostsRecord.hosts !== undefined && !Array.isArray(workerHostsRecord.hosts)) {
|
|
8
|
+
throw new Error('provider worker hosts metadata "hosts" must be an array.');
|
|
9
|
+
}
|
|
10
|
+
const hostEntries = Array.isArray(workerHostsRecord?.hosts)
|
|
11
|
+
? workerHostsRecord.hosts
|
|
12
|
+
: Array.isArray(record?.worker_hosts)
|
|
13
|
+
? record.worker_hosts
|
|
14
|
+
: Array.isArray(record?.workerHosts)
|
|
15
|
+
? record.workerHosts
|
|
16
|
+
: [];
|
|
17
|
+
if (hostEntries.length === 0) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const names = new Set();
|
|
21
|
+
return hostEntries.map((entry, index) => {
|
|
22
|
+
const host = asRecord(entry);
|
|
23
|
+
if (!host) {
|
|
24
|
+
throw new Error(`provider worker host entry #${index + 1} must be an object.`);
|
|
25
|
+
}
|
|
26
|
+
const name = readRequiredString(host, 'name', `provider worker host entry #${index + 1}`);
|
|
27
|
+
if (names.has(name)) {
|
|
28
|
+
throw new Error(`provider worker host "${name}" is duplicated.`);
|
|
29
|
+
}
|
|
30
|
+
names.add(name);
|
|
31
|
+
const transport = readOptionalString(host, 'transport') ?? 'ssh';
|
|
32
|
+
if (transport !== 'ssh') {
|
|
33
|
+
throw new Error(`provider worker host "${name}" transport must be "ssh" for the current distributed worker-host lane.`);
|
|
34
|
+
}
|
|
35
|
+
const sshDestination = readRequiredString(host, 'ssh_destination', `provider worker host "${name}"`);
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
transport: 'ssh',
|
|
39
|
+
ssh_destination: sshDestination,
|
|
40
|
+
ssh_options: readStringArray(host, 'ssh_options'),
|
|
41
|
+
max_concurrent_agents: readOptionalPositiveInteger(host, 'max_concurrent_agents', `provider worker host "${name}"`) ??
|
|
42
|
+
DEFAULT_PROVIDER_WORKER_HOST_MAX_CONCURRENT_AGENTS,
|
|
43
|
+
node_path: readOptionalString(host, 'node_path')
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function cloneProviderWorkerHostConfigs(hosts) {
|
|
48
|
+
return Array.isArray(hosts)
|
|
49
|
+
? hosts.map((host) => ({
|
|
50
|
+
...host,
|
|
51
|
+
ssh_options: [...host.ssh_options]
|
|
52
|
+
}))
|
|
53
|
+
: [];
|
|
54
|
+
}
|
|
55
|
+
export function normalizeProviderWorkerHostName(value) {
|
|
56
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
57
|
+
}
|
|
58
|
+
export function findProviderWorkerHost(hosts, name) {
|
|
59
|
+
const normalizedName = normalizeProviderWorkerHostName(name);
|
|
60
|
+
if (!normalizedName || !Array.isArray(hosts)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return hosts.find((host) => host.name === normalizedName) ?? null;
|
|
64
|
+
}
|
|
65
|
+
export function selectProviderWorkerHost(input) {
|
|
66
|
+
const hosts = cloneProviderWorkerHostConfigs(input.hosts);
|
|
67
|
+
const occupancy = buildProviderWorkerHostOccupancy({
|
|
68
|
+
claims: input.claims,
|
|
69
|
+
currentProviderKey: input.currentProviderKey
|
|
70
|
+
});
|
|
71
|
+
if (hosts.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
kind: 'local',
|
|
74
|
+
host: null,
|
|
75
|
+
occupancy
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const preferred = findProviderWorkerHost(hosts, input.preferredHost);
|
|
79
|
+
if (preferred && hasProviderWorkerHostCapacity(preferred, occupancy)) {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'remote',
|
|
82
|
+
host: preferred,
|
|
83
|
+
occupancy
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const selected = [...hosts]
|
|
87
|
+
.map((host, index) => ({
|
|
88
|
+
host,
|
|
89
|
+
index,
|
|
90
|
+
used: occupancy.get(host.name) ?? 0,
|
|
91
|
+
remaining: host.max_concurrent_agents - (occupancy.get(host.name) ?? 0)
|
|
92
|
+
}))
|
|
93
|
+
.filter((entry) => entry.remaining > 0)
|
|
94
|
+
.sort((left, right) => {
|
|
95
|
+
if (right.remaining !== left.remaining) {
|
|
96
|
+
return right.remaining - left.remaining;
|
|
97
|
+
}
|
|
98
|
+
if (left.used !== right.used) {
|
|
99
|
+
return left.used - right.used;
|
|
100
|
+
}
|
|
101
|
+
return left.index - right.index;
|
|
102
|
+
})[0]?.host ?? null;
|
|
103
|
+
if (!selected) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'exhausted',
|
|
106
|
+
host: null,
|
|
107
|
+
occupancy
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
kind: 'remote',
|
|
112
|
+
host: selected,
|
|
113
|
+
occupancy
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildProviderWorkerHostOccupancy(input) {
|
|
117
|
+
const occupancy = new Map();
|
|
118
|
+
if (!Array.isArray(input.claims)) {
|
|
119
|
+
return occupancy;
|
|
120
|
+
}
|
|
121
|
+
for (const claim of input.claims) {
|
|
122
|
+
if (claim.provider_key && input.currentProviderKey && claim.provider_key === input.currentProviderKey) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!ACTIVE_PROVIDER_WORKER_HOST_CLAIM_STATES.has(normalizeClaimState(claim.state))) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const workerHost = normalizeProviderWorkerHostName(claim.worker_host);
|
|
129
|
+
if (!workerHost) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
occupancy.set(workerHost, (occupancy.get(workerHost) ?? 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
return occupancy;
|
|
135
|
+
}
|
|
136
|
+
function hasProviderWorkerHostCapacity(host, occupancy) {
|
|
137
|
+
return (occupancy.get(host.name) ?? 0) < host.max_concurrent_agents;
|
|
138
|
+
}
|
|
139
|
+
function normalizeClaimState(value) {
|
|
140
|
+
return typeof value === 'string' ? value : '';
|
|
141
|
+
}
|
|
142
|
+
function asRecord(value) {
|
|
143
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
144
|
+
? value
|
|
145
|
+
: null;
|
|
146
|
+
}
|
|
147
|
+
function readOptionalString(record, key) {
|
|
148
|
+
const value = record[key];
|
|
149
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
150
|
+
}
|
|
151
|
+
function readRequiredString(record, key, source) {
|
|
152
|
+
const value = readOptionalString(record, key);
|
|
153
|
+
if (!value) {
|
|
154
|
+
throw new Error(`${source} requires a non-empty ${key}.`);
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
function readStringArray(record, key) {
|
|
159
|
+
const value = record[key];
|
|
160
|
+
if (value === undefined || value === null) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(value)) {
|
|
164
|
+
throw new Error(`${key} must be an array of strings.`);
|
|
165
|
+
}
|
|
166
|
+
return value.map((entry, index) => {
|
|
167
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
168
|
+
throw new Error(`${key}[${index}] must be a non-empty string.`);
|
|
169
|
+
}
|
|
170
|
+
return entry;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function readPositiveInteger(record, key, source) {
|
|
174
|
+
const value = record[key];
|
|
175
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
if (typeof value === 'string' && /^\d+$/u.test(value.trim())) {
|
|
179
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
180
|
+
if (parsed > 0) {
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`${source} requires ${key} to be a positive integer.`);
|
|
185
|
+
}
|
|
186
|
+
function readOptionalPositiveInteger(record, key, source) {
|
|
187
|
+
if (record[key] === undefined || record[key] === null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return readPositiveInteger(record, key, source);
|
|
191
|
+
}
|