@straiffi/archon 1.8.23 → 1.10.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 (221) hide show
  1. package/dist/client/assets/{ActivityView-BaIZ8eHC.js → ActivityView-C5OrI0uK.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-BABPYwJP.js +1 -0
  3. package/dist/client/assets/VerificationDialog-LN87YY0r.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Dl5UF67P.js → architectureDiagram-3BPJPVTR-DBXhcjPI.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DCcERfrZ.js → blockDiagram-GPEHLZMM-c3auwNH7.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BGhbASFm.js → c4Diagram-AAUBKEIU-DH2YznEQ.js} +1 -1
  7. package/dist/client/assets/channel-CYsnLOcX.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-C1ARozTV.js → chunk-2J33WTMH-D0_MoyXZ.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BEovLizF.js → chunk-3OPIFGDE-DLvSp1DN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-C5MmlLa_.js → chunk-5ZQYHXKU-B4sX8Htk.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-8y8jBk4n.js → chunk-727SXJPM-CkdnShG1.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-q-mByAd3.js → chunk-AQP2D5EJ-BdPfgkcs.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-BOx2mLSz.js → chunk-CSCIHK7Q-CK-DpVF6.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-zEwQFq7q.js → chunk-KSCS5N6A-DSbTWVl2.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-C80z6rY_.js → chunk-L5ZTLDWV-DuQ4EPLl.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-BLaEsGPd.js → chunk-LZXEDZCA-CtpoXWMY.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-x26BZ95L.js → chunk-ND2GUHAM-CDfNOP6o.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-D2WAxr1p.js → chunk-NZK2D7GU-s6ReY4ev.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DDPCrzKh.js → chunk-O5CBEL6O-DxqYGKhS.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CE-JcvIc.js → chunk-WU5MYG2G-BvrhxyV8.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-CElk31Q4.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-lnAahwmt.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-Bf7wpUk_.js → dagre-BM42HDAG-DEK_rftr.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-BQoKmc3B.js → diagram-2AECGRRQ-CpobcZnv.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-D2jarPLn.js → diagram-5GNKFQAL-zL1D1L_8.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-Bcbbjvsl.js → diagram-KO2AKTUF-gyW-Vc7c.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-t862RRBQ.js → diagram-LMA3HP47-DDEYc8sh.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CacbMcO0.js → diagram-OG6HWLK6-BtUrfJnE.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DRQ79baI.js → erDiagram-TEJ5UH35-qGrmpmmk.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-BfW4KEdc.js → flowDiagram-I6XJVG4X-DO1Nm1FA.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BPIl0TAj.js → ganttDiagram-6RSMTGT7-DGuMVCAc.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CRIusBMV.js → gitGraphDiagram-PVQCEYII-CehPAc8-.js} +1 -1
  33. package/dist/client/assets/index-1CokwSFm.css +2 -0
  34. package/dist/client/assets/index-DtefHOHC.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-DTKzK8EJ.js → infoDiagram-5YYISTIA-BkRaMDId.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DVIHZLvL.js → ishikawaDiagram-YF4QCWOH-B45ubODJ.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-CQBZ4tCx.js → journeyDiagram-JHISSGLW-BLxrlitK.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-D7naegR0.js → kanban-definition-UN3LZRKU-DmyeJDyy.js} +1 -1
  39. package/dist/client/assets/{line-CizZlaeV.js → line-BhwjsX6t.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-NrDqgpZa.js → mermaid-parser.core-CPvivy_u.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CwVO3oX0.js → mermaid.core-Dr0kld-g.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BWxy3Aa_.js → mindmap-definition-RKZ34NQL-dfudPzBR.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-B3gXpmBn.js → pieDiagram-4H26LBE5-BhsIQVNX.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dpp4g8Al.js → quadrantDiagram-W4KKPZXB-CAKxScLD.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-lFKXnxml.js → requirementDiagram-4Y6WPE33-Br6jIbke.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BBdtwqrS.js → sankeyDiagram-5OEKKPKP-BqujNgnY.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-ANpVZzqm.js → sequenceDiagram-3UESZ5HK-Dr_VM_ei.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-D58ZxmKY.js → stateDiagram-AJRCARHV-CDbSbbTW.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-p-kfFJi7.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-B6FK8IQC.js → timeline-definition-PNZ67QCA-CnuLuWDs.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-lFWtZEEY.js → vennDiagram-CIIHVFJN-CVAhxdBZ.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DteAVp4l.js → wardleyDiagram-YWT4CUSO-Dmczey27.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CNcDlRLN.js → xychartDiagram-2RQKCTM6-Bl6JGLCU.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +225 -12
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +88 -27
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +368 -143
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentProvider.js.map +1 -1
  62. package/dist/server/lib/approvalReconciliation.js +14 -2
  63. package/dist/server/lib/approvalReconciliation.js.map +1 -1
  64. package/dist/server/lib/buildExecutionEnvironment.js +255 -58
  65. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  66. package/dist/server/lib/chatOperations.js +125 -3
  67. package/dist/server/lib/chatOperations.js.map +1 -1
  68. package/dist/server/lib/chatPostBuildRuns.js +12 -6
  69. package/dist/server/lib/chatPostBuildRuns.js.map +1 -1
  70. package/dist/server/lib/chatTargets.js +2 -1
  71. package/dist/server/lib/chatTargets.js.map +1 -1
  72. package/dist/server/lib/checkDefinitionCwd.js +34 -0
  73. package/dist/server/lib/checkDefinitionCwd.js.map +1 -0
  74. package/dist/server/lib/checkExecutionEnvironment.js +95 -0
  75. package/dist/server/lib/checkExecutionEnvironment.js.map +1 -0
  76. package/dist/server/lib/checkPolicyDefinitionIds.js +41 -0
  77. package/dist/server/lib/checkPolicyDefinitionIds.js.map +1 -0
  78. package/dist/server/lib/checkRepairDispatch.js +2 -0
  79. package/dist/server/lib/checkRepairDispatch.js.map +1 -1
  80. package/dist/server/lib/checkRunner.js +371 -175
  81. package/dist/server/lib/checkRunner.js.map +1 -1
  82. package/dist/server/lib/checkRuns.js +99 -8
  83. package/dist/server/lib/checkRuns.js.map +1 -1
  84. package/dist/server/lib/claudeAgentProvider.js +3 -6
  85. package/dist/server/lib/claudeAgentProvider.js.map +1 -1
  86. package/dist/server/lib/claudeStdin.js +27 -0
  87. package/dist/server/lib/claudeStdin.js.map +1 -0
  88. package/dist/server/lib/claudeValidationOutput.js +4 -4
  89. package/dist/server/lib/claudeValidationOutput.js.map +1 -1
  90. package/dist/server/lib/completedAssistantOutput.js +38 -0
  91. package/dist/server/lib/completedAssistantOutput.js.map +1 -0
  92. package/dist/server/lib/consequentialActionAuthority.js +99 -8
  93. package/dist/server/lib/consequentialActionAuthority.js.map +1 -1
  94. package/dist/server/lib/consequentialActions.js +42 -2
  95. package/dist/server/lib/consequentialActions.js.map +1 -1
  96. package/dist/server/lib/dockerSandboxCredentials.js +10 -3
  97. package/dist/server/lib/dockerSandboxCredentials.js.map +1 -1
  98. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +9 -8
  99. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  100. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +197 -81
  101. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  102. package/dist/server/lib/dockerSandboxExecutionProvider.js +22 -14
  103. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  104. package/dist/server/lib/dockerSandboxNetworkPolicy.js +23 -14
  105. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -1
  106. package/dist/server/lib/dockerSandboxReleaseProfiles.js +91 -0
  107. package/dist/server/lib/dockerSandboxReleaseProfiles.js.map +1 -0
  108. package/dist/server/lib/dockerSandboxRuntimeController.js +16 -1
  109. package/dist/server/lib/dockerSandboxRuntimeController.js.map +1 -1
  110. package/dist/server/lib/executionCredentialCapabilities.js +9 -6
  111. package/dist/server/lib/executionCredentialCapabilities.js.map +1 -1
  112. package/dist/server/lib/executionEnvironment.js.map +1 -1
  113. package/dist/server/lib/executionEnvironmentLifecycle.js +7 -0
  114. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  115. package/dist/server/lib/executionEnvironmentOperations.js +241 -0
  116. package/dist/server/lib/executionEnvironmentOperations.js.map +1 -0
  117. package/dist/server/lib/executionEnvironmentReconciliation.js +98 -3
  118. package/dist/server/lib/executionEnvironmentReconciliation.js.map +1 -1
  119. package/dist/server/lib/executionEnvironmentRoutes.js +66 -27
  120. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  121. package/dist/server/lib/executionEnvironmentValidation.js +21 -12
  122. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  123. package/dist/server/lib/executionEnvironmentValidationOperations.js +58 -31
  124. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  125. package/dist/server/lib/executionEnvironments.js +187 -9
  126. package/dist/server/lib/executionEnvironments.js.map +1 -1
  127. package/dist/server/lib/executionProfileCheckPolicy.js +14 -0
  128. package/dist/server/lib/executionProfileCheckPolicy.js.map +1 -0
  129. package/dist/server/lib/executionProfileRoutes.js +9 -1
  130. package/dist/server/lib/executionProfileRoutes.js.map +1 -1
  131. package/dist/server/lib/executionProfileStore.js +5 -3
  132. package/dist/server/lib/executionProfileStore.js.map +1 -1
  133. package/dist/server/lib/executionProfiles.js +97 -15
  134. package/dist/server/lib/executionProfiles.js.map +1 -1
  135. package/dist/server/lib/executionReconciliation.js +43 -6
  136. package/dist/server/lib/executionReconciliation.js.map +1 -1
  137. package/dist/server/lib/executionRunAuthority.js +145 -24
  138. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  139. package/dist/server/lib/executionTargets.js +30 -4
  140. package/dist/server/lib/executionTargets.js.map +1 -1
  141. package/dist/server/lib/executionWorkloads.js +75 -0
  142. package/dist/server/lib/executionWorkloads.js.map +1 -0
  143. package/dist/server/lib/git.js +10 -1
  144. package/dist/server/lib/git.js.map +1 -1
  145. package/dist/server/lib/opencodeAgentProvider.js +3 -1
  146. package/dist/server/lib/opencodeAgentProvider.js.map +1 -1
  147. package/dist/server/lib/opencodeSessionOperations.js +5 -3
  148. package/dist/server/lib/opencodeSessionOperations.js.map +1 -1
  149. package/dist/server/lib/planQuestions.js +2 -1
  150. package/dist/server/lib/planQuestions.js.map +1 -1
  151. package/dist/server/lib/postBuildCheckRuns.js +88 -7
  152. package/dist/server/lib/postBuildCheckRuns.js.map +1 -1
  153. package/dist/server/lib/preview.js +47 -6
  154. package/dist/server/lib/preview.js.map +1 -1
  155. package/dist/server/lib/previewProxy.js +11 -3
  156. package/dist/server/lib/previewProxy.js.map +1 -1
  157. package/dist/server/lib/productionCheckApprovals.js +97 -35
  158. package/dist/server/lib/productionCheckApprovals.js.map +1 -1
  159. package/dist/server/lib/projectMemorySuggestions.js +29 -6
  160. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  161. package/dist/server/lib/requiredCheckPolicy.js +3 -40
  162. package/dist/server/lib/requiredCheckPolicy.js.map +1 -1
  163. package/dist/server/lib/reviewAgentWatchdog.js +75 -0
  164. package/dist/server/lib/reviewAgentWatchdog.js.map +1 -0
  165. package/dist/server/lib/reviewFindingReconciliation.js +176 -0
  166. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -0
  167. package/dist/server/lib/reviewFindings.js +33 -11
  168. package/dist/server/lib/reviewFindings.js.map +1 -1
  169. package/dist/server/lib/run.js +370 -80
  170. package/dist/server/lib/run.js.map +1 -1
  171. package/dist/server/lib/serviceRuns.js +196 -0
  172. package/dist/server/lib/serviceRuns.js.map +1 -0
  173. package/dist/server/lib/sessionTargets.js +34 -0
  174. package/dist/server/lib/sessionTargets.js.map +1 -1
  175. package/dist/server/lib/shell.js +20 -0
  176. package/dist/server/lib/shell.js.map +1 -1
  177. package/dist/server/lib/specArtifact.js +36 -15
  178. package/dist/server/lib/specArtifact.js.map +1 -1
  179. package/dist/server/lib/specBundleGroups.js +4 -4
  180. package/dist/server/lib/specBundleGroups.js.map +1 -1
  181. package/dist/server/lib/specOperations.js +2 -0
  182. package/dist/server/lib/specOperations.js.map +1 -1
  183. package/dist/server/lib/specReviewOperations.js +2 -0
  184. package/dist/server/lib/specReviewOperations.js.map +1 -1
  185. package/dist/server/lib/specTicketMaterialization.js +37 -24
  186. package/dist/server/lib/specTicketMaterialization.js.map +1 -1
  187. package/dist/server/lib/ticketFollowUpOperations.js +28 -7
  188. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  189. package/dist/server/lib/ticketRunOperations.js +1 -0
  190. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  191. package/dist/server/lib/types.js +1 -0
  192. package/dist/server/lib/types.js.map +1 -1
  193. package/dist/server/lib/workloadExecutionEnvironment.js +2 -0
  194. package/dist/server/lib/workloadExecutionEnvironment.js.map +1 -0
  195. package/dist/server/lib/workspaceIdentity.js +30 -6
  196. package/dist/server/lib/workspaceIdentity.js.map +1 -1
  197. package/dist/server/workers/build.js +150 -9
  198. package/dist/server/workers/build.js.map +1 -1
  199. package/dist/server/workers/chat.js +142 -15
  200. package/dist/server/workers/chat.js.map +1 -1
  201. package/dist/server/workers/followUp.js +97 -28
  202. package/dist/server/workers/followUp.js.map +1 -1
  203. package/dist/server/workers/plan.js +295 -48
  204. package/dist/server/workers/plan.js.map +1 -1
  205. package/dist/server/workers/planFollowUp.js +77 -7
  206. package/dist/server/workers/planFollowUp.js.map +1 -1
  207. package/dist/server/workers/review.js +149 -38
  208. package/dist/server/workers/review.js.map +1 -1
  209. package/dist/server/workers/specReview.js +43 -5
  210. package/dist/server/workers/specReview.js.map +1 -1
  211. package/dist/server/workers/specSession.js +156 -31
  212. package/dist/server/workers/specSession.js.map +1 -1
  213. package/package.json +1 -1
  214. package/dist/client/assets/GitHubActionsDialog-BFCqikDY.js +0 -1
  215. package/dist/client/assets/VerificationDialog-B1_6HMdt.js +0 -4
  216. package/dist/client/assets/channel-BfskvH2B.js +0 -1
  217. package/dist/client/assets/classDiagram-4FO5ZUOK-D4HPfxL3.js +0 -1
  218. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DQQ_WqAU.js +0 -1
  219. package/dist/client/assets/index-CZ1z72fo.js +0 -276
  220. package/dist/client/assets/index-SMj5ZmMs.css +0 -2
  221. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cv8cmDfz.js +0 -1
@@ -1,22 +1,22 @@
1
1
  import { randomUUID } from 'crypto';
2
- import { spawn } from 'child_process';
3
2
  import db from '../db.js';
4
- import { existsSync, lstatSync, realpathSync } from 'fs';
5
- import { basename, relative, resolve } from 'path';
3
+ import { existsSync, lstatSync } from 'fs';
6
4
  import { StringDecoder } from 'string_decoder';
7
- import { findActiveCheckBatchForTarget, findActiveCheckAutoFixForTarget, canAutoFixStartCheckBatch, getCheckBatchDetails, getCheckAttemptDetails, inspectCheckBatchAggregation, listCheckAttemptDetails, rerunCheckBatch, setCheckBatchStopRequested, } from './checkRuns.js';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import { findActiveCheckBatchForTarget, findActiveCheckAutoFixForTarget, canAutoFixStartCheckBatch, bindCheckBatchEnvironment, captureCheckAttemptPlacement, getCheckBatchDetails, getCheckAttemptDetails, inspectCheckBatchAggregation, listCheckAttemptDetails, rerunCheckBatch, setCheckBatchStopRequested, } from './checkRuns.js';
8
7
  import { getCheckDefinitionFingerprint } from './checkDefinitions.js';
9
8
  import { captureCheckWorkspaceIdentity } from './checkWorkspaceIdentity.js';
10
9
  import { getProjectById, getProjectCheckDefinitions } from './projects.js';
11
10
  import { aggregateCheckBatch, completeCheckAttempt, startCheckAttempt, withExecutionRunTransaction, } from './executionRuns.js';
12
- import { resolveCommandWorkspacePath } from './testCommandRunner.js';
13
- import { createWorkspaceProcessEnvironment } from './workspaceProcessEnvironment.js';
14
- import { createGovernedHostRuntimeEnvironment } from './workspaceProcessEnvironment.js';
11
+ import { resolveDefinitionCwd } from './checkDefinitionCwd.js';
12
+ import { getHostShellArguments, resolveShell } from './shell.js';
15
13
  import { cancelApprovalRequest, consumeApprovalRequest, createApprovalRequestInTransaction, emitApprovalRequestCreated, getApprovalRequest, invalidateApprovalRequest, registerApprovalRequestTerminalHandler, } from './approvalRequests.js';
16
14
  import { fingerprintConsequentialAction } from './consequentialActions.js';
17
15
  import { buildProductionCheckAction, PRODUCTION_CHECK_APPROVAL_TTL_MS } from './productionCheckApprovals.js';
18
16
  import { appendCheckAttemptEvidence, appendCheckDiagnostics, completeCheckDiagnosticParser, initializeCheckDiagnosticParser, } from './checkDiagnostics.js';
19
17
  import { createCheckDiagnosticParserSession } from './checkDiagnosticParsers.js';
18
+ import { resolveCheckBatchExecutionEnvironment } from './checkExecutionEnvironment.js';
19
+ import { createExecutionProfileSelectionMetadata } from './executionRunAuthority.js';
20
20
  const MAX_CONCURRENT_CHECK_ATTEMPTS = 2;
21
21
  const OUTPUT_FLUSH_BYTES = 32 * 1024;
22
22
  const OUTPUT_FLUSH_DELAY_MS = 25;
@@ -42,6 +42,8 @@ const invalidatedBatches = new Set();
42
42
  const stoppedBatches = new Set();
43
43
  const forcedAttemptOutcomes = new Map();
44
44
  const batchWaiters = new Map();
45
+ const batchExecutions = new Map();
46
+ const batchReleasePromises = new Map();
45
47
  let admittedAttempts = 0;
46
48
  let acceptingAdmissions = true;
47
49
  let inFlightAdmissions = 0;
@@ -76,63 +78,13 @@ export const closeCheckRunnerAdmissions = () => {
76
78
  export const openCheckRunnerAdmissions = () => {
77
79
  acceptingAdmissions = true;
78
80
  };
79
- const resolveShell = () => process.env.SHELL?.trim() || '/bin/zsh';
80
81
  export const getCheckShellArguments = (shell, command, governedProductionEnvironment = false) => {
81
- if (!governedProductionEnvironment) {
82
- return ['-i', '-c', command];
83
- }
84
- const shellName = basename(shell);
85
- if (shellName === 'zsh') {
86
- return ['-f', '-c', command];
87
- }
88
- if (shellName === 'bash') {
89
- return ['--noprofile', '--norc', '-c', command];
90
- }
91
- if (shellName === 'fish') {
92
- return ['--no-config', '-c', command];
93
- }
94
- return ['-c', command];
95
- };
96
- const createShellEnvironment = (cwd, definition, governedProductionEnvironment = false) => {
97
- const env = definition.consequence === 'production' && governedProductionEnvironment
98
- ? createGovernedHostRuntimeEnvironment(cwd).variables
99
- : createWorkspaceProcessEnvironment(cwd);
100
- delete env.PORT;
101
- return env;
82
+ return getHostShellArguments(shell, command, !governedProductionEnvironment);
102
83
  };
103
84
  const identitiesMatch = (left, right) => (left.state === 'available'
104
85
  && right.state === 'available'
105
86
  && left.fingerprint === right.fingerprint);
106
- const isPathInside = (root, candidate) => {
107
- const pathFromRoot = relative(root, candidate).replaceAll('\\', '/');
108
- return pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !pathFromRoot.includes('/../'));
109
- };
110
- export const resolveDefinitionCwd = (workspaceCwd, definition) => {
111
- const workspacePath = resolveCommandWorkspacePath(definition);
112
- if (!workspacePath) {
113
- return workspaceCwd;
114
- }
115
- const resolvedPath = resolve(workspaceCwd, workspacePath);
116
- if (!isPathInside(workspaceCwd, resolvedPath)) {
117
- throw new Error(`Check definition ${definition.id} working directory must stay inside the target workspace`);
118
- }
119
- let realWorkspacePath;
120
- let realResolvedPath;
121
- try {
122
- realWorkspacePath = realpathSync(workspaceCwd);
123
- realResolvedPath = realpathSync(resolvedPath);
124
- }
125
- catch {
126
- throw new Error(`Check definition ${definition.id} working directory is unavailable`);
127
- }
128
- if (!isPathInside(realWorkspacePath, realResolvedPath)) {
129
- throw new Error(`Check definition ${definition.id} working directory must stay inside the target workspace`);
130
- }
131
- if (!lstatSync(realResolvedPath).isDirectory()) {
132
- throw new Error(`Check definition ${definition.id} working directory is unavailable`);
133
- }
134
- return definition.cwd?.trim() ? realResolvedPath : workspaceCwd;
135
- };
87
+ export { resolveDefinitionCwd } from './checkDefinitionCwd.js';
136
88
  const validateDefinitions = (selectedDefinitions, availableDefinitions) => {
137
89
  if (selectedDefinitions.length === 0) {
138
90
  throw new Error('At least one check definition must be selected');
@@ -298,29 +250,93 @@ const appendProcessOutput = (record, text) => {
298
250
  record.flushTimer = setTimeout(() => requestOutputFlush(record), OUTPUT_FLUSH_DELAY_MS);
299
251
  }
300
252
  };
301
- const signalProcessGroup = (child, signal) => {
302
- if (child.pid && process.platform !== 'win32') {
303
- try {
304
- process.kill(-child.pid, signal);
305
- return;
306
- }
307
- catch (error) {
308
- if (error.code === 'ESRCH') {
309
- return;
253
+ const terminateProcess = (record) => {
254
+ if (record.batchExecution.environment.provider === 'docker_sandbox' && record.terminalCause !== 'user_stop') {
255
+ for (const sibling of processes.values()) {
256
+ if (sibling.batchRunId === record.batchRunId && sibling.attemptRunId !== record.attemptRunId) {
257
+ sibling.terminalCause ??= 'environment_loss';
310
258
  }
311
259
  }
260
+ void captureCheckWorkspaceIdentity(record.cwd).then(identity => (failQueuedBatch(record.batchRunId, 'environment_loss', identity))).catch(error => {
261
+ console.error('Unable to terminalize queued attempts after sandbox loss', {
262
+ batch_run_id: record.batchRunId,
263
+ error: error instanceof Error ? error.message : String(error),
264
+ });
265
+ });
266
+ }
267
+ void record.handle.interrupt('SIGTERM');
268
+ if (!record.killTimer) {
269
+ record.killTimer = setTimeout(() => void record.handle.interrupt('SIGKILL'), STOP_GRACE_MS);
312
270
  }
271
+ };
272
+ const releaseBatchExecution = (batchRunId) => {
273
+ const existing = batchReleasePromises.get(batchRunId);
274
+ if (existing) {
275
+ return existing;
276
+ }
277
+ const execution = batchExecutions.get(batchRunId);
278
+ if (!execution) {
279
+ return Promise.resolve();
280
+ }
281
+ const release = execution.execution.release().finally(() => {
282
+ batchExecutions.delete(batchRunId);
283
+ batchReleasePromises.delete(batchRunId);
284
+ });
285
+ batchReleasePromises.set(batchRunId, release);
286
+ return release;
287
+ };
288
+ const restoreBatchExecution = async (input) => {
289
+ if (!input.batch.batch_environment || !input.batch.runtime_compatibility || !input.attempt.placement) {
290
+ throw new Error('Check batch execution evidence is incomplete');
291
+ }
292
+ const project = getProjectById(input.projectId);
293
+ if (!project) {
294
+ throw new Error('The check batch project is unavailable');
295
+ }
296
+ const execution = await resolveCheckBatchExecutionEnvironment({
297
+ project,
298
+ batchRunId: input.batch.execution_run_id,
299
+ targetKind: input.batch.target_kind,
300
+ targetBundleId: input.batch.target_bundle_id,
301
+ cwd: input.workspaceCwd,
302
+ pinnedEnvironment: input.batch.batch_environment,
303
+ });
304
+ const restoredPlacement = execution.createAttemptPlacement(input.attempt.execution_run_id);
305
+ if (!isDeepStrictEqual(execution.environment, input.batch.batch_environment)
306
+ || !isDeepStrictEqual(execution.compatibility, input.batch.runtime_compatibility)
307
+ || !isDeepStrictEqual(restoredPlacement, input.attempt.placement)) {
308
+ await execution.execution.release();
309
+ throw new Error('The persisted check batch execution environment could not be restored exactly');
310
+ }
311
+ batchExecutions.set(input.batch.execution_run_id, execution);
312
+ return execution;
313
+ };
314
+ const rollbackUnadmittedBatch = async (input) => {
315
+ await releaseBatchExecution(input.batchRunId);
313
316
  try {
314
- child.kill(signal);
317
+ db.transaction(() => {
318
+ db.prepare('DELETE FROM execution_runs WHERE parent_run_id = ? AND project_id = ?').run(input.batchRunId, input.projectId);
319
+ db.prepare('DELETE FROM execution_runs WHERE id = ? AND project_id = ?').run(input.batchRunId, input.projectId);
320
+ })();
321
+ return;
315
322
  }
316
323
  catch {
317
- // The process may have exited between the liveness check and the signal.
318
- }
319
- };
320
- const terminateProcess = (record) => {
321
- signalProcessGroup(record.child, 'SIGTERM');
322
- if (!record.killTimer) {
323
- record.killTimer = setTimeout(() => signalProcessGroup(record.child, 'SIGKILL'), STOP_GRACE_MS);
324
+ const message = input.error instanceof Error ? input.error.message : String(input.error);
325
+ for (const attemptRunId of input.attemptRunIds) {
326
+ const attempt = getCheckAttemptDetails(input.projectId, attemptRunId);
327
+ if (!attempt?.outcome) {
328
+ completeCheckAttempt({
329
+ projectId: input.projectId,
330
+ runId: attemptRunId,
331
+ outcome: 'infrastructure_failure',
332
+ finalWorkspaceIdentity: input.identity,
333
+ exitError: `Check batch environment admission failed: ${message}`,
334
+ }, input.emitter);
335
+ }
336
+ }
337
+ aggregateCheckBatch(input.projectId, input.batchRunId, input.emitter, {
338
+ finalWorkspaceIdentity: input.identity,
339
+ });
324
340
  }
325
341
  };
326
342
  const waitForProcessFinalization = (record) => {
@@ -364,6 +380,7 @@ const aggregateBatchAfterAttempt = async (entry) => {
364
380
  aggregateCheckBatch(entry.projectId, entry.batchRunId, entry.emitter, {
365
381
  finalWorkspaceIdentity: batchFinalIdentity,
366
382
  });
383
+ await releaseBatchExecution(entry.batchRunId);
367
384
  };
368
385
  const completeAttempt = async (entry, outcome, finalIdentity, options = {}) => {
369
386
  completeCheckAttempt({
@@ -390,6 +407,7 @@ const finalizeProcess = (record, code, signal, error) => {
390
407
  clearTimeout(record.killTimer);
391
408
  }
392
409
  record.finalizationPromise = (async () => {
410
+ await record.placementVerificationPromise;
393
411
  appendProcessOutput(record, record.stdoutDecoder.end());
394
412
  appendProcessOutput(record, record.stderrDecoder.end());
395
413
  try {
@@ -455,7 +473,7 @@ const failQueuedBatch = async (batchRunId, outcome, identity) => {
455
473
  await completeAttempt(entry, outcome, identity, {
456
474
  exitError: outcome === 'workspace_changed_before_start'
457
475
  ? 'Workspace identity changed before this check could start'
458
- : null,
476
+ : outcome === 'environment_loss' ? 'The batch-owned sandbox was lost while sibling attempts were active' : null,
459
477
  });
460
478
  }
461
479
  };
@@ -486,7 +504,7 @@ const runQueuedAttempt = async (entry) => {
486
504
  if (entry.approvalRequestId) {
487
505
  const request = getApprovalRequest(entry.projectId, entry.approvalRequestId, { emitter: entry.emitter });
488
506
  const batch = getCheckBatchDetails(entry.projectId, entry.batchRunId);
489
- if (!request || request.status !== 'consumed' || !batch) {
507
+ if (!request || request.status !== 'consumed' || request.action.kind !== 'custom_check' || !batch) {
490
508
  throw new Error('Production check approval is no longer consumable');
491
509
  }
492
510
  const currentDefinitions = getProjectCheckDefinitions(getProjectById(entry.projectId));
@@ -502,6 +520,9 @@ const runQueuedAttempt = async (entry) => {
502
520
  batch,
503
521
  workspaceIdentity: identity,
504
522
  expiresAt: request.expires_at,
523
+ launcher: request.action.target.launcher ?? 'host_shell',
524
+ batchEnvironment: request.action.target.batch_environment ?? null,
525
+ attemptPlacement: entry.expectedPlacement,
505
526
  });
506
527
  if (fingerprintConsequentialAction(current.action) !== request.action_fingerprint) {
507
528
  throw new Error('Production check action changed before spawn');
@@ -513,15 +534,27 @@ const runQueuedAttempt = async (entry) => {
513
534
  runId: entry.attemptRunId,
514
535
  }, entry.emitter);
515
536
  await new Promise((resolveAttempt, rejectAttempt) => {
537
+ let handle;
516
538
  let child;
517
539
  try {
518
540
  const shell = resolveShell();
519
- child = spawn(shell, getCheckShellArguments(shell, entry.definition.command, entry.governedProductionEnvironment), {
541
+ const runtimeEnvironment = entry.batchExecution.execution.runtimeEnvironment;
542
+ const governedHostEnvironment = entry.batchExecution.execution.provider.kind === 'host'
543
+ && runtimeEnvironment.kind === 'host'
544
+ && runtimeEnvironment.inheritance === 'governed';
545
+ handle = entry.batchExecution.execution.provider.execute({
546
+ phase: 'agent',
547
+ target: entry.batchExecution.target,
548
+ command: shell,
549
+ args: getCheckShellArguments(shell, entry.definition.command, entry.governedProductionEnvironment || governedHostEnvironment),
520
550
  cwd: spawnCwd,
551
+ launcherEnvironment: entry.batchExecution.execution.launcherEnvironment,
552
+ runtimeEnvironment: entry.batchExecution.execution.runtimeEnvironment,
521
553
  detached: process.platform !== 'win32',
522
- env: createShellEnvironment(entry.cwd, entry.definition, entry.governedProductionEnvironment),
523
554
  stdio: ['ignore', 'pipe', 'pipe'],
555
+ placementExecutionRunId: entry.attemptRunId,
524
556
  });
557
+ child = handle.child;
525
558
  }
526
559
  catch (error) {
527
560
  const spawnError = error instanceof Error ? error : new Error(String(error));
@@ -533,6 +566,7 @@ const runQueuedAttempt = async (entry) => {
533
566
  const record = {
534
567
  ...entry,
535
568
  child,
569
+ handle,
536
570
  stdoutDecoder: new StringDecoder('utf8'),
537
571
  stderrDecoder: new StringDecoder('utf8'),
538
572
  pendingOutput: '',
@@ -545,6 +579,7 @@ const runQueuedAttempt = async (entry) => {
545
579
  spawned: false,
546
580
  finalized: false,
547
581
  finalizationPromise: null,
582
+ placementVerificationPromise: Promise.resolve(),
548
583
  diagnosticParser: createCheckDiagnosticParserSession({
549
584
  definition: entry.definition,
550
585
  workspaceCwd: entry.cwd,
@@ -554,6 +589,22 @@ const runQueuedAttempt = async (entry) => {
554
589
  persistenceFailure: null,
555
590
  };
556
591
  processes.set(entry.attemptRunId, record);
592
+ record.placementVerificationPromise = Promise.all([
593
+ handle.placementFailure,
594
+ handle.placementIdentity ?? Promise.resolve(null),
595
+ ]).then(([failure, placement]) => {
596
+ if (failure
597
+ || (handle.placementIdentity && !placement)
598
+ || (placement && !isDeepStrictEqual(placement, entry.expectedPlacement))) {
599
+ record.terminalCause ??= 'infrastructure_failure';
600
+ terminateProcess(record);
601
+ }
602
+ }).catch(() => {
603
+ if (!record.terminalCause) {
604
+ record.terminalCause ??= 'infrastructure_failure';
605
+ terminateProcess(record);
606
+ }
607
+ });
557
608
  child.once('spawn', () => {
558
609
  record.spawned = true;
559
610
  });
@@ -629,6 +680,8 @@ const enqueueCheckAttempts = (input) => {
629
680
  emitter: input.emitter,
630
681
  approvalRequestId: attempt.approvalRequestId,
631
682
  governedProductionEnvironment: attempt.governedProductionEnvironment,
683
+ batchExecution: input.batchExecution,
684
+ expectedPlacement: attempt.expectedPlacement,
632
685
  })));
633
686
  pumpScheduler();
634
687
  };
@@ -670,7 +723,11 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
670
723
  if (initialIdentity.state !== 'available') {
671
724
  throw new Error(`Check workspace identity is unavailable: ${initialIdentity.message}`);
672
725
  }
673
- const currentDefinitions = getProjectCheckDefinitions(getProjectById(input.projectId));
726
+ const project = getProjectById(input.projectId);
727
+ if (!project) {
728
+ throw new Error(`Project ${input.projectId} was not found`);
729
+ }
730
+ const currentDefinitions = getProjectCheckDefinitions(project);
674
731
  validateDefinitions(input.selectedDefinitions, currentDefinitions);
675
732
  for (const definition of input.selectedDefinitions) {
676
733
  resolveDefinitionCwd(input.cwd, definition);
@@ -685,86 +742,136 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
685
742
  const scopeLinks = targetBundleId
686
743
  ? [{ entityType: 'bundle', entityId: targetBundleId, relationship: 'context' }]
687
744
  : [];
688
- withExecutionRunTransaction(transaction => {
689
- const transactionConflict = findActiveCheckBatchForTarget(input.projectId, input.targetKind, targetBundleId);
690
- if (transactionConflict) {
691
- throw new Error(`Check batch ${transactionConflict} is already active for this target`);
692
- }
693
- const transactionAutoFixConflict = findActiveCheckAutoFixForTarget(input.projectId, input.targetKind, targetBundleId);
694
- if (transactionAutoFixConflict && transactionAutoFixConflict !== input.autoFixParentRunId) {
695
- throw new Error(`Check auto-fix ${transactionAutoFixConflict} is already active for this target`);
696
- }
697
- assertAutoFixAdmission();
698
- transaction.createExecutionRun({
699
- id: batchRunId,
700
- projectId: input.projectId,
701
- kind: 'check_batch',
702
- initiatedBy: input.automationParentRunId || input.autoFixParentRunId ? 'automation' : input.postBuildParentRunId ? 'system' : 'user',
703
- parentRunId: input.automationParentRunId ?? input.postBuildParentRunId ?? input.autoFixParentRunId ?? null,
704
- domainStatus: 'queued',
705
- metadata: input.automationBoundaryKind ? { automation_boundary_kind: input.automationBoundaryKind } : null,
706
- sourceReference: input.sourceReference ?? null,
707
- links: scopeLinks,
708
- eventType: 'check_batch_created',
709
- allowedActiveAutomationChildRunId: input.allowedActiveAutomationChildRunId,
710
- checkBatchDetails: {
711
- ...checkTarget,
712
- targetLabel: input.targetLabel,
713
- selectedDefinitions: input.selectedDefinitions,
714
- initialWorkspaceIdentity: initialIdentity,
715
- rerunOfBatchRunId: input.rerunOfBatchRunId,
716
- automationBoundaryPurpose,
717
- purpose: input.autoFixParentRunId ? 'auto_fix' : input.postBuildParentRunId ? 'post_build' : input.automationParentRunId ? 'automation_gate' : 'manual',
718
- causalSourceRunId: input.autoFixOriginBatchRunId ?? input.postBuildParentRunId ?? null,
719
- },
720
- });
721
- for (const attempt of attempts) {
745
+ let batchExecution;
746
+ let placements;
747
+ try {
748
+ withExecutionRunTransaction(transaction => {
749
+ const transactionConflict = findActiveCheckBatchForTarget(input.projectId, input.targetKind, targetBundleId);
750
+ if (transactionConflict) {
751
+ throw new Error(`Check batch ${transactionConflict} is already active for this target`);
752
+ }
753
+ const transactionAutoFixConflict = findActiveCheckAutoFixForTarget(input.projectId, input.targetKind, targetBundleId);
754
+ if (transactionAutoFixConflict && transactionAutoFixConflict !== input.autoFixParentRunId) {
755
+ throw new Error(`Check auto-fix ${transactionAutoFixConflict} is already active for this target`);
756
+ }
757
+ assertAutoFixAdmission();
722
758
  transaction.createExecutionRun({
723
- id: attempt.runId,
759
+ id: batchRunId,
724
760
  projectId: input.projectId,
725
- kind: 'check_attempt',
726
- parentRunId: batchRunId,
727
- sourceReference: { kind: 'project', id: input.projectId },
728
- links: scopeLinks,
761
+ kind: 'check_batch',
762
+ initiatedBy: input.automationParentRunId || input.autoFixParentRunId ? 'automation' : input.postBuildParentRunId ? 'system' : 'user',
763
+ parentRunId: input.automationParentRunId ?? input.postBuildParentRunId ?? input.autoFixParentRunId ?? null,
729
764
  domainStatus: 'queued',
730
- eventType: 'check_attempt_created',
731
- checkAttemptDetails: {
732
- batchRunId,
733
- definition: attempt.definition,
765
+ metadata: {
766
+ ...createExecutionProfileSelectionMetadata(input.projectId),
767
+ ...(input.automationBoundaryKind ? { automation_boundary_kind: input.automationBoundaryKind } : {}),
768
+ },
769
+ sourceReference: input.sourceReference ?? null,
770
+ links: scopeLinks,
771
+ eventType: 'check_batch_created',
772
+ allowedActiveAutomationChildRunId: input.allowedActiveAutomationChildRunId,
773
+ checkBatchDetails: {
774
+ ...checkTarget,
775
+ targetLabel: input.targetLabel,
776
+ selectedDefinitions: input.selectedDefinitions,
734
777
  initialWorkspaceIdentity: initialIdentity,
735
- rerunOfAttemptRunId: input.rerunAttemptRunIds?.[attempt.definition.id] ?? null,
778
+ rerunOfBatchRunId: input.rerunOfBatchRunId,
779
+ automationBoundaryPurpose,
780
+ purpose: input.autoFixParentRunId ? 'auto_fix' : input.postBuildParentRunId ? 'post_build' : input.automationParentRunId ? 'automation_gate' : 'manual',
781
+ causalSourceRunId: input.autoFixOriginBatchRunId ?? input.postBuildParentRunId ?? null,
736
782
  },
737
783
  });
738
- }
739
- const batch = getCheckBatchDetails(input.projectId, batchRunId);
740
- for (const attempt of attempts.filter(candidate => candidate.definition.consequence === 'production')) {
741
- const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
742
- const approval = buildProductionCheckAction({
784
+ for (const attempt of attempts) {
785
+ transaction.createExecutionRun({
786
+ id: attempt.runId,
787
+ projectId: input.projectId,
788
+ kind: 'check_attempt',
789
+ parentRunId: batchRunId,
790
+ sourceReference: { kind: 'project', id: input.projectId },
791
+ links: scopeLinks,
792
+ domainStatus: 'queued',
793
+ eventType: 'check_attempt_created',
794
+ checkAttemptDetails: {
795
+ batchRunId,
796
+ definition: attempt.definition,
797
+ initialWorkspaceIdentity: initialIdentity,
798
+ rerunOfAttemptRunId: input.rerunAttemptRunIds?.[attempt.definition.id] ?? null,
799
+ },
800
+ });
801
+ }
802
+ input.onBatchCreated?.(batchRunId);
803
+ }, emitter);
804
+ batchExecution = await resolveCheckBatchExecutionEnvironment({
805
+ project,
806
+ batchRunId,
807
+ targetKind: input.targetKind,
808
+ targetBundleId,
809
+ cwd: input.cwd,
810
+ });
811
+ batchExecutions.set(batchRunId, batchExecution);
812
+ bindCheckBatchEnvironment({
813
+ projectId: input.projectId,
814
+ batchRunId,
815
+ compatibility: batchExecution.compatibility,
816
+ environment: batchExecution.environment,
817
+ });
818
+ placements = new Map(attempts.map(attempt => {
819
+ const placement = batchExecution.createAttemptPlacement(attempt.runId);
820
+ captureCheckAttemptPlacement({ projectId: input.projectId, attemptRunId: attempt.runId, placement });
821
+ return [attempt.runId, placement];
822
+ }));
823
+ withExecutionRunTransaction(transaction => {
824
+ const batch = getCheckBatchDetails(input.projectId, batchRunId);
825
+ for (const attempt of attempts.filter(candidate => candidate.definition.consequence === 'production')) {
826
+ const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
827
+ const placement = placements.get(attempt.runId);
828
+ const sandboxEnvironment = batchExecution.environment.provider === 'docker_sandbox'
829
+ ? {
830
+ id: batchExecution.environment.environment_id,
831
+ revision: batchExecution.environment.environment_revision,
832
+ validation_fingerprint: batchExecution.environment.environment_validation_fingerprint,
833
+ }
834
+ : null;
835
+ const approval = buildProductionCheckAction({
836
+ projectId: input.projectId,
837
+ workspaceCwd: input.cwd,
838
+ effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
839
+ definition: attempt.definition,
840
+ batch,
841
+ workspaceIdentity: initialIdentity,
842
+ expiresAt,
843
+ launcher: sandboxEnvironment ? 'docker_sandbox_exec' : 'host_shell',
844
+ batchEnvironment: sandboxEnvironment,
845
+ attemptPlacement: placement,
846
+ });
847
+ createdApprovals.push(createApprovalRequestInTransaction({
848
+ projectId: input.projectId,
849
+ executionRunId: attempt.runId,
850
+ action: approval.action,
851
+ authority: approval.authority,
852
+ expiresAt,
853
+ }, transaction));
854
+ }
855
+ transaction.transitionExecutionRun({
743
856
  projectId: input.projectId,
744
- workspaceCwd: input.cwd,
745
- effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
746
- definition: attempt.definition,
747
- batch,
748
- workspaceIdentity: initialIdentity,
749
- expiresAt,
857
+ runId: batchRunId,
858
+ status: 'running',
859
+ domainStatus: 'running',
860
+ eventType: 'check_batch_started',
750
861
  });
751
- createdApprovals.push(createApprovalRequestInTransaction({
752
- projectId: input.projectId,
753
- executionRunId: attempt.runId,
754
- action: approval.action,
755
- authority: approval.authority,
756
- expiresAt,
757
- }, transaction));
758
- }
759
- input.onBatchCreated?.(batchRunId);
760
- transaction.transitionExecutionRun({
862
+ }, emitter);
863
+ }
864
+ catch (error) {
865
+ await rollbackUnadmittedBatch({
761
866
  projectId: input.projectId,
762
- runId: batchRunId,
763
- status: 'running',
764
- domainStatus: 'running',
765
- eventType: 'check_batch_started',
867
+ batchRunId,
868
+ attemptRunIds: attempts.map(attempt => attempt.runId),
869
+ identity: initialIdentity,
870
+ error,
871
+ emitter,
766
872
  });
767
- }, emitter);
873
+ throw error;
874
+ }
768
875
  for (const approval of createdApprovals) {
769
876
  emitApprovalRequestCreated(emitter, approval);
770
877
  }
@@ -773,8 +880,12 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
773
880
  projectId: input.projectId,
774
881
  batchRunId,
775
882
  cwd: input.cwd,
776
- attempts: ordinaryAttempts,
883
+ attempts: ordinaryAttempts.map(attempt => ({
884
+ ...attempt,
885
+ expectedPlacement: placements.get(attempt.runId),
886
+ })),
777
887
  initialIdentity,
888
+ batchExecution,
778
889
  emitter,
779
890
  });
780
891
  return getCheckBatchDetails(input.projectId, batchRunId);
@@ -797,7 +908,11 @@ export const rerunCheckBatchAndSchedule = async (input, emitter) => withCheckAdm
797
908
  if (initialIdentity.state !== 'available') {
798
909
  throw new Error(`Check workspace identity is unavailable: ${initialIdentity.message}`);
799
910
  }
800
- const currentDefinitions = getProjectCheckDefinitions(getProjectById(input.projectId));
911
+ const project = getProjectById(input.projectId);
912
+ if (!project) {
913
+ throw new Error(`Project ${input.projectId} was not found`);
914
+ }
915
+ const currentDefinitions = getProjectCheckDefinitions(project);
801
916
  const currentById = new Map(currentDefinitions.map(definition => [definition.id, definition]));
802
917
  for (const definitionId of selectedDefinitionIds) {
803
918
  const definition = currentById.get(definitionId);
@@ -815,29 +930,76 @@ export const rerunCheckBatchAndSchedule = async (input, emitter) => withCheckAdm
815
930
  selection: input.selection,
816
931
  availableDefinitions: currentDefinitions,
817
932
  initialWorkspaceIdentity: initialIdentity,
933
+ profileSelectionMetadata: createExecutionProfileSelectionMetadata(input.projectId),
818
934
  });
819
- const batch = getCheckBatchDetails(input.projectId, result.batchRunId);
820
- for (const attempt of result.attempts.filter(candidate => candidate.definition.consequence === 'production')) {
821
- const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
822
- const approval = buildProductionCheckAction({
823
- projectId: input.projectId,
824
- workspaceCwd: input.cwd,
825
- effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
826
- definition: attempt.definition,
827
- batch,
828
- workspaceIdentity: initialIdentity,
829
- expiresAt,
830
- });
831
- createdApprovals.push(createApprovalRequestInTransaction({
832
- projectId: input.projectId,
833
- executionRunId: attempt.runId,
834
- action: approval.action,
835
- authority: approval.authority,
836
- expiresAt,
837
- }, transaction));
838
- }
839
935
  return result;
840
936
  }, emitter);
937
+ let batchExecution;
938
+ let placements;
939
+ try {
940
+ batchExecution = await resolveCheckBatchExecutionEnvironment({
941
+ project,
942
+ batchRunId: created.batchRunId,
943
+ targetKind: created.batch.target_kind,
944
+ targetBundleId: created.batch.target_bundle_id,
945
+ cwd: input.cwd,
946
+ });
947
+ batchExecutions.set(created.batchRunId, batchExecution);
948
+ bindCheckBatchEnvironment({
949
+ projectId: input.projectId,
950
+ batchRunId: created.batchRunId,
951
+ compatibility: batchExecution.compatibility,
952
+ environment: batchExecution.environment,
953
+ });
954
+ placements = new Map(created.attempts.map(attempt => {
955
+ const placement = batchExecution.createAttemptPlacement(attempt.runId);
956
+ captureCheckAttemptPlacement({ projectId: input.projectId, attemptRunId: attempt.runId, placement });
957
+ return [attempt.runId, placement];
958
+ }));
959
+ withExecutionRunTransaction(transaction => {
960
+ const batch = getCheckBatchDetails(input.projectId, created.batchRunId);
961
+ for (const attempt of created.attempts.filter(candidate => candidate.definition.consequence === 'production')) {
962
+ const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
963
+ const sandboxEnvironment = batchExecution.environment.provider === 'docker_sandbox'
964
+ ? {
965
+ id: batchExecution.environment.environment_id,
966
+ revision: batchExecution.environment.environment_revision,
967
+ validation_fingerprint: batchExecution.environment.environment_validation_fingerprint,
968
+ }
969
+ : null;
970
+ const approval = buildProductionCheckAction({
971
+ projectId: input.projectId,
972
+ workspaceCwd: input.cwd,
973
+ effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
974
+ definition: attempt.definition,
975
+ batch,
976
+ workspaceIdentity: initialIdentity,
977
+ expiresAt,
978
+ launcher: sandboxEnvironment ? 'docker_sandbox_exec' : 'host_shell',
979
+ batchEnvironment: sandboxEnvironment,
980
+ attemptPlacement: placements.get(attempt.runId),
981
+ });
982
+ createdApprovals.push(createApprovalRequestInTransaction({
983
+ projectId: input.projectId,
984
+ executionRunId: attempt.runId,
985
+ action: approval.action,
986
+ authority: approval.authority,
987
+ expiresAt,
988
+ }, transaction));
989
+ }
990
+ }, emitter);
991
+ }
992
+ catch (error) {
993
+ await rollbackUnadmittedBatch({
994
+ projectId: input.projectId,
995
+ batchRunId: created.batchRunId,
996
+ attemptRunIds: created.attempts.map(attempt => attempt.runId),
997
+ identity: initialIdentity,
998
+ error,
999
+ emitter,
1000
+ });
1001
+ throw error;
1002
+ }
841
1003
  for (const approval of createdApprovals) {
842
1004
  emitApprovalRequestCreated(emitter, approval);
843
1005
  }
@@ -846,12 +1008,16 @@ export const rerunCheckBatchAndSchedule = async (input, emitter) => withCheckAdm
846
1008
  projectId: input.projectId,
847
1009
  batchRunId: created.batchRunId,
848
1010
  cwd: input.cwd,
849
- attempts: ordinaryAttempts,
1011
+ attempts: ordinaryAttempts.map(attempt => ({
1012
+ ...attempt,
1013
+ expectedPlacement: placements.get(attempt.runId),
1014
+ })),
850
1015
  initialIdentity,
1016
+ batchExecution,
851
1017
  emitter,
852
1018
  });
853
1019
  return {
854
- batch: created.batch,
1020
+ batch: getCheckBatchDetails(input.projectId, created.batchRunId),
855
1021
  attempts: listCheckAttemptDetails(input.projectId, created.batchRunId),
856
1022
  };
857
1023
  });
@@ -927,7 +1093,20 @@ export const resumeProductionCheckApproval = async (projectId, requestId, emitte
927
1093
  invalidateApprovalRequest({ projectId, requestId, reason: 'check_batch_no_longer_active', emitter });
928
1094
  return false;
929
1095
  }
1096
+ if (!batch.batch_environment || !batch.runtime_compatibility || !attempt.placement) {
1097
+ invalidateApprovalRequest({ projectId, requestId, reason: 'check_batch_environment_unavailable', emitter });
1098
+ return false;
1099
+ }
1100
+ const attemptPlacement = attempt.placement;
930
1101
  const workspaceCwd = request.action.workspace_git.workspace_path;
1102
+ let batchExecution = batchExecutions.get(batch.execution_run_id);
1103
+ try {
1104
+ batchExecution ??= await restoreBatchExecution({ projectId, workspaceCwd, batch, attempt });
1105
+ }
1106
+ catch {
1107
+ invalidateApprovalRequest({ projectId, requestId, reason: 'check_batch_environment_unavailable', emitter });
1108
+ return false;
1109
+ }
931
1110
  let currentIdentity;
932
1111
  let currentDefinition;
933
1112
  try {
@@ -964,6 +1143,9 @@ export const resumeProductionCheckApproval = async (projectId, requestId, emitte
964
1143
  batch,
965
1144
  workspaceIdentity: currentIdentity,
966
1145
  expiresAt: request.expires_at,
1146
+ launcher: request.action.target.launcher ?? 'host_shell',
1147
+ batchEnvironment: request.action.target.batch_environment ?? null,
1148
+ attemptPlacement,
967
1149
  });
968
1150
  }
969
1151
  catch {
@@ -986,8 +1168,10 @@ export const resumeProductionCheckApproval = async (projectId, requestId, emitte
986
1168
  definition: currentDefinition,
987
1169
  approvalRequestId: requestId,
988
1170
  governedProductionEnvironment: request.authority.selected_profile.compatibility === 'governed',
1171
+ expectedPlacement: attemptPlacement,
989
1172
  }],
990
1173
  initialIdentity: attempt.initial_workspace_identity,
1174
+ batchExecution,
991
1175
  emitter,
992
1176
  });
993
1177
  return true;
@@ -1003,6 +1187,15 @@ export const finalizeProductionCheckApproval = (projectId, requestId, emitter) =
1003
1187
  }
1004
1188
  aggregateCheckBatch(projectId, attempt.batch_run_id, emitter);
1005
1189
  notifyBatchWaiters(projectId, attempt.batch_run_id);
1190
+ const batch = getCheckBatchDetails(projectId, attempt.batch_run_id);
1191
+ if (batch && ['completed', 'failed', 'cancelled', 'interrupted'].includes(batch.execution_status)) {
1192
+ void releaseBatchExecution(attempt.batch_run_id).catch(error => {
1193
+ console.error('Check batch environment release failed', {
1194
+ batch_run_id: attempt.batch_run_id,
1195
+ error: error instanceof Error ? error.message : String(error),
1196
+ });
1197
+ });
1198
+ }
1006
1199
  };
1007
1200
  registerApprovalRequestTerminalHandler((request, emitter) => {
1008
1201
  if (request.action_kind === 'custom_check') {
@@ -1083,6 +1276,9 @@ export const shutdownCheckRunner = async () => {
1083
1276
  }));
1084
1277
  }
1085
1278
  finally {
1279
+ await Promise.all([...batchIds]
1280
+ .filter(batchRunId => !approvalHeldBatchIds.has(batchRunId))
1281
+ .map(releaseBatchExecution));
1086
1282
  for (const batchRunId of batchIds) {
1087
1283
  for (const attempt of durableAttempts.filter(candidate => candidate.batch_run_id === batchRunId)) {
1088
1284
  forcedAttemptOutcomes.delete(attempt.attempt_run_id);