@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.
Files changed (311) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +46 -317
  3. package/bin/codex-orchestrator.js +161 -0
  4. package/codex.orchestrator.json +149 -13
  5. package/dist/bin/codex-orchestrator.js +797 -1154
  6. package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
  7. package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +22 -4
  8. package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +3 -3
  9. package/dist/orchestrator/src/cli/adapters/CommandTester.js +2 -2
  10. package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +295 -11
  11. package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
  12. package/dist/orchestrator/src/cli/coStatusCliShell.js +451 -0
  13. package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
  14. package/dist/orchestrator/src/cli/codexCliShell.js +119 -0
  15. package/dist/orchestrator/src/cli/codexDefaultsSetup.js +265 -36
  16. package/dist/orchestrator/src/cli/config/delegationConfig.js +317 -5
  17. package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +2 -3
  18. package/dist/orchestrator/src/cli/config/userConfig.js +28 -13
  19. package/dist/orchestrator/src/cli/control/authenticatedControlRouteGate.js +69 -0
  20. package/dist/orchestrator/src/cli/control/authenticatedRouteComposition.js +267 -0
  21. package/dist/orchestrator/src/cli/control/authenticatedRouteController.js +5 -0
  22. package/dist/orchestrator/src/cli/control/authenticatedRouteDispatcher.js +41 -0
  23. package/dist/orchestrator/src/cli/control/compatibilityIssuePresenter.js +1035 -0
  24. package/dist/orchestrator/src/cli/control/confirmationApproveController.js +62 -0
  25. package/dist/orchestrator/src/cli/control/confirmationCreateController.js +69 -0
  26. package/dist/orchestrator/src/cli/control/confirmationIssueConsumeController.js +43 -0
  27. package/dist/orchestrator/src/cli/control/confirmationListController.js +22 -0
  28. package/dist/orchestrator/src/cli/control/confirmationValidateController.js +58 -0
  29. package/dist/orchestrator/src/cli/control/confirmations.js +25 -3
  30. package/dist/orchestrator/src/cli/control/controlActionCancelConfirmation.js +65 -0
  31. package/dist/orchestrator/src/cli/control/controlActionController.js +77 -0
  32. package/dist/orchestrator/src/cli/control/controlActionControllerSequencing.js +161 -0
  33. package/dist/orchestrator/src/cli/control/controlActionExecution.js +142 -0
  34. package/dist/orchestrator/src/cli/control/controlActionFinalization.js +43 -0
  35. package/dist/orchestrator/src/cli/control/controlActionOutcome.js +60 -0
  36. package/dist/orchestrator/src/cli/control/controlActionPreflight.js +476 -0
  37. package/dist/orchestrator/src/cli/control/controlAuthenticatedRouteHandoff.js +57 -0
  38. package/dist/orchestrator/src/cli/control/controlBootstrapAssembly.js +39 -0
  39. package/dist/orchestrator/src/cli/control/controlBootstrapMetadataPersistence.js +16 -0
  40. package/dist/orchestrator/src/cli/control/controlEventTransport.js +49 -0
  41. package/dist/orchestrator/src/cli/control/controlExpiryLifecycle.js +102 -0
  42. package/dist/orchestrator/src/cli/control/controlHostOwnership.js +480 -0
  43. package/dist/orchestrator/src/cli/control/controlHostSupervision.js +630 -0
  44. package/dist/orchestrator/src/cli/control/controlOversightFacade.js +8 -0
  45. package/dist/orchestrator/src/cli/control/controlOversightReadContract.js +1 -0
  46. package/dist/orchestrator/src/cli/control/controlOversightReadService.js +16 -0
  47. package/dist/orchestrator/src/cli/control/controlOversightUpdateContract.js +1 -0
  48. package/dist/orchestrator/src/cli/control/controlPersistenceFiles.js +6 -0
  49. package/dist/orchestrator/src/cli/control/controlQuestionChildResolution.js +18 -0
  50. package/dist/orchestrator/src/cli/control/controlRequestContext.js +42 -0
  51. package/dist/orchestrator/src/cli/control/controlRequestController.js +9 -0
  52. package/dist/orchestrator/src/cli/control/controlRequestPredispatch.js +17 -0
  53. package/dist/orchestrator/src/cli/control/controlRequestRouteDispatch.js +44 -0
  54. package/dist/orchestrator/src/cli/control/controlRuntime.js +1003 -0
  55. package/dist/orchestrator/src/cli/control/controlServer.js +23 -1456
  56. package/dist/orchestrator/src/cli/control/controlServerAuditAndErrorHelpers.js +115 -0
  57. package/dist/orchestrator/src/cli/control/controlServerAuthenticatedRouteBranch.js +29 -0
  58. package/dist/orchestrator/src/cli/control/controlServerBootstrapLifecycle.js +30 -0
  59. package/dist/orchestrator/src/cli/control/controlServerBootstrapStartSequence.js +21 -0
  60. package/dist/orchestrator/src/cli/control/controlServerOwnedRuntimeLifecycle.js +67 -0
  61. package/dist/orchestrator/src/cli/control/controlServerPublicLifecycle.js +756 -0
  62. package/dist/orchestrator/src/cli/control/controlServerPublicRouteHelpers.js +86 -0
  63. package/dist/orchestrator/src/cli/control/controlServerReadyInstanceLifecycle.js +25 -0
  64. package/dist/orchestrator/src/cli/control/controlServerReadyInstanceStartup.js +18 -0
  65. package/dist/orchestrator/src/cli/control/controlServerRequestBodyHelpers.js +37 -0
  66. package/dist/orchestrator/src/cli/control/controlServerRequestShell.js +40 -0
  67. package/dist/orchestrator/src/cli/control/controlServerRequestShellBinding.js +17 -0
  68. package/dist/orchestrator/src/cli/control/controlServerSeedLoading.js +27 -0
  69. package/dist/orchestrator/src/cli/control/controlServerSeededRuntimeAssembly.js +186 -0
  70. package/dist/orchestrator/src/cli/control/controlServerStartupInputPreparation.js +31 -0
  71. package/dist/orchestrator/src/cli/control/controlServerStartupSequence.js +49 -0
  72. package/dist/orchestrator/src/cli/control/controlState.js +233 -2
  73. package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +1904 -0
  74. package/dist/orchestrator/src/cli/control/controlTelegramBridgeBootstrapLifecycle.js +22 -0
  75. package/dist/orchestrator/src/cli/control/controlTelegramBridgeLifecycle.js +67 -0
  76. package/dist/orchestrator/src/cli/control/controlTelegramBridgeOversightFacadeFactory.js +8 -0
  77. package/dist/orchestrator/src/cli/control/controlTelegramCommandController.js +49 -0
  78. package/dist/orchestrator/src/cli/control/controlTelegramDispatchRead.js +40 -0
  79. package/dist/orchestrator/src/cli/control/controlTelegramPollingController.js +89 -0
  80. package/dist/orchestrator/src/cli/control/controlTelegramProjectionNotificationController.js +29 -0
  81. package/dist/orchestrator/src/cli/control/controlTelegramPushState.js +63 -0
  82. package/dist/orchestrator/src/cli/control/controlTelegramQuestionRead.js +13 -0
  83. package/dist/orchestrator/src/cli/control/controlTelegramReadController.js +216 -0
  84. package/dist/orchestrator/src/cli/control/controlTelegramUpdateHandler.js +63 -0
  85. package/dist/orchestrator/src/cli/control/controlWatcher.js +73 -5
  86. package/dist/orchestrator/src/cli/control/delegationRegisterController.js +35 -0
  87. package/dist/orchestrator/src/cli/control/dynamicToolBridgePolicy.js +139 -0
  88. package/dist/orchestrator/src/cli/control/eventsSseController.js +12 -0
  89. package/dist/orchestrator/src/cli/control/linearBudgetState.js +1789 -0
  90. package/dist/orchestrator/src/cli/control/linearDispatchSource.js +1137 -0
  91. package/dist/orchestrator/src/cli/control/linearGraphqlClient.js +150 -0
  92. package/dist/orchestrator/src/cli/control/linearRateLimit.js +102 -0
  93. package/dist/orchestrator/src/cli/control/linearWebhookController.js +499 -0
  94. package/dist/orchestrator/src/cli/control/liveLinearAdvisoryRuntime.js +70 -0
  95. package/dist/orchestrator/src/cli/control/observabilityApiController.js +173 -0
  96. package/dist/orchestrator/src/cli/control/observabilityReadModel.js +500 -0
  97. package/dist/orchestrator/src/cli/control/observabilitySurface.js +284 -0
  98. package/dist/orchestrator/src/cli/control/observabilityUpdateNotifier.js +22 -0
  99. package/dist/orchestrator/src/cli/control/operatorDashboardPresenter.js +252 -0
  100. package/dist/orchestrator/src/cli/control/providerAgentCapacity.js +70 -0
  101. package/dist/orchestrator/src/cli/control/providerControlHostFreshnessGauge.js +1068 -0
  102. package/dist/orchestrator/src/cli/control/providerIntakeState.js +473 -0
  103. package/dist/orchestrator/src/cli/control/providerIssueHandoff.js +6811 -0
  104. package/dist/orchestrator/src/cli/control/providerIssueObservability.js +1348 -0
  105. package/dist/orchestrator/src/cli/control/providerIssueRetryQueue.js +84 -0
  106. package/dist/orchestrator/src/cli/control/providerLinearRuntimeProof.js +588 -0
  107. package/dist/orchestrator/src/cli/control/providerLinearScreenshotProof.js +473 -0
  108. package/dist/orchestrator/src/cli/control/providerLinearWorkerTruth.js +383 -0
  109. package/dist/orchestrator/src/cli/control/providerLinearWorkflowAudit.js +254 -0
  110. package/dist/orchestrator/src/cli/control/providerLinearWorkflowFacade.js +5573 -0
  111. package/dist/orchestrator/src/cli/control/providerLinearWorkflowStates.js +115 -0
  112. package/dist/orchestrator/src/cli/control/providerMergeCloseout.js +1868 -0
  113. package/dist/orchestrator/src/cli/control/providerOperatorAutopilot.js +1580 -0
  114. package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLifecycle.js +154 -0
  115. package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLocalRolloutExecution.js +1006 -0
  116. package/dist/orchestrator/src/cli/control/providerPollingHealth.js +435 -0
  117. package/dist/orchestrator/src/cli/control/providerTerminalCleanup.js +516 -0
  118. package/dist/orchestrator/src/cli/control/providerWorkerHosts.js +191 -0
  119. package/dist/orchestrator/src/cli/control/providerWorkflowConfigStore.js +515 -0
  120. package/dist/orchestrator/src/cli/control/questionChildResolutionAdapter.js +361 -0
  121. package/dist/orchestrator/src/cli/control/questionQueueController.js +181 -0
  122. package/dist/orchestrator/src/cli/control/questionReadRetryDeduplication.js +9 -0
  123. package/dist/orchestrator/src/cli/control/questionReadSequence.js +10 -0
  124. package/dist/orchestrator/src/cli/control/securityViolationController.js +27 -0
  125. package/dist/orchestrator/src/cli/control/selectedRunProjection.js +1885 -0
  126. package/dist/orchestrator/src/cli/control/telegramOversightApiClient.js +48 -0
  127. package/dist/orchestrator/src/cli/control/telegramOversightBridge.js +180 -0
  128. package/dist/orchestrator/src/cli/control/telegramOversightBridgeProjectionDeliveryQueue.js +25 -0
  129. package/dist/orchestrator/src/cli/control/telegramOversightBridgeRuntimeLifecycle.js +45 -0
  130. package/dist/orchestrator/src/cli/control/telegramOversightBridgeStateStore.js +77 -0
  131. package/dist/orchestrator/src/cli/control/telegramOversightControlActionApiClient.js +45 -0
  132. package/dist/orchestrator/src/cli/control/trackerDispatchPilot.js +439 -0
  133. package/dist/orchestrator/src/cli/control/uiDataController.js +34 -0
  134. package/dist/orchestrator/src/cli/control/uiSessionController.js +100 -0
  135. package/dist/orchestrator/src/cli/controlHostCliShell.js +860 -0
  136. package/dist/orchestrator/src/cli/controlHostFreshnessGaugeCliShell.js +129 -0
  137. package/dist/orchestrator/src/cli/controlHostSupervisionCliShell.js +2127 -0
  138. package/dist/orchestrator/src/cli/delegationCliShell.js +62 -0
  139. package/dist/orchestrator/src/cli/delegationServer.js +567 -678
  140. package/dist/orchestrator/src/cli/delegationServerCliShell.js +52 -0
  141. package/dist/orchestrator/src/cli/delegationServerQuestionFlowShell.js +228 -0
  142. package/dist/orchestrator/src/cli/delegationServerToolDispatchShell.js +411 -0
  143. package/dist/orchestrator/src/cli/delegationServerTransport.js +274 -0
  144. package/dist/orchestrator/src/cli/delegationSetup.js +51 -171
  145. package/dist/orchestrator/src/cli/devtoolsCliShell.js +34 -0
  146. package/dist/orchestrator/src/cli/doctor.js +678 -164
  147. package/dist/orchestrator/src/cli/doctorCliRequestShell.js +72 -0
  148. package/dist/orchestrator/src/cli/doctorCliShell.js +138 -0
  149. package/dist/orchestrator/src/cli/doctorUsage.js +119 -15
  150. package/dist/orchestrator/src/cli/exec/experience.js +16 -2
  151. package/dist/orchestrator/src/cli/exec/summary.js +3 -0
  152. package/dist/orchestrator/src/cli/execCliShell.js +51 -0
  153. package/dist/orchestrator/src/cli/flowCliRequestShell.js +44 -0
  154. package/dist/orchestrator/src/cli/flowCliShell.js +239 -0
  155. package/dist/orchestrator/src/cli/frontendTestCliRequestShell.js +80 -0
  156. package/dist/orchestrator/src/cli/frontendTestCliShell.js +41 -0
  157. package/dist/orchestrator/src/cli/init.js +95 -1
  158. package/dist/orchestrator/src/cli/initCliShell.js +50 -0
  159. package/dist/orchestrator/src/cli/linearCliShell.js +1200 -0
  160. package/dist/orchestrator/src/cli/mcpEnableCliShell.js +132 -0
  161. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +3 -2
  162. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +56 -0
  163. package/dist/orchestrator/src/cli/orchestrator.js +66 -1376
  164. package/dist/orchestrator/src/cli/planCliShell.js +19 -0
  165. package/dist/orchestrator/src/cli/prCliShell.js +41 -0
  166. package/dist/orchestrator/src/cli/providerLinearChildLanePhaseContract.js +204 -0
  167. package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +1835 -0
  168. package/dist/orchestrator/src/cli/providerLinearChildLaneShell.js +2420 -0
  169. package/dist/orchestrator/src/cli/providerLinearChildStreamShell.js +385 -0
  170. package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +6834 -0
  171. package/dist/orchestrator/src/cli/resumeCliShell.js +14 -0
  172. package/dist/orchestrator/src/cli/reviewCliLaunchShell.js +72 -0
  173. package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
  174. package/dist/orchestrator/src/cli/rlm/context.js +94 -7
  175. package/dist/orchestrator/src/cli/rlm/rlmCodexRuntimeShell.js +546 -0
  176. package/dist/orchestrator/src/cli/rlm/symbolic.js +4 -2
  177. package/dist/orchestrator/src/cli/rlmCliRequestShell.js +42 -0
  178. package/dist/orchestrator/src/cli/rlmCompletionCliShell.js +46 -0
  179. package/dist/orchestrator/src/cli/rlmLaunchCliShell.js +51 -0
  180. package/dist/orchestrator/src/cli/rlmRunner.js +83 -523
  181. package/dist/orchestrator/src/cli/run/blockMemory.js +500 -0
  182. package/dist/orchestrator/src/cli/run/manifest.js +410 -73
  183. package/dist/orchestrator/src/cli/run/manifestPersister.js +45 -14
  184. package/dist/orchestrator/src/cli/run/runMemoryController.js +216 -0
  185. package/dist/orchestrator/src/cli/run/source0.js +690 -0
  186. package/dist/orchestrator/src/cli/run/workspacePath.js +101 -0
  187. package/dist/orchestrator/src/cli/runtime/mode.js +2 -1
  188. package/dist/orchestrator/src/cli/runtime/provider.js +39 -2
  189. package/dist/orchestrator/src/cli/selfCheckCliShell.js +12 -0
  190. package/dist/orchestrator/src/cli/services/commandRunner.js +698 -18
  191. package/dist/orchestrator/src/cli/services/execRuntime.js +66 -1
  192. package/dist/orchestrator/src/cli/services/orchestratorAutoScoutEvidenceRecorder.js +71 -0
  193. package/dist/orchestrator/src/cli/services/orchestratorCloudBranchResolution.js +8 -0
  194. package/dist/orchestrator/src/cli/services/orchestratorCloudEnvironmentResolution.js +22 -0
  195. package/dist/orchestrator/src/cli/services/orchestratorCloudExecutionLifecycleShell.js +39 -0
  196. package/dist/orchestrator/src/cli/services/orchestratorCloudPromptBuilder.js +37 -0
  197. package/dist/orchestrator/src/cli/services/orchestratorCloudRouteFallbackContract.js +45 -0
  198. package/dist/orchestrator/src/cli/services/orchestratorCloudRouteShell.js +36 -0
  199. package/dist/orchestrator/src/cli/services/orchestratorCloudTargetExecutor.js +277 -0
  200. package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycle.js +98 -0
  201. package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycleShell.js +54 -0
  202. package/dist/orchestrator/src/cli/services/orchestratorExecutionLifecycle.js +112 -0
  203. package/dist/orchestrator/src/cli/services/orchestratorExecutionModePolicy.js +27 -0
  204. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteAdapterShell.js +59 -0
  205. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteDecisionShell.js +57 -0
  206. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteState.js +21 -0
  207. package/dist/orchestrator/src/cli/services/orchestratorExecutionRouter.js +2 -0
  208. package/dist/orchestrator/src/cli/services/orchestratorLocalPipelineExecutor.js +149 -0
  209. package/dist/orchestrator/src/cli/services/orchestratorLocalRouteShell.js +63 -0
  210. package/dist/orchestrator/src/cli/services/orchestratorPlanShell.js +54 -0
  211. package/dist/orchestrator/src/cli/services/orchestratorPlanTargetTracker.js +16 -0
  212. package/dist/orchestrator/src/cli/services/orchestratorResumePreparationShell.js +84 -0
  213. package/dist/orchestrator/src/cli/services/orchestratorResumeTokenValidation.js +15 -0
  214. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleCompletion.js +31 -0
  215. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleExecutionRegistration.js +37 -0
  216. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleOrchestrationShell.js +83 -0
  217. package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleTaskManagerShell.js +37 -0
  218. package/dist/orchestrator/src/cli/services/orchestratorRuntimeManifestMutation.js +20 -0
  219. package/dist/orchestrator/src/cli/services/orchestratorStartPreparationShell.js +56 -0
  220. package/dist/orchestrator/src/cli/services/orchestratorStatusShell.js +70 -0
  221. package/dist/orchestrator/src/cli/services/pipelineResolver.js +7 -3
  222. package/dist/orchestrator/src/cli/services/plannerMemory.js +119 -0
  223. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -3
  224. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +9 -0
  225. package/dist/orchestrator/src/cli/setupBootstrapShell.js +114 -0
  226. package/dist/orchestrator/src/cli/setupCliShell.js +51 -0
  227. package/dist/orchestrator/src/cli/skillsCliShell.js +56 -0
  228. package/dist/orchestrator/src/cli/startCliRequestShell.js +53 -0
  229. package/dist/orchestrator/src/cli/startCliShell.js +68 -0
  230. package/dist/orchestrator/src/cli/statusCliShell.js +22 -0
  231. package/dist/orchestrator/src/cli/utils/authProvenanceFingerprint.js +27 -0
  232. package/dist/orchestrator/src/cli/utils/cloudPreflight.js +285 -7
  233. package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
  234. package/dist/orchestrator/src/cli/utils/delegationConfigParser.js +250 -0
  235. package/dist/orchestrator/src/cli/utils/delegationMcpHealth.js +1382 -0
  236. package/dist/orchestrator/src/cli/utils/devtools.js +2 -54
  237. package/dist/orchestrator/src/cli/utils/mcpServerEntry.js +53 -0
  238. package/dist/orchestrator/src/cli/utils/packageProgramResolver.js +151 -0
  239. package/dist/orchestrator/src/cli/utils/providerOverrideEnv.js +71 -0
  240. package/dist/orchestrator/src/cli/utils/trailingJsonObject.js +59 -0
  241. package/dist/orchestrator/src/learning/crystalizer.js +2 -2
  242. package/dist/orchestrator/src/manager.js +74 -4
  243. package/dist/orchestrator/src/persistence/ExperienceStore.js +233 -49
  244. package/dist/orchestrator/src/persistence/TaskStateStore.js +6 -6
  245. package/dist/orchestrator/src/persistence/lockFile.js +70 -4
  246. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +39 -0
  247. package/dist/orchestrator/src/sync/createCloudSyncWorker.js +3 -2
  248. package/dist/orchestrator/src/utils/atomicWrite.js +17 -2
  249. package/dist/packages/orchestrator/src/exec/unified-exec.js +99 -6
  250. package/dist/packages/orchestrator/src/instructions/promptPacks.js +150 -19
  251. package/dist/packages/sdk-node/src/orchestrator.js +137 -13
  252. package/dist/packages/shared/config/designConfig.js +8 -1
  253. package/dist/packages/shared/streams/stdio.js +1 -1
  254. package/dist/scripts/design/pipeline/permit.js +15 -0
  255. package/dist/scripts/lib/docs-catalog.js +399 -0
  256. package/dist/scripts/lib/docs-helpers.js +87 -5
  257. package/dist/scripts/lib/pr-watch-merge.js +1088 -80
  258. package/dist/scripts/lib/provider-run-contract.js +26 -0
  259. package/dist/scripts/lib/review-command-intent-classification.js +532 -0
  260. package/dist/scripts/lib/review-command-probe-classification.js +385 -0
  261. package/dist/scripts/lib/review-execution-boundary-preflight.js +279 -0
  262. package/dist/scripts/lib/review-execution-runtime.js +753 -0
  263. package/dist/scripts/lib/review-execution-state.js +1144 -0
  264. package/dist/scripts/lib/review-execution-telemetry.js +215 -0
  265. package/dist/scripts/lib/review-inspection-target-parsing.js +78 -0
  266. package/dist/scripts/lib/review-launch-attempt.js +601 -0
  267. package/dist/scripts/lib/review-meta-surface-boundary-analysis.js +300 -0
  268. package/dist/scripts/lib/review-meta-surface-normalization.js +746 -0
  269. package/dist/scripts/lib/review-non-interactive-handoff.js +61 -0
  270. package/dist/scripts/lib/review-prompt-context.js +376 -0
  271. package/dist/scripts/lib/review-scope-advisory.js +286 -0
  272. package/dist/scripts/lib/review-scope-paths.js +123 -0
  273. package/dist/scripts/lib/review-shell-command-parser.js +389 -0
  274. package/dist/scripts/lib/review-shell-env-interpreter.js +340 -0
  275. package/dist/scripts/lib/run-manifests.js +192 -36
  276. package/dist/scripts/lib/spark-policy-classifier.js +593 -0
  277. package/dist/scripts/run-review.js +507 -1777
  278. package/docs/README.md +43 -20
  279. package/docs/book/README.md +19 -0
  280. package/docs/book/codex-cli-0124-adoption.md +68 -0
  281. package/docs/book/local-hook-impact.md +73 -0
  282. package/docs/book/operations.md +60 -0
  283. package/docs/book/public-posture.md +34 -0
  284. package/docs/book/setup.md +91 -0
  285. package/docs/book/skills.md +11 -0
  286. package/docs/guides/codex-version-policy.md +104 -0
  287. package/docs/public/downstream-setup.md +113 -0
  288. package/docs/public/provider-onboarding.md +173 -0
  289. package/package.json +23 -10
  290. package/plugins/codex-orchestrator/.codex-plugin/plugin.json +30 -0
  291. package/plugins/codex-orchestrator/.mcp.json +13 -0
  292. package/plugins/codex-orchestrator/launcher.mjs +361 -0
  293. package/schemas/manifest.json +411 -0
  294. package/skills/README.md +26 -0
  295. package/skills/collab-subagents-first/SKILL.md +1 -1
  296. package/skills/delegation-usage/DELEGATION_GUIDE.md +30 -12
  297. package/skills/delegation-usage/SKILL.md +25 -14
  298. package/skills/land/SKILL.md +77 -0
  299. package/skills/linear/SKILL.md +255 -0
  300. package/skills/release/SKILL.md +47 -3
  301. package/skills/standalone-review/SKILL.md +6 -1
  302. package/templates/README.md +4 -2
  303. package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
  304. package/templates/codex/.codex/agents/worker-complex.toml +1 -1
  305. package/templates/codex/.codex/config.toml +3 -4
  306. package/templates/codex/.codex/providers/README.md +13 -0
  307. package/templates/codex/.codex/providers/control.example.json +18 -0
  308. package/templates/codex/.codex/providers/provider.env.example +15 -0
  309. package/templates/codex/AGENTS.md +15 -8
  310. package/templates/codex/mcp-client.json +5 -1
  311. 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
- * always supplies a custom prompt (to include manifest evidence), so it will
9
- * try real scope flags first and fall back to embedding scope hints into the
10
- * prompt if the CLI rejects the flag/prompt combination.
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 { execFile, spawn } from 'node:child_process';
13
- import { createReadStream, createWriteStream } from 'node:fs';
14
- import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
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 { createInterface } from 'node:readline';
18
- import { promisify } from 'node:util';
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 REVIEW_DISABLE_DELEGATION_CONFIG_OVERRIDE = 'mcp_servers.delegation.enabled=false';
45
- const REVIEW_DELEGATION_STARTUP_LINE_RE = /\bmcp:\s*delegation\s+(starting|ready)\b/i;
46
- const REVIEW_PROGRESS_SIGNAL_LINE_RE = /^(thinking|exec|codex)\b/i;
47
- const REVIEW_HEAVY_SCRIPT_TARGETS = new Set([
48
- 'test',
49
- 'lint',
50
- 'build',
51
- 'typecheck',
52
- 'check',
53
- 'docs:check',
54
- 'docs:freshness'
55
- ]);
56
- const REVIEW_PACKAGE_RUN_SUBCOMMAND_ALIASES = new Set(['run', 'run-script', 'rum', 'urn']);
57
- const REVIEW_PACKAGE_TEST_SUBCOMMAND_ALIASES = new Set(['test', 't', 'tst']);
58
- const REVIEW_SHELL_COMMANDS = new Set([
59
- 'bash',
60
- 'sh',
61
- 'zsh',
62
- 'ksh',
63
- 'fish',
64
- 'pwsh',
65
- 'powershell',
66
- 'cmd',
67
- 'cmd.exe'
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 parseArgs(argv) {
215
- const options = { runsDir: defaultRunsDir };
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 = path.resolve(repoRoot, entry.value);
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 = path.resolve(repoRoot, entry.value);
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.MANIFEST ?? process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH;
356
+ const envManifest = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH ?? process.env.MANIFEST;
276
357
  if (envManifest && envManifest.trim().length > 0) {
277
- options.manifest = path.resolve(repoRoot, envManifest.trim());
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.MCP_RUNNER_TASK_ID?.trim() || process.env.TASK?.trim();
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 main() {
333
- const options = parseArgs(process.argv.slice(2));
334
- if (options.help) {
335
- printReviewWrapperHelp();
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 = await resolveManifestPath(options);
345
- const relativeManifest = path.relative(repoRoot, manifestPath);
346
- const taskLabel = options.task ?? process.env.MCP_RUNNER_TASK_ID ?? process.env.TASK ?? 'unknown-task';
347
- const notes = resolveReviewNotes({
348
- notes: process.env.NOTES?.trim(),
349
- taskLabel,
350
- relativeManifest
351
- });
352
- const diffBudgetOverride = process.env.DIFF_BUDGET_OVERRIDE_REASON?.trim();
353
- const promptLines = [
354
- `Review task: ${taskLabel}`,
355
- `Evidence manifest: ${relativeManifest}`,
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
- if (notes) {
365
- promptLines.push('', 'Agent notes:', notes);
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
- promptLines.push('', 'Execution constraints (bounded review mode):', '- Keep this review focused on changed files and nearby dependencies.', '- Avoid full validation suites (for example `npm run test`, `npm run lint`, `npm run build`) during this pass.', '- If broader validation would improve confidence, list follow-up commands instead of executing them.');
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] review scope metrics unavailable (git scope stats could not be resolved).');
392
- }
393
- if (scopeAssessment.largeScope) {
394
- const detail = scopeMetrics ?? 'metrics unavailable';
395
- console.warn(`[run-review] large uncommitted review scope detected (${detail}; thresholds: ${scopeAssessment.fileThreshold} files / ${scopeAssessment.lineThreshold} lines).`);
396
- console.warn('[run-review] this scope profile is known to produce long CO review traversals; prefer scoped reviews (`--base`/`--commit`) when practical.');
397
- promptLines.push('', `Scope advisory: large uncommitted diff detected (${detail}; thresholds: ${scopeAssessment.fileThreshold} files / ${scopeAssessment.lineThreshold} lines).`, 'Prioritize highest-risk findings first and report actionable issues early; avoid exhaustive low-signal traversal before surfacing initial findings.', 'If full coverage is incomplete, call out residual risk areas explicitly.');
398
- }
399
- }
400
- if (diffBudgetOverride) {
401
- promptLines.push('', `Diff budget override: ${diffBudgetOverride}`);
402
- }
403
- const prompt = promptLines.join('\n');
404
- const artifactPaths = await prepareReviewArtifacts(manifestPath, prompt);
405
- const nonInteractive = options.nonInteractive ?? shouldForceNonInteractive();
406
- const reviewEnv = { ...process.env };
407
- const stdinIsTTY = process.stdin?.isTTY === true;
408
- if (nonInteractive) {
409
- reviewEnv.CODEX_NON_INTERACTIVE = reviewEnv.CODEX_NON_INTERACTIVE ?? '1';
410
- reviewEnv.CODEX_NO_INTERACTIVE = reviewEnv.CODEX_NO_INTERACTIVE ?? '1';
411
- reviewEnv.CODEX_INTERACTIVE = reviewEnv.CODEX_INTERACTIVE ?? '0';
412
- }
413
- if (nonInteractive &&
414
- !envFlagEnabled(process.env.FORCE_CODEX_REVIEW) &&
415
- (envFlagEnabled(process.env.CI) ||
416
- !stdinIsTTY ||
417
- envFlagEnabled(process.env.CODEX_REVIEW_NON_INTERACTIVE) ||
418
- envFlagEnabled(process.env.CODEX_NON_INTERACTIVE) ||
419
- envFlagEnabled(process.env.CODEX_NO_INTERACTIVE) ||
420
- envFlagEnabled(process.env.CODEX_NONINTERACTIVE))) {
421
- console.log('Codex review handoff (non-interactive):');
422
- console.log('---');
423
- console.log(prompt);
424
- console.log('---');
425
- console.log(`Review prompt saved to: ${path.relative(repoRoot, artifactPaths.promptPath)}`);
426
- console.log('Set FORCE_CODEX_REVIEW=1 to invoke `codex review` in this environment.');
427
- return;
428
- }
429
- const runtimeContext = await resolveReviewRuntimeContext({
430
- options,
431
- manifestPath,
432
- env: reviewEnv
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.warn(`[run-review] review telemetry unavailable (persistence failed); see earlier telemetry error logs.`);
510
- }
511
- }
512
- catch (error) {
513
- if (shouldRetryWithoutScopeFlags(error)) {
514
- console.log('[run-review] codex CLI rejected scope flags with a custom prompt; retrying without flags.');
515
- const unscopedArgs = buildReviewArgs(options, prompt, {
516
- includeScopeFlags: false,
517
- disableDelegationMcp
518
- });
519
- const resolvedUnscoped = resolveReviewCommand(unscopedArgs, runtimeContext);
520
- try {
521
- await runReview(resolvedUnscoped);
522
- const telemetrySummary = await writeTelemetry('succeeded');
523
- console.log(`Review output saved to: ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
524
- if (telemetrySummary) {
525
- console.log(`Review telemetry saved to: ${path.relative(repoRoot, artifactPaths.telemetryPath)}`);
526
- }
527
- else {
528
- console.warn(`[run-review] review telemetry unavailable (persistence failed); see earlier telemetry error logs.`);
529
- }
530
- return;
531
- }
532
- catch (retryError) {
533
- await maybeCaptureReviewFailureIssueLog({
534
- enabled: autoIssueLogEnabled,
535
- error: retryError,
536
- taskFilter: options.task ?? null,
537
- manifestPath,
538
- outputLogPath: artifactPaths.outputLogPath
539
- });
540
- const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
541
- const telemetrySummary = await writeTelemetry('failed', retryMessage);
542
- if (telemetrySummary) {
543
- logReviewTelemetrySummary(telemetrySummary, artifactPaths.telemetryPath);
544
- }
545
- if (retryError instanceof CodexReviewError && retryError.timedOut) {
546
- console.error(`Review output log (partial): ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
547
- }
548
- throw retryError;
549
- }
550
- }
551
- await maybeCaptureReviewFailureIssueLog({
552
- enabled: autoIssueLogEnabled,
553
- error,
554
- taskFilter: options.task ?? null,
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
- outputLogPath: artifactPaths.outputLogPath
549
+ prompt,
550
+ repoRoot,
551
+ runnerLogExists,
552
+ runnerLogPath,
553
+ stdinIsTTY
557
554
  });
558
- const errorMessage = error instanceof Error ? error.message : String(error);
559
- const telemetrySummary = await writeTelemetry('failed', errorMessage);
560
- if (telemetrySummary) {
561
- logReviewTelemetrySummary(telemetrySummary, artifactPaths.telemetryPath);
555
+ if (handedOff) {
556
+ return typeof process.exitCode === 'number' ? process.exitCode : 0;
562
557
  }
563
- if (error instanceof CodexReviewError && error.timedOut) {
564
- console.error(`Review output log (partial): ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
565
- }
566
- throw error;
567
- }
568
- }
569
- main().catch((error) => {
570
- console.error('[run-review] failed:', error.message ?? error);
571
- process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
572
- });
573
- async function ensureReviewCommandAvailable(context) {
574
- const resolved = resolveRuntimeCodexCommand(['--help'], context);
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
- child.stderr?.on('data', (chunk) => {
618
- output += chunk.toString();
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
- child.once('error', (error) => finalize({ error: error instanceof Error ? error : new Error(String(error)) }));
621
- child.once('close', () => {
622
- finalize({ ok: output.includes(' review') });
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
- if (!hasReview) {
626
- throw new Error('codex CLI is missing the `review` subcommand (or is not installed).');
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
- async function resolveReviewRuntimeContext(params) {
630
- const runId = await resolveReviewRunId(params.manifestPath);
631
- const requestedMode = params.options.runtimeMode ??
632
- parseRuntimeMode(params.env.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE ??
633
- params.env.CODEX_ORCHESTRATOR_RUNTIME_MODE ??
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
- const raw = await readFile(manifestPath, 'utf8');
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
- return null;
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
- if (options.base) {
660
- return { mode: 'base', args: ['--base', options.base] };
654
+ try {
655
+ candidateUrls.add(pathToFileURL(realpathSync(entryArg)).href);
661
656
  }
662
- if (options.uncommitted) {
663
- return { mode: 'uncommitted', args: ['--uncommitted'] };
657
+ catch {
658
+ // Missing or unreadable entry points should not be treated as direct execution.
664
659
  }
665
- return null;
660
+ return candidateUrls.has(metaUrl);
666
661
  }
667
- function resolveEffectiveScopeMode(options) {
668
- if (options.commit) {
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 buildReviewArgs(options, prompt, opts) {
677
- const args = [];
678
- if (opts.disableDelegationMcp) {
679
- args.push('-c', REVIEW_DISABLE_DELEGATION_CONFIG_OVERRIDE);
680
- }
681
- args.push('review');
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.push(prompt);
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
- lines.push(`Review scope hint: commit \`${options.commit}\``);
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
- lines.push(`Review scope hint: diff vs base \`${options.base}\``);
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
- return await new Promise((resolve) => {
809
- const stream = createReadStream(absolutePath);
810
- let sawData = false;
811
- let sawBinaryByte = false;
812
- let newlineCount = 0;
813
- let lastByte = 0;
814
- let settled = false;
815
- const settle = (value) => {
816
- if (settled) {
817
- return;
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
- stream.once('close', () => {
842
- if (!sawData || sawBinaryByte) {
843
- settle(0);
844
- }
690
+ child.stdout?.on('data', (chunk) => {
691
+ process.stdout.write(chunk);
845
692
  });
846
- stream.once('end', () => {
847
- if (!sawData || sawBinaryByte) {
848
- settle(0);
849
- return;
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
- async function countWorkingTreeLines(paths) {
856
- let total = 0;
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
- async function assessReviewScope(options) {
863
- const mode = resolveEffectiveScopeMode(options);
864
- const fileThreshold = resolveLargeScopeFileThreshold();
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
- const exceedsFileThreshold = changedFiles !== null && changedFiles >= fileThreshold;
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 formatScopeMetrics(scope) {
908
- const parts = [];
909
- if (scope.changedFiles !== null) {
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
- }