@kbediako/codex-orchestrator 0.1.38 → 0.2.1
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 +46 -317
- package/bin/codex-orchestrator.js +161 -0
- package/codex.orchestrator.json +149 -13
- package/dist/bin/codex-orchestrator.js +797 -1154
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
- 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 +295 -11
- package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
- package/dist/orchestrator/src/cli/coStatusCliShell.js +451 -0
- package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
- package/dist/orchestrator/src/cli/codexCliShell.js +119 -0
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +265 -36
- 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 +630 -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 +1003 -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 +1904 -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 +1885 -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 +678 -164
- 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 +95 -1
- 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 +1835 -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 +6834 -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 +698 -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 +285 -7
- package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
- 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/manager.js +74 -4
- 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 +399 -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/README.md +43 -20
- package/docs/book/README.md +19 -0
- package/docs/book/codex-cli-0124-adoption.md +68 -0
- package/docs/book/local-hook-impact.md +73 -0
- package/docs/book/operations.md +60 -0
- package/docs/book/public-posture.md +34 -0
- package/docs/book/setup.md +91 -0
- package/docs/book/skills.md +11 -0
- package/docs/guides/codex-version-policy.md +104 -0
- package/docs/public/downstream-setup.md +113 -0
- package/docs/public/provider-onboarding.md +173 -0
- package/package.json +23 -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 +361 -0
- package/schemas/manifest.json +411 -0
- package/skills/README.md +26 -0
- package/skills/collab-subagents-first/SKILL.md +1 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +30 -12
- package/skills/delegation-usage/SKILL.md +25 -14
- 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 +15 -8
- package/templates/codex/mcp-client.json +5 -1
- package/docs/assets/setup.gif +0 -0
|
@@ -5,70 +5,58 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Note: some codex CLI versions reject combining diff-scoping flags
|
|
7
7
|
* (`--uncommitted`, `--base`, `--commit`) with a custom prompt. This wrapper
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* still writes the full prompt artifact for audit continuity. Explicit scoped
|
|
9
|
+
* launches omit any prompt argument because the current Codex CLI still treats
|
|
10
|
+
* stdin (`-`) as `[PROMPT]` and rejects it when scope flags are present, so the
|
|
11
|
+
* bounded live context transport for scoped runs is `--title` instead.
|
|
11
12
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
15
|
+
import { stat } from 'node:fs/promises';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import process from 'node:process';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from '../orchestrator/src/cli/runtime/index.js';
|
|
20
|
-
import { runDoctor } from '../orchestrator/src/cli/doctor.js';
|
|
21
|
-
import { formatDoctorIssueLogSummary, writeDoctorIssueLog } from '../orchestrator/src/cli/doctorIssueLog.js';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { parseRuntimeMode } from '../orchestrator/src/cli/runtime/index.js';
|
|
22
20
|
import { parseArgs as parseCliArgs, hasFlag } from './lib/cli-args.js';
|
|
23
21
|
import { pathExists } from './lib/docs-helpers.js';
|
|
22
|
+
import { allowHeavyReviewCommands, enforceBoundedReviewMode, prepareReviewExecutionBoundaryPreflight, REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY, REVIEW_ENFORCE_BOUNDED_MODE_ENV_KEY } from './lib/review-execution-boundary-preflight.js';
|
|
23
|
+
import { runReviewLaunchAttemptShell } from './lib/review-launch-attempt.js';
|
|
24
|
+
import { prepareReviewNonInteractiveHandoffShell, shouldForceNonInteractive, shouldPrintNonInteractiveHandoff } from './lib/review-non-interactive-handoff.js';
|
|
25
|
+
import { addBoundedReviewConstraintsToScopedTitle, buildActiveCloseoutProvenanceLines, buildReviewPromptContext } from './lib/review-prompt-context.js';
|
|
26
|
+
import { runCodexReview } from './lib/review-execution-runtime.js';
|
|
27
|
+
import { logReviewTelemetrySummary as logReviewExecutionTelemetrySummary, writeReviewExecutionTelemetry } from './lib/review-execution-telemetry.js';
|
|
28
|
+
import { assessReviewScope, getLargeScopeGateError, buildLargeScopeAdvisoryPromptLines, buildScopeNotes, collectReviewScopePaths, formatScopeMetrics, logReviewScopeAssessment, resolveLargeScopeOverrideReason, resolveEffectiveScopeMode } from './lib/review-scope-advisory.js';
|
|
24
29
|
import { collectManifests, resolveEnvironmentPaths } from './lib/run-manifests.js';
|
|
25
|
-
const execFileAsync = promisify(execFile);
|
|
26
|
-
const { repoRoot, runsRoot: defaultRunsDir } = resolveEnvironmentPaths();
|
|
27
|
-
const DEFAULT_REVIEW_STARTUP_LOOP_MIN_EVENTS = 8;
|
|
28
|
-
const DEFAULT_REVIEW_MONITOR_INTERVAL_SECONDS = 60;
|
|
29
|
-
const DEFAULT_LARGE_SCOPE_FILE_THRESHOLD = 25;
|
|
30
|
-
const DEFAULT_LARGE_SCOPE_LINE_THRESHOLD = 1200;
|
|
31
|
-
const REVIEW_COMMAND_CHECK_TIMEOUT_MS = 30_000;
|
|
32
|
-
const REVIEW_ARTIFACTS_DIRNAME = 'review';
|
|
33
|
-
const REVIEW_OUTPUT_PREVIEW_LIMIT = 32_768;
|
|
34
30
|
const BENIGN_STDIO_ERROR_CODES = new Set(['EPIPE', 'ERR_STREAM_DESTROYED']);
|
|
35
31
|
const REVIEW_AUTO_ISSUE_LOG_ENV_KEY = 'CODEX_REVIEW_AUTO_ISSUE_LOG';
|
|
36
32
|
const REVIEW_ENABLE_DELEGATION_MCP_ENV_KEY = 'CODEX_REVIEW_ENABLE_DELEGATION_MCP';
|
|
37
33
|
const REVIEW_DISABLE_DELEGATION_MCP_ENV_KEY = 'CODEX_REVIEW_DISABLE_DELEGATION_MCP';
|
|
38
|
-
const REVIEW_MONITOR_INTERVAL_ENV_KEY = 'CODEX_REVIEW_MONITOR_INTERVAL_SECONDS';
|
|
39
|
-
const REVIEW_LARGE_SCOPE_FILE_THRESHOLD_ENV_KEY = 'CODEX_REVIEW_LARGE_SCOPE_FILE_THRESHOLD';
|
|
40
|
-
const REVIEW_LARGE_SCOPE_LINE_THRESHOLD_ENV_KEY = 'CODEX_REVIEW_LARGE_SCOPE_LINE_THRESHOLD';
|
|
41
|
-
const REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY = 'CODEX_REVIEW_ALLOW_HEAVY_COMMANDS';
|
|
42
|
-
const REVIEW_ENFORCE_BOUNDED_MODE_ENV_KEY = 'CODEX_REVIEW_ENFORCE_BOUNDED_MODE';
|
|
43
34
|
const REVIEW_TELEMETRY_DEBUG_ENV_KEY = 'CODEX_REVIEW_DEBUG_TELEMETRY';
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const REVIEW_OUTPUT_SUMMARY_TAIL_LINE_LIMIT = 20;
|
|
70
|
-
const REVIEW_OUTPUT_SUMMARY_HEAVY_COMMAND_LIMIT = 8;
|
|
71
|
-
const REVIEW_OUTPUT_SUMMARY_COMMAND_LIMIT = 64;
|
|
35
|
+
const REVIEW_SURFACE_ENV_KEY = 'CODEX_REVIEW_SURFACE';
|
|
36
|
+
const PROVIDER_LINEAR_WORKER_PIPELINE_ID = 'provider-linear-worker';
|
|
37
|
+
const PROVIDER_WORKSPACE_ROOT_DIRNAME = '.workspaces';
|
|
38
|
+
const PRESERVE_PROVIDER_ARTIFACT_ROOTS_ENV = 'CODEX_ORCHESTRATOR_PRESERVE_PROVIDER_ARTIFACT_ROOTS';
|
|
39
|
+
function buildExplicitScopeRetryGateError(options) {
|
|
40
|
+
if (options.commit) {
|
|
41
|
+
return 'explicit `--commit` review scope must remain auditable; rerun without that flag only if you intentionally want the wrapper default working-tree review.';
|
|
42
|
+
}
|
|
43
|
+
if (options.base) {
|
|
44
|
+
return 'explicit `--base` review scope must remain auditable; rerun without that flag only if you intentionally want the wrapper default working-tree review.';
|
|
45
|
+
}
|
|
46
|
+
if (options.uncommitted) {
|
|
47
|
+
return 'explicit `--uncommitted` review scope must remain auditable; rerun without that flag only if you intentionally want the wrapper default working-tree review.';
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function buildExplicitScopeSurfaceGateError(options, reviewSurface) {
|
|
52
|
+
if (reviewSurface === 'diff') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!(options.base || options.commit || options.uncommitted)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return `explicit scoped review cannot honor --surface ${reviewSurface} because current Codex CLI rejects inline prompt transport under --base/--commit/--uncommitted; the wrapper only has bounded --title transport there when supported and otherwise falls back to artifact-only scoped context, so rerun with the default diff surface or drop the explicit scope if you need ${reviewSurface} prompt context.`;
|
|
59
|
+
}
|
|
72
60
|
function installStdioErrorGuards() {
|
|
73
61
|
const guard = (error) => {
|
|
74
62
|
const code = typeof error?.code === 'string' ? error.code : '';
|
|
@@ -83,90 +71,6 @@ function installStdioErrorGuards() {
|
|
|
83
71
|
process.stderr.on('error', guard);
|
|
84
72
|
}
|
|
85
73
|
installStdioErrorGuards();
|
|
86
|
-
async function resolveTaskChecklistPath(taskKey) {
|
|
87
|
-
const direct = path.join(repoRoot, 'tasks', `tasks-${taskKey}.md`);
|
|
88
|
-
if (await pathExists(direct)) {
|
|
89
|
-
return direct;
|
|
90
|
-
}
|
|
91
|
-
if (!/^\d{4}$/.test(taskKey)) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const tasksDir = path.join(repoRoot, 'tasks');
|
|
95
|
-
let entries = [];
|
|
96
|
-
try {
|
|
97
|
-
entries = await readdir(tasksDir);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const candidates = entries
|
|
103
|
-
.filter((name) => name.startsWith(`tasks-${taskKey}-`) && name.endsWith('.md'))
|
|
104
|
-
.map((name) => path.join(tasksDir, name))
|
|
105
|
-
.sort();
|
|
106
|
-
if (candidates.length === 1) {
|
|
107
|
-
return candidates[0] ?? null;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
function extractTaskHeaderBulletLines(taskChecklist) {
|
|
112
|
-
const lines = taskChecklist.split('\n');
|
|
113
|
-
const checklistIndex = lines.findIndex((line) => line.trim() === '## Checklist');
|
|
114
|
-
const headerLines = checklistIndex === -1 ? lines : lines.slice(0, checklistIndex);
|
|
115
|
-
return headerLines
|
|
116
|
-
.map((line) => line.trimEnd())
|
|
117
|
-
.filter((line) => line.startsWith('- '));
|
|
118
|
-
}
|
|
119
|
-
function extractBacktickedPath(line) {
|
|
120
|
-
const match = line.match(/`([^`]+)`/);
|
|
121
|
-
return match?.[1] ?? null;
|
|
122
|
-
}
|
|
123
|
-
function extractMarkdownSection(content, heading) {
|
|
124
|
-
const lines = content.split('\n');
|
|
125
|
-
const headingLine = `## ${heading}`;
|
|
126
|
-
const startIndex = lines.findIndex((line) => line.trim() === headingLine);
|
|
127
|
-
if (startIndex === -1) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
const body = [];
|
|
131
|
-
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
132
|
-
const line = lines[index] ?? '';
|
|
133
|
-
if (line.trim().startsWith('## ')) {
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
body.push(line);
|
|
137
|
-
}
|
|
138
|
-
return body;
|
|
139
|
-
}
|
|
140
|
-
async function buildTaskContext(taskKey) {
|
|
141
|
-
const checklistPath = await resolveTaskChecklistPath(taskKey);
|
|
142
|
-
if (!checklistPath) {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
const relativeChecklist = path.relative(repoRoot, checklistPath);
|
|
146
|
-
const checklist = await readFile(checklistPath, 'utf8');
|
|
147
|
-
const headerBullets = extractTaskHeaderBulletLines(checklist);
|
|
148
|
-
const lines = ['Task context:', `- Task checklist: \`${relativeChecklist}\``];
|
|
149
|
-
for (const bullet of headerBullets) {
|
|
150
|
-
lines.push(bullet);
|
|
151
|
-
}
|
|
152
|
-
const prdLine = headerBullets.find((line) => line.toLowerCase().includes('primary prd:'));
|
|
153
|
-
const prdPath = prdLine ? extractBacktickedPath(prdLine) : null;
|
|
154
|
-
if (prdPath) {
|
|
155
|
-
const absPrdPath = path.resolve(repoRoot, prdPath);
|
|
156
|
-
if (await pathExists(absPrdPath)) {
|
|
157
|
-
const prd = await readFile(absPrdPath, 'utf8');
|
|
158
|
-
const summary = extractMarkdownSection(prd, 'Summary');
|
|
159
|
-
const summaryBullets = summary
|
|
160
|
-
?.map((line) => line.trimEnd())
|
|
161
|
-
.filter((line) => line.trim().startsWith('- '))
|
|
162
|
-
.slice(0, 6) ?? [];
|
|
163
|
-
if (summaryBullets.length > 0) {
|
|
164
|
-
lines.push('', `PRD summary (\`${prdPath}\`):`, ...summaryBullets);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return lines;
|
|
169
|
-
}
|
|
170
74
|
function parseBooleanOptionValue(raw, label) {
|
|
171
75
|
if (typeof raw === 'boolean') {
|
|
172
76
|
return raw;
|
|
@@ -190,6 +94,16 @@ function parseRuntimeModeOption(raw, label) {
|
|
|
190
94
|
}
|
|
191
95
|
return parsed;
|
|
192
96
|
}
|
|
97
|
+
function parseReviewSurfaceOption(raw, label) {
|
|
98
|
+
if (typeof raw !== 'string') {
|
|
99
|
+
throw new Error(`${label} requires a value. Expected one of: diff, audit, architecture.`);
|
|
100
|
+
}
|
|
101
|
+
const normalized = raw.trim().toLowerCase();
|
|
102
|
+
if (normalized === 'diff' || normalized === 'audit' || normalized === 'architecture') {
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Invalid ${label} value "${raw}". Expected one of: diff, audit, architecture.`);
|
|
106
|
+
}
|
|
193
107
|
function inferTaskFromManifestPath(manifestPath) {
|
|
194
108
|
const segments = path.normalize(manifestPath).split(path.sep).filter((segment) => segment.length > 0);
|
|
195
109
|
const fileName = segments.at(-1);
|
|
@@ -211,8 +125,172 @@ function inferTaskFromManifestPath(manifestPath) {
|
|
|
211
125
|
}
|
|
212
126
|
return null;
|
|
213
127
|
}
|
|
214
|
-
function
|
|
215
|
-
const
|
|
128
|
+
function resolveReviewEnvironmentPaths() {
|
|
129
|
+
const { repoRoot, runsRoot, outRoot } = resolveEnvironmentPaths();
|
|
130
|
+
return {
|
|
131
|
+
repoRoot,
|
|
132
|
+
defaultRunsDir: runsRoot,
|
|
133
|
+
defaultOutDir: outRoot
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function resolveProviderTaskIdFromEnv(env, repoRoot) {
|
|
137
|
+
const candidates = [env.CODEX_ORCHESTRATOR_TASK_ID?.trim(), env.MCP_RUNNER_TASK_ID?.trim(), env.TASK?.trim()].filter(Boolean);
|
|
138
|
+
const repoTask = repoRoot && env.CODEX_ORCHESTRATOR_PIPELINE_ID === PROVIDER_LINEAR_WORKER_PIPELINE_ID && path.basename(path.dirname(repoRoot)) === PROVIDER_WORKSPACE_ROOT_DIRNAME ? path.basename(repoRoot) : null;
|
|
139
|
+
if (repoTask && candidates.includes(repoTask))
|
|
140
|
+
return repoTask;
|
|
141
|
+
return env.MCP_RUNNER_TASK_ID?.trim() || env.TASK?.trim() || env.CODEX_ORCHESTRATOR_TASK_ID?.trim() || null;
|
|
142
|
+
}
|
|
143
|
+
function isPathWithinRoot(root, candidate) {
|
|
144
|
+
const relativePath = path.relative(root, candidate);
|
|
145
|
+
return (relativePath === '' ||
|
|
146
|
+
(!relativePath.startsWith('..') &&
|
|
147
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
148
|
+
!path.isAbsolute(relativePath)));
|
|
149
|
+
}
|
|
150
|
+
function firstPathSegment(relativePath) {
|
|
151
|
+
return relativePath.split(path.sep).filter((segment) => segment.length > 0)[0] ?? '';
|
|
152
|
+
}
|
|
153
|
+
function isDefaultRunsLayoutSegment(segment) {
|
|
154
|
+
return segment === '.runs' || segment === 'runs';
|
|
155
|
+
}
|
|
156
|
+
function isProviderIssueWorkspaceRootForEnv(repoRoot, env) {
|
|
157
|
+
if (env.CODEX_ORCHESTRATOR_PIPELINE_ID !== PROVIDER_LINEAR_WORKER_PIPELINE_ID) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const taskId = resolveProviderTaskIdFromEnv(env, repoRoot);
|
|
161
|
+
return Boolean(taskId &&
|
|
162
|
+
path.basename(repoRoot) === taskId &&
|
|
163
|
+
path.basename(path.dirname(repoRoot)) === PROVIDER_WORKSPACE_ROOT_DIRNAME);
|
|
164
|
+
}
|
|
165
|
+
function resolveConfiguredReviewRoot(env) {
|
|
166
|
+
const configuredRoot = env.CODEX_ORCHESTRATOR_ROOT?.trim();
|
|
167
|
+
if (!configuredRoot) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return path.isAbsolute(configuredRoot)
|
|
171
|
+
? path.resolve(configuredRoot)
|
|
172
|
+
: path.resolve(process.cwd(), configuredRoot);
|
|
173
|
+
}
|
|
174
|
+
function resolveProviderSharedRootForEnv(repoRoot, env) {
|
|
175
|
+
if (!isProviderIssueWorkspaceRootForEnv(repoRoot, env)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const sharedRoot = path.dirname(path.dirname(repoRoot));
|
|
179
|
+
return resolveConfiguredReviewRoot(env) === sharedRoot ? sharedRoot : null;
|
|
180
|
+
}
|
|
181
|
+
function resolveConfiguredReviewArtifactRoot(repoRoot, env, configured, fallbackDirname) {
|
|
182
|
+
const inheritedSharedRoot = resolveProviderSharedRootForEnv(repoRoot, env);
|
|
183
|
+
const baseRoot = inheritedSharedRoot ?? repoRoot;
|
|
184
|
+
const normalized = configured?.trim();
|
|
185
|
+
if (!normalized) {
|
|
186
|
+
return path.resolve(baseRoot, fallbackDirname);
|
|
187
|
+
}
|
|
188
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(baseRoot, normalized);
|
|
189
|
+
}
|
|
190
|
+
function resolveWorkspaceArtifactRootForSharedRoot(repoRoot, env, sharedArtifactRoot, fallbackDirname, allowCustomCounterpart = false) {
|
|
191
|
+
const inheritedSharedRoot = resolveProviderSharedRootForEnv(repoRoot, env);
|
|
192
|
+
const sharedRoot = inheritedSharedRoot ?? path.dirname(path.dirname(repoRoot));
|
|
193
|
+
if (!isPathWithinRoot(sharedRoot, sharedArtifactRoot)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const relativeArtifactRoot = path.relative(sharedRoot, sharedArtifactRoot);
|
|
197
|
+
const firstSegment = firstPathSegment(relativeArtifactRoot);
|
|
198
|
+
if ((fallbackDirname === '.runs' && isDefaultRunsLayoutSegment(firstSegment)) ||
|
|
199
|
+
firstSegment === fallbackDirname ||
|
|
200
|
+
allowCustomCounterpart) {
|
|
201
|
+
return path.resolve(repoRoot, relativeArtifactRoot);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function resolveReviewEnvPath(raw, repoRoot, env = process.env) {
|
|
206
|
+
const inheritedSharedRoot = resolveProviderSharedRootForEnv(repoRoot, env);
|
|
207
|
+
const relativeBaseRoot = inheritedSharedRoot ?? repoRoot;
|
|
208
|
+
const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(relativeBaseRoot, raw);
|
|
209
|
+
if (!isProviderIssueWorkspaceRootForEnv(repoRoot, env) || isPathWithinRoot(repoRoot, resolved)) {
|
|
210
|
+
return resolved;
|
|
211
|
+
}
|
|
212
|
+
const sharedRunsRoot = resolveConfiguredReviewArtifactRoot(repoRoot, env, env.CODEX_ORCHESTRATOR_RUNS_DIR || '.runs', '.runs');
|
|
213
|
+
if (!isPathWithinRoot(sharedRunsRoot, resolved)) {
|
|
214
|
+
return resolved;
|
|
215
|
+
}
|
|
216
|
+
const workspaceRunsRoot = resolveWorkspaceArtifactRootForSharedRoot(repoRoot, env, sharedRunsRoot, '.runs', true);
|
|
217
|
+
if (!workspaceRunsRoot) {
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
const workspaceCandidate = path.resolve(workspaceRunsRoot, path.relative(sharedRunsRoot, resolved));
|
|
221
|
+
return existsSync(workspaceCandidate) ? workspaceCandidate : resolved;
|
|
222
|
+
}
|
|
223
|
+
function resolveReviewRunsDirPath(raw, repoRoot, env = process.env) {
|
|
224
|
+
const inheritedSharedRoot = resolveProviderSharedRootForEnv(repoRoot, env);
|
|
225
|
+
return path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(inheritedSharedRoot ?? repoRoot, raw);
|
|
226
|
+
}
|
|
227
|
+
function inferRunsRootFromManifestPath(manifestPath, env = process.env, repoRoot = null) {
|
|
228
|
+
const resolvedManifestPath = path.resolve(manifestPath);
|
|
229
|
+
if (path.basename(resolvedManifestPath) !== 'manifest.json') {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const runDir = path.dirname(resolvedManifestPath);
|
|
233
|
+
const layoutDir = path.dirname(runDir);
|
|
234
|
+
if (path.basename(layoutDir) !== 'cli' && path.basename(layoutDir) !== 'mcp') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const taskDir = path.dirname(layoutDir);
|
|
238
|
+
const runsRoot = path.dirname(taskDir);
|
|
239
|
+
if (path.basename(runsRoot) === '.runs' || path.basename(runsRoot) === 'runs') {
|
|
240
|
+
return runsRoot;
|
|
241
|
+
}
|
|
242
|
+
if (repoRoot) {
|
|
243
|
+
const configuredRunsRoot = resolveConfiguredReviewArtifactRoot(repoRoot, env, env.CODEX_ORCHESTRATOR_RUNS_DIR, '.runs');
|
|
244
|
+
if (isPathWithinRoot(configuredRunsRoot, resolvedManifestPath)) {
|
|
245
|
+
return configuredRunsRoot;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function resolveReviewExecutionArtifactRoots(env, environmentPaths, manifestPath) {
|
|
251
|
+
const manifestRunsRoot = inferRunsRootFromManifestPath(manifestPath, env, environmentPaths.repoRoot);
|
|
252
|
+
if (!manifestRunsRoot ||
|
|
253
|
+
!isProviderIssueWorkspaceRootForEnv(environmentPaths.repoRoot, env) ||
|
|
254
|
+
isPathWithinRoot(environmentPaths.defaultRunsDir, manifestPath)) {
|
|
255
|
+
return {
|
|
256
|
+
runsDir: environmentPaths.defaultRunsDir,
|
|
257
|
+
outDir: environmentPaths.defaultOutDir,
|
|
258
|
+
preserveProviderArtifactRoots: false
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const configuredOutDir = env.CODEX_ORCHESTRATOR_OUT_DIR?.trim();
|
|
262
|
+
const resolvedConfiguredOutDir = configuredOutDir
|
|
263
|
+
? resolveConfiguredReviewArtifactRoot(environmentPaths.repoRoot, env, configuredOutDir, 'out')
|
|
264
|
+
: null;
|
|
265
|
+
const workspaceConfiguredOutDir = resolvedConfiguredOutDir
|
|
266
|
+
? resolveWorkspaceArtifactRootForSharedRoot(environmentPaths.repoRoot, env, resolvedConfiguredOutDir, 'out', isPathWithinRoot(environmentPaths.repoRoot, manifestPath))
|
|
267
|
+
: null;
|
|
268
|
+
const outDir = workspaceConfiguredOutDir ?? resolvedConfiguredOutDir ?? environmentPaths.defaultOutDir;
|
|
269
|
+
return {
|
|
270
|
+
runsDir: manifestRunsRoot,
|
|
271
|
+
outDir,
|
|
272
|
+
preserveProviderArtifactRoots: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function buildReviewExecutionEnv(env, environmentPaths, manifestPath) {
|
|
276
|
+
const artifactRoots = resolveReviewExecutionArtifactRoots(env, environmentPaths, manifestPath);
|
|
277
|
+
const preserveProviderArtifactRoots = artifactRoots.preserveProviderArtifactRoots ||
|
|
278
|
+
env[PRESERVE_PROVIDER_ARTIFACT_ROOTS_ENV] === '1';
|
|
279
|
+
const reviewEnv = {
|
|
280
|
+
...env,
|
|
281
|
+
CODEX_ORCHESTRATOR_ROOT: environmentPaths.repoRoot,
|
|
282
|
+
CODEX_ORCHESTRATOR_RUNS_DIR: artifactRoots.runsDir,
|
|
283
|
+
CODEX_ORCHESTRATOR_OUT_DIR: artifactRoots.outDir,
|
|
284
|
+
CODEX_ORCHESTRATOR_RUN_DIR: path.dirname(manifestPath),
|
|
285
|
+
CODEX_ORCHESTRATOR_MANIFEST_PATH: manifestPath
|
|
286
|
+
};
|
|
287
|
+
if (preserveProviderArtifactRoots) {
|
|
288
|
+
reviewEnv[PRESERVE_PROVIDER_ARTIFACT_ROOTS_ENV] = '1';
|
|
289
|
+
}
|
|
290
|
+
return reviewEnv;
|
|
291
|
+
}
|
|
292
|
+
function parseArgs(argv, environmentPaths) {
|
|
293
|
+
const options = { runsDir: environmentPaths.defaultRunsDir };
|
|
216
294
|
const { args, entries, positionals } = parseCliArgs(argv);
|
|
217
295
|
if (hasFlag(args, 'help') || hasFlag(args, 'h') || positionals.includes('help')) {
|
|
218
296
|
options.help = true;
|
|
@@ -220,10 +298,10 @@ function parseArgs(argv) {
|
|
|
220
298
|
}
|
|
221
299
|
for (const entry of entries) {
|
|
222
300
|
if (entry.key === 'manifest' && typeof entry.value === 'string') {
|
|
223
|
-
options.manifest =
|
|
301
|
+
options.manifest = resolveReviewEnvPath(entry.value, environmentPaths.repoRoot);
|
|
224
302
|
}
|
|
225
303
|
else if (entry.key === 'runs-dir' && typeof entry.value === 'string') {
|
|
226
|
-
options.runsDir =
|
|
304
|
+
options.runsDir = resolveReviewRunsDirPath(entry.value, environmentPaths.repoRoot);
|
|
227
305
|
}
|
|
228
306
|
else if (entry.key === 'task' && typeof entry.value === 'string') {
|
|
229
307
|
options.task = entry.value;
|
|
@@ -255,6 +333,9 @@ function parseArgs(argv) {
|
|
|
255
333
|
else if (entry.key === 'disable-delegation-mcp') {
|
|
256
334
|
options.disableDelegationMcp = parseBooleanOptionValue(entry.value, '--disable-delegation-mcp');
|
|
257
335
|
}
|
|
336
|
+
else if (entry.key === 'surface') {
|
|
337
|
+
options.surface = parseReviewSurfaceOption(entry.value, '--surface');
|
|
338
|
+
}
|
|
258
339
|
}
|
|
259
340
|
if (hasFlag(args, 'uncommitted')) {
|
|
260
341
|
options.uncommitted = true;
|
|
@@ -272,9 +353,9 @@ function parseArgs(argv) {
|
|
|
272
353
|
options.disableDelegationMcp = true;
|
|
273
354
|
}
|
|
274
355
|
if (!options.manifest) {
|
|
275
|
-
const envManifest = process.env.
|
|
356
|
+
const envManifest = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH ?? process.env.MANIFEST;
|
|
276
357
|
if (envManifest && envManifest.trim().length > 0) {
|
|
277
|
-
options.manifest =
|
|
358
|
+
options.manifest = resolveReviewEnvPath(envManifest.trim(), environmentPaths.repoRoot);
|
|
278
359
|
}
|
|
279
360
|
}
|
|
280
361
|
if (!options.task && options.manifest) {
|
|
@@ -284,11 +365,17 @@ function parseArgs(argv) {
|
|
|
284
365
|
}
|
|
285
366
|
}
|
|
286
367
|
if (!options.task && !options.manifest) {
|
|
287
|
-
const taskFromEnv = process.env
|
|
368
|
+
const taskFromEnv = resolveProviderTaskIdFromEnv(process.env, environmentPaths.repoRoot);
|
|
288
369
|
if (taskFromEnv) {
|
|
289
370
|
options.task = taskFromEnv;
|
|
290
371
|
}
|
|
291
372
|
}
|
|
373
|
+
if (options.surface === undefined) {
|
|
374
|
+
const fromEnv = process.env[REVIEW_SURFACE_ENV_KEY];
|
|
375
|
+
if (typeof fromEnv === 'string' && fromEnv.trim().length > 0) {
|
|
376
|
+
options.surface = parseReviewSurfaceOption(fromEnv, REVIEW_SURFACE_ENV_KEY);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
292
379
|
if (options.autoIssueLog === undefined) {
|
|
293
380
|
const fromEnv = process.env[REVIEW_AUTO_ISSUE_LOG_ENV_KEY];
|
|
294
381
|
if (typeof fromEnv === 'string' && fromEnv.trim().length > 0) {
|
|
@@ -309,10 +396,19 @@ function parseArgs(argv) {
|
|
|
309
396
|
}
|
|
310
397
|
return options;
|
|
311
398
|
}
|
|
312
|
-
async function resolveManifestPath(options) {
|
|
399
|
+
async function resolveManifestPath(options, repoRoot) {
|
|
313
400
|
if (options.manifest) {
|
|
401
|
+
if (!(await pathExists(options.manifest)))
|
|
402
|
+
throw new Error(`Manifest not found: ${options.manifest}`);
|
|
314
403
|
return options.manifest;
|
|
315
404
|
}
|
|
405
|
+
const runDirManifest = await resolveManifestPathFromRunDir(repoRoot);
|
|
406
|
+
const requestedTask = options.task?.trim();
|
|
407
|
+
const runDirTask = runDirManifest ? inferTaskFromManifestPath(runDirManifest) : null;
|
|
408
|
+
if (runDirManifest &&
|
|
409
|
+
(!requestedTask || runDirTask === null || (typeof runDirTask === 'string' && runDirTask === requestedTask))) {
|
|
410
|
+
return runDirManifest;
|
|
411
|
+
}
|
|
316
412
|
const manifests = await collectManifests(options.runsDir, options.task);
|
|
317
413
|
if (manifests.length === 0) {
|
|
318
414
|
throw new Error('No run manifests found. Provide --manifest or execute the orchestrator first.');
|
|
@@ -327,1670 +423,305 @@ async function resolveManifestPath(options) {
|
|
|
327
423
|
}
|
|
328
424
|
}));
|
|
329
425
|
scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
330
|
-
return scored[0]?.manifestPath ?? manifests[0];
|
|
426
|
+
return resolveReviewEnvPath(scored[0]?.manifestPath ?? manifests[0], repoRoot);
|
|
331
427
|
}
|
|
332
|
-
async function
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (shouldRunDiffBudget()) {
|
|
339
|
-
await runDiffBudget(options);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
console.log('[run-review] skipping diff budget (already executed by pipeline).');
|
|
428
|
+
async function resolveManifestPathFromRunDir(repoRoot) {
|
|
429
|
+
const configuredRunDir = process.env.CODEX_ORCHESTRATOR_RUN_DIR?.trim();
|
|
430
|
+
if (!configuredRunDir) {
|
|
431
|
+
return null;
|
|
343
432
|
}
|
|
344
|
-
const manifestPath =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
];
|
|
357
|
-
const taskKey = options.task ?? process.env.MCP_RUNNER_TASK_ID ?? process.env.TASK;
|
|
358
|
-
if (taskKey) {
|
|
359
|
-
const contextLines = await buildTaskContext(taskKey);
|
|
360
|
-
if (contextLines.length > 0) {
|
|
361
|
-
promptLines.push('', ...contextLines);
|
|
433
|
+
const manifestPath = path.join(resolveReviewEnvPath(configuredRunDir, repoRoot), 'manifest.json');
|
|
434
|
+
return (await pathExists(manifestPath)) ? manifestPath : null;
|
|
435
|
+
}
|
|
436
|
+
export async function runReviewCli(argv = process.argv.slice(2)) {
|
|
437
|
+
process.exitCode = 0;
|
|
438
|
+
try {
|
|
439
|
+
const environmentPaths = resolveReviewEnvironmentPaths();
|
|
440
|
+
const { repoRoot } = environmentPaths;
|
|
441
|
+
const options = parseArgs(argv, environmentPaths);
|
|
442
|
+
if (options.help) {
|
|
443
|
+
printReviewWrapperHelp();
|
|
444
|
+
return 0;
|
|
362
445
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
promptLines.push('', 'Please review the current changes and confirm:', '- The solution is minimal and avoids unnecessary abstraction/scope', '- README/SOP docs match the implemented behavior', '- Commands/scripts are non-interactive (no TTY prompts)', '- Evidence + checklist mirroring requirements are satisfied', '', 'Call out any remaining documentation/code mismatches or guardrail violations.');
|
|
368
|
-
const allowHeavyCommands = allowHeavyReviewCommands();
|
|
369
|
-
const enforceBoundedMode = !allowHeavyCommands && enforceBoundedReviewMode();
|
|
370
|
-
if (allowHeavyCommands) {
|
|
371
|
-
console.log(`[run-review] heavy review commands allowed (${REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY}=1).`);
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
console.log(`[run-review] bounded review guidance enabled by default (set ${REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY}=1 to opt into unrestricted heavy-command execution).`);
|
|
375
|
-
if (enforceBoundedMode) {
|
|
376
|
-
console.log(`[run-review] bounded enforcement enabled (${REVIEW_ENFORCE_BOUNDED_MODE_ENV_KEY}=1); heavy command starts will terminate the review.`);
|
|
446
|
+
const reviewSurface = options.surface ?? 'diff';
|
|
447
|
+
const explicitScopeSurfaceGateError = buildExplicitScopeSurfaceGateError(options, reviewSurface);
|
|
448
|
+
if (explicitScopeSurfaceGateError) {
|
|
449
|
+
throw new Error(explicitScopeSurfaceGateError);
|
|
377
450
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const scopeNotes = await buildScopeNotes(options);
|
|
381
|
-
if (scopeNotes.length > 0) {
|
|
382
|
-
promptLines.push('', ...scopeNotes);
|
|
383
|
-
}
|
|
384
|
-
const scopeAssessment = await assessReviewScope(options);
|
|
385
|
-
const scopeMetrics = formatScopeMetrics(scopeAssessment);
|
|
386
|
-
if (scopeAssessment.mode === 'uncommitted') {
|
|
387
|
-
if (scopeMetrics) {
|
|
388
|
-
console.log(`[run-review] review scope metrics: ${scopeMetrics}.`);
|
|
451
|
+
if (shouldRunDiffBudget()) {
|
|
452
|
+
await runDiffBudget(options, repoRoot);
|
|
389
453
|
}
|
|
390
454
|
else {
|
|
391
|
-
console.log('[run-review]
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
console.log(`[run-review] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}.`);
|
|
435
|
-
await ensureReviewCommandAvailable(runtimeContext);
|
|
436
|
-
const disableDelegationMcp = options.disableDelegationMcp ??
|
|
437
|
-
(options.enableDelegationMcp === undefined ? false : !options.enableDelegationMcp);
|
|
438
|
-
if (disableDelegationMcp) {
|
|
439
|
-
console.log('[run-review] delegation MCP disabled for this review (explicit opt-out via --disable-delegation-mcp or CODEX_REVIEW_DISABLE_DELEGATION_MCP=1).');
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
console.log('[run-review] delegation MCP enabled for this review (default; set --disable-delegation-mcp or CODEX_REVIEW_DISABLE_DELEGATION_MCP=1 to disable).');
|
|
443
|
-
}
|
|
444
|
-
const scopedReviewArgs = buildReviewArgs(options, prompt, {
|
|
445
|
-
includeScopeFlags: true,
|
|
446
|
-
disableDelegationMcp
|
|
447
|
-
});
|
|
448
|
-
const resolvedScoped = resolveReviewCommand(scopedReviewArgs, runtimeContext);
|
|
449
|
-
console.log(`Review prompt saved to: ${path.relative(repoRoot, artifactPaths.promptPath)}`);
|
|
450
|
-
console.log(`Review output log: ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
|
|
451
|
-
console.log(`Launching Codex review (evidence: ${relativeManifest})`);
|
|
452
|
-
const timeoutMs = resolveReviewTimeoutMs();
|
|
453
|
-
if (timeoutMs !== null) {
|
|
454
|
-
console.log(`[run-review] enforcing codex review timeout at ${Math.round(timeoutMs / 1000)}s (configured via CODEX_REVIEW_TIMEOUT_SECONDS).`);
|
|
455
|
-
}
|
|
456
|
-
const stallTimeoutMs = resolveReviewStallTimeoutMs();
|
|
457
|
-
if (stallTimeoutMs !== null) {
|
|
458
|
-
console.log(`[run-review] enforcing codex review stall timeout at ${Math.round(stallTimeoutMs / 1000)}s of no output (configured via CODEX_REVIEW_STALL_TIMEOUT_SECONDS).`);
|
|
459
|
-
}
|
|
460
|
-
const startupLoopTimeoutMs = resolveReviewStartupLoopTimeoutMs();
|
|
461
|
-
const startupLoopMinEvents = resolveReviewStartupLoopMinEvents();
|
|
462
|
-
if (startupLoopTimeoutMs !== null) {
|
|
463
|
-
console.log(`[run-review] enforcing delegation-startup loop timeout at ${Math.round(startupLoopTimeoutMs / 1000)}s after ${startupLoopMinEvents} startup events (configured via CODEX_REVIEW_STARTUP_LOOP_TIMEOUT_SECONDS).`);
|
|
464
|
-
}
|
|
465
|
-
const monitorIntervalMs = resolveReviewMonitorIntervalMs();
|
|
466
|
-
if (monitorIntervalMs === null) {
|
|
467
|
-
console.log('[run-review] patience-first monitor checkpoints disabled (configured via CODEX_REVIEW_MONITOR_INTERVAL_SECONDS=0).');
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
console.log(`[run-review] patience-first monitor checkpoints every ${formatDurationMs(monitorIntervalMs)} (set CODEX_REVIEW_MONITOR_INTERVAL_SECONDS=0 to disable).`);
|
|
471
|
-
}
|
|
472
|
-
const autoIssueLogEnabled = options.autoIssueLog ?? false;
|
|
473
|
-
const runReview = async (resolved) => runCodexReview({
|
|
474
|
-
command: resolved.command,
|
|
475
|
-
args: resolved.args,
|
|
476
|
-
env: runtimeContext.env,
|
|
477
|
-
stdio: nonInteractive ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'pipe', 'pipe'],
|
|
478
|
-
blockHeavyCommands: enforceBoundedMode,
|
|
479
|
-
timeoutMs,
|
|
480
|
-
stallTimeoutMs,
|
|
481
|
-
startupLoopTimeoutMs,
|
|
482
|
-
startupLoopMinEvents,
|
|
483
|
-
monitorIntervalMs,
|
|
484
|
-
outputLogPath: artifactPaths.outputLogPath
|
|
485
|
-
});
|
|
486
|
-
const writeTelemetry = async (status, errorMessage) => {
|
|
487
|
-
try {
|
|
488
|
-
return await persistReviewTelemetry({
|
|
489
|
-
telemetryPath: artifactPaths.telemetryPath,
|
|
490
|
-
outputLogPath: artifactPaths.outputLogPath,
|
|
491
|
-
status,
|
|
492
|
-
error: errorMessage ?? null
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
catch (telemetryError) {
|
|
496
|
-
const telemetryMessage = telemetryError instanceof Error ? telemetryError.message : String(telemetryError);
|
|
497
|
-
console.error(`[run-review] failed to persist review telemetry: ${telemetryMessage}`);
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
try {
|
|
502
|
-
await runReview(resolvedScoped);
|
|
503
|
-
const telemetrySummary = await writeTelemetry('succeeded');
|
|
504
|
-
console.log(`Review output saved to: ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
|
|
505
|
-
if (telemetrySummary) {
|
|
506
|
-
console.log(`Review telemetry saved to: ${path.relative(repoRoot, artifactPaths.telemetryPath)}`);
|
|
455
|
+
console.log('[run-review] skipping diff budget (already executed by pipeline).');
|
|
456
|
+
}
|
|
457
|
+
const manifestPath = await resolveManifestPath(options, repoRoot);
|
|
458
|
+
const relativeManifest = path.relative(repoRoot, manifestPath);
|
|
459
|
+
const runnerLogPath = path.join(path.dirname(manifestPath), 'runner.ndjson');
|
|
460
|
+
const runnerLogExists = await pathExists(runnerLogPath);
|
|
461
|
+
const relativeRunnerLog = path.relative(repoRoot, runnerLogPath);
|
|
462
|
+
const manifestTask = inferTaskFromManifestPath(manifestPath);
|
|
463
|
+
const envTask = resolveProviderTaskIdFromEnv(process.env, repoRoot);
|
|
464
|
+
const taskKey = options.task ?? envTask ?? manifestTask;
|
|
465
|
+
const taskLabel = taskKey ?? 'unknown-task';
|
|
466
|
+
const diffBudgetOverride = process.env.DIFF_BUDGET_OVERRIDE_REASON?.trim();
|
|
467
|
+
const scopeMode = resolveEffectiveScopeMode(options);
|
|
468
|
+
const allowHeavyCommands = allowHeavyReviewCommands();
|
|
469
|
+
const { promptLines, reviewTaskContext, activeCloseoutBundleRoots, scopedReviewerVisibleTitle } = await buildReviewPromptContext({
|
|
470
|
+
repoRoot,
|
|
471
|
+
taskKey,
|
|
472
|
+
taskLabel,
|
|
473
|
+
reviewSurface,
|
|
474
|
+
relativeManifest,
|
|
475
|
+
runnerLogExists,
|
|
476
|
+
relativeRunnerLog,
|
|
477
|
+
notes: process.env.NOTES,
|
|
478
|
+
scopeMode,
|
|
479
|
+
includeBoundedReviewConstraints: !allowHeavyCommands
|
|
480
|
+
});
|
|
481
|
+
const explicitScopedReview = Boolean(options.base || options.commit || options.uncommitted);
|
|
482
|
+
const explicitReviewTitle = typeof options.title === 'string' && options.title.trim().length > 0 ? options.title.trim() : null;
|
|
483
|
+
const effectiveReviewTitle = explicitReviewTitle
|
|
484
|
+
? explicitScopedReview && !allowHeavyCommands
|
|
485
|
+
? addBoundedReviewConstraintsToScopedTitle({ title: explicitReviewTitle })
|
|
486
|
+
: explicitReviewTitle
|
|
487
|
+
: explicitScopedReview
|
|
488
|
+
? scopedReviewerVisibleTitle
|
|
489
|
+
: null;
|
|
490
|
+
const effectiveTitleSource = explicitReviewTitle
|
|
491
|
+
? 'user'
|
|
492
|
+
: explicitScopedReview
|
|
493
|
+
? 'notes-surface'
|
|
494
|
+
: undefined;
|
|
495
|
+
const enforceBoundedMode = !allowHeavyCommands && enforceBoundedReviewMode();
|
|
496
|
+
if (allowHeavyCommands) {
|
|
497
|
+
console.log(`[run-review] heavy review commands allowed (${REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY}=1).`);
|
|
507
498
|
}
|
|
508
499
|
else {
|
|
509
|
-
console.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
500
|
+
console.log(`[run-review] bounded review guidance enabled by default (set ${REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY}=1 to opt into unrestricted heavy-command execution).`);
|
|
501
|
+
if (enforceBoundedMode) {
|
|
502
|
+
console.log(`[run-review] bounded enforcement enabled (${REVIEW_ENFORCE_BOUNDED_MODE_ENV_KEY}=1); heavy command starts will terminate the review.`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const scopePathCollection = await collectReviewScopePaths(options, repoRoot);
|
|
506
|
+
const scopeNotes = buildScopeNotes(options, scopePathCollection);
|
|
507
|
+
if (scopeNotes.length > 0) {
|
|
508
|
+
promptLines.push('', ...scopeNotes);
|
|
509
|
+
}
|
|
510
|
+
const activeCloseoutProvenanceLines = buildActiveCloseoutProvenanceLines(repoRoot, activeCloseoutBundleRoots);
|
|
511
|
+
if (activeCloseoutProvenanceLines.length > 0) {
|
|
512
|
+
promptLines.push('', ...activeCloseoutProvenanceLines);
|
|
513
|
+
}
|
|
514
|
+
const scopeAssessment = await assessReviewScope(options, repoRoot);
|
|
515
|
+
const scopeMetrics = formatScopeMetrics(scopeAssessment);
|
|
516
|
+
const largeScopeOverrideReason = resolveLargeScopeOverrideReason(process.env);
|
|
517
|
+
const stdinIsTTY = process.stdin?.isTTY === true;
|
|
518
|
+
const promptOnlyHandoff = shouldPrintNonInteractiveHandoff({
|
|
519
|
+
env: process.env,
|
|
520
|
+
nonInteractive: options.nonInteractive ?? shouldForceNonInteractive(process.env, stdinIsTTY),
|
|
521
|
+
stdinIsTTY
|
|
522
|
+
});
|
|
523
|
+
logReviewScopeAssessment(scopeAssessment, scopeMetrics, console, largeScopeOverrideReason);
|
|
524
|
+
const largeScopeGateError = getLargeScopeGateError(scopeAssessment, scopeMetrics, largeScopeOverrideReason);
|
|
525
|
+
const explicitScopeRetryGateError = buildExplicitScopeRetryGateError(options);
|
|
526
|
+
const retryWithoutScopeFlagsAssessment = explicitScopeRetryGateError !== null || resolveEffectiveScopeMode(options) === 'uncommitted'
|
|
527
|
+
? null
|
|
528
|
+
: await assessReviewScope({}, repoRoot);
|
|
529
|
+
const retryWithoutScopeFlagsGateError = explicitScopeRetryGateError ??
|
|
530
|
+
(retryWithoutScopeFlagsAssessment === null
|
|
531
|
+
? null
|
|
532
|
+
: getLargeScopeGateError(retryWithoutScopeFlagsAssessment, formatScopeMetrics(retryWithoutScopeFlagsAssessment), null));
|
|
533
|
+
if (largeScopeGateError && !promptOnlyHandoff) {
|
|
534
|
+
throw new Error(largeScopeGateError);
|
|
535
|
+
}
|
|
536
|
+
const scopeAdvisoryPromptLines = buildLargeScopeAdvisoryPromptLines(scopeAssessment, scopeMetrics, largeScopeOverrideReason);
|
|
537
|
+
if (scopeAdvisoryPromptLines.length > 0) {
|
|
538
|
+
promptLines.push('', ...scopeAdvisoryPromptLines);
|
|
539
|
+
}
|
|
540
|
+
if (reviewSurface === 'audit' && diffBudgetOverride) {
|
|
541
|
+
promptLines.push('', `Diff budget override: ${diffBudgetOverride}`);
|
|
542
|
+
}
|
|
543
|
+
const prompt = promptLines.join('\n');
|
|
544
|
+
const reviewBaseEnv = buildReviewExecutionEnv(process.env, environmentPaths, manifestPath);
|
|
545
|
+
const { artifactPaths, nonInteractive, reviewEnv, handedOff } = await prepareReviewNonInteractiveHandoffShell({
|
|
546
|
+
cliNonInteractive: options.nonInteractive,
|
|
547
|
+
env: reviewBaseEnv,
|
|
555
548
|
manifestPath,
|
|
556
|
-
|
|
549
|
+
prompt,
|
|
550
|
+
repoRoot,
|
|
551
|
+
runnerLogExists,
|
|
552
|
+
runnerLogPath,
|
|
553
|
+
stdinIsTTY
|
|
557
554
|
});
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (telemetrySummary) {
|
|
561
|
-
logReviewTelemetrySummary(telemetrySummary, artifactPaths.telemetryPath);
|
|
555
|
+
if (handedOff) {
|
|
556
|
+
return typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
562
557
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const hasReview = await new Promise((resolve, reject) => {
|
|
576
|
-
const detached = process.platform !== 'win32';
|
|
577
|
-
const child = spawn(resolved.command, resolved.args, { stdio: ['ignore', 'pipe', 'pipe'], detached });
|
|
578
|
-
let output = '';
|
|
579
|
-
let settled = false;
|
|
580
|
-
let hardKillArmed = false;
|
|
581
|
-
let killHandle;
|
|
582
|
-
const timeoutHandle = setTimeout(() => {
|
|
583
|
-
if (settled) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
signalChildProcess(child, 'SIGTERM', detached);
|
|
587
|
-
hardKillArmed = true;
|
|
588
|
-
killHandle = setTimeout(() => {
|
|
589
|
-
if (child.exitCode === null) {
|
|
590
|
-
signalChildProcess(child, 'SIGKILL', detached);
|
|
591
|
-
}
|
|
592
|
-
}, 5000);
|
|
593
|
-
killHandle.unref();
|
|
594
|
-
settled = true;
|
|
595
|
-
reject(new Error('codex --help timed out while checking the review subcommand.'));
|
|
596
|
-
}, REVIEW_COMMAND_CHECK_TIMEOUT_MS);
|
|
597
|
-
timeoutHandle.unref();
|
|
598
|
-
const finalize = (outcome) => {
|
|
599
|
-
if (settled) {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
settled = true;
|
|
603
|
-
clearTimeout(timeoutHandle);
|
|
604
|
-
if (killHandle && !hardKillArmed) {
|
|
605
|
-
clearTimeout(killHandle);
|
|
606
|
-
}
|
|
607
|
-
if ('error' in outcome) {
|
|
608
|
-
reject(outcome.error);
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
resolve(outcome.ok);
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
child.stdout?.on('data', (chunk) => {
|
|
615
|
-
output += chunk.toString();
|
|
558
|
+
const boundaryPreflight = await prepareReviewExecutionBoundaryPreflight({
|
|
559
|
+
cliOptions: options,
|
|
560
|
+
manifestPath,
|
|
561
|
+
env: reviewEnv,
|
|
562
|
+
repoRoot,
|
|
563
|
+
reviewSurface,
|
|
564
|
+
architectureSurfacePaths: reviewTaskContext.architectureSurfacePaths,
|
|
565
|
+
scopeTouchedPaths: scopePathCollection.paths,
|
|
566
|
+
activeCloseoutBundleRoots,
|
|
567
|
+
runnerLogExists,
|
|
568
|
+
runnerLogPath,
|
|
569
|
+
allowHeavyCommands
|
|
616
570
|
});
|
|
617
|
-
|
|
618
|
-
|
|
571
|
+
const { runtimeContext, timeoutMs, stallTimeoutMs, startupLoopTimeoutMs, startupLoopMinEvents, monitorIntervalMs, lowSignalTimeoutMs, verdictStabilityTimeoutMs, metaSurfaceTimeoutMs, allowedMetaSurfaceKinds, touchedPaths, startupAnchorMode, enforceStartupAnchorBoundary, enforceActiveCloseoutBundleRereadBoundary, enforceRelevantReinspectionDwellBoundary, auditStartupAnchorPaths, allowedMetaSurfacePaths, auditStartupAnchorEnvVarPaths, allowedMetaSurfaceEnvVarPaths } = boundaryPreflight;
|
|
572
|
+
const autoIssueLogEnabled = options.autoIssueLog ?? false;
|
|
573
|
+
const runReview = async (resolved) => runCodexReview({
|
|
574
|
+
command: resolved.command,
|
|
575
|
+
args: resolved.args,
|
|
576
|
+
env: runtimeContext.env,
|
|
577
|
+
stdio: nonInteractive ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'pipe', 'pipe'],
|
|
578
|
+
activeCloseoutBundleRoots,
|
|
579
|
+
blockHeavyCommands: enforceBoundedMode,
|
|
580
|
+
allowValidationCommandIntents: allowHeavyCommands,
|
|
581
|
+
timeoutMs,
|
|
582
|
+
stallTimeoutMs,
|
|
583
|
+
startupLoopTimeoutMs,
|
|
584
|
+
startupLoopMinEvents,
|
|
585
|
+
monitorIntervalMs,
|
|
586
|
+
lowSignalTimeoutMs,
|
|
587
|
+
verdictStabilityTimeoutMs,
|
|
588
|
+
metaSurfaceTimeoutMs,
|
|
589
|
+
enforceStartupAnchorBoundary,
|
|
590
|
+
enforceActiveCloseoutBundleRereadBoundary,
|
|
591
|
+
enforceRelevantReinspectionDwellBoundary,
|
|
592
|
+
allowedMetaSurfaceKinds: [...allowedMetaSurfaceKinds],
|
|
593
|
+
scopeMode,
|
|
594
|
+
startupAnchorMode,
|
|
595
|
+
auditStartupAnchorPaths,
|
|
596
|
+
allowedMetaSurfacePaths,
|
|
597
|
+
auditStartupAnchorEnvVarPaths,
|
|
598
|
+
allowedMetaSurfaceEnvVarPaths,
|
|
599
|
+
repoRoot,
|
|
600
|
+
touchedPaths,
|
|
601
|
+
outputLogPath: artifactPaths.outputLogPath
|
|
619
602
|
});
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
603
|
+
const writeTelemetry = async (state, status, errorMessage, terminationBoundary, launchContext) => writeReviewExecutionTelemetry({
|
|
604
|
+
state,
|
|
605
|
+
status,
|
|
606
|
+
error: errorMessage ?? null,
|
|
607
|
+
terminationBoundary,
|
|
608
|
+
launchContext: launchContext ?? null,
|
|
609
|
+
outputLogPath: artifactPaths.outputLogPath,
|
|
610
|
+
repoRoot,
|
|
611
|
+
telemetryPath: artifactPaths.telemetryPath,
|
|
612
|
+
includeRawTelemetry: envFlagEnabled(process.env[REVIEW_TELEMETRY_DEBUG_ENV_KEY]),
|
|
613
|
+
telemetryDebugEnvKey: REVIEW_TELEMETRY_DEBUG_ENV_KEY
|
|
623
614
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
615
|
+
await runReviewLaunchAttemptShell({
|
|
616
|
+
cliOptions: {
|
|
617
|
+
...options,
|
|
618
|
+
title: effectiveReviewTitle ?? undefined,
|
|
619
|
+
titleSource: effectiveTitleSource
|
|
620
|
+
},
|
|
621
|
+
prompt,
|
|
622
|
+
retryWithoutScopeFlagsGateError,
|
|
623
|
+
runtimeContext,
|
|
624
|
+
repoRoot,
|
|
625
|
+
manifestPath,
|
|
626
|
+
artifactPaths,
|
|
627
|
+
autoIssueLogEnabled,
|
|
628
|
+
telemetryDebugEnabled: envFlagEnabled(process.env[REVIEW_TELEMETRY_DEBUG_ENV_KEY]),
|
|
629
|
+
telemetryDebugEnvKey: REVIEW_TELEMETRY_DEBUG_ENV_KEY,
|
|
630
|
+
runReview,
|
|
631
|
+
writeTelemetry,
|
|
632
|
+
logTelemetrySummary: logReviewExecutionTelemetrySummary,
|
|
633
|
+
logTerminationBoundaryFallback
|
|
634
|
+
});
|
|
635
|
+
return typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
console.error('[run-review] failed:', error instanceof Error ? error.message : String(error));
|
|
639
|
+
process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
|
|
640
|
+
return process.exitCode;
|
|
627
641
|
}
|
|
628
642
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
null);
|
|
635
|
-
return await createRuntimeCodexCommandContext({
|
|
636
|
-
requestedMode,
|
|
637
|
-
executionMode: 'mcp',
|
|
638
|
-
repoRoot,
|
|
639
|
-
env: params.env,
|
|
640
|
-
runId: runId ?? `review-${Date.now()}`
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
async function resolveReviewRunId(manifestPath) {
|
|
643
|
+
export function isDirectExecution(entryArg = process.argv[1], metaUrl = import.meta.url) {
|
|
644
|
+
if (typeof entryArg !== 'string' || entryArg.length === 0) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const candidateUrls = new Set();
|
|
644
648
|
try {
|
|
645
|
-
|
|
646
|
-
const parsed = JSON.parse(raw);
|
|
647
|
-
return typeof parsed.run_id === 'string' && parsed.run_id.trim().length > 0
|
|
648
|
-
? parsed.run_id.trim()
|
|
649
|
-
: null;
|
|
649
|
+
candidateUrls.add(pathToFileURL(path.resolve(entryArg)).href);
|
|
650
650
|
}
|
|
651
651
|
catch {
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
function resolveScopeFlag(options) {
|
|
656
|
-
if (options.commit) {
|
|
657
|
-
return { mode: 'commit', args: ['--commit', options.commit] };
|
|
652
|
+
// Fall through to the realpath candidate so missing/cwd issues still fail closed.
|
|
658
653
|
}
|
|
659
|
-
|
|
660
|
-
|
|
654
|
+
try {
|
|
655
|
+
candidateUrls.add(pathToFileURL(realpathSync(entryArg)).href);
|
|
661
656
|
}
|
|
662
|
-
|
|
663
|
-
|
|
657
|
+
catch {
|
|
658
|
+
// Missing or unreadable entry points should not be treated as direct execution.
|
|
664
659
|
}
|
|
665
|
-
return
|
|
660
|
+
return candidateUrls.has(metaUrl);
|
|
666
661
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return 'commit';
|
|
670
|
-
}
|
|
671
|
-
if (options.base) {
|
|
672
|
-
return 'base';
|
|
673
|
-
}
|
|
674
|
-
return 'uncommitted';
|
|
662
|
+
if (isDirectExecution()) {
|
|
663
|
+
void runReviewCli();
|
|
675
664
|
}
|
|
676
|
-
function
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (options.title) {
|
|
683
|
-
args.push('--title', options.title);
|
|
684
|
-
}
|
|
685
|
-
const scopeFlag = resolveScopeFlag(options);
|
|
686
|
-
if (opts.includeScopeFlags && scopeFlag) {
|
|
687
|
-
args.push(...scopeFlag.args);
|
|
665
|
+
async function runDiffBudget(options, repoRoot) {
|
|
666
|
+
const scriptPath = path.join(repoRoot, 'scripts', 'diff-budget.mjs');
|
|
667
|
+
const relativeScriptPath = path.relative(repoRoot, scriptPath);
|
|
668
|
+
if (!(await pathExists(scriptPath))) {
|
|
669
|
+
console.log(`[run-review] skipping diff budget (missing ${relativeScriptPath}; downstream npm environment detected).`);
|
|
670
|
+
return;
|
|
688
671
|
}
|
|
689
|
-
args
|
|
690
|
-
return args;
|
|
691
|
-
}
|
|
692
|
-
function resolveReviewCommand(reviewArgs, context) {
|
|
693
|
-
return resolveRuntimeCodexCommand(reviewArgs, context);
|
|
694
|
-
}
|
|
695
|
-
async function buildScopeNotes(options) {
|
|
696
|
-
const lines = [];
|
|
697
|
-
const details = [];
|
|
672
|
+
const args = [scriptPath];
|
|
698
673
|
if (options.commit) {
|
|
699
|
-
|
|
700
|
-
const summary = await tryGit([
|
|
701
|
-
'show',
|
|
702
|
-
'--no-color',
|
|
703
|
-
'--name-status',
|
|
704
|
-
'--no-patch',
|
|
705
|
-
'--format=medium',
|
|
706
|
-
options.commit
|
|
707
|
-
]);
|
|
708
|
-
if (summary) {
|
|
709
|
-
details.push(summary);
|
|
710
|
-
}
|
|
674
|
+
args.push('--commit', options.commit);
|
|
711
675
|
}
|
|
712
676
|
else if (options.base) {
|
|
713
|
-
|
|
714
|
-
const diff = await tryGit(['diff', '--no-color', '--name-status', `${options.base}...HEAD`]);
|
|
715
|
-
if (diff) {
|
|
716
|
-
details.push(diff);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
lines.push('Review scope hint: uncommitted working tree changes (default).');
|
|
721
|
-
const status = await tryGit(['status', '--porcelain=v1', '-b']);
|
|
722
|
-
if (status) {
|
|
723
|
-
details.push(status);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (details.length > 0) {
|
|
727
|
-
lines.push('', 'Git scope summary:', '```', ...details, '```');
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
lines.push('', 'Git scope summary: unavailable (git command failed).');
|
|
731
|
-
}
|
|
732
|
-
return lines;
|
|
733
|
-
}
|
|
734
|
-
function resolveLargeScopeFileThreshold() {
|
|
735
|
-
const configured = process.env[REVIEW_LARGE_SCOPE_FILE_THRESHOLD_ENV_KEY]?.trim();
|
|
736
|
-
if (!configured) {
|
|
737
|
-
return DEFAULT_LARGE_SCOPE_FILE_THRESHOLD;
|
|
738
|
-
}
|
|
739
|
-
const parsed = Number(configured);
|
|
740
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) {
|
|
741
|
-
throw new Error(`${REVIEW_LARGE_SCOPE_FILE_THRESHOLD_ENV_KEY} must be a positive integer.`);
|
|
742
|
-
}
|
|
743
|
-
return parsed;
|
|
744
|
-
}
|
|
745
|
-
function resolveLargeScopeLineThreshold() {
|
|
746
|
-
const configured = process.env[REVIEW_LARGE_SCOPE_LINE_THRESHOLD_ENV_KEY]?.trim();
|
|
747
|
-
if (!configured) {
|
|
748
|
-
return DEFAULT_LARGE_SCOPE_LINE_THRESHOLD;
|
|
749
|
-
}
|
|
750
|
-
const parsed = Number(configured);
|
|
751
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) {
|
|
752
|
-
throw new Error(`${REVIEW_LARGE_SCOPE_LINE_THRESHOLD_ENV_KEY} must be a positive integer.`);
|
|
753
|
-
}
|
|
754
|
-
return parsed;
|
|
755
|
-
}
|
|
756
|
-
function parseStatusPathCount(statusOutput) {
|
|
757
|
-
const paths = new Set();
|
|
758
|
-
for (const rawLine of statusOutput.split(/\r?\n/u)) {
|
|
759
|
-
const line = rawLine.trimEnd();
|
|
760
|
-
if (!line) {
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
if (line.startsWith('## ')) {
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
const pathPortion = line.slice(3).trim();
|
|
767
|
-
if (!pathPortion) {
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
const currentPath = pathPortion.includes(' -> ')
|
|
771
|
-
? pathPortion.split(' -> ').at(-1)?.trim() ?? pathPortion
|
|
772
|
-
: pathPortion;
|
|
773
|
-
if (currentPath) {
|
|
774
|
-
paths.add(currentPath);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
return paths.size;
|
|
778
|
-
}
|
|
779
|
-
function parseNumstatLineDelta(numstatOutput) {
|
|
780
|
-
let total = 0;
|
|
781
|
-
for (const rawLine of numstatOutput.split(/\r?\n/u)) {
|
|
782
|
-
const line = rawLine.trim();
|
|
783
|
-
if (!line) {
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
|
-
const [added, deleted] = line.split(/\s+/u);
|
|
787
|
-
const addCount = Number(added);
|
|
788
|
-
const delCount = Number(deleted);
|
|
789
|
-
total += Number.isFinite(addCount) ? addCount : 0;
|
|
790
|
-
total += Number.isFinite(delCount) ? delCount : 0;
|
|
791
|
-
}
|
|
792
|
-
return total;
|
|
793
|
-
}
|
|
794
|
-
function parseNullDelimitedPaths(raw) {
|
|
795
|
-
return raw.split('\u0000').filter((entry) => entry.length > 0);
|
|
796
|
-
}
|
|
797
|
-
async function countWorkingTreeFileLines(relativePath) {
|
|
798
|
-
const absolutePath = path.resolve(repoRoot, relativePath);
|
|
799
|
-
try {
|
|
800
|
-
const fileStat = await stat(absolutePath);
|
|
801
|
-
if (!fileStat.isFile()) {
|
|
802
|
-
return 0;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch {
|
|
806
|
-
return 0;
|
|
677
|
+
args.push('--base', options.base);
|
|
807
678
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
settled = true;
|
|
820
|
-
resolve(value);
|
|
821
|
-
};
|
|
822
|
-
stream.once('error', () => settle(0));
|
|
823
|
-
stream.on('data', (chunk) => {
|
|
824
|
-
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
825
|
-
if (!buffer || buffer.length === 0) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
sawData = true;
|
|
829
|
-
if (buffer.includes(0x00)) {
|
|
830
|
-
sawBinaryByte = true;
|
|
831
|
-
stream.destroy();
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
for (const byte of buffer.values()) {
|
|
835
|
-
if (byte === 0x0a) {
|
|
836
|
-
newlineCount += 1;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
lastByte = buffer[buffer.length - 1] ?? lastByte;
|
|
679
|
+
const diffBudgetEnv = { ...process.env };
|
|
680
|
+
// The review wrapper's scope is driven by explicit CLI flags; inherited base env vars
|
|
681
|
+
// would silently change the default uncommitted review surface.
|
|
682
|
+
delete diffBudgetEnv.BASE_SHA;
|
|
683
|
+
delete diffBudgetEnv.DIFF_BUDGET_BASE;
|
|
684
|
+
await new Promise((resolve, reject) => {
|
|
685
|
+
const child = spawn('node', args, {
|
|
686
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
687
|
+
env: diffBudgetEnv,
|
|
688
|
+
cwd: repoRoot
|
|
840
689
|
});
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
settle(0);
|
|
844
|
-
}
|
|
690
|
+
child.stdout?.on('data', (chunk) => {
|
|
691
|
+
process.stdout.write(chunk);
|
|
845
692
|
});
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
693
|
+
child.stderr?.on('data', (chunk) => {
|
|
694
|
+
process.stderr.write(chunk);
|
|
695
|
+
});
|
|
696
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
697
|
+
child.once('close', (code, signal) => {
|
|
698
|
+
if (code === 0) {
|
|
699
|
+
resolve();
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
reject(new Error(code === null
|
|
703
|
+
? `diff budget terminated by signal ${signal ?? 'unknown'}`
|
|
704
|
+
: `diff budget exited with code ${code}`));
|
|
850
705
|
}
|
|
851
|
-
settle(newlineCount + (lastByte === 0x0a ? 0 : 1));
|
|
852
706
|
});
|
|
853
707
|
});
|
|
854
708
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
for (const relativePath of paths) {
|
|
858
|
-
total += await countWorkingTreeFileLines(relativePath);
|
|
859
|
-
}
|
|
860
|
-
return total;
|
|
709
|
+
function shouldRunDiffBudget() {
|
|
710
|
+
return !(envFlagEnabled(process.env.DIFF_BUDGET_STAGE) || envFlagEnabled(process.env.SKIP_DIFF_BUDGET));
|
|
861
711
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const lineThreshold = resolveLargeScopeLineThreshold();
|
|
866
|
-
if (mode !== 'uncommitted') {
|
|
867
|
-
return {
|
|
868
|
-
mode,
|
|
869
|
-
changedFiles: null,
|
|
870
|
-
changedLines: null,
|
|
871
|
-
largeScope: false,
|
|
872
|
-
fileThreshold,
|
|
873
|
-
lineThreshold
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
const status = await tryGit(['status', '--porcelain=v1']);
|
|
877
|
-
const diff = await tryGit(['diff', '--numstat']);
|
|
878
|
-
const cachedDiff = await tryGit(['diff', '--cached', '--numstat']);
|
|
879
|
-
const untracked = await tryGit(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
880
|
-
const untrackedPaths = untracked ? parseNullDelimitedPaths(untracked) : [];
|
|
881
|
-
const untrackedLines = untrackedPaths.length > 0 ? await countWorkingTreeLines(untrackedPaths) : null;
|
|
882
|
-
const changedFiles = status ? parseStatusPathCount(status) : null;
|
|
883
|
-
let changedLines = null;
|
|
884
|
-
if (diff || cachedDiff || untrackedLines !== null) {
|
|
885
|
-
changedLines = 0;
|
|
886
|
-
if (diff) {
|
|
887
|
-
changedLines += parseNumstatLineDelta(diff);
|
|
888
|
-
}
|
|
889
|
-
if (cachedDiff) {
|
|
890
|
-
changedLines += parseNumstatLineDelta(cachedDiff);
|
|
891
|
-
}
|
|
892
|
-
if (untrackedLines !== null) {
|
|
893
|
-
changedLines += untrackedLines;
|
|
894
|
-
}
|
|
712
|
+
function logTerminationBoundaryFallback(boundaryRecord) {
|
|
713
|
+
if (!boundaryRecord) {
|
|
714
|
+
return;
|
|
895
715
|
}
|
|
896
|
-
|
|
897
|
-
const exceedsLineThreshold = changedLines !== null && changedLines >= lineThreshold;
|
|
898
|
-
return {
|
|
899
|
-
mode,
|
|
900
|
-
changedFiles,
|
|
901
|
-
changedLines,
|
|
902
|
-
largeScope: exceedsFileThreshold || exceedsLineThreshold,
|
|
903
|
-
fileThreshold,
|
|
904
|
-
lineThreshold
|
|
905
|
-
};
|
|
716
|
+
console.error(`[run-review] termination boundary: ${boundaryRecord.kind} (${boundaryRecord.provenance}).`);
|
|
906
717
|
}
|
|
907
|
-
function
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
parts.push(`${scope.changedFiles} files`);
|
|
911
|
-
}
|
|
912
|
-
if (scope.changedLines !== null) {
|
|
913
|
-
parts.push(`${scope.changedLines} lines`);
|
|
914
|
-
}
|
|
915
|
-
if (parts.length === 0) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
return parts.join(', ');
|
|
919
|
-
}
|
|
920
|
-
function resolveReviewArtifactsDir(manifestPath) {
|
|
921
|
-
const configuredRunDir = process.env.CODEX_ORCHESTRATOR_RUN_DIR?.trim();
|
|
922
|
-
const runDir = configuredRunDir && configuredRunDir.length > 0 ? configuredRunDir : path.dirname(manifestPath);
|
|
923
|
-
return path.join(runDir, REVIEW_ARTIFACTS_DIRNAME);
|
|
924
|
-
}
|
|
925
|
-
async function prepareReviewArtifacts(manifestPath, prompt) {
|
|
926
|
-
const reviewDir = resolveReviewArtifactsDir(manifestPath);
|
|
927
|
-
await mkdir(reviewDir, { recursive: true });
|
|
928
|
-
const promptPath = path.join(reviewDir, 'prompt.txt');
|
|
929
|
-
const outputLogPath = path.join(reviewDir, 'output.log');
|
|
930
|
-
const telemetryPath = path.join(reviewDir, 'telemetry.json');
|
|
931
|
-
await writeFile(promptPath, `${prompt}\n`, 'utf8');
|
|
932
|
-
return { reviewDir, promptPath, outputLogPath, telemetryPath };
|
|
933
|
-
}
|
|
934
|
-
async function maybeCaptureReviewFailureIssueLog(options) {
|
|
935
|
-
if (!options.enabled) {
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
const errorMessage = options.error instanceof Error ? options.error.message : String(options.error);
|
|
939
|
-
const issueNotes = [
|
|
940
|
-
'Automatic failure capture for standalone review wrapper.',
|
|
941
|
-
`Error: ${errorMessage}`,
|
|
942
|
-
`Manifest: ${path.relative(repoRoot, options.manifestPath)}`,
|
|
943
|
-
`Output log: ${path.relative(repoRoot, options.outputLogPath)}`
|
|
944
|
-
].join(' | ');
|
|
945
|
-
try {
|
|
946
|
-
const issueLog = await writeDoctorIssueLog({
|
|
947
|
-
doctor: runDoctor(),
|
|
948
|
-
issueTitle: 'Auto issue log: standalone review failed',
|
|
949
|
-
issueNotes,
|
|
950
|
-
taskFilter: options.taskFilter
|
|
951
|
-
});
|
|
952
|
-
console.error('[run-review] captured review failure issue log:');
|
|
953
|
-
for (const line of formatDoctorIssueLogSummary(issueLog)) {
|
|
954
|
-
console.error(`[run-review] ${line}`);
|
|
955
|
-
}
|
|
956
|
-
return issueLog;
|
|
957
|
-
}
|
|
958
|
-
catch (issueError) {
|
|
959
|
-
const message = issueError instanceof Error ? issueError.message : String(issueError);
|
|
960
|
-
console.error(`[run-review] failed to capture review issue log: ${message}`);
|
|
961
|
-
return null;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
function normalizeReviewCommandLine(line) {
|
|
965
|
-
const trimmed = line.trim();
|
|
966
|
-
if (!trimmed) {
|
|
967
|
-
return '';
|
|
968
|
-
}
|
|
969
|
-
const succeededIndex = trimmed.indexOf(' succeeded in ');
|
|
970
|
-
if (succeededIndex >= 0) {
|
|
971
|
-
return trimmed.slice(0, succeededIndex).trimEnd();
|
|
972
|
-
}
|
|
973
|
-
const exitedIndex = trimmed.indexOf(' exited ');
|
|
974
|
-
if (exitedIndex >= 0) {
|
|
975
|
-
return trimmed.slice(0, exitedIndex).trimEnd();
|
|
976
|
-
}
|
|
977
|
-
return trimmed;
|
|
978
|
-
}
|
|
979
|
-
function splitShellControlSegments(command) {
|
|
980
|
-
if (!command.trim()) {
|
|
981
|
-
return [];
|
|
982
|
-
}
|
|
983
|
-
const segments = [];
|
|
984
|
-
let current = '';
|
|
985
|
-
let quote = null;
|
|
986
|
-
let escaped = false;
|
|
987
|
-
const pushCurrent = () => {
|
|
988
|
-
const trimmed = current.trim();
|
|
989
|
-
if (trimmed.length > 0) {
|
|
990
|
-
segments.push(trimmed);
|
|
991
|
-
}
|
|
992
|
-
current = '';
|
|
993
|
-
};
|
|
994
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
995
|
-
const char = command[index] ?? '';
|
|
996
|
-
const next = command[index + 1] ?? '';
|
|
997
|
-
if (escaped) {
|
|
998
|
-
current += char;
|
|
999
|
-
escaped = false;
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
if (char === '\\' && quote !== "'") {
|
|
1003
|
-
current += char;
|
|
1004
|
-
escaped = true;
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
if (quote) {
|
|
1008
|
-
if (char === quote) {
|
|
1009
|
-
quote = null;
|
|
1010
|
-
}
|
|
1011
|
-
current += char;
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
1015
|
-
quote = char;
|
|
1016
|
-
current += char;
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
if (char === ';' || char === '\n') {
|
|
1020
|
-
pushCurrent();
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
if (char === '&') {
|
|
1024
|
-
if (next === '&') {
|
|
1025
|
-
pushCurrent();
|
|
1026
|
-
index += 1;
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
pushCurrent();
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (char === '|') {
|
|
1033
|
-
if (next === '|') {
|
|
1034
|
-
pushCurrent();
|
|
1035
|
-
index += 1;
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
pushCurrent();
|
|
1039
|
-
continue;
|
|
1040
|
-
}
|
|
1041
|
-
current += char;
|
|
1042
|
-
}
|
|
1043
|
-
pushCurrent();
|
|
1044
|
-
return segments;
|
|
1045
|
-
}
|
|
1046
|
-
function tokenizeShellSegment(segment) {
|
|
1047
|
-
const tokens = [];
|
|
1048
|
-
let current = '';
|
|
1049
|
-
let quote = null;
|
|
1050
|
-
let escaped = false;
|
|
1051
|
-
const pushCurrent = () => {
|
|
1052
|
-
if (current.length > 0) {
|
|
1053
|
-
tokens.push(current);
|
|
1054
|
-
current = '';
|
|
1055
|
-
}
|
|
1056
|
-
};
|
|
1057
|
-
for (let index = 0; index < segment.length; index += 1) {
|
|
1058
|
-
const char = segment[index] ?? '';
|
|
1059
|
-
if (escaped) {
|
|
1060
|
-
current += char;
|
|
1061
|
-
escaped = false;
|
|
1062
|
-
continue;
|
|
1063
|
-
}
|
|
1064
|
-
if (char === '\\' && quote !== "'") {
|
|
1065
|
-
escaped = true;
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
if (quote) {
|
|
1069
|
-
if (char === quote) {
|
|
1070
|
-
quote = null;
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
current += char;
|
|
1074
|
-
continue;
|
|
1075
|
-
}
|
|
1076
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
1077
|
-
quote = char;
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
if (/\s/u.test(char)) {
|
|
1081
|
-
pushCurrent();
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
current += char;
|
|
1085
|
-
}
|
|
1086
|
-
pushCurrent();
|
|
1087
|
-
return tokens;
|
|
1088
|
-
}
|
|
1089
|
-
function normalizeCommandToken(token) {
|
|
1090
|
-
const normalized = token.trim().replace(/\\/gu, '/');
|
|
1091
|
-
const basename = normalized.split('/').pop() ?? normalized;
|
|
1092
|
-
return basename.replace(/\.(?:exe|cmd|bat|ps1)$/i, '').toLowerCase();
|
|
1093
|
-
}
|
|
1094
|
-
function stripLeadingEnvAssignments(tokens) {
|
|
1095
|
-
let index = 0;
|
|
1096
|
-
while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(tokens[index] ?? '')) {
|
|
1097
|
-
index += 1;
|
|
1098
|
-
}
|
|
1099
|
-
return tokens.slice(index);
|
|
1100
|
-
}
|
|
1101
|
-
function packageOptionConsumesValue(option) {
|
|
1102
|
-
if (/^--(?:prefix|workspace|filter|cwd)$/iu.test(option)) {
|
|
1103
|
-
return true;
|
|
1104
|
-
}
|
|
1105
|
-
if (/^-(?:C|w)$/iu.test(option)) {
|
|
1106
|
-
return true;
|
|
1107
|
-
}
|
|
1108
|
-
return false;
|
|
1109
|
-
}
|
|
1110
|
-
function resolvePackageScriptTarget(args) {
|
|
1111
|
-
let index = 0;
|
|
1112
|
-
while (index < args.length) {
|
|
1113
|
-
const token = args[index] ?? '';
|
|
1114
|
-
const normalized = token.toLowerCase();
|
|
1115
|
-
if (normalized === '--') {
|
|
1116
|
-
const fallback = args[index + 1];
|
|
1117
|
-
return fallback ? fallback.toLowerCase() : null;
|
|
1118
|
-
}
|
|
1119
|
-
if (REVIEW_PACKAGE_TEST_SUBCOMMAND_ALIASES.has(normalized)) {
|
|
1120
|
-
return 'test';
|
|
1121
|
-
}
|
|
1122
|
-
if (REVIEW_PACKAGE_RUN_SUBCOMMAND_ALIASES.has(normalized)) {
|
|
1123
|
-
index += 1;
|
|
1124
|
-
while (index < args.length) {
|
|
1125
|
-
const candidate = args[index] ?? '';
|
|
1126
|
-
const candidateNormalized = candidate.toLowerCase();
|
|
1127
|
-
if (candidateNormalized === '--') {
|
|
1128
|
-
index += 1;
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
if (candidate.startsWith('-')) {
|
|
1132
|
-
index += packageOptionConsumesValue(candidate) ? 2 : 1;
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
return candidateNormalized;
|
|
1136
|
-
}
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
if (token.startsWith('-')) {
|
|
1140
|
-
index += packageOptionConsumesValue(token) ? 2 : 1;
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
|
-
return normalized;
|
|
1144
|
-
}
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
function unwrapEnvCommandTokens(tokens) {
|
|
1148
|
-
if (tokens.length === 0 || normalizeCommandToken(tokens[0] ?? '') !== 'env') {
|
|
1149
|
-
return tokens;
|
|
1150
|
-
}
|
|
1151
|
-
let index = 1;
|
|
1152
|
-
while (index < tokens.length) {
|
|
1153
|
-
const token = tokens[index] ?? '';
|
|
1154
|
-
const normalized = token.toLowerCase();
|
|
1155
|
-
if (token === '--') {
|
|
1156
|
-
index += 1;
|
|
1157
|
-
break;
|
|
1158
|
-
}
|
|
1159
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(token)) {
|
|
1160
|
-
index += 1;
|
|
1161
|
-
continue;
|
|
1162
|
-
}
|
|
1163
|
-
if (normalized === '-u' || normalized === '--unset') {
|
|
1164
|
-
index += 2;
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
if (normalized.startsWith('--unset=')) {
|
|
1168
|
-
index += 1;
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (token.startsWith('-')) {
|
|
1172
|
-
index += 1;
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
break;
|
|
1176
|
-
}
|
|
1177
|
-
return tokens.slice(index);
|
|
1178
|
-
}
|
|
1179
|
-
function hasHeavyCommandTokens(tokens) {
|
|
1180
|
-
if (tokens.length === 0) {
|
|
1181
|
-
return false;
|
|
1182
|
-
}
|
|
1183
|
-
const unwrappedTokens = unwrapEnvCommandTokens(tokens);
|
|
1184
|
-
if (unwrappedTokens.length === 0) {
|
|
1185
|
-
return false;
|
|
1186
|
-
}
|
|
1187
|
-
if (unwrappedTokens.length !== tokens.length) {
|
|
1188
|
-
return hasHeavyCommandTokens(unwrappedTokens);
|
|
1189
|
-
}
|
|
1190
|
-
const command = normalizeCommandToken(unwrappedTokens[0] ?? '');
|
|
1191
|
-
const args = unwrappedTokens.slice(1);
|
|
1192
|
-
if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
|
|
1193
|
-
const scriptTarget = resolvePackageScriptTarget(args);
|
|
1194
|
-
return scriptTarget !== null && REVIEW_HEAVY_SCRIPT_TARGETS.has(scriptTarget);
|
|
1195
|
-
}
|
|
1196
|
-
if (command === 'pytest') {
|
|
1197
|
-
return true;
|
|
1198
|
-
}
|
|
1199
|
-
if (command === 'python' || command === 'python3' || command === 'py') {
|
|
1200
|
-
for (let index = 0; index < args.length - 1; index += 1) {
|
|
1201
|
-
if ((args[index] ?? '').toLowerCase() !== '-m') {
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
if (normalizeCommandToken(args[index + 1] ?? '') === 'pytest') {
|
|
1205
|
-
return true;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
const firstArg = normalizeCommandToken(args[0] ?? '');
|
|
1210
|
-
if (command === 'go' && firstArg === 'test') {
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
if (command === 'cargo' && firstArg === 'test') {
|
|
1214
|
-
return true;
|
|
1215
|
-
}
|
|
1216
|
-
if (command === 'mvn' || command === 'mvnw' || command === 'gradle' || command === 'gradlew') {
|
|
1217
|
-
return args.some((arg) => {
|
|
1218
|
-
const normalized = normalizeCommandToken(arg);
|
|
1219
|
-
return normalized === 'test' || normalized.endsWith(':test');
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
function isShellCommandFlagWithPayload(flag) {
|
|
1225
|
-
const normalized = flag.toLowerCase();
|
|
1226
|
-
if (normalized === '/c' || normalized === '-c') {
|
|
1227
|
-
return true;
|
|
1228
|
-
}
|
|
1229
|
-
return /^-[^-]*c[^-]*$/u.test(normalized);
|
|
1230
|
-
}
|
|
1231
|
-
function extractShellCommandPayload(tokens) {
|
|
1232
|
-
if (tokens.length < 2) {
|
|
1233
|
-
return null;
|
|
1234
|
-
}
|
|
1235
|
-
const command = normalizeCommandToken(tokens[0] ?? '');
|
|
1236
|
-
if (!REVIEW_SHELL_COMMANDS.has(command)) {
|
|
1237
|
-
return null;
|
|
1238
|
-
}
|
|
1239
|
-
for (let index = 1; index < tokens.length; index += 1) {
|
|
1240
|
-
if (!isShellCommandFlagWithPayload(tokens[index] ?? '')) {
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
if (command === 'cmd') {
|
|
1244
|
-
const payload = tokens.slice(index + 1).join(' ').trim();
|
|
1245
|
-
return payload.length > 0 ? payload : null;
|
|
1246
|
-
}
|
|
1247
|
-
const payload = tokens[index + 1];
|
|
1248
|
-
return payload ? payload.trim() : null;
|
|
1249
|
-
}
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
function detectHeavyReviewCommandFromSegment(segment, depth = 0) {
|
|
1253
|
-
const tokens = stripLeadingEnvAssignments(tokenizeShellSegment(segment));
|
|
1254
|
-
if (tokens.length === 0) {
|
|
1255
|
-
return null;
|
|
1256
|
-
}
|
|
1257
|
-
if (depth < 3) {
|
|
1258
|
-
const payload = extractShellCommandPayload(tokens);
|
|
1259
|
-
if (payload) {
|
|
1260
|
-
const nestedSegments = splitShellControlSegments(payload);
|
|
1261
|
-
for (const nestedSegment of nestedSegments) {
|
|
1262
|
-
const nestedHeavyCommand = detectHeavyReviewCommandFromSegment(nestedSegment, depth + 1);
|
|
1263
|
-
if (nestedHeavyCommand) {
|
|
1264
|
-
return nestedHeavyCommand;
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
return hasHeavyCommandTokens(tokens) ? segment.trim() : null;
|
|
1270
|
-
}
|
|
1271
|
-
function detectHeavyReviewCommand(line) {
|
|
1272
|
-
const segments = splitShellControlSegments(line);
|
|
1273
|
-
for (const segment of segments) {
|
|
1274
|
-
const heavyCommand = detectHeavyReviewCommandFromSegment(segment);
|
|
1275
|
-
if (heavyCommand) {
|
|
1276
|
-
return heavyCommand;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
return null;
|
|
1280
|
-
}
|
|
1281
|
-
function isLikelyReviewCommandLine(line) {
|
|
1282
|
-
if (!line) {
|
|
1283
|
-
return false;
|
|
1284
|
-
}
|
|
1285
|
-
if (detectHeavyReviewCommand(line)) {
|
|
1286
|
-
return true;
|
|
1287
|
-
}
|
|
1288
|
-
if (/^(?:npm|pnpm|yarn|bun|node|npx|git|bash|sh|zsh|python|pytest|go|cargo|mvn|gradle(?:w)?)\b/i.test(line)) {
|
|
1289
|
-
return true;
|
|
1290
|
-
}
|
|
1291
|
-
if (line.includes(' in ') && /\s-\w+\s+/u.test(line)) {
|
|
1292
|
-
return true;
|
|
1293
|
-
}
|
|
1294
|
-
return false;
|
|
1295
|
-
}
|
|
1296
|
-
async function summarizeReviewOutputLog(outputLogPath) {
|
|
1297
|
-
if (!(await pathExists(outputLogPath))) {
|
|
1298
|
-
return null;
|
|
1299
|
-
}
|
|
1300
|
-
const lineReader = createInterface({
|
|
1301
|
-
input: createReadStream(outputLogPath, { encoding: 'utf8' }),
|
|
1302
|
-
crlfDelay: Infinity
|
|
1303
|
-
});
|
|
1304
|
-
const commandStarts = [];
|
|
1305
|
-
const heavyCommandStarts = [];
|
|
1306
|
-
const lastLines = [];
|
|
1307
|
-
let lineCount = 0;
|
|
1308
|
-
let completionCount = 0;
|
|
1309
|
-
let startupEvents = 0;
|
|
1310
|
-
let reviewProgressSignals = 0;
|
|
1311
|
-
let awaitingCommandLine = false;
|
|
1312
|
-
for await (const rawLine of lineReader) {
|
|
1313
|
-
lineCount += 1;
|
|
1314
|
-
const line = String(rawLine ?? '').trimEnd();
|
|
1315
|
-
const trimmed = line.trim();
|
|
1316
|
-
if (trimmed.length > 0) {
|
|
1317
|
-
lastLines.push(trimmed);
|
|
1318
|
-
if (lastLines.length > REVIEW_OUTPUT_SUMMARY_TAIL_LINE_LIMIT) {
|
|
1319
|
-
lastLines.shift();
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
if (REVIEW_DELEGATION_STARTUP_LINE_RE.test(trimmed)) {
|
|
1323
|
-
startupEvents += 1;
|
|
1324
|
-
}
|
|
1325
|
-
if (REVIEW_PROGRESS_SIGNAL_LINE_RE.test(trimmed)) {
|
|
1326
|
-
reviewProgressSignals += 1;
|
|
1327
|
-
}
|
|
1328
|
-
if (trimmed === 'exec') {
|
|
1329
|
-
awaitingCommandLine = true;
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
if (awaitingCommandLine && trimmed.length > 0) {
|
|
1333
|
-
const commandLine = normalizeReviewCommandLine(trimmed);
|
|
1334
|
-
if (isLikelyReviewCommandLine(commandLine)) {
|
|
1335
|
-
if (commandStarts.length >= REVIEW_OUTPUT_SUMMARY_COMMAND_LIMIT) {
|
|
1336
|
-
commandStarts.shift();
|
|
1337
|
-
}
|
|
1338
|
-
commandStarts.push(commandLine);
|
|
1339
|
-
if (detectHeavyReviewCommand(commandLine)) {
|
|
1340
|
-
if (heavyCommandStarts.length < REVIEW_OUTPUT_SUMMARY_HEAVY_COMMAND_LIMIT) {
|
|
1341
|
-
heavyCommandStarts.push(commandLine);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
awaitingCommandLine = false;
|
|
1345
|
-
}
|
|
1346
|
-
else if (REVIEW_PROGRESS_SIGNAL_LINE_RE.test(trimmed) || /\bsucceeded in\b|\bexited\b/i.test(trimmed)) {
|
|
1347
|
-
awaitingCommandLine = false;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
if (/\bsucceeded in\b|\bexited\b/i.test(trimmed)) {
|
|
1351
|
-
completionCount += 1;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
return {
|
|
1355
|
-
lineCount,
|
|
1356
|
-
commandStarts,
|
|
1357
|
-
completionCount,
|
|
1358
|
-
startupEvents,
|
|
1359
|
-
reviewProgressSignals,
|
|
1360
|
-
heavyCommandStarts,
|
|
1361
|
-
lastLines
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
async function persistReviewTelemetry(options) {
|
|
1365
|
-
const summary = await summarizeReviewOutputLog(options.outputLogPath);
|
|
1366
|
-
const includeRawTelemetry = envFlagEnabled(process.env[REVIEW_TELEMETRY_DEBUG_ENV_KEY]);
|
|
1367
|
-
const persistedSummary = sanitizeTelemetrySummaryForPersistence(summary, includeRawTelemetry);
|
|
1368
|
-
const payload = {
|
|
1369
|
-
version: 1,
|
|
1370
|
-
generated_at: new Date().toISOString(),
|
|
1371
|
-
status: options.status,
|
|
1372
|
-
error: sanitizeTelemetryErrorForPersistence(options.error ?? null, includeRawTelemetry),
|
|
1373
|
-
output_log_path: path.relative(repoRoot, options.outputLogPath),
|
|
1374
|
-
summary: persistedSummary
|
|
1375
|
-
};
|
|
1376
|
-
await writeFile(options.telemetryPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
1377
|
-
return summary;
|
|
1378
|
-
}
|
|
1379
|
-
function sanitizeTelemetrySummaryForPersistence(summary, includeRawTelemetry) {
|
|
1380
|
-
if (!summary || includeRawTelemetry) {
|
|
1381
|
-
return summary;
|
|
1382
|
-
}
|
|
1383
|
-
return {
|
|
1384
|
-
...summary,
|
|
1385
|
-
commandStarts: redactTelemetryLines(summary.commandStarts, 'command'),
|
|
1386
|
-
heavyCommandStarts: redactTelemetryLines(summary.heavyCommandStarts, 'heavy-command'),
|
|
1387
|
-
lastLines: redactTelemetryLines(summary.lastLines, 'output-line')
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
function redactTelemetryLines(lines, label) {
|
|
1391
|
-
return lines.map((_line, index) => `[redacted ${label} ${index + 1}; set ${REVIEW_TELEMETRY_DEBUG_ENV_KEY}=1 to persist raw values]`);
|
|
1392
|
-
}
|
|
1393
|
-
function sanitizeTelemetryErrorForPersistence(error, includeRawTelemetry) {
|
|
1394
|
-
if (!error || includeRawTelemetry) {
|
|
1395
|
-
return error;
|
|
1396
|
-
}
|
|
1397
|
-
return `[redacted error; set ${REVIEW_TELEMETRY_DEBUG_ENV_KEY}=1 to persist raw values]`;
|
|
1398
|
-
}
|
|
1399
|
-
function logReviewTelemetrySummary(summary, telemetryPath) {
|
|
1400
|
-
const debugTelemetry = envFlagEnabled(process.env[REVIEW_TELEMETRY_DEBUG_ENV_KEY]);
|
|
1401
|
-
console.error(`[run-review] review telemetry: ${summary.commandStarts.length} command start(s), ${summary.heavyCommandStarts.length} heavy command start(s), ${summary.startupEvents} delegation startup event(s), ${summary.reviewProgressSignals} review progress signal(s).`);
|
|
1402
|
-
const lastCommand = summary.commandStarts.at(-1);
|
|
1403
|
-
if (lastCommand) {
|
|
1404
|
-
if (debugTelemetry) {
|
|
1405
|
-
console.error(`[run-review] last command started: ${lastCommand}`);
|
|
1406
|
-
}
|
|
1407
|
-
else {
|
|
1408
|
-
console.error(`[run-review] last command started: [redacted] (set ${REVIEW_TELEMETRY_DEBUG_ENV_KEY}=1 to print raw command text).`);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
if (summary.completionCount < summary.commandStarts.length) {
|
|
1412
|
-
console.error(`[run-review] command completions observed: ${summary.completionCount}; possible in-flight command at termination.`);
|
|
1413
|
-
}
|
|
1414
|
-
if (summary.heavyCommandStarts.length > 0) {
|
|
1415
|
-
if (debugTelemetry) {
|
|
1416
|
-
console.error(`[run-review] heavy commands detected: ${summary.heavyCommandStarts.join(' | ')}`);
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
console.error(`[run-review] heavy commands detected: ${summary.heavyCommandStarts.length} sample(s) captured (set ${REVIEW_TELEMETRY_DEBUG_ENV_KEY}=1 to print raw command text).`);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
if (summary.lastLines.length > 0) {
|
|
1423
|
-
if (debugTelemetry) {
|
|
1424
|
-
console.error(`[run-review] output tail: ${summary.lastLines.join(' || ')}`);
|
|
1425
|
-
}
|
|
1426
|
-
else {
|
|
1427
|
-
console.error(`[run-review] output tail captured: ${summary.lastLines.length} line(s) hidden by default (set ${REVIEW_TELEMETRY_DEBUG_ENV_KEY}=1 to print raw tail).`);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
console.error(`[run-review] review telemetry saved to: ${path.relative(repoRoot, telemetryPath)}`);
|
|
1431
|
-
}
|
|
1432
|
-
class CodexReviewError extends Error {
|
|
1433
|
-
exitCode;
|
|
1434
|
-
signal;
|
|
1435
|
-
timedOut;
|
|
1436
|
-
outputPreview;
|
|
1437
|
-
constructor(message, options) {
|
|
1438
|
-
super(message);
|
|
1439
|
-
this.name = 'CodexReviewError';
|
|
1440
|
-
this.exitCode = options.exitCode;
|
|
1441
|
-
this.signal = options.signal;
|
|
1442
|
-
this.timedOut = options.timedOut;
|
|
1443
|
-
this.outputPreview = options.outputPreview;
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
function shouldRetryWithoutScopeFlags(error) {
|
|
1447
|
-
if (!error || typeof error !== 'object') {
|
|
1448
|
-
return false;
|
|
1449
|
-
}
|
|
1450
|
-
const preview = 'outputPreview' in error ? String(error.outputPreview ?? '') : '';
|
|
1451
|
-
const message = 'message' in error ? String(error.message ?? '') : '';
|
|
1452
|
-
const combined = `${message}\n${preview}`.toLowerCase();
|
|
1453
|
-
return (combined.includes('unknown option') ||
|
|
1454
|
-
combined.includes('unknown flag') ||
|
|
1455
|
-
combined.includes('unrecognized option') ||
|
|
1456
|
-
combined.includes('cannot be used with') ||
|
|
1457
|
-
combined.includes('cannot be combined') ||
|
|
1458
|
-
combined.includes('incompatible with') ||
|
|
1459
|
-
combined.includes('prompt cannot') ||
|
|
1460
|
-
combined.includes('custom prompt') ||
|
|
1461
|
-
combined.includes('with a prompt'));
|
|
1462
|
-
}
|
|
1463
|
-
function trackReviewStartupLoopSignals(chunk, state, stream) {
|
|
1464
|
-
if (state.reviewProgressObserved) {
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
const pendingFragment = stream === 'stdout'
|
|
1468
|
-
? state.pendingStdoutFragment
|
|
1469
|
-
: state.pendingStderrFragment;
|
|
1470
|
-
const combined = `${pendingFragment}${chunk}`;
|
|
1471
|
-
const lines = combined.split(/\r?\n/u);
|
|
1472
|
-
const nextPendingFragment = lines.pop() ?? '';
|
|
1473
|
-
if (stream === 'stdout') {
|
|
1474
|
-
state.pendingStdoutFragment = nextPendingFragment;
|
|
1475
|
-
}
|
|
1476
|
-
else {
|
|
1477
|
-
state.pendingStderrFragment = nextPendingFragment;
|
|
1478
|
-
}
|
|
1479
|
-
for (const line of lines) {
|
|
1480
|
-
const trimmed = line.trim();
|
|
1481
|
-
if (!trimmed) {
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
if (REVIEW_DELEGATION_STARTUP_LINE_RE.test(trimmed)) {
|
|
1485
|
-
state.startupEvents += 1;
|
|
1486
|
-
continue;
|
|
1487
|
-
}
|
|
1488
|
-
if (REVIEW_PROGRESS_SIGNAL_LINE_RE.test(trimmed)) {
|
|
1489
|
-
state.reviewProgressObserved = true;
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
function trackReviewCommandSignals(chunk, state, stream, blockHeavyCommands) {
|
|
1495
|
-
const pendingFragment = stream === 'stdout'
|
|
1496
|
-
? state.pendingStdoutFragment
|
|
1497
|
-
: state.pendingStderrFragment;
|
|
1498
|
-
const combined = `${pendingFragment}${chunk}`;
|
|
1499
|
-
const lines = combined.split(/\r?\n/u);
|
|
1500
|
-
const nextPendingFragment = lines.pop() ?? '';
|
|
1501
|
-
if (stream === 'stdout') {
|
|
1502
|
-
state.pendingStdoutFragment = nextPendingFragment;
|
|
1503
|
-
}
|
|
1504
|
-
else {
|
|
1505
|
-
state.pendingStderrFragment = nextPendingFragment;
|
|
1506
|
-
}
|
|
1507
|
-
for (const line of lines) {
|
|
1508
|
-
const trimmed = line.trim();
|
|
1509
|
-
if (!trimmed) {
|
|
1510
|
-
continue;
|
|
1511
|
-
}
|
|
1512
|
-
if (trimmed === 'exec') {
|
|
1513
|
-
state.awaitingCommandLine = true;
|
|
1514
|
-
continue;
|
|
1515
|
-
}
|
|
1516
|
-
if (!state.awaitingCommandLine) {
|
|
1517
|
-
continue;
|
|
1518
|
-
}
|
|
1519
|
-
const commandLine = normalizeReviewCommandLine(trimmed);
|
|
1520
|
-
if (!isLikelyReviewCommandLine(commandLine)) {
|
|
1521
|
-
if (REVIEW_PROGRESS_SIGNAL_LINE_RE.test(trimmed) || /\bsucceeded in\b|\bexited\b/i.test(trimmed)) {
|
|
1522
|
-
state.awaitingCommandLine = false;
|
|
1523
|
-
}
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
state.awaitingCommandLine = false;
|
|
1527
|
-
if (blockHeavyCommands && detectHeavyReviewCommand(commandLine) && !state.blockedHeavyCommand) {
|
|
1528
|
-
state.blockedHeavyCommand = commandLine;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
function installSignalForwarders(child, detached) {
|
|
1533
|
-
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
1534
|
-
const handlers = new Map();
|
|
1535
|
-
const uninstall = () => {
|
|
1536
|
-
for (const [signal, handler] of handlers.entries()) {
|
|
1537
|
-
process.removeListener(signal, handler);
|
|
1538
|
-
}
|
|
1539
|
-
handlers.clear();
|
|
1540
|
-
};
|
|
1541
|
-
for (const signal of signals) {
|
|
1542
|
-
const handler = () => {
|
|
1543
|
-
signalChildProcess(child, signal, detached);
|
|
1544
|
-
uninstall();
|
|
1545
|
-
try {
|
|
1546
|
-
process.kill(process.pid, signal);
|
|
1547
|
-
}
|
|
1548
|
-
catch {
|
|
1549
|
-
process.exitCode = signal === 'SIGINT' ? 130 : 143;
|
|
1550
|
-
}
|
|
1551
|
-
};
|
|
1552
|
-
handlers.set(signal, handler);
|
|
1553
|
-
process.once(signal, handler);
|
|
1554
|
-
}
|
|
1555
|
-
return uninstall;
|
|
1556
|
-
}
|
|
1557
|
-
function writeToStreamSafely(target, chunk) {
|
|
1558
|
-
if (target.destroyed || target.writableEnded) {
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
try {
|
|
1562
|
-
target.write(chunk, (error) => {
|
|
1563
|
-
if (!error) {
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
const code = error?.code;
|
|
1567
|
-
if (typeof code === 'string' && BENIGN_STDIO_ERROR_CODES.has(code)) {
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
// Best effort only; stdout/stderr mirroring should not fail the run.
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
catch (error) {
|
|
1574
|
-
const code = error?.code;
|
|
1575
|
-
if (typeof code === 'string' && BENIGN_STDIO_ERROR_CODES.has(code)) {
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
throw error;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
async function runCodexReview(options) {
|
|
1582
|
-
const detached = process.platform !== 'win32';
|
|
1583
|
-
const child = spawn(options.command, options.args, {
|
|
1584
|
-
stdio: options.stdio,
|
|
1585
|
-
env: options.env,
|
|
1586
|
-
cwd: repoRoot,
|
|
1587
|
-
detached
|
|
1588
|
-
});
|
|
1589
|
-
const outputStream = createWriteStream(options.outputLogPath, { flags: 'w' });
|
|
1590
|
-
const outputClosed = new Promise((resolve) => {
|
|
1591
|
-
outputStream.once('close', () => resolve());
|
|
1592
|
-
outputStream.once('error', () => resolve());
|
|
1593
|
-
});
|
|
1594
|
-
const uninstallSignalForwarders = installSignalForwarders(child, detached);
|
|
1595
|
-
let preview = '';
|
|
1596
|
-
let lastOutputAtMs = Date.now();
|
|
1597
|
-
const startupLoopState = {
|
|
1598
|
-
startupEvents: 0,
|
|
1599
|
-
reviewProgressObserved: false,
|
|
1600
|
-
pendingStdoutFragment: '',
|
|
1601
|
-
pendingStderrFragment: ''
|
|
1602
|
-
};
|
|
1603
|
-
const commandSignalState = {
|
|
1604
|
-
awaitingCommandLine: false,
|
|
1605
|
-
pendingStdoutFragment: '',
|
|
1606
|
-
pendingStderrFragment: '',
|
|
1607
|
-
blockedHeavyCommand: null
|
|
1608
|
-
};
|
|
1609
|
-
const capture = (chunk, target, stream) => {
|
|
1610
|
-
lastOutputAtMs = Date.now();
|
|
1611
|
-
if (!outputStream.writableEnded && !outputStream.destroyed) {
|
|
1612
|
-
outputStream.write(chunk);
|
|
1613
|
-
}
|
|
1614
|
-
writeToStreamSafely(target, chunk);
|
|
1615
|
-
const next = chunk.toString('utf8');
|
|
1616
|
-
trackReviewStartupLoopSignals(next, startupLoopState, stream);
|
|
1617
|
-
trackReviewCommandSignals(next, commandSignalState, stream, options.blockHeavyCommands);
|
|
1618
|
-
if (preview.length < REVIEW_OUTPUT_PREVIEW_LIMIT) {
|
|
1619
|
-
preview = `${preview}${next}`.slice(0, REVIEW_OUTPUT_PREVIEW_LIMIT);
|
|
1620
|
-
}
|
|
1621
|
-
};
|
|
1622
|
-
const onStdout = (chunk) => capture(chunk, process.stdout, 'stdout');
|
|
1623
|
-
const onStderr = (chunk) => capture(chunk, process.stderr, 'stderr');
|
|
1624
|
-
child.stdout?.on('data', onStdout);
|
|
1625
|
-
child.stderr?.on('data', onStderr);
|
|
1626
|
-
let cleanedUp = false;
|
|
1627
|
-
const cleanup = () => {
|
|
1628
|
-
if (cleanedUp) {
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
cleanedUp = true;
|
|
1632
|
-
uninstallSignalForwarders();
|
|
1633
|
-
child.stdout?.off('data', onStdout);
|
|
1634
|
-
child.stderr?.off('data', onStderr);
|
|
1635
|
-
try {
|
|
1636
|
-
outputStream.end();
|
|
1637
|
-
}
|
|
1638
|
-
catch {
|
|
1639
|
-
// ignore best-effort close
|
|
1640
|
-
}
|
|
1641
|
-
};
|
|
1642
|
-
try {
|
|
1643
|
-
await waitForChildExit(child, {
|
|
1644
|
-
timeoutMs: options.timeoutMs,
|
|
1645
|
-
stallTimeoutMs: options.stallTimeoutMs,
|
|
1646
|
-
startupLoopTimeoutMs: options.startupLoopTimeoutMs,
|
|
1647
|
-
startupLoopMinEvents: options.startupLoopMinEvents,
|
|
1648
|
-
monitorIntervalMs: options.monitorIntervalMs,
|
|
1649
|
-
blockHeavyCommands: options.blockHeavyCommands,
|
|
1650
|
-
getLastOutputAtMs: () => lastOutputAtMs,
|
|
1651
|
-
getStartupLoopState: () => startupLoopState,
|
|
1652
|
-
getBlockedHeavyCommand: () => commandSignalState.blockedHeavyCommand,
|
|
1653
|
-
detached,
|
|
1654
|
-
onCleanup: cleanup
|
|
1655
|
-
});
|
|
1656
|
-
await outputClosed;
|
|
1657
|
-
return { preview };
|
|
1658
|
-
}
|
|
1659
|
-
catch (error) {
|
|
1660
|
-
cleanup();
|
|
1661
|
-
await outputClosed;
|
|
1662
|
-
if (error instanceof CodexReviewError) {
|
|
1663
|
-
error.outputPreview = preview || error.outputPreview;
|
|
1664
|
-
}
|
|
1665
|
-
throw error;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
async function runDiffBudget(options) {
|
|
1669
|
-
const scriptPath = path.join(repoRoot, 'scripts', 'diff-budget.mjs');
|
|
1670
|
-
const relativeScriptPath = path.relative(repoRoot, scriptPath);
|
|
1671
|
-
if (!(await pathExists(scriptPath))) {
|
|
1672
|
-
console.log(`[run-review] skipping diff budget (missing ${relativeScriptPath}; downstream npm environment detected).`);
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
const args = [scriptPath];
|
|
1676
|
-
if (options.commit) {
|
|
1677
|
-
args.push('--commit', options.commit);
|
|
1678
|
-
}
|
|
1679
|
-
else if (options.base) {
|
|
1680
|
-
args.push('--base', options.base);
|
|
1681
|
-
}
|
|
1682
|
-
await new Promise((resolve, reject) => {
|
|
1683
|
-
const child = spawn('node', args, { stdio: 'inherit', env: process.env, cwd: repoRoot });
|
|
1684
|
-
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
1685
|
-
child.once('exit', (code) => {
|
|
1686
|
-
if (code === 0) {
|
|
1687
|
-
resolve();
|
|
1688
|
-
}
|
|
1689
|
-
else {
|
|
1690
|
-
reject(new Error(`diff budget exited with code ${code}`));
|
|
1691
|
-
}
|
|
1692
|
-
});
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
function shouldRunDiffBudget() {
|
|
1696
|
-
return !(envFlagEnabled(process.env.DIFF_BUDGET_STAGE) || envFlagEnabled(process.env.SKIP_DIFF_BUDGET));
|
|
1697
|
-
}
|
|
1698
|
-
function envFlagEnabled(value) {
|
|
1699
|
-
if (!value) {
|
|
1700
|
-
return false;
|
|
718
|
+
function envFlagEnabled(value) {
|
|
719
|
+
if (!value) {
|
|
720
|
+
return false;
|
|
1701
721
|
}
|
|
1702
722
|
const normalized = value.trim().toLowerCase();
|
|
1703
723
|
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
1704
724
|
}
|
|
1705
|
-
function formatBoundedHeavyCommandFailure(blockedCommand) {
|
|
1706
|
-
const guidance = `Set ${REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY}=1 to allow full validation commands.`;
|
|
1707
|
-
if (!envFlagEnabled(process.env[REVIEW_TELEMETRY_DEBUG_ENV_KEY])) {
|
|
1708
|
-
return `codex review attempted heavy command in bounded mode. ${guidance}`;
|
|
1709
|
-
}
|
|
1710
|
-
return `codex review attempted heavy command in bounded mode: ${blockedCommand}. ${guidance}`;
|
|
1711
|
-
}
|
|
1712
|
-
function allowHeavyReviewCommands() {
|
|
1713
|
-
return envFlagEnabled(process.env[REVIEW_ALLOW_HEAVY_COMMANDS_ENV_KEY]);
|
|
1714
|
-
}
|
|
1715
|
-
function enforceBoundedReviewMode() {
|
|
1716
|
-
return envFlagEnabled(process.env[REVIEW_ENFORCE_BOUNDED_MODE_ENV_KEY]);
|
|
1717
|
-
}
|
|
1718
|
-
function shouldForceNonInteractive() {
|
|
1719
|
-
const stdinIsTTY = process.stdin?.isTTY === true;
|
|
1720
|
-
return (!stdinIsTTY ||
|
|
1721
|
-
envFlagEnabled(process.env.CI) ||
|
|
1722
|
-
envFlagEnabled(process.env.CODEX_REVIEW_NON_INTERACTIVE) ||
|
|
1723
|
-
envFlagEnabled(process.env.CODEX_NON_INTERACTIVE) ||
|
|
1724
|
-
envFlagEnabled(process.env.CODEX_NO_INTERACTIVE) ||
|
|
1725
|
-
envFlagEnabled(process.env.CODEX_NONINTERACTIVE));
|
|
1726
|
-
}
|
|
1727
|
-
function resolveReviewTimeoutMs() {
|
|
1728
|
-
const configured = process.env.CODEX_REVIEW_TIMEOUT_SECONDS?.trim();
|
|
1729
|
-
if (!configured) {
|
|
1730
|
-
return null;
|
|
1731
|
-
}
|
|
1732
|
-
const parsedSeconds = Number(configured);
|
|
1733
|
-
if (!Number.isFinite(parsedSeconds)) {
|
|
1734
|
-
throw new Error('CODEX_REVIEW_TIMEOUT_SECONDS must be a finite number.');
|
|
1735
|
-
}
|
|
1736
|
-
if (parsedSeconds <= 0) {
|
|
1737
|
-
return null;
|
|
1738
|
-
}
|
|
1739
|
-
return Math.round(parsedSeconds * 1000);
|
|
1740
|
-
}
|
|
1741
|
-
function resolveReviewStallTimeoutMs() {
|
|
1742
|
-
const configured = process.env.CODEX_REVIEW_STALL_TIMEOUT_SECONDS?.trim();
|
|
1743
|
-
if (!configured) {
|
|
1744
|
-
return null;
|
|
1745
|
-
}
|
|
1746
|
-
const parsedSeconds = Number(configured);
|
|
1747
|
-
if (!Number.isFinite(parsedSeconds)) {
|
|
1748
|
-
throw new Error('CODEX_REVIEW_STALL_TIMEOUT_SECONDS must be a finite number.');
|
|
1749
|
-
}
|
|
1750
|
-
if (parsedSeconds <= 0) {
|
|
1751
|
-
return null;
|
|
1752
|
-
}
|
|
1753
|
-
return Math.round(parsedSeconds * 1000);
|
|
1754
|
-
}
|
|
1755
|
-
function resolveReviewStartupLoopTimeoutMs() {
|
|
1756
|
-
const configured = process.env.CODEX_REVIEW_STARTUP_LOOP_TIMEOUT_SECONDS?.trim();
|
|
1757
|
-
if (!configured) {
|
|
1758
|
-
return null;
|
|
1759
|
-
}
|
|
1760
|
-
const parsedSeconds = Number(configured);
|
|
1761
|
-
if (!Number.isFinite(parsedSeconds)) {
|
|
1762
|
-
throw new Error('CODEX_REVIEW_STARTUP_LOOP_TIMEOUT_SECONDS must be a finite number.');
|
|
1763
|
-
}
|
|
1764
|
-
if (parsedSeconds <= 0) {
|
|
1765
|
-
return null;
|
|
1766
|
-
}
|
|
1767
|
-
return Math.round(parsedSeconds * 1000);
|
|
1768
|
-
}
|
|
1769
|
-
function resolveReviewStartupLoopMinEvents() {
|
|
1770
|
-
const configured = process.env.CODEX_REVIEW_STARTUP_LOOP_MIN_EVENTS?.trim();
|
|
1771
|
-
if (!configured) {
|
|
1772
|
-
return DEFAULT_REVIEW_STARTUP_LOOP_MIN_EVENTS;
|
|
1773
|
-
}
|
|
1774
|
-
const parsed = Number(configured);
|
|
1775
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) {
|
|
1776
|
-
throw new Error('CODEX_REVIEW_STARTUP_LOOP_MIN_EVENTS must be a positive integer.');
|
|
1777
|
-
}
|
|
1778
|
-
return parsed;
|
|
1779
|
-
}
|
|
1780
|
-
function resolveReviewMonitorIntervalMs() {
|
|
1781
|
-
const configured = process.env[REVIEW_MONITOR_INTERVAL_ENV_KEY]?.trim();
|
|
1782
|
-
if (!configured) {
|
|
1783
|
-
return DEFAULT_REVIEW_MONITOR_INTERVAL_SECONDS * 1000;
|
|
1784
|
-
}
|
|
1785
|
-
const parsedSeconds = Number(configured);
|
|
1786
|
-
if (!Number.isFinite(parsedSeconds)) {
|
|
1787
|
-
throw new Error(`${REVIEW_MONITOR_INTERVAL_ENV_KEY} must be a finite number.`);
|
|
1788
|
-
}
|
|
1789
|
-
if (parsedSeconds <= 0) {
|
|
1790
|
-
return null;
|
|
1791
|
-
}
|
|
1792
|
-
return Math.round(parsedSeconds * 1000);
|
|
1793
|
-
}
|
|
1794
|
-
function formatDurationMs(durationMs) {
|
|
1795
|
-
const roundedMs = Math.max(0, Math.round(durationMs));
|
|
1796
|
-
if (roundedMs < 1000) {
|
|
1797
|
-
return `${roundedMs}ms`;
|
|
1798
|
-
}
|
|
1799
|
-
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
1800
|
-
if (totalSeconds < 60) {
|
|
1801
|
-
return `${totalSeconds}s`;
|
|
1802
|
-
}
|
|
1803
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1804
|
-
const seconds = totalSeconds % 60;
|
|
1805
|
-
if (minutes < 60) {
|
|
1806
|
-
return `${minutes}m ${seconds}s`;
|
|
1807
|
-
}
|
|
1808
|
-
const hours = Math.floor(minutes / 60);
|
|
1809
|
-
const remainingMinutes = minutes % 60;
|
|
1810
|
-
return `${hours}h ${remainingMinutes}m ${seconds}s`;
|
|
1811
|
-
}
|
|
1812
|
-
async function waitForChildExit(child, options) {
|
|
1813
|
-
await new Promise((resolve, reject) => {
|
|
1814
|
-
let settled = false;
|
|
1815
|
-
let timeoutHandle;
|
|
1816
|
-
let stallHandle;
|
|
1817
|
-
let startupLoopHandle;
|
|
1818
|
-
let monitorHandle;
|
|
1819
|
-
let heavyCommandHandle;
|
|
1820
|
-
let killHandle;
|
|
1821
|
-
let hardKillArmed = false;
|
|
1822
|
-
const startMs = Date.now();
|
|
1823
|
-
const cleanup = () => {
|
|
1824
|
-
if (timeoutHandle) {
|
|
1825
|
-
clearTimeout(timeoutHandle);
|
|
1826
|
-
}
|
|
1827
|
-
if (stallHandle) {
|
|
1828
|
-
clearInterval(stallHandle);
|
|
1829
|
-
}
|
|
1830
|
-
if (startupLoopHandle) {
|
|
1831
|
-
clearInterval(startupLoopHandle);
|
|
1832
|
-
}
|
|
1833
|
-
if (monitorHandle) {
|
|
1834
|
-
clearInterval(monitorHandle);
|
|
1835
|
-
}
|
|
1836
|
-
if (heavyCommandHandle) {
|
|
1837
|
-
clearInterval(heavyCommandHandle);
|
|
1838
|
-
}
|
|
1839
|
-
if (killHandle && !hardKillArmed) {
|
|
1840
|
-
clearTimeout(killHandle);
|
|
1841
|
-
}
|
|
1842
|
-
child.removeListener('error', onError);
|
|
1843
|
-
child.removeListener('close', onClose);
|
|
1844
|
-
options.onCleanup?.();
|
|
1845
|
-
};
|
|
1846
|
-
const settleWithError = (error) => {
|
|
1847
|
-
if (settled) {
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
settled = true;
|
|
1851
|
-
cleanup();
|
|
1852
|
-
reject(error);
|
|
1853
|
-
};
|
|
1854
|
-
const onError = (error) => {
|
|
1855
|
-
settleWithError(error instanceof Error ? error : new Error(String(error)));
|
|
1856
|
-
};
|
|
1857
|
-
const onClose = (code, signal) => {
|
|
1858
|
-
if (settled) {
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
settled = true;
|
|
1862
|
-
cleanup();
|
|
1863
|
-
const blockedCommand = options.getBlockedHeavyCommand();
|
|
1864
|
-
if (code === 0 && options.blockHeavyCommands && blockedCommand) {
|
|
1865
|
-
reject(new CodexReviewError(formatBoundedHeavyCommandFailure(blockedCommand), {
|
|
1866
|
-
exitCode: 1,
|
|
1867
|
-
signal: null,
|
|
1868
|
-
timedOut: false,
|
|
1869
|
-
outputPreview: ''
|
|
1870
|
-
}));
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
if (code === 0) {
|
|
1874
|
-
resolve();
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
const suffix = signal ? ` (signal ${signal})` : '';
|
|
1878
|
-
reject(new CodexReviewError(`codex review exited with code ${code}${suffix}`, {
|
|
1879
|
-
exitCode: code,
|
|
1880
|
-
signal,
|
|
1881
|
-
timedOut: false,
|
|
1882
|
-
outputPreview: ''
|
|
1883
|
-
}));
|
|
1884
|
-
};
|
|
1885
|
-
child.once('error', onError);
|
|
1886
|
-
child.once('close', onClose);
|
|
1887
|
-
const requestTermination = (message, timedOut = true) => {
|
|
1888
|
-
if (settled) {
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
signalChildProcess(child, 'SIGTERM', options.detached);
|
|
1892
|
-
hardKillArmed = true;
|
|
1893
|
-
killHandle = setTimeout(() => {
|
|
1894
|
-
if (child.exitCode === null) {
|
|
1895
|
-
signalChildProcess(child, 'SIGKILL', options.detached);
|
|
1896
|
-
}
|
|
1897
|
-
}, 5000);
|
|
1898
|
-
killHandle.unref();
|
|
1899
|
-
settleWithError(new CodexReviewError(message, {
|
|
1900
|
-
exitCode: 1,
|
|
1901
|
-
signal: null,
|
|
1902
|
-
timedOut,
|
|
1903
|
-
outputPreview: ''
|
|
1904
|
-
}));
|
|
1905
|
-
};
|
|
1906
|
-
const timeoutMs = options.timeoutMs;
|
|
1907
|
-
if (timeoutMs !== null) {
|
|
1908
|
-
timeoutHandle = setTimeout(() => {
|
|
1909
|
-
requestTermination(`codex review timed out after ${Math.round(timeoutMs / 1000)}s (set CODEX_REVIEW_TIMEOUT_SECONDS=0 to disable).`);
|
|
1910
|
-
}, timeoutMs);
|
|
1911
|
-
timeoutHandle.unref();
|
|
1912
|
-
}
|
|
1913
|
-
const stallTimeoutMs = options.stallTimeoutMs;
|
|
1914
|
-
if (stallTimeoutMs !== null) {
|
|
1915
|
-
const checkIntervalMs = Math.min(5000, Math.max(1000, Math.round(stallTimeoutMs / 4)));
|
|
1916
|
-
stallHandle = setInterval(() => {
|
|
1917
|
-
const idleMs = Date.now() - options.getLastOutputAtMs();
|
|
1918
|
-
if (idleMs < stallTimeoutMs) {
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
requestTermination(`codex review stalled with no output for ${Math.round(stallTimeoutMs / 1000)}s (set CODEX_REVIEW_STALL_TIMEOUT_SECONDS=0 to disable).`);
|
|
1922
|
-
}, checkIntervalMs);
|
|
1923
|
-
stallHandle.unref();
|
|
1924
|
-
}
|
|
1925
|
-
const startupLoopTimeoutMs = options.startupLoopTimeoutMs;
|
|
1926
|
-
if (startupLoopTimeoutMs !== null && options.startupLoopMinEvents > 0) {
|
|
1927
|
-
const loopCheckIntervalMs = Math.min(5000, Math.max(1000, Math.round(startupLoopTimeoutMs / 6)));
|
|
1928
|
-
startupLoopHandle = setInterval(() => {
|
|
1929
|
-
if (Date.now() - startMs < startupLoopTimeoutMs) {
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
const state = options.getStartupLoopState();
|
|
1933
|
-
if (state.reviewProgressObserved || state.startupEvents < options.startupLoopMinEvents) {
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
requestTermination(`codex review appears stuck in delegation startup loop after ${Math.round(startupLoopTimeoutMs / 1000)}s (${state.startupEvents} startup events, no review progress). Set CODEX_REVIEW_STARTUP_LOOP_TIMEOUT_SECONDS=0 to disable.`);
|
|
1937
|
-
}, loopCheckIntervalMs);
|
|
1938
|
-
startupLoopHandle.unref();
|
|
1939
|
-
}
|
|
1940
|
-
if (options.blockHeavyCommands) {
|
|
1941
|
-
heavyCommandHandle = setInterval(() => {
|
|
1942
|
-
const blockedCommand = options.getBlockedHeavyCommand();
|
|
1943
|
-
if (!blockedCommand) {
|
|
1944
|
-
return;
|
|
1945
|
-
}
|
|
1946
|
-
requestTermination(formatBoundedHeavyCommandFailure(blockedCommand), false);
|
|
1947
|
-
}, 250);
|
|
1948
|
-
heavyCommandHandle.unref();
|
|
1949
|
-
}
|
|
1950
|
-
const monitorIntervalMs = options.monitorIntervalMs;
|
|
1951
|
-
if (monitorIntervalMs !== null) {
|
|
1952
|
-
const checkpointIntervalMs = Math.max(1000, monitorIntervalMs);
|
|
1953
|
-
monitorHandle = setInterval(() => {
|
|
1954
|
-
const elapsedMs = Date.now() - startMs;
|
|
1955
|
-
const idleMs = Date.now() - options.getLastOutputAtMs();
|
|
1956
|
-
const state = options.getStartupLoopState();
|
|
1957
|
-
const startupStatus = state.reviewProgressObserved
|
|
1958
|
-
? 'review progress observed'
|
|
1959
|
-
: `${state.startupEvents} delegation startup events, no review progress yet`;
|
|
1960
|
-
console.log(`[run-review] waiting on codex review (${formatDurationMs(elapsedMs)} elapsed, ${formatDurationMs(idleMs)} idle; ${startupStatus}).`);
|
|
1961
|
-
}, checkpointIntervalMs);
|
|
1962
|
-
monitorHandle.unref();
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
function signalChildProcess(child, signal, detached) {
|
|
1967
|
-
if (detached && typeof child.pid === 'number' && child.pid > 0) {
|
|
1968
|
-
try {
|
|
1969
|
-
process.kill(-child.pid, signal);
|
|
1970
|
-
return;
|
|
1971
|
-
}
|
|
1972
|
-
catch {
|
|
1973
|
-
// Fallback to direct child signal below.
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
try {
|
|
1977
|
-
child.kill(signal);
|
|
1978
|
-
}
|
|
1979
|
-
catch {
|
|
1980
|
-
// Best effort only.
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
function resolveReviewNotes(options) {
|
|
1984
|
-
if (options.notes) {
|
|
1985
|
-
return options.notes;
|
|
1986
|
-
}
|
|
1987
|
-
const fallback = `Goal: standalone review handoff | ` +
|
|
1988
|
-
`Summary: auto-generated NOTES fallback (task=${options.taskLabel}, manifest=${options.relativeManifest}) | ` +
|
|
1989
|
-
'Risks: missing custom intent details may reduce review precision';
|
|
1990
|
-
console.warn('[run-review] NOTES was not provided; using a generated fallback. ' +
|
|
1991
|
-
'Set NOTES="Goal: ... | Summary: ... | Risks: ... | Questions (optional): ..." for higher-signal review context.');
|
|
1992
|
-
return fallback;
|
|
1993
|
-
}
|
|
1994
725
|
function printReviewWrapperHelp() {
|
|
1995
726
|
console.log(`Usage: npm run review -- [options]
|
|
1996
727
|
|
|
@@ -2005,6 +736,7 @@ Options:
|
|
|
2005
736
|
--base <ref> Review diff from base ref.
|
|
2006
737
|
--commit <sha> Review a single commit.
|
|
2007
738
|
--title <title> Set a custom review title.
|
|
739
|
+
--surface <diff|audit|architecture> Review surface (default: diff).
|
|
2008
740
|
--non-interactive Force non-interactive Codex review mode.
|
|
2009
741
|
--auto-issue-log[=true|false] Capture issue bundle on failure.
|
|
2010
742
|
--enable-delegation-mcp[=true|false] Enable delegation MCP for this review run.
|
|
@@ -2015,15 +747,13 @@ Environment:
|
|
|
2015
747
|
NOTES (recommended) Goal/summary/risks context. If omitted, wrapper generates fallback notes.
|
|
2016
748
|
MANIFEST Alternative manifest path source.
|
|
2017
749
|
MCP_RUNNER_TASK_ID / TASK Task id fallback when --task is omitted.
|
|
750
|
+
${REVIEW_SURFACE_ENV_KEY} Review surface fallback when --surface is omitted.
|
|
751
|
+
CODEX_REVIEW_LARGE_SCOPE_OVERRIDE_REASON Auditable override for large uncommitted review scope gating.
|
|
752
|
+
|
|
753
|
+
Behavior:
|
|
754
|
+
Explicit --uncommitted/--base/--commit wrapper runs keep prompt/context in review/prompt.txt
|
|
755
|
+
and launch codex review without any prompt argument because current CLI still treats stdin (\`-\`) as [PROMPT]; reviewer-visible scoped context first rides on --title (user-provided when present, otherwise synthesized from NOTES + surface) with bounded no-validation guidance visible where the current Codex review surface honors titles. If Codex rejects a synthesized scoped title, the wrapper retries the same explicit scope without \`--title\` and falls back to artifact-only context. If bounded review blocks a validation command, the wrapper retries once with a reviewer-visible inline no-validation prompt that names the original scope and runs under a read-only sandbox override; successful retry preserves the command-intent boundary in telemetry as bounded-success.
|
|
756
|
+
Explicit scoped wrapper runs Support only the default diff surface; audit/architecture still require prompt-capable unscoped review.
|
|
757
|
+
Unscoped wrapper runs Pass the saved prompt/context inline to codex review.
|
|
2018
758
|
`);
|
|
2019
759
|
}
|
|
2020
|
-
async function tryGit(args) {
|
|
2021
|
-
try {
|
|
2022
|
-
const { stdout } = await execFileAsync('git', args, { maxBuffer: 1024 * 1024, cwd: repoRoot });
|
|
2023
|
-
const trimmed = String(stdout ?? '').trimEnd();
|
|
2024
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
2025
|
-
}
|
|
2026
|
-
catch {
|
|
2027
|
-
return null;
|
|
2028
|
-
}
|
|
2029
|
-
}
|