stageflow 0.23.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (511) hide show
  1. package/README.md +24 -5
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +94 -0
  5. package/dist/a2a/contracts.js +164 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +46 -0
  9. package/dist/a2a/registry.js +178 -0
  10. package/dist/a2a/server.d.ts +36 -0
  11. package/dist/a2a/server.js +344 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +545 -0
  14. package/dist/a2a/store.d.ts +109 -0
  15. package/dist/a2a/store.js +202 -0
  16. package/dist/agent/activity.d.ts +3 -0
  17. package/dist/agent/bootProviderConfig.d.ts +21 -0
  18. package/dist/agent/bootProviderConfig.js +74 -0
  19. package/dist/agent/piAdapter.d.ts +9 -1
  20. package/dist/agent/piAdapter.js +58 -3
  21. package/dist/agent/piIsolatedMcp.d.ts +2 -0
  22. package/dist/agent/piIsolatedMcp.js +10 -2
  23. package/dist/agent/port.d.ts +6 -0
  24. package/dist/agent/streamLogRedact.d.ts +1 -1
  25. package/dist/agent/streamLogRedact.js +4 -11
  26. package/dist/cli/a2aCommand.d.ts +8 -0
  27. package/dist/cli/a2aCommand.js +131 -0
  28. package/dist/cli/artifactCommand.js +9 -16
  29. package/dist/cli/backupCommand.d.ts +9 -0
  30. package/dist/cli/backupCommand.js +111 -0
  31. package/dist/cli/debugRunCommand.d.ts +10 -0
  32. package/dist/cli/debugRunCommand.js +109 -0
  33. package/dist/cli/doctorCommand.d.ts +32 -0
  34. package/dist/cli/doctorCommand.js +453 -0
  35. package/dist/cli/envelopeCommand.js +9 -2
  36. package/dist/cli/exportAllCommand.d.ts +55 -0
  37. package/dist/cli/exportAllCommand.js +218 -0
  38. package/dist/cli/exportRunCommand.js +23 -21
  39. package/dist/cli/hostClient.d.ts +21 -3
  40. package/dist/cli/hostClient.js +93 -12
  41. package/dist/cli/initTemplates.d.ts +1 -1
  42. package/dist/cli/initTemplates.js +8 -1
  43. package/dist/cli/resolveSafeOutPath.d.ts +2 -0
  44. package/dist/cli/resolveSafeOutPath.js +40 -0
  45. package/dist/cli/restoreCommand.d.ts +10 -0
  46. package/dist/cli/restoreCommand.js +90 -0
  47. package/dist/cli/runCommand.d.ts +3 -2
  48. package/dist/cli/runCommand.js +93 -11
  49. package/dist/cli/runOutput.d.ts +4 -0
  50. package/dist/cli/runOutput.js +33 -7
  51. package/dist/cli/runStageCommand.d.ts +34 -0
  52. package/dist/cli/runStageCommand.js +426 -0
  53. package/dist/cli/runsCommand.d.ts +1 -1
  54. package/dist/cli/runsCommand.js +162 -8
  55. package/dist/cli.js +190 -30
  56. package/dist/config/catalogRelativePath.d.ts +62 -0
  57. package/dist/config/catalogRelativePath.js +118 -0
  58. package/dist/config/configOrigin.d.ts +27 -0
  59. package/dist/config/configOrigin.js +59 -0
  60. package/dist/config/hostConfig.d.ts +55 -0
  61. package/dist/config/hostConfig.js +333 -0
  62. package/dist/config/listSkills.d.ts +26 -0
  63. package/dist/config/listSkills.js +190 -3
  64. package/dist/config/loadOutcome.d.ts +1 -1
  65. package/dist/config/loadPipeline.js +23 -2
  66. package/dist/config/loadStage.js +18 -0
  67. package/dist/config/loadTask.d.ts +2 -0
  68. package/dist/config/loadTask.js +57 -18
  69. package/dist/config/materializeStageModels.js +5 -1
  70. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  71. package/dist/config/mergePipelineIncludes.js +19 -0
  72. package/dist/config/migrateYaml.js +6 -11
  73. package/dist/config/multiProjectCatalog.d.ts +30 -0
  74. package/dist/config/multiProjectCatalog.js +142 -0
  75. package/dist/config/normalizePipelineStageEntry.js +20 -1
  76. package/dist/config/pipelineStageKeys.js +2 -0
  77. package/dist/config/resolveCatalogRoots.d.ts +27 -0
  78. package/dist/config/resolveCatalogRoots.js +76 -0
  79. package/dist/config/resolveModel.d.ts +7 -1
  80. package/dist/config/resolveModel.js +11 -1
  81. package/dist/config/resolveStageMcpServers.d.ts +9 -1
  82. package/dist/config/resolveStageMcpServers.js +36 -9
  83. package/dist/config/secretFromEnvOrFile.d.ts +9 -0
  84. package/dist/config/secretFromEnvOrFile.js +34 -0
  85. package/dist/config/seededCatalog.d.ts +11 -0
  86. package/dist/config/seededCatalog.js +29 -0
  87. package/dist/config/toolRequires.d.ts +24 -0
  88. package/dist/config/toolRequires.js +115 -0
  89. package/dist/config/validateCatalog.d.ts +5 -1
  90. package/dist/config/validateCatalog.js +140 -1
  91. package/dist/diagnostics/checks.d.ts +39 -0
  92. package/dist/diagnostics/checks.js +135 -0
  93. package/dist/errors/codes.d.ts +14 -0
  94. package/dist/errors/codes.js +59 -0
  95. package/dist/git/cache.d.ts +14 -0
  96. package/dist/git/cache.js +150 -0
  97. package/dist/git/credentials.d.ts +9 -0
  98. package/dist/git/credentials.js +88 -0
  99. package/dist/git/errors.d.ts +20 -0
  100. package/dist/git/errors.js +94 -0
  101. package/dist/git/exec.d.ts +16 -0
  102. package/dist/git/exec.js +135 -0
  103. package/dist/git/operations.d.ts +29 -0
  104. package/dist/git/operations.js +153 -0
  105. package/dist/git/version.d.ts +1 -0
  106. package/dist/git/version.js +10 -0
  107. package/dist/logging/audit.d.ts +16 -0
  108. package/dist/logging/audit.js +19 -0
  109. package/dist/logging/commonWords.d.ts +2 -0
  110. package/dist/logging/commonWords.js +13 -0
  111. package/dist/logging/logger.d.ts +49 -0
  112. package/dist/logging/logger.js +193 -0
  113. package/dist/logging/namedSecrets.d.ts +6 -0
  114. package/dist/logging/namedSecrets.js +58 -0
  115. package/dist/logging/redact.d.ts +13 -0
  116. package/dist/logging/redact.js +88 -0
  117. package/dist/mcp/catalogTools.js +591 -76
  118. package/dist/mcp/checkoutTools.d.ts +52 -0
  119. package/dist/mcp/checkoutTools.js +332 -0
  120. package/dist/mcp/controlTools.js +179 -5
  121. package/dist/mcp/deps.d.ts +1 -0
  122. package/dist/mcp/getStartedTool.d.ts +3 -0
  123. package/dist/mcp/getStartedTool.js +62 -0
  124. package/dist/mcp/readArtifact.d.ts +11 -0
  125. package/dist/mcp/readArtifact.js +46 -30
  126. package/dist/mcp/server.js +1 -0
  127. package/dist/mcp/tools.js +4 -0
  128. package/dist/net/proxy.d.ts +14 -0
  129. package/dist/net/proxy.js +78 -0
  130. package/dist/package-meta.d.ts +2 -1
  131. package/dist/package-meta.js +2 -1
  132. package/dist/preflight/bash.d.ts +8 -0
  133. package/dist/preflight/bash.js +34 -0
  134. package/dist/preflight/claudeRoot.d.ts +11 -0
  135. package/dist/preflight/claudeRoot.js +23 -0
  136. package/dist/preflight/mcpCommands.d.ts +8 -0
  137. package/dist/preflight/mcpCommands.js +36 -0
  138. package/dist/preflight/pipelinePreflight.d.ts +40 -0
  139. package/dist/preflight/pipelinePreflight.js +192 -0
  140. package/dist/preflight/tls.d.ts +6 -0
  141. package/dist/preflight/tls.js +18 -0
  142. package/dist/preflight/toolchain.d.ts +45 -0
  143. package/dist/preflight/toolchain.js +219 -0
  144. package/dist/project/findProjectRoot.js +3 -4
  145. package/dist/project/globalHome.d.ts +7 -0
  146. package/dist/project/globalHome.js +63 -2
  147. package/dist/project/resolveProjectContext.js +2 -2
  148. package/dist/project/resolveStageflowContext.js +2 -2
  149. package/dist/projection/projectRun.d.ts +2 -1
  150. package/dist/projection/projectRun.js +1 -0
  151. package/dist/runstore/assertTmpdir.d.ts +1 -0
  152. package/dist/runstore/assertTmpdir.js +21 -0
  153. package/dist/runstore/backup.d.ts +52 -0
  154. package/dist/runstore/backup.js +292 -0
  155. package/dist/runstore/createStore.d.ts +27 -0
  156. package/dist/runstore/createStore.js +75 -7
  157. package/dist/runstore/debugBundle.d.ts +60 -0
  158. package/dist/runstore/debugBundle.js +143 -0
  159. package/dist/runstore/diskUsage.d.ts +50 -0
  160. package/dist/runstore/diskUsage.js +214 -0
  161. package/dist/runstore/exportRunPayload.d.ts +8 -0
  162. package/dist/runstore/exportRunPayload.js +23 -0
  163. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  164. package/dist/runstore/normalizeCatalogPath.js +10 -0
  165. package/dist/runstore/paths.d.ts +5 -0
  166. package/dist/runstore/paths.js +145 -1
  167. package/dist/runstore/port.d.ts +119 -8
  168. package/dist/runstore/port.js +8 -1
  169. package/dist/runstore/restore.d.ts +81 -0
  170. package/dist/runstore/restore.js +355 -0
  171. package/dist/runstore/retention.d.ts +27 -0
  172. package/dist/runstore/retention.js +74 -0
  173. package/dist/runstore/runManifest.d.ts +146 -0
  174. package/dist/runstore/runManifest.js +288 -0
  175. package/dist/runstore/runProjection.d.ts +3 -1
  176. package/dist/runstore/runProjection.js +63 -1
  177. package/dist/runstore/sqlite/SqliteRunStore.d.ts +37 -3
  178. package/dist/runstore/sqlite/SqliteRunStore.js +401 -227
  179. package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
  180. package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
  181. package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
  182. package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
  183. package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
  184. package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
  185. package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
  186. package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
  187. package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
  188. package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
  189. package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
  190. package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
  191. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
  192. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
  193. package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
  194. package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
  195. package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
  196. package/dist/runstore/sqlite/migrations/index.js +72 -0
  197. package/dist/runstore/sqlite/schema.d.ts +20 -1
  198. package/dist/runstore/sqlite/schema.js +283 -1
  199. package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
  200. package/dist/runstore/sqlite/storeOpenError.js +11 -0
  201. package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
  202. package/dist/runstore/sqlite/storeSchemaError.js +8 -0
  203. package/dist/runstore/stageExecution.js +3 -0
  204. package/dist/runstore/storeFilesystem.d.ts +32 -0
  205. package/dist/runstore/storeFilesystem.js +148 -0
  206. package/dist/runstore/submission.d.ts +11 -0
  207. package/dist/runstore/submission.js +7 -0
  208. package/dist/runstore/trackProjection.js +2 -0
  209. package/dist/runstore/workspaceLayout.d.ts +13 -0
  210. package/dist/runstore/workspaceLayout.js +76 -0
  211. package/dist/runtime/completionCheckRunner.d.ts +3 -0
  212. package/dist/runtime/completionCheckRunner.js +45 -7
  213. package/dist/runtime/containerLimits.d.ts +16 -0
  214. package/dist/runtime/containerLimits.js +84 -0
  215. package/dist/runtime/credentialMaterialisation.d.ts +28 -0
  216. package/dist/runtime/credentialMaterialisation.js +128 -0
  217. package/dist/runtime/gitCheckoutCapability.js +7 -13
  218. package/dist/runtime/pipelineRunner.d.ts +38 -1
  219. package/dist/runtime/pipelineRunner.js +179 -15
  220. package/dist/runtime/pipelineScheduler.d.ts +15 -0
  221. package/dist/runtime/pipelineScheduler.js +167 -42
  222. package/dist/runtime/reloadRunCatalog.js +12 -1
  223. package/dist/runtime/repositoryMaterialize.d.ts +47 -0
  224. package/dist/runtime/repositoryMaterialize.js +235 -0
  225. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  226. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  227. package/dist/runtime/resumeTimedOut.d.ts +1 -1
  228. package/dist/runtime/resumeTimedOut.js +5 -2
  229. package/dist/runtime/runChangeBus.js +9 -0
  230. package/dist/runtime/runDeletion.d.ts +7 -0
  231. package/dist/runtime/runDeletion.js +37 -0
  232. package/dist/runtime/runManager.d.ts +179 -4
  233. package/dist/runtime/runManager.js +1644 -83
  234. package/dist/runtime/runRetentionSweep.d.ts +31 -0
  235. package/dist/runtime/runRetentionSweep.js +328 -0
  236. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  237. package/dist/runtime/runRetryCoordinator.js +19 -2
  238. package/dist/runtime/runSkills.d.ts +40 -0
  239. package/dist/runtime/runSkills.js +173 -0
  240. package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
  241. package/dist/runtime/stageAttemptBootstrap.js +195 -24
  242. package/dist/runtime/stageCacheEnv.d.ts +6 -0
  243. package/dist/runtime/stageCacheEnv.js +34 -0
  244. package/dist/runtime/stageConcurrency.d.ts +2 -1
  245. package/dist/runtime/stageConcurrency.js +4 -2
  246. package/dist/runtime/stageEnvironment.d.ts +38 -0
  247. package/dist/runtime/stageEnvironment.js +193 -0
  248. package/dist/runtime/stageProcessLauncher.d.ts +18 -0
  249. package/dist/runtime/stageProcessLauncher.js +194 -38
  250. package/dist/runtime/stageRecovery.d.ts +3 -1
  251. package/dist/runtime/stageRecovery.js +19 -4
  252. package/dist/runtime/stageRoots.d.ts +38 -1
  253. package/dist/runtime/stageRoots.js +118 -0
  254. package/dist/runtime/stageRunner.d.ts +1 -0
  255. package/dist/runtime/stageRunner.js +3 -1
  256. package/dist/runtime/stageSecretDecl.d.ts +9 -0
  257. package/dist/runtime/stageSecretDecl.js +102 -0
  258. package/dist/runtime/stageSecrets.d.ts +40 -0
  259. package/dist/runtime/stageSecrets.js +181 -0
  260. package/dist/runtime/stageWorker.d.ts +3 -2
  261. package/dist/runtime/stageWorker.js +58 -10
  262. package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
  263. package/dist/runtime/stageWorkerProtocol.js +8 -0
  264. package/dist/runtime/startPayload.d.ts +32 -0
  265. package/dist/runtime/startPayload.js +59 -0
  266. package/dist/runtime/taskInput.js +14 -5
  267. package/dist/runtime/verifiedStageExecution.d.ts +1 -0
  268. package/dist/runtime/verifiedStageExecution.js +2 -1
  269. package/dist/runtime/workspaceBinding.d.ts +20 -0
  270. package/dist/runtime/workspaceBinding.js +60 -0
  271. package/dist/server/allowedHosts.d.ts +19 -0
  272. package/dist/server/allowedHosts.js +179 -0
  273. package/dist/server/bootstrap.d.ts +38 -2
  274. package/dist/server/bootstrap.js +219 -4
  275. package/dist/server/controlToken.d.ts +47 -0
  276. package/dist/server/controlToken.js +241 -0
  277. package/dist/server/createHttpHost.d.ts +11 -2
  278. package/dist/server/createHttpHost.js +109 -10
  279. package/dist/server/ensureGlobalService.d.ts +5 -1
  280. package/dist/server/ensureGlobalService.js +35 -1
  281. package/dist/server/healthSurfaces.d.ts +6 -0
  282. package/dist/server/healthSurfaces.js +98 -0
  283. package/dist/server/http.d.ts +12 -1
  284. package/dist/server/http.js +1085 -485
  285. package/dist/server/listenHost.d.ts +5 -0
  286. package/dist/server/listenHost.js +56 -0
  287. package/dist/server/mcpHost.d.ts +10 -1
  288. package/dist/server/mcpHost.js +36 -6
  289. package/dist/server/operatorResults.d.ts +2 -2
  290. package/dist/server/operatorResults.js +40 -29
  291. package/dist/server/requestAuthContext.d.ts +12 -0
  292. package/dist/server/requestAuthContext.js +21 -0
  293. package/dist/server/shutdown.d.ts +69 -0
  294. package/dist/server/shutdown.js +219 -0
  295. package/dist/types/pipeline.d.ts +7 -0
  296. package/dist/types/stage.d.ts +8 -0
  297. package/dist/types/task.d.ts +8 -0
  298. package/dist/ui/assets/index-B6hlSXXn.js +120 -0
  299. package/dist/ui/index.html +1 -1
  300. package/examples/README.md +98 -0
  301. package/examples/archify-on-pr/README.md +213 -0
  302. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
  303. package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
  304. package/examples/archify-on-pr/author-diagrams.yaml +106 -0
  305. package/examples/archify-on-pr/detect-changes.yaml +89 -0
  306. package/examples/ci-validate/README.md +35 -0
  307. package/examples/ci-validate/check.yaml +13 -0
  308. package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
  309. package/examples/ci-validate/validate.sh +5 -0
  310. package/examples/compliance-review-demo/README.md +125 -0
  311. package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
  312. package/examples/compliance-review-demo/connect-account.yaml +47 -0
  313. package/examples/compliance-review-demo/map-to-control.yaml +123 -0
  314. package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
  315. package/examples/compliance-review-demo/publish-report.yaml +79 -0
  316. package/examples/compliance-review-demo/review-verdict.yaml +122 -0
  317. package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
  318. package/examples/compliance-review-demo/store-evidence.yaml +143 -0
  319. package/examples/conditional-fork/README.md +89 -0
  320. package/examples/conditional-fork/branch-a.yaml +13 -0
  321. package/examples/conditional-fork/branch-b.yaml +13 -0
  322. package/examples/conditional-fork/decide.yaml +35 -0
  323. package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
  324. package/examples/conditional-fork/fork-demo.task.yaml +8 -0
  325. package/examples/context7-mcp/README.md +42 -0
  326. package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
  327. package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
  328. package/examples/context7-mcp/fetch-docs.yaml +52 -0
  329. package/examples/context7-mcp/resolve-library.yaml +31 -0
  330. package/examples/context7-mcp/write-brief.yaml +52 -0
  331. package/examples/feature-loop/README.md +104 -0
  332. package/examples/feature-loop/address-feedback.yaml +148 -0
  333. package/examples/feature-loop/align.yaml +116 -0
  334. package/examples/feature-loop/decompose.yaml +86 -0
  335. package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
  336. package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
  337. package/examples/feature-loop/feature-loop.task.yaml +25 -0
  338. package/examples/feature-loop/implement.yaml +128 -0
  339. package/examples/feature-loop/plan.yaml +111 -0
  340. package/examples/feature-loop/publish.yaml +167 -0
  341. package/examples/feature-loop/review.yaml +146 -0
  342. package/examples/feature-loop/verify.yaml +106 -0
  343. package/examples/feedback-loop/README.md +72 -0
  344. package/examples/feedback-loop/VERIFY.md +208 -0
  345. package/examples/feedback-loop/continue-only.task.yaml +4 -0
  346. package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
  347. package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
  348. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
  349. package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
  350. package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
  351. package/examples/feedback-loop/implement.yaml +14 -0
  352. package/examples/feedback-loop/plan.yaml +14 -0
  353. package/examples/feedback-loop/review.yaml +15 -0
  354. package/examples/feedback-loop/send-back-once.task.yaml +6 -0
  355. package/examples/feedback-loop/submit.yaml +12 -0
  356. package/examples/generic-fan-in/README.md +151 -0
  357. package/examples/generic-fan-in/clarify.yaml +18 -0
  358. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
  359. package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
  360. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
  361. package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
  362. package/examples/generic-fan-in/research.yaml +45 -0
  363. package/examples/generic-fan-in/synthesize.yaml +66 -0
  364. package/examples/generic-fan-in/validation.yaml +45 -0
  365. package/examples/github-release/README.md +78 -0
  366. package/examples/github-release/draft-release-notes.yaml +64 -0
  367. package/examples/github-release/github-release.pipeline.yaml +11 -0
  368. package/examples/github-release/github-release.task.yaml +13 -0
  369. package/examples/github-release/publish-github-release.yaml +64 -0
  370. package/examples/hello-world/README.md +28 -0
  371. package/examples/hello-world/hello.pipeline.yaml +4 -0
  372. package/examples/hello-world/my-task.task.yaml +6 -0
  373. package/examples/hello-world/research.yaml +18 -0
  374. package/examples/mcp-hitl-tour/README.md +113 -0
  375. package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
  376. package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
  377. package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
  378. package/examples/mcp-hitl-tour/gather.yaml +31 -0
  379. package/examples/mcp-hitl-tour/intake.yaml +62 -0
  380. package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
  381. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
  382. package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
  383. package/examples/mcp-hitl-tour/polish.yaml +33 -0
  384. package/examples/mcp-hitl-tour/review.yaml +17 -0
  385. package/examples/mcp-hitl-tour/ship.yaml +25 -0
  386. package/examples/oss-issue-contribution/README.md +218 -0
  387. package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
  388. package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
  389. package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
  390. package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
  391. package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
  392. package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
  393. package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
  394. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
  395. package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
  396. package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
  397. package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
  398. package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
  399. package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
  400. package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
  401. package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
  402. package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
  403. package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
  404. package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
  405. package/examples/oss-quick-fix/README.md +65 -0
  406. package/examples/oss-quick-fix/implement-fix.yaml +84 -0
  407. package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
  408. package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
  409. package/examples/oss-quick-fix/ship-fix.yaml +81 -0
  410. package/examples/oss-quick-fix/understand-issue.yaml +67 -0
  411. package/examples/oss-quick-fix/verify-fix.yaml +73 -0
  412. package/examples/plan-review/README.md +37 -0
  413. package/examples/plan-review/plan-review-followup.yaml +13 -0
  414. package/examples/plan-review/plan-review.pipeline.yaml +9 -0
  415. package/examples/plan-review/plan-review.task.yaml +9 -0
  416. package/examples/plan-review/plan-review.yaml +25 -0
  417. package/examples/playwright-mcp/README.md +39 -0
  418. package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
  419. package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
  420. package/examples/playwright-mcp/screenshot.yaml +40 -0
  421. package/examples/retry-tour/README.md +75 -0
  422. package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
  423. package/examples/retry-tour/retry-tour.task.yaml +9 -0
  424. package/examples/route-if-tour/README.md +202 -0
  425. package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
  426. package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
  427. package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
  428. package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
  429. package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
  430. package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
  431. package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
  432. package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
  433. package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
  434. package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
  435. package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
  436. package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
  437. package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
  438. package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
  439. package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
  440. package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
  441. package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
  442. package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
  443. package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
  444. package/examples/route-wiring-smoke-test/README.md +377 -0
  445. package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
  446. package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
  447. package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
  448. package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
  449. package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
  450. package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
  451. package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
  452. package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
  453. package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
  454. package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
  455. package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
  456. package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
  457. package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
  458. package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
  459. package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
  460. package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
  461. package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
  462. package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
  463. package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
  464. package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
  465. package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
  466. package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
  467. package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
  468. package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
  469. package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
  470. package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
  471. package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
  472. package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
  473. package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
  474. package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
  475. package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
  476. package/examples/ship-feature/README.md +190 -0
  477. package/examples/ship-feature/feat-address-feedback.yaml +119 -0
  478. package/examples/ship-feature/feat-implement.yaml +180 -0
  479. package/examples/ship-feature/feat-plan.yaml +120 -0
  480. package/examples/ship-feature/feat-review.yaml +109 -0
  481. package/examples/ship-feature/feat-ship.yaml +203 -0
  482. package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
  483. package/examples/ship-feature/ship-feature.task.yaml +78 -0
  484. package/examples/stage-mcp/README.md +144 -0
  485. package/examples/stage-mcp/echo-mcp.mjs +20 -0
  486. package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
  487. package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
  488. package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
  489. package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
  490. package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
  491. package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
  492. package/examples/stage-mcp/use-echo.yaml +20 -0
  493. package/examples/verify-tour/README.md +80 -0
  494. package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
  495. package/examples/verify-tour/verify-tour.task.yaml +11 -0
  496. package/package.json +6 -1
  497. package/skills/stageflow/SKILL.md +6 -6
  498. package/skills/stageflow/references/control-surface.md +11 -5
  499. package/skills/stageflow/scripts/detect-host.mjs +14 -1
  500. package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
  501. package/skills/stageflow-author/SKILL.md +4 -3
  502. package/skills/stageflow-author/references/catalog-mapping.md +16 -0
  503. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  504. package/skills/stageflow-run/SKILL.md +20 -12
  505. package/skills/stageflow-run/references/mcp-call.md +9 -1
  506. package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
  507. package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
  508. package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
  509. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
  510. package/skills/stageflow-setup/SKILL.md +40 -8
  511. package/dist/ui/assets/index-Da5EHU6M.js +0 -119
@@ -1,62 +1,277 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile, realpath } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { RunSubmissionExistsError } from "../runstore/submission.js";
4
+ import { access, readFile, realpath } from "node:fs/promises";
3
5
  import path from "node:path";
4
6
  import { findProjectRoot } from "../project/findProjectRoot.js";
5
- import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
7
+ import { globalStageflowHome } from "../project/globalHome.js";
8
+ import { normalizeCatalogPath, normalizeProjectRoot } from "../runstore/normalizeCatalogPath.js";
9
+ import { durableRootDiskBreakdown, readFilesystemSize, refreshRunDiskUsage, resolveMinFreeDiskFloor, } from "../runstore/diskUsage.js";
10
+ import { newRunId } from "../runstore/paths.js";
6
11
  import { loadRunContext } from "./resumeReconstruct.js";
7
- import { assertTimedOutStageEligible, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
8
- import { loadTaskFromYaml } from "../config/loadTask.js";
12
+ import { assertResumableStage, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
13
+ import { loadTaskFromYamlOutcome } from "../config/loadTask.js";
14
+ import { buildValidationResult, loadPipelineValidated, } from "../config/validateCatalog.js";
9
15
  import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
10
- import { PipelineValidationError, startPipeline, } from "./pipelineRunner.js";
16
+ import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
17
+ import { InlinePipelineTooLargeError, PipelinePreflightError, PipelineValidationError, QueuedRunActivationAborted, startPipeline, } from "./pipelineRunner.js";
18
+ import { INLINE_PIPELINE_TOO_LARGE, pipelineBodyBytes, pipelinePersistenceForStart, } from "./startPayload.js";
19
+ import { materializeRunSkills, validateSkillsPayload, } from "./runSkills.js";
11
20
  import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
12
21
  import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
13
22
  import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
14
23
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
15
24
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
25
+ import { logger as rootLogger } from "../logging/logger.js";
26
+ import { registerNamedSecrets } from "../logging/namedSecrets.js";
27
+ import { shouldRegisterValue } from "../logging/redact.js";
28
+ import { bindingFromFields, buildInitialRunManifest, collectDeclaredSecretValues, finaliseStoredRunManifest, } from "../runstore/runManifest.js";
29
+ import { getRequestAuth } from "../server/requestAuthContext.js";
30
+ import { STAGE_ENV_PASSTHROUGH } from "./stageEnvironment.js";
31
+ import { proxyHealthFields } from "../net/proxy.js";
32
+ import { getContainerLimits } from "./containerLimits.js";
33
+ import { stageflowCacheRoot } from "./stageCacheEnv.js";
34
+ import { assertClaudeNotRoot, ClaudeRootError } from "../preflight/claudeRoot.js";
35
+ import { preflightFailureCode, runPipelinePreflight, } from "../preflight/pipelinePreflight.js";
36
+ import { asAgentBackendId } from "../agent/agentBackend.js";
16
37
  import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
17
38
  import { StageHitlController, waitKey, } from "./stageHitl.js";
18
- import { resolveAndValidateCheckout, } from "./stageRoots.js";
39
+ import { resolveAndValidateCheckout, resolveEffectiveGitIdentity, stageBindingEnvFromRun, } from "./stageRoots.js";
19
40
  import { orchestrateAnswerResume } from "./answerResume.js";
20
41
  import { reconstructAndContinue as resumeReconstructAndContinue } from "./resumeReconstruct.js";
21
- import { resumeSessionFilePath } from "./stageAttemptContext.js";
22
- import { resolveStartTaskInput } from "./taskInput.js";
23
- import { failStageAsInterrupted, syncRunStatusFromStages, } from "./stageRecovery.js";
42
+ import { attemptContext, resumeSessionFilePath } from "./stageAttemptContext.js";
43
+ import { checkTaskEntryInput, resolveStartTaskInput } from "./taskInput.js";
44
+ import { markStageInterrupted, OPERATOR_CANCEL_REASON, syncRunStatusFromStages, } from "./stageRecovery.js";
45
+ import { deleteRunEverywhere } from "./runDeletion.js";
46
+ import { runRetentionSweep, } from "./runRetentionSweep.js";
24
47
  import { blocksGenericRetry, manualRecoveryEligibility, readManualRecoveryState, } from "./manualRecoveryState.js";
48
+ import { resolveWorkspaceBinding } from "./workspaceBinding.js";
49
+ import { derivedBindingKindFromMeta, materializeWorkspaceBinding, StartLinkError, } from "./repositoryMaterialize.js";
50
+ export const PROJECT_ROOT_UNAVAILABLE_REASON = "project_root_unavailable";
51
+ export const INSUFFICIENT_DISK_CANCEL_REASON = "insufficient_disk";
52
+ function existingSubmissionResult(existing, request) {
53
+ if (existing.requestHash !== request.requestHash) {
54
+ return { ok: false, status: 409, reason: "Submission key was already used for different input" };
55
+ }
56
+ return { ok: true, runId: existing.runId, reused: true };
57
+ }
58
+ export const FORCE_DELETE_CANCEL_REASON = "delete_run: force";
25
59
  const DEFAULT_MAX_CONCURRENT = 3;
26
- const STARTUP_RECONCILE_REASON = "process_interrupted: no active worker (server restart)";
60
+ const DEFAULT_MAX_QUEUED = 32;
61
+ const DEFAULT_MAX_AUTO_RESUMES = 3;
62
+ const STARTUP_RECONCILE_REASON = "orphaned_no_worker";
63
+ const AUTO_RESUME_CAPPED_REASON = "auto_resume_capped";
64
+ export const STAGEFLOW_AUTO_RESUME_INTERRUPTED = "STAGEFLOW_AUTO_RESUME_INTERRUPTED";
65
+ export const STAGEFLOW_MAX_AUTO_RESUMES = "STAGEFLOW_MAX_AUTO_RESUMES";
27
66
  const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
67
+ const log = rootLogger.child({ component: "runtime" });
28
68
  function parseMaxConcurrent(raw) {
29
69
  if (raw === undefined || raw.trim() === "")
30
70
  return DEFAULT_MAX_CONCURRENT;
31
71
  const n = Number.parseInt(raw, 10);
32
- if (!Number.isFinite(n) || n < 1)
33
- return DEFAULT_MAX_CONCURRENT;
72
+ if (!Number.isFinite(n) || n < 1 || String(n) !== raw.trim()) {
73
+ throw new Error(`Invalid value for STAGEFLOW_MAX_CONCURRENT_RUNS: ${JSON.stringify(raw)}`);
74
+ }
34
75
  return n;
35
76
  }
77
+ function parseMaxQueued(raw) {
78
+ if (raw === undefined || raw.trim() === "")
79
+ return DEFAULT_MAX_QUEUED;
80
+ const n = Number.parseInt(raw, 10);
81
+ if (!Number.isFinite(n) || n < 0)
82
+ return DEFAULT_MAX_QUEUED;
83
+ return n;
84
+ }
85
+ export function isAutoResumeInterruptedEnabled(env = process.env) {
86
+ const raw = env[STAGEFLOW_AUTO_RESUME_INTERRUPTED];
87
+ if (raw === undefined || raw.trim() === "")
88
+ return false;
89
+ const normalized = raw.trim().toLowerCase();
90
+ return normalized !== "0" && normalized !== "false";
91
+ }
92
+ export function parseMaxAutoResumes(env = process.env) {
93
+ const raw = env[STAGEFLOW_MAX_AUTO_RESUMES];
94
+ if (raw === undefined || raw.trim() === "")
95
+ return DEFAULT_MAX_AUTO_RESUMES;
96
+ const n = Number.parseInt(raw, 10);
97
+ if (!Number.isFinite(n) || n < 0)
98
+ return DEFAULT_MAX_AUTO_RESUMES;
99
+ return n;
100
+ }
101
+ /** Private FIFO-per-project_root + round-robin dequeue (KTD3). */
102
+ class AdmissionQueue {
103
+ byRoot = new Map();
104
+ rrOrder = [];
105
+ rrIndex = 0;
106
+ get size() {
107
+ let n = 0;
108
+ for (const list of this.byRoot.values())
109
+ n += list.length;
110
+ return n;
111
+ }
112
+ clear() {
113
+ this.byRoot.clear();
114
+ this.rrOrder = [];
115
+ this.rrIndex = 0;
116
+ }
117
+ enqueue(projectRoot, entry) {
118
+ let list = this.byRoot.get(projectRoot);
119
+ if (list === undefined) {
120
+ list = [];
121
+ this.byRoot.set(projectRoot, list);
122
+ this.rrOrder.push(projectRoot);
123
+ }
124
+ list.push(entry);
125
+ return this.size;
126
+ }
127
+ requeueFront(projectRoot, entry) {
128
+ let list = this.byRoot.get(projectRoot);
129
+ if (list === undefined) {
130
+ list = [];
131
+ this.byRoot.set(projectRoot, list);
132
+ this.rrOrder.push(projectRoot);
133
+ }
134
+ list.unshift(entry);
135
+ }
136
+ remove(runId) {
137
+ for (const [root, list] of this.byRoot) {
138
+ const idx = list.findIndex((e) => e.runId === runId);
139
+ if (idx < 0)
140
+ continue;
141
+ list.splice(idx, 1);
142
+ if (list.length === 0) {
143
+ this.byRoot.delete(root);
144
+ const orderIdx = this.rrOrder.indexOf(root);
145
+ if (orderIdx >= 0) {
146
+ this.rrOrder.splice(orderIdx, 1);
147
+ if (this.rrOrder.length === 0) {
148
+ this.rrIndex = 0;
149
+ }
150
+ else if (orderIdx < this.rrIndex) {
151
+ this.rrIndex -= 1;
152
+ }
153
+ else if (this.rrIndex >= this.rrOrder.length) {
154
+ this.rrIndex = 0;
155
+ }
156
+ }
157
+ }
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ positionOf(runId) {
163
+ let position = 0;
164
+ const rootCount = this.rrOrder.length;
165
+ if (rootCount === 0)
166
+ return undefined;
167
+ const heads = this.rrOrder.map((root) => ({
168
+ root,
169
+ list: this.byRoot.get(root) ?? [],
170
+ i: 0,
171
+ }));
172
+ let rr = this.rrIndex % rootCount;
173
+ let remaining = this.size;
174
+ while (remaining > 0) {
175
+ let advanced = false;
176
+ for (let step = 0; step < rootCount; step++) {
177
+ const slot = heads[(rr + step) % rootCount];
178
+ if (slot === undefined || slot.i >= slot.list.length)
179
+ continue;
180
+ const entry = slot.list[slot.i];
181
+ slot.i += 1;
182
+ remaining -= 1;
183
+ position += 1;
184
+ advanced = true;
185
+ if (entry?.runId === runId)
186
+ return position;
187
+ rr = (rr + step + 1) % rootCount;
188
+ break;
189
+ }
190
+ if (!advanced)
191
+ break;
192
+ }
193
+ return undefined;
194
+ }
195
+ dequeueNext(skipRoots, skipCallers) {
196
+ if (this.rrOrder.length === 0)
197
+ return undefined;
198
+ const start = this.rrIndex % this.rrOrder.length;
199
+ for (let step = 0; step < this.rrOrder.length; step++) {
200
+ const idx = (start + step) % this.rrOrder.length;
201
+ const root = this.rrOrder[idx];
202
+ if (root === undefined)
203
+ continue;
204
+ if (skipRoots?.has(root))
205
+ continue;
206
+ const list = this.byRoot.get(root);
207
+ if (list === undefined || list.length === 0)
208
+ continue;
209
+ let pickIdx = 0;
210
+ if (skipCallers !== undefined && skipCallers.size > 0) {
211
+ pickIdx = list.findIndex((e) => {
212
+ const c = e.callerId;
213
+ if (c === undefined || c === null || c === "")
214
+ return true;
215
+ return !skipCallers.has(c.toLowerCase());
216
+ });
217
+ if (pickIdx < 0)
218
+ continue;
219
+ }
220
+ const [entry] = list.splice(pickIdx, 1);
221
+ if (entry === undefined)
222
+ continue;
223
+ if (list.length === 0) {
224
+ this.byRoot.delete(root);
225
+ this.rrOrder.splice(idx, 1);
226
+ if (this.rrOrder.length === 0) {
227
+ this.rrIndex = 0;
228
+ }
229
+ else {
230
+ this.rrIndex = idx % this.rrOrder.length;
231
+ }
232
+ }
233
+ else {
234
+ this.rrIndex = (idx + 1) % this.rrOrder.length;
235
+ }
236
+ return { projectRoot: root, entry };
237
+ }
238
+ return undefined;
239
+ }
240
+ }
36
241
  async function toCheckoutLeaseKey(absPath) {
37
242
  try {
38
243
  return await realpath(absPath);
39
244
  }
40
245
  catch {
41
246
  const fallback = path.resolve(absPath);
42
- console.error(`invariant: checkout realpath failed for ${absPath}; using path.resolve fallback ${fallback}`);
247
+ log.error("checkout.realpath_failed", `invariant: checkout realpath failed for ${absPath}; using path.resolve fallback ${fallback}`, { abs_path: absPath, fallback });
43
248
  return fallback;
44
249
  }
45
250
  }
46
251
  export class RunManager {
47
252
  options;
253
+ submissionsInFlight = new Map();
48
254
  active = new Map();
255
+ schedulingHalts = new Map();
49
256
  checkoutLeases = new Map();
50
257
  provisionalIds = new Set();
258
+ admissionQueue = new AdmissionQueue();
259
+ pendingQueuedStarts = new Map();
260
+ queuedDone = new Map();
261
+ admissionDrainInFlight = false;
51
262
  resumeInFlight = new Set();
52
263
  retryInFlight = new Set();
53
264
  retryStartOwner = new Map();
54
265
  retryStartWaiters = new Map();
55
266
  trackingGeneration = 0;
56
267
  maxConcurrent;
268
+ maxQueued;
269
+ maxConcurrentPerProject;
270
+ callerQuotas;
57
271
  maxActiveStagesPerRun;
58
272
  executionMode;
59
273
  stageProcessLauncher;
274
+ a2aStore;
60
275
  hitl;
61
276
  attachedWaiting = new Set();
62
277
  retryCoordinator = new RunRetryCoordinator();
@@ -90,15 +305,24 @@ export class RunManager {
90
305
  cwd;
91
306
  projectRoot;
92
307
  isGitProject;
308
+ acceptingWork = true;
93
309
  constructor(options) {
94
310
  this.options = options;
95
311
  this.cwd = options.cwd ?? process.cwd();
96
312
  this.projectRoot = options.projectRoot ?? this.cwd;
97
313
  this.isGitProject = options.isGitProject ?? false;
314
+ this.a2aStore = options.a2aStore;
98
315
  this.maxConcurrent =
99
316
  options.maxConcurrent ??
100
317
  readMaxConcurrentFromGlobal() ??
101
318
  parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
319
+ this.maxQueued =
320
+ options.maxQueued ?? parseMaxQueued(process.env.STAGEFLOW_MAX_QUEUED);
321
+ this.maxConcurrentPerProject = options.maxConcurrentPerProject;
322
+ this.callerQuotas = new Map(Object.entries(options.callerQuotas ?? {}).map(([id, n]) => [
323
+ id.toLowerCase(),
324
+ n,
325
+ ]));
102
326
  this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
103
327
  this.executionMode = readStageExecutionMode(process.env, options.executionMode);
104
328
  this.stageProcessLauncher =
@@ -125,6 +349,57 @@ export class RunManager {
125
349
  getActiveCount() {
126
350
  return this.active.size;
127
351
  }
352
+ stopAcceptingWork() {
353
+ this.acceptingWork = false;
354
+ for (const halt of this.schedulingHalts.values()) {
355
+ halt.halted = true;
356
+ halt.hostShutdown = true;
357
+ }
358
+ }
359
+ isAcceptingWork() {
360
+ return this.acceptingWork;
361
+ }
362
+ async drainActiveStages(options) {
363
+ for (const halt of this.schedulingHalts.values()) {
364
+ halt.halted = true;
365
+ halt.hostShutdown = true;
366
+ }
367
+ const launcher = this.stageProcessLauncher;
368
+ if (launcher === undefined || launcher.activeCount() === 0) {
369
+ return { forced: false };
370
+ }
371
+ const previouslyActive = launcher.getActiveStageProcesses();
372
+ const seenRuns = new Set();
373
+ for (const { runId, stageId } of previouslyActive) {
374
+ await markStageInterrupted({
375
+ store: this.options.store,
376
+ runId,
377
+ stageId,
378
+ reason: "host_shutdown",
379
+ status: "interrupted",
380
+ });
381
+ seenRuns.add(runId);
382
+ }
383
+ for (const runId of seenRuns) {
384
+ await syncRunStatusFromStages(this.options.store, runId).catch(() => undefined);
385
+ }
386
+ launcher.signalAllActive("SIGTERM");
387
+ while (launcher.activeCount() > 0 &&
388
+ Date.now() < options.deadlineMs &&
389
+ !(options.isEscalated?.() ?? false)) {
390
+ await new Promise((resolve) => setTimeout(resolve, 25));
391
+ }
392
+ const remaining = launcher.getActiveStageProcesses();
393
+ if (remaining.length === 0) {
394
+ return { forced: false };
395
+ }
396
+ launcher.signalAllActive("SIGKILL");
397
+ const killWaitUntil = Date.now() + 500;
398
+ while (launcher.activeCount() > 0 && Date.now() < killWaitUntil) {
399
+ await new Promise((resolve) => setTimeout(resolve, 25));
400
+ }
401
+ return { forced: true };
402
+ }
128
403
  getActiveRunIds() {
129
404
  return [...this.active.keys()];
130
405
  }
@@ -134,6 +409,7 @@ export class RunManager {
134
409
  const maxConcurrent = this.getMaxConcurrent();
135
410
  const maxActiveStageProcessesRaw = readMaxActiveStageProcesses(process.env);
136
411
  const activeStageProcesses = this.stageProcessLauncher?.activeCount() ?? 0;
412
+ const limits = getContainerLimits();
137
413
  return {
138
414
  ok: true,
139
415
  activeRunIds,
@@ -144,8 +420,83 @@ export class RunManager {
144
420
  maxActiveStageProcesses: Number.isFinite(maxActiveStageProcessesRaw)
145
421
  ? maxActiveStageProcessesRaw
146
422
  : null,
423
+ stage_env_passthrough: process.env[STAGE_ENV_PASSTHROUGH]?.trim() === "all",
424
+ proxy: proxyHealthFields(process.env),
425
+ container: {
426
+ max_old_space_size_mb: limits.maxOldSpaceSizeMb,
427
+ max_active_stage_processes: limits.maxActiveStageProcesses,
428
+ memory_limit_bytes: limits.memoryLimitBytes ?? null,
429
+ source: limits.source,
430
+ },
431
+ cache: { root: stageflowCacheRoot() },
432
+ };
433
+ }
434
+ getPerProjectCapacity() {
435
+ const byRoot = new Map();
436
+ for (const entry of this.active.values()) {
437
+ const root = entry.projectRoot ?? this.projectRoot;
438
+ byRoot.set(root, (byRoot.get(root) ?? 0) + 1);
439
+ }
440
+ return {
441
+ maxConcurrent: this.maxConcurrentPerProject,
442
+ projects: [...byRoot.entries()].map(([project_root, activeCount]) => ({
443
+ project_root,
444
+ activeCount,
445
+ maxConcurrent: this.maxConcurrentPerProject,
446
+ })),
147
447
  };
148
448
  }
449
+ countActiveForProject(projectRoot) {
450
+ const normalized = normalizeCatalogPath(projectRoot);
451
+ let n = 0;
452
+ for (const entry of this.active.values()) {
453
+ const root = normalizeCatalogPath(entry.projectRoot ?? this.projectRoot);
454
+ if (root === normalized)
455
+ n += 1;
456
+ }
457
+ return n;
458
+ }
459
+ countActiveForCaller(callerId) {
460
+ const normalized = callerId.toLowerCase();
461
+ let n = 0;
462
+ for (const entry of this.active.values()) {
463
+ if (entry.callerId?.toLowerCase() === normalized)
464
+ n += 1;
465
+ }
466
+ return n;
467
+ }
468
+ callerQuotaLimit(callerId) {
469
+ if (callerId === null || callerId === undefined || callerId === "") {
470
+ return undefined;
471
+ }
472
+ return this.callerQuotas.get(callerId.toLowerCase());
473
+ }
474
+ /** Capacity plus on-demand durable-root disk breakdown (KTD16). */
475
+ async getHealthWithDisk() {
476
+ const base = this.getHealth();
477
+ try {
478
+ const disk = await durableRootDiskBreakdown(globalStageflowHome());
479
+ return { ...base, disk };
480
+ }
481
+ catch {
482
+ return {
483
+ ...base,
484
+ disk: {
485
+ runs_bytes: 0,
486
+ worktrees_bytes: 0,
487
+ repos_bytes: 0,
488
+ state_db_bytes: 0,
489
+ a2a_artifacts_bytes: 0,
490
+ cache_bytes: 0,
491
+ free_bytes: 0,
492
+ },
493
+ };
494
+ }
495
+ }
496
+ /** Refresh cached `disk_bytes` for one run after a terminal transition. */
497
+ async refreshRunDiskBytes(runId) {
498
+ await refreshRunDiskUsage(this.options.store, runId);
499
+ }
149
500
  getHitlController() {
150
501
  return this.hitl;
151
502
  }
@@ -180,16 +531,27 @@ export class RunManager {
180
531
  }
181
532
  let checkoutKey;
182
533
  let durableCheckoutRoot;
534
+ let attachCallerId = null;
535
+ let attachProjectRoot;
183
536
  try {
184
537
  const meta = await this.options.store.readRunMeta(runId);
185
- const checkoutRoot = meta.checkout_root;
186
- if (checkoutRoot !== undefined && checkoutRoot !== "") {
187
- durableCheckoutRoot = checkoutRoot;
188
- checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
538
+ attachCallerId = meta.caller_id ?? null;
539
+ attachProjectRoot = meta.project_root
540
+ ? normalizeCatalogPath(meta.project_root)
541
+ : undefined;
542
+ const kind = derivedBindingKindFromMeta(meta);
543
+ if (kind === "checkout") {
544
+ const checkoutRoot = meta.checkout_root;
545
+ if (checkoutRoot !== undefined && checkoutRoot !== "") {
546
+ durableCheckoutRoot = checkoutRoot;
547
+ checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
548
+ }
189
549
  }
190
550
  }
191
551
  catch (err) {
192
- console.error(`invariant: attach failed reading checkout_root for run ${runId}; tracking without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
552
+ log
553
+ .child({ run_id: runId })
554
+ .error("attach.checkout_root_failed", `invariant: attach failed reading checkout_root for run ${runId}; tracking without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
193
555
  }
194
556
  const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
195
557
  if (conflictHolder !== undefined) {
@@ -203,6 +565,8 @@ export class RunManager {
203
565
  this.active.set(runId, {
204
566
  checkoutKey,
205
567
  durableCheckoutRoot,
568
+ projectRoot: attachProjectRoot,
569
+ callerId: attachCallerId,
206
570
  generation: ++this.trackingGeneration,
207
571
  });
208
572
  for (const stage of waitingStages) {
@@ -224,27 +588,74 @@ export class RunManager {
224
588
  const reconciled = [];
225
589
  const runs = await this.options.store.listRuns();
226
590
  for (const summary of runs) {
591
+ let meta;
592
+ try {
593
+ meta = await this.options.store.readRunMeta(summary.run_id);
594
+ }
595
+ catch (err) {
596
+ log
597
+ .child({ run_id: summary.run_id })
598
+ .error("reconcile.read_meta_failed", `reconcileOrphanedStages: failed to read run meta ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
599
+ continue;
600
+ }
601
+ if (meta.status === "queued")
602
+ continue;
227
603
  let detail;
228
604
  try {
229
605
  detail = await this.options.store.readRun(summary.run_id);
230
606
  }
231
607
  catch (err) {
232
- console.error(`reconcileOrphanedStages: failed to read run ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
608
+ log
609
+ .child({ run_id: summary.run_id })
610
+ .error("reconcile.read_run_failed", `reconcileOrphanedStages: failed to read run ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
233
611
  continue;
234
612
  }
235
613
  const runId = summary.run_id;
614
+ const cancelled = meta.status === "cancelled";
236
615
  let runChanged = false;
237
616
  for (const stage of detail.stages) {
617
+ if (cancelled) {
618
+ if (stage.status !== "pending" &&
619
+ stage.status !== "running" &&
620
+ stage.status !== "waiting_for_input") {
621
+ continue;
622
+ }
623
+ try {
624
+ await markStageInterrupted({
625
+ store: this.options.store,
626
+ runId,
627
+ stageId: stage.stage_id,
628
+ reason: OPERATOR_CANCEL_REASON,
629
+ status: "failed",
630
+ });
631
+ reconciled.push({
632
+ runId,
633
+ stageId: stage.stage_id,
634
+ reason: OPERATOR_CANCEL_REASON,
635
+ });
636
+ runChanged = true;
637
+ log
638
+ .child({ run_id: runId, stage_id: stage.stage_id })
639
+ .error("reconcile.orphaned_stage", `reconcileOrphanedStages: failed orphaned stage ${runId}/${stage.stage_id}: ${OPERATOR_CANCEL_REASON}`, { reason: OPERATOR_CANCEL_REASON });
640
+ }
641
+ catch (err) {
642
+ log
643
+ .child({ run_id: runId, stage_id: stage.stage_id })
644
+ .error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
645
+ }
646
+ continue;
647
+ }
238
648
  if (stage.status !== "running")
239
649
  continue;
240
650
  if (this.hasActiveWorker(runId, stage.stage_id))
241
651
  continue;
242
652
  try {
243
- await failStageAsInterrupted({
653
+ await markStageInterrupted({
244
654
  store: this.options.store,
245
655
  runId,
246
656
  stageId: stage.stage_id,
247
657
  reason: STARTUP_RECONCILE_REASON,
658
+ status: "interrupted",
248
659
  });
249
660
  reconciled.push({
250
661
  runId,
@@ -252,26 +663,106 @@ export class RunManager {
252
663
  reason: STARTUP_RECONCILE_REASON,
253
664
  });
254
665
  runChanged = true;
255
- console.error(`reconcileOrphanedStages: failed orphaned stage ${runId}/${stage.stage_id}: ${STARTUP_RECONCILE_REASON}`);
666
+ log
667
+ .child({ run_id: runId, stage_id: stage.stage_id })
668
+ .error("reconcile.orphaned_stage", `reconcileOrphanedStages: interrupted orphaned stage ${runId}/${stage.stage_id}: ${STARTUP_RECONCILE_REASON}`, { reason: STARTUP_RECONCILE_REASON });
256
669
  }
257
670
  catch (err) {
258
- console.error(`reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
671
+ log
672
+ .child({ run_id: runId, stage_id: stage.stage_id })
673
+ .error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
259
674
  }
260
675
  }
261
- if (runChanged) {
676
+ if (runChanged && !cancelled) {
262
677
  try {
263
678
  await syncRunStatusFromStages(this.options.store, runId);
264
679
  }
265
680
  catch (err) {
266
- console.error(`reconcileOrphanedStages: failed to sync run status for ${runId}: ${err instanceof Error ? err.message : String(err)}`);
681
+ log
682
+ .child({ run_id: runId })
683
+ .error("reconcile.sync_status_failed", `reconcileOrphanedStages: failed to sync run status for ${runId}: ${err instanceof Error ? err.message : String(err)}`);
267
684
  }
268
685
  }
269
686
  }
270
687
  if (reconciled.length > 0) {
271
- console.error(`reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`);
688
+ log.error("reconcile.complete", `reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`, { count: reconciled.length });
272
689
  }
273
690
  return { reconciled };
274
691
  }
692
+ async autoResumeInterruptedStages() {
693
+ const resumed = [];
694
+ const capped = [];
695
+ const skipped = [];
696
+ if (!isAutoResumeInterruptedEnabled()) {
697
+ return { resumed, capped, skipped };
698
+ }
699
+ const maxAutoResumes = parseMaxAutoResumes();
700
+ const interrupted = await this.options.store.listInterruptedStageExecutions();
701
+ for (const execution of interrupted) {
702
+ const runId = execution.run_id;
703
+ const stageId = execution.stage_id;
704
+ const attempt = execution.attempt;
705
+ const latest = await this.options.store.getLatestStageExecution(runId, stageId);
706
+ if (latest === null || latest.attempt !== attempt) {
707
+ skipped.push({
708
+ runId,
709
+ stageId,
710
+ reason: "interrupted attempt is not the latest",
711
+ });
712
+ continue;
713
+ }
714
+ if (execution.auto_resume_count >= maxAutoResumes) {
715
+ try {
716
+ await markStageInterrupted({
717
+ store: this.options.store,
718
+ runId,
719
+ stageId,
720
+ reason: AUTO_RESUME_CAPPED_REASON,
721
+ status: "interrupted",
722
+ attemptCtx: attemptContext(attempt),
723
+ });
724
+ capped.push({ runId, stageId });
725
+ log
726
+ .child({ run_id: runId, stage_id: stageId, attempt })
727
+ .info("auto_resume.capped", `autoResumeInterruptedStages: capped ${runId}/${stageId} at ${execution.auto_resume_count}`, { auto_resume_count: execution.auto_resume_count });
728
+ }
729
+ catch (err) {
730
+ skipped.push({
731
+ runId,
732
+ stageId,
733
+ reason: err instanceof Error ? err.message : String(err),
734
+ });
735
+ }
736
+ continue;
737
+ }
738
+ try {
739
+ await this.options.store.updateStageExecution(runId, stageId, attempt, {
740
+ auto_resume_count: execution.auto_resume_count + 1,
741
+ });
742
+ const result = await this.resumeTimedOutStage(runId, stageId, {
743
+ source: "auto",
744
+ });
745
+ if (result.ok) {
746
+ resumed.push({ runId, stageId });
747
+ }
748
+ else {
749
+ skipped.push({
750
+ runId,
751
+ stageId,
752
+ reason: result.reason ?? "auto resume failed",
753
+ });
754
+ }
755
+ }
756
+ catch (err) {
757
+ skipped.push({
758
+ runId,
759
+ stageId,
760
+ reason: err instanceof Error ? err.message : String(err),
761
+ });
762
+ }
763
+ }
764
+ return { resumed, capped, skipped };
765
+ }
275
766
  async resumeStalledSchedules() {
276
767
  const resumed = [];
277
768
  const runs = await this.options.store.listRuns();
@@ -287,11 +778,37 @@ export class RunManager {
287
778
  }
288
779
  }
289
780
  catch (err) {
290
- console.error(`resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
781
+ log
782
+ .child({ run_id: runId })
783
+ .error("resume.stalled_failed", `resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
291
784
  }
292
785
  }
293
786
  return resumed;
294
787
  }
788
+ /** Rebuild in-memory admission queue from persisted `queued` rows (R27). */
789
+ async reenqueuePersistedQueuedRuns() {
790
+ const queued = await this.options.store.listRuns({ status: "queued" });
791
+ const ordered = [...queued].sort((a, b) => a.created_at.localeCompare(b.created_at));
792
+ this.admissionQueue.clear();
793
+ for (const row of ordered) {
794
+ const root = normalizeCatalogPath(row.project_root ?? this.projectRoot);
795
+ let callerId = null;
796
+ try {
797
+ const meta = await this.options.store.readRunMeta(row.run_id);
798
+ callerId = meta.caller_id ?? null;
799
+ }
800
+ catch {
801
+ /* leave null; drain stamps from meta on quota miss */
802
+ }
803
+ this.admissionQueue.enqueue(root, {
804
+ runId: row.run_id,
805
+ createdAt: row.created_at,
806
+ callerId,
807
+ });
808
+ this.ensureQueuedDone(row.run_id);
809
+ }
810
+ await this.drainAdmissionQueue();
811
+ }
295
812
  async abandonStage(runId, stageId) {
296
813
  let detail;
297
814
  try {
@@ -330,11 +847,12 @@ export class RunManager {
330
847
  await this.stageProcessLauncher.cancelRun(runId);
331
848
  }
332
849
  }
333
- await failStageAsInterrupted({
850
+ await markStageInterrupted({
334
851
  store: this.options.store,
335
852
  runId,
336
853
  stageId,
337
854
  reason: OPERATOR_ABANDON_REASON,
855
+ status: "failed",
338
856
  });
339
857
  if (this.retryCoordinator.isActive(runId) &&
340
858
  this.retryCoordinator.getActiveRoots(runId)?.has(stageId)) {
@@ -348,13 +866,250 @@ export class RunManager {
348
866
  }
349
867
  return { ok: true, runId, stageId };
350
868
  }
351
- async startRun(input) {
352
- const cwd = this.options.cwd ?? process.cwd();
353
- // An inline pipeline has no filesystem anchor to derive a project root from.
354
- const derivedProjectRoot = typeof input.pipeline === "string"
355
- ? (findProjectRoot(path.dirname(path.resolve(cwd, input.pipeline))) ??
356
- this.projectRoot)
357
- : this.projectRoot;
869
+ async cancelRun(runId, reason) {
870
+ let meta;
871
+ try {
872
+ meta = await this.options.store.readRunMeta(runId);
873
+ }
874
+ catch {
875
+ return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
876
+ }
877
+ if (meta.status === "cancelled") {
878
+ return { ok: true, runId };
879
+ }
880
+ if (meta.status === "succeeded" || meta.status === "failed") {
881
+ return {
882
+ ok: false,
883
+ reason: `Run is ${meta.status} and cannot be cancelled`,
884
+ status: 409,
885
+ };
886
+ }
887
+ const trimmedReason = reason.trim();
888
+ if (trimmedReason.length === 0) {
889
+ return {
890
+ ok: false,
891
+ reason: "Cancel reason is required",
892
+ status: 400,
893
+ };
894
+ }
895
+ await this.options.store.updateRunStatus(runId, "cancelled");
896
+ await this.options.store.setCancelReason(runId, trimmedReason);
897
+ await finaliseStoredRunManifest(this.options.store, runId).catch(() => undefined);
898
+ await this.refreshRunDiskBytes(runId).catch(() => undefined);
899
+ const halt = this.schedulingHalts.get(runId);
900
+ if (halt !== undefined) {
901
+ halt.halted = true;
902
+ }
903
+ this.admissionQueue.remove(runId);
904
+ this.pendingQueuedStarts.delete(runId);
905
+ this.resolveQueuedDone(runId, {
906
+ ok: false,
907
+ outcome: "cancelled",
908
+ runDir: this.options.store.getWorkspaceDir(runId),
909
+ runId,
910
+ reason: trimmedReason,
911
+ });
912
+ if (this.stageProcessLauncher !== undefined) {
913
+ await this.stageProcessLauncher.cancelRun(runId);
914
+ }
915
+ const detail = await this.options.store.readRun(runId);
916
+ for (const stage of detail.stages) {
917
+ if (stage.status !== "pending" &&
918
+ stage.status !== "running" &&
919
+ stage.status !== "waiting_for_input") {
920
+ continue;
921
+ }
922
+ const wasWaiting = stage.status === "waiting_for_input";
923
+ await markStageInterrupted({
924
+ store: this.options.store,
925
+ runId,
926
+ stageId: stage.stage_id,
927
+ reason: OPERATOR_CANCEL_REASON,
928
+ status: "failed",
929
+ });
930
+ if (wasWaiting) {
931
+ this.hitl.clearLiveWait(runId, stage.stage_id);
932
+ }
933
+ }
934
+ if (this.active.has(runId)) {
935
+ this.removeActiveEntry(runId, false);
936
+ }
937
+ return { ok: true, runId };
938
+ }
939
+ setA2aStore(store) {
940
+ this.a2aStore = store;
941
+ }
942
+ async deleteRun(runId, options = {
943
+ channel: "rest",
944
+ }) {
945
+ const force = options.force === true;
946
+ const channel = options.channel;
947
+ let meta;
948
+ try {
949
+ meta = await this.options.store.readRunMeta(runId);
950
+ }
951
+ catch {
952
+ return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
953
+ }
954
+ const activeStatus = meta.status === "created" ||
955
+ meta.status === "queued" ||
956
+ meta.status === "running";
957
+ if (activeStatus && !force) {
958
+ return {
959
+ ok: false,
960
+ reason: `Run is ${meta.status} and cannot be deleted without force`,
961
+ status: 409,
962
+ };
963
+ }
964
+ if (activeStatus && force) {
965
+ const settle = this.active.get(runId)?.done;
966
+ const cancelled = await this.cancelRun(runId, FORCE_DELETE_CANCEL_REASON);
967
+ if (!cancelled.ok && cancelled.status !== 404) {
968
+ return cancelled;
969
+ }
970
+ if (settle !== undefined) {
971
+ try {
972
+ await this.waitForPromise(settle, 30_000);
973
+ }
974
+ catch (err) {
975
+ return {
976
+ ok: false,
977
+ reason: err instanceof Error ? err.message : String(err),
978
+ status: 500,
979
+ };
980
+ }
981
+ }
982
+ }
983
+ const a2aStore = this.a2aStore;
984
+ if (a2aStore === undefined) {
985
+ return {
986
+ ok: false,
987
+ reason: "A2A store is not available for delete_run",
988
+ status: 500,
989
+ };
990
+ }
991
+ try {
992
+ await deleteRunEverywhere(this.options.store, a2aStore, runId);
993
+ }
994
+ catch (err) {
995
+ const message = err instanceof Error ? err.message : String(err);
996
+ if (message.startsWith("Run not found:")) {
997
+ return { ok: false, reason: message, status: 404 };
998
+ }
999
+ return { ok: false, reason: message, status: 500 };
1000
+ }
1001
+ log.child({ run_id: runId }).info("delete_run", "run deleted", {
1002
+ force,
1003
+ channel,
1004
+ });
1005
+ return { ok: true, runId };
1006
+ }
1007
+ async gcRuns(options = {}) {
1008
+ const execute = options.execute === true;
1009
+ const channel = options.channel ?? "rest";
1010
+ let report;
1011
+ try {
1012
+ report = await runRetentionSweep(this.options.store, this.a2aStore, {
1013
+ execute,
1014
+ now: options.now,
1015
+ windows: options.windows,
1016
+ env: options.env,
1017
+ artifactMaxBytes: options.artifactMaxBytes,
1018
+ bareCacheTtlMs: options.bareCacheTtlMs,
1019
+ });
1020
+ }
1021
+ catch (err) {
1022
+ return {
1023
+ ok: false,
1024
+ reason: err instanceof Error ? err.message : String(err),
1025
+ status: 500,
1026
+ };
1027
+ }
1028
+ if (execute) {
1029
+ for (const runId of report.slimmed) {
1030
+ await this.refreshRunDiskBytes(runId).catch(() => undefined);
1031
+ }
1032
+ log.info("run_retention_sweep", "retention sweep executed", {
1033
+ channel,
1034
+ slimmed: report.slimmed,
1035
+ purged: report.purged,
1036
+ bareCachesEvicted: report.bareCachesEvicted,
1037
+ });
1038
+ }
1039
+ return { ok: true, ...report };
1040
+ }
1041
+ async waitForPromise(promise, timeoutMs) {
1042
+ let timer;
1043
+ try {
1044
+ await Promise.race([
1045
+ promise.then(() => undefined, () => undefined),
1046
+ new Promise((_, reject) => {
1047
+ timer = setTimeout(() => {
1048
+ reject(new Error(`Timed out waiting for run to settle after force cancel`));
1049
+ }, timeoutMs);
1050
+ }),
1051
+ ]);
1052
+ }
1053
+ finally {
1054
+ if (timer !== undefined)
1055
+ clearTimeout(timer);
1056
+ }
1057
+ }
1058
+ async startRunOnce(input, submission) {
1059
+ if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
1060
+ return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
1061
+ }
1062
+ const pending = this.submissionsInFlight.get(submission.key);
1063
+ if (pending) {
1064
+ if (pending.requestHash !== submission.requestHash) {
1065
+ return { ok: false, status: 409, reason: "Submission key was already used for different input" };
1066
+ }
1067
+ const result = await pending.result;
1068
+ return result.ok ? { ...result, reused: true } : result;
1069
+ }
1070
+ const result = this.startSubmittedRun(input, submission);
1071
+ this.submissionsInFlight.set(submission.key, { requestHash: submission.requestHash, result });
1072
+ try {
1073
+ return await result;
1074
+ }
1075
+ finally {
1076
+ this.submissionsInFlight.delete(submission.key);
1077
+ }
1078
+ }
1079
+ async startSubmittedRun(input, submission) {
1080
+ const existing = await this.options.store.getRunBySubmission(submission.key);
1081
+ if (existing)
1082
+ return existingSubmissionResult(existing, submission);
1083
+ try {
1084
+ const result = await this.startRun(input, submission);
1085
+ return result.ok ? { ok: true, runId: result.runId, reused: false, done: result.done } : result;
1086
+ }
1087
+ catch (error) {
1088
+ if (error instanceof RunSubmissionExistsError)
1089
+ return existingSubmissionResult(error.submission, submission);
1090
+ throw error;
1091
+ }
1092
+ }
1093
+ async startRun(input, submission) {
1094
+ if (!this.acceptingWork) {
1095
+ return {
1096
+ ok: false,
1097
+ reason: "Host is shutting down",
1098
+ status: 503,
1099
+ code: "shutting_down",
1100
+ };
1101
+ }
1102
+ const hostCwd = this.options.cwd ?? process.cwd();
1103
+ const wireRoot = typeof input.projectRoot === "string" && input.projectRoot.trim().length > 0
1104
+ ? normalizeProjectRoot(input.projectRoot)
1105
+ : undefined;
1106
+ // Wire root wins. Otherwise path pipelines may derive via findProjectRoot;
1107
+ // inline-only without projectRoot keeps Host projectRoot.
1108
+ const derivedProjectRoot = wireRoot ??
1109
+ (typeof input.pipeline === "string"
1110
+ ? (findProjectRoot(path.dirname(path.resolve(hostCwd, input.pipeline))) ?? this.projectRoot)
1111
+ : this.projectRoot);
1112
+ const cwd = wireRoot ?? hostCwd;
358
1113
  let resolved;
359
1114
  try {
360
1115
  resolved = resolveStartTaskInput(input, cwd);
@@ -389,24 +1144,57 @@ export class RunManager {
389
1144
  gitSha: input.gitSha,
390
1145
  ciPrUrl: input.ciPrUrl,
391
1146
  ciJobUrl: input.ciJobUrl,
392
- }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
1147
+ }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
393
1148
  }
394
- async rerun(runId) {
1149
+ async rerun(runId, options) {
1150
+ if (!this.acceptingWork) {
1151
+ return {
1152
+ ok: false,
1153
+ reason: "Host is shutting down",
1154
+ status: 503,
1155
+ code: "shutting_down",
1156
+ };
1157
+ }
395
1158
  const cwd = this.options.cwd ?? process.cwd();
396
1159
  let pipeline;
397
1160
  let taskYaml;
398
1161
  let rerunCwd = cwd;
399
1162
  let rerunProjectRoot;
1163
+ let skipGates;
1164
+ let ciIdentity;
1165
+ let pinned;
400
1166
  try {
401
1167
  const meta = await this.options.store.readRunMeta(runId);
402
- if (!meta.pipeline_path) {
1168
+ if (meta.pipeline_source === "inline") {
1169
+ const body = await this.options.store.readPipelineBody(runId);
1170
+ if (body === null || body === "") {
1171
+ return {
1172
+ ok: false,
1173
+ reason: `Run ${runId} is missing pipeline_body; re-run of an inline pipeline requires the stored body.`,
1174
+ status: 400,
1175
+ };
1176
+ }
1177
+ try {
1178
+ pipeline = JSON.parse(body);
1179
+ }
1180
+ catch {
1181
+ return {
1182
+ ok: false,
1183
+ reason: `Run ${runId} has invalid pipeline_body JSON`,
1184
+ status: 400,
1185
+ };
1186
+ }
1187
+ }
1188
+ else if (meta.pipeline_path) {
1189
+ pipeline = normalizeCatalogPath(meta.pipeline_path);
1190
+ }
1191
+ else {
403
1192
  return {
404
1193
  ok: false,
405
- reason: `Run ${runId} is missing pipeline_path; re-run requires stored catalog locators.`,
1194
+ reason: `Run ${runId} has neither pipeline_path nor pipeline_body; cannot re-run.`,
406
1195
  status: 400,
407
1196
  };
408
1197
  }
409
- pipeline = normalizeCatalogPath(meta.pipeline_path);
410
1198
  rerunCwd = meta.project_root
411
1199
  ? normalizeCatalogPath(meta.project_root)
412
1200
  : cwd;
@@ -414,16 +1202,44 @@ export class RunManager {
414
1202
  ? normalizeCatalogPath(meta.project_root)
415
1203
  : undefined;
416
1204
  taskYaml = await this.options.store.readTaskYaml(runId);
1205
+ skipGates = meta.skip_gates;
1206
+ ciIdentity = {
1207
+ ...(meta.git_sha !== undefined ? { gitSha: meta.git_sha } : {}),
1208
+ ...(meta.ci_pr_url !== undefined ? { ciPrUrl: meta.ci_pr_url } : {}),
1209
+ ...(meta.ci_job_url !== undefined ? { ciJobUrl: meta.ci_job_url } : {}),
1210
+ };
1211
+ if (options?.pinned) {
1212
+ if (meta.resolved_sha === undefined ||
1213
+ meta.resolved_sha === "" ||
1214
+ meta.ref === undefined ||
1215
+ meta.ref === "") {
1216
+ return {
1217
+ ok: false,
1218
+ reason: `Run ${runId} has no resolved_sha/ref to pin`,
1219
+ status: 400,
1220
+ code: "pinned_sha_unavailable",
1221
+ };
1222
+ }
1223
+ pinned = { ref: meta.ref, resolvedSha: meta.resolved_sha };
1224
+ }
417
1225
  }
418
1226
  catch {
419
1227
  return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
420
1228
  }
421
- return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined, rerunProjectRoot);
1229
+ return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, skipGates, ciIdentity, rerunProjectRoot, undefined, undefined, pinned, options?.callerId);
422
1230
  }
423
1231
  async retryStage(runId, stageId) {
424
1232
  return this.retryStageInternal(runId, stageId, true);
425
1233
  }
426
- async resumeTimedOutStage(runId, stageId) {
1234
+ async resumeTimedOutStage(runId, stageId, options) {
1235
+ if (!this.acceptingWork) {
1236
+ return {
1237
+ ok: false,
1238
+ reason: "Host is shutting down",
1239
+ status: 503,
1240
+ code: "shutting_down",
1241
+ };
1242
+ }
427
1243
  const resumeKey = waitKey(runId, stageId);
428
1244
  if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
429
1245
  return {
@@ -446,13 +1262,16 @@ export class RunManager {
446
1262
  this.resumeInFlight.delete(resumeKey);
447
1263
  return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
448
1264
  }
449
- const eligibility = assertTimedOutStageEligible(stageSnap.status, stageSnap.events);
1265
+ const eligibility = assertResumableStage(stageSnap.status, stageSnap.events);
450
1266
  if (!eligibility.ok) {
451
1267
  this.resumeInFlight.delete(resumeKey);
452
1268
  return eligibility;
453
1269
  }
454
1270
  const latest = await this.options.store.getLatestStageExecution(runId, stageId);
455
1271
  const attemptIndex = latest?.attempt ?? 1;
1272
+ if ((options?.source ?? "explicit") === "explicit" && latest !== null) {
1273
+ await this.options.store.updateStageExecution(runId, stageId, attemptIndex, { auto_resume_count: 0 });
1274
+ }
456
1275
  const wasActive = this.active.has(runId);
457
1276
  const tracked = await this.ensureResumeTracked(runId);
458
1277
  if (!tracked.ok) {
@@ -570,14 +1389,23 @@ export class RunManager {
570
1389
  finally {
571
1390
  this.retryInFlight.delete(retryKey);
572
1391
  }
573
- }
574
- async retryStageInternal(runId, stageId, awaitRoot, options) {
1392
+ }
1393
+ async retryStageInternal(runId, stageId, awaitRoot, options) {
1394
+ if (!this.acceptingWork) {
1395
+ return {
1396
+ ok: false,
1397
+ reason: "Host is shutting down",
1398
+ status: 503,
1399
+ code: "shutting_down",
1400
+ };
1401
+ }
575
1402
  const retryKey = waitKey(runId, stageId);
576
1403
  if (this.retryInFlight.has(retryKey)) {
577
1404
  return {
578
1405
  ok: false,
579
1406
  reason: `Retry already in progress for run ${runId} stage ${stageId}`,
580
1407
  status: 409,
1408
+ code: "retry_in_progress",
581
1409
  };
582
1410
  }
583
1411
  this.retryInFlight.add(retryKey);
@@ -588,6 +1416,7 @@ export class RunManager {
588
1416
  ok: false,
589
1417
  reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
590
1418
  status: 409,
1419
+ code: "manual_recovery_required",
591
1420
  };
592
1421
  }
593
1422
  try {
@@ -1016,6 +1845,7 @@ export class RunManager {
1016
1845
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1017
1846
  executionMode: this.executionMode,
1018
1847
  stageProcessLauncher: this.stageProcessLauncher,
1848
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1019
1849
  });
1020
1850
  this.registerResumeUntrack(runId, done);
1021
1851
  const rest = await done;
@@ -1060,6 +1890,13 @@ export class RunManager {
1060
1890
  try {
1061
1891
  const runMeta = await store.readRunMeta(runId);
1062
1892
  const runProjectRoot = runMeta.project_root ?? this.projectRoot;
1893
+ const { meta, task, loaded } = await loadRunContext(store, runId, runProjectRoot);
1894
+ const stageBinding = stageBindingEnvFromRun({
1895
+ meta,
1896
+ task,
1897
+ runWorkspaceDir: workspaceDir,
1898
+ hostEnv: process.env,
1899
+ });
1063
1900
  const launchResult = await launcher.launch({
1064
1901
  runId,
1065
1902
  stageId,
@@ -1068,6 +1905,8 @@ export class RunManager {
1068
1905
  resumeAnswer: opaqueAnswer,
1069
1906
  attempt,
1070
1907
  sessionFilePath,
1908
+ env: stageBinding.env,
1909
+ bindingKind: stageBinding.kind,
1071
1910
  ...(this.options.operatorCatalog !== undefined
1072
1911
  ? { operatorCatalog: this.options.operatorCatalog }
1073
1912
  : {}),
@@ -1090,7 +1929,6 @@ export class RunManager {
1090
1929
  catch {
1091
1930
  // downstream resume may read envelope from store
1092
1931
  }
1093
- const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
1094
1932
  const rest = await resumeRun({
1095
1933
  prepared: {
1096
1934
  task,
@@ -1109,6 +1947,7 @@ export class RunManager {
1109
1947
  initialPrior,
1110
1948
  executionMode: this.executionMode,
1111
1949
  stageProcessLauncher: launcher,
1950
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1112
1951
  });
1113
1952
  if (rest.outcome === "failed") {
1114
1953
  return { ok: false, reason: rest.reason };
@@ -1156,36 +1995,273 @@ export class RunManager {
1156
1995
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
1157
1996
  }
1158
1997
  }
1159
- async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath) {
1998
+ async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission, pinned, callerId, skills) {
1999
+ const persistence = pipelinePersistenceForStart(pipeline);
2000
+ if (!persistence.ok) {
2001
+ return {
2002
+ ok: false,
2003
+ reason: `Inline pipeline body is ${persistence.bytes} bytes; max is ${persistence.maxBytes}`,
2004
+ status: 400,
2005
+ code: INLINE_PIPELINE_TOO_LARGE,
2006
+ };
2007
+ }
2008
+ const skillsCheck = validateSkillsPayload(skills, {
2009
+ pipelineBytes: pipelineBodyBytes(persistence.fields),
2010
+ });
2011
+ if (!skillsCheck.ok) {
2012
+ return {
2013
+ ok: false,
2014
+ reason: skillsCheck.reason,
2015
+ status: 400,
2016
+ code: skillsCheck.code,
2017
+ };
2018
+ }
2019
+ const validatedSkills = skillsCheck.skills;
2020
+ const hasSkills = Object.keys(validatedSkills).length > 0;
2021
+ const resolvedProjectRoot = normalizeCatalogPath(projectRoot ?? this.projectRoot);
2022
+ let task;
1160
2023
  let checkoutKey;
2024
+ let pathCheckoutRoot;
2025
+ let binding;
2026
+ let pipelineId;
2027
+ let pipelineDag;
2028
+ let pipelinePath;
2029
+ let loadedPipeline;
2030
+ let toolchainChecks = [];
1161
2031
  try {
1162
- const task = loadTaskFromYaml(taskYaml, taskLabel);
1163
- const checkoutRoot = await resolveAndValidateCheckout(task, checkoutOverride, cwd);
1164
- checkoutKey =
1165
- checkoutRoot !== undefined
1166
- ? await toCheckoutLeaseKey(checkoutRoot)
2032
+ const loadedTask = loadTaskFromYamlOutcome(taskYaml, taskLabel);
2033
+ if (!loadedTask.ok) {
2034
+ const issue = loadedTask.issues[0];
2035
+ return {
2036
+ ok: false,
2037
+ reason: issue?.message ?? "Invalid task",
2038
+ status: 400,
2039
+ ...(issue?.code !== undefined
2040
+ ? { code: issue.code }
2041
+ : {}),
2042
+ };
2043
+ }
2044
+ task = loadedTask.value;
2045
+ const bindingOutcome = resolveWorkspaceBinding(task, { checkoutOverride });
2046
+ if (!bindingOutcome.ok) {
2047
+ const issue = bindingOutcome.issues[0];
2048
+ return {
2049
+ ok: false,
2050
+ reason: issue?.message ?? "Invalid workspace binding",
2051
+ status: 400,
2052
+ ...(issue?.code !== undefined
2053
+ ? { code: issue.code }
2054
+ : {}),
2055
+ };
2056
+ }
2057
+ binding = bindingOutcome.value;
2058
+ if (binding.kind === "checkout") {
2059
+ pathCheckoutRoot = await resolveAndValidateCheckout(task, checkoutOverride, cwd);
2060
+ checkoutKey =
2061
+ pathCheckoutRoot !== undefined
2062
+ ? await toCheckoutLeaseKey(pathCheckoutRoot)
2063
+ : undefined;
2064
+ }
2065
+ const loadResult = await loadPipelineValidated(pipeline, {
2066
+ cwd,
2067
+ projectRoot: resolvedProjectRoot,
2068
+ validateStages: true,
2069
+ });
2070
+ if (!loadResult.ok) {
2071
+ throw new PipelineValidationError(buildValidationResult("pipeline", loadResult.findings, false));
2072
+ }
2073
+ loadedPipeline = loadResult.loaded;
2074
+ const pairing = checkTaskEntryInput(task, loadResult.loaded, {
2075
+ cwd,
2076
+ taskPath,
2077
+ });
2078
+ if (pairing.some((finding) => finding.severity === "error")) {
2079
+ throw new PipelineValidationError(buildValidationResult("pipeline", pairing, false));
2080
+ }
2081
+ pipelineId = loadResult.loaded.pipeline.id;
2082
+ pipelineDag = buildPipelineDagSnapshotFromLoaded(loadResult.loaded);
2083
+ pipelinePath =
2084
+ typeof pipeline === "string"
2085
+ ? normalizeCatalogPath(loadResult.loaded.pipelinePath)
1167
2086
  : undefined;
2087
+ try {
2088
+ const pipelineAgent = asAgentBackendId(loadResult.loaded.pipeline.agent);
2089
+ assertClaudeNotRoot({ backendId: pipelineAgent });
2090
+ for (const stage of loadResult.loaded.stages) {
2091
+ assertClaudeNotRoot({
2092
+ backendId: asAgentBackendId(stage.agent) ?? pipelineAgent,
2093
+ });
2094
+ }
2095
+ }
2096
+ catch (err) {
2097
+ if (err instanceof ClaudeRootError) {
2098
+ return { ok: false, reason: err.message, status: 400, code: err.code };
2099
+ }
2100
+ throw err;
2101
+ }
2102
+ const preflight = await runPipelinePreflight(loadResult.loaded, {
2103
+ projectRoot: resolvedProjectRoot,
2104
+ forStart: true,
2105
+ });
2106
+ if (!preflight.ok) {
2107
+ const code = preflightFailureCode(preflight, { forStart: true }) ??
2108
+ "missing_tool";
2109
+ return {
2110
+ ok: false,
2111
+ reason: `Pipeline preflight failed: ${code}`,
2112
+ status: 400,
2113
+ code,
2114
+ };
2115
+ }
2116
+ toolchainChecks = preflight.toolchain.checks;
1168
2117
  }
1169
2118
  catch (err) {
2119
+ if (err instanceof PipelineValidationError)
2120
+ throw err;
2121
+ if (err instanceof PipelinePreflightError) {
2122
+ return {
2123
+ ok: false,
2124
+ reason: err.message,
2125
+ status: 400,
2126
+ code: err.code,
2127
+ };
2128
+ }
1170
2129
  return {
1171
2130
  ok: false,
1172
2131
  reason: err instanceof Error ? err.message : String(err),
1173
2132
  status: 400,
1174
2133
  };
1175
2134
  }
1176
- const reserved = this.tryReserve(checkoutKey);
1177
- if (!reserved.ok)
1178
- return reserved.failure;
2135
+ const diskGate = await this.checkDiskFloorAdmission();
2136
+ if (diskGate !== undefined)
2137
+ return diskGate;
2138
+ const admitted = this.tryAdmitOrEnqueue(checkoutKey, resolvedProjectRoot, callerId);
2139
+ if (admitted.action === "reject")
2140
+ return admitted.failure;
2141
+ if (admitted.action === "enqueue") {
2142
+ const runId = newRunId();
2143
+ const gitIdentity = resolveEffectiveGitIdentity(process.env, task.git_identity);
2144
+ const createdAt = new Date().toISOString();
2145
+ const auth = getRequestAuth();
2146
+ const surface = auth?.surface ?? "cli";
2147
+ const secretNames = loadedPipeline.stages.flatMap((s) => (s.secrets ?? []).map((d) => d.name));
2148
+ const namedSecrets = collectDeclaredSecretValues(secretNames).filter((s) => shouldRegisterValue(s.value));
2149
+ if (namedSecrets.length > 0) {
2150
+ registerNamedSecrets(namedSecrets);
2151
+ }
2152
+ const runManifest = buildInitialRunManifest({
2153
+ runId,
2154
+ createdAt,
2155
+ callerId: callerId ?? null,
2156
+ surface,
2157
+ binding: bindingFromFields({}),
2158
+ pipelineSource: persistence.fields.pipelineSource,
2159
+ pipelinePath,
2160
+ pipelineBody: persistence.fields.pipelineBody ?? null,
2161
+ taskYaml,
2162
+ taskPath: taskPath
2163
+ ? normalizeCatalogPath(path.resolve(cwd, taskPath))
2164
+ : undefined,
2165
+ skills: hasSkills ? validatedSkills : undefined,
2166
+ stages: loadedPipeline.stages,
2167
+ toolchain: toolchainChecks,
2168
+ namedSecrets,
2169
+ });
2170
+ const created = await this.options.store.createRun({
2171
+ submission,
2172
+ runId,
2173
+ pipelineId,
2174
+ taskYaml,
2175
+ taskId: task.id,
2176
+ pipelineDag,
2177
+ pipelinePath,
2178
+ taskPath: taskPath
2179
+ ? normalizeCatalogPath(path.resolve(cwd, taskPath))
2180
+ : undefined,
2181
+ projectRoot: resolvedProjectRoot,
2182
+ gitSha: ciIdentity?.gitSha,
2183
+ ciPrUrl: ciIdentity?.ciPrUrl,
2184
+ ciJobUrl: ciIdentity?.ciJobUrl,
2185
+ gitAuthorName: gitIdentity.name,
2186
+ gitAuthorEmail: gitIdentity.email,
2187
+ status: "queued",
2188
+ pipelineSource: persistence.fields.pipelineSource,
2189
+ ...(persistence.fields.pipelineBody !== undefined
2190
+ ? { pipelineBody: persistence.fields.pipelineBody }
2191
+ : {}),
2192
+ ...(callerId !== undefined && callerId !== null
2193
+ ? { callerId }
2194
+ : {}),
2195
+ runManifest,
2196
+ skipGates,
2197
+ });
2198
+ if (hasSkills) {
2199
+ await materializeRunSkills(created.workspaceDir, validatedSkills);
2200
+ }
2201
+ const meta = await this.options.store.readRunMeta(created.runId);
2202
+ const queuePosition = this.admissionQueue.enqueue(resolvedProjectRoot, {
2203
+ runId: created.runId,
2204
+ createdAt: meta.created_at,
2205
+ callerId: callerId ?? meta.caller_id ?? null,
2206
+ });
2207
+ this.pendingQueuedStarts.set(created.runId, {
2208
+ taskYaml,
2209
+ pipeline,
2210
+ taskLabel,
2211
+ cwd,
2212
+ projectRoot: resolvedProjectRoot,
2213
+ checkoutOverride,
2214
+ skipGates,
2215
+ ciIdentity,
2216
+ taskPath,
2217
+ submission,
2218
+ pinned,
2219
+ pathCheckoutRoot,
2220
+ binding,
2221
+ checkoutKey,
2222
+ callerId,
2223
+ ...(hasSkills ? { skills: validatedSkills } : {}),
2224
+ });
2225
+ const done = this.ensureQueuedDone(created.runId);
2226
+ return {
2227
+ ok: true,
2228
+ runId: created.runId,
2229
+ done,
2230
+ queued: true,
2231
+ queuePosition,
2232
+ ...(admitted.reason === "caller_quota"
2233
+ ? { queuedCode: "busy_caller_quota" }
2234
+ : {}),
2235
+ };
2236
+ }
2237
+ const runId = newRunId();
2238
+ let rollback = async () => { };
1179
2239
  try {
2240
+ const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
2241
+ runId,
2242
+ task,
2243
+ binding,
2244
+ checkoutRoot: pathCheckoutRoot,
2245
+ pinned,
2246
+ });
2247
+ rollback = linkRollback;
2248
+ const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
1180
2249
  const started = await startPipeline({
2250
+ submission,
1181
2251
  agent: this.options.agent,
1182
2252
  store: this.options.store,
1183
2253
  taskYaml,
1184
2254
  taskPath,
1185
2255
  pipeline,
1186
2256
  cwd,
1187
- projectRoot: projectRoot ?? this.projectRoot,
2257
+ projectRoot: resolvedProjectRoot,
1188
2258
  checkoutOverride,
2259
+ runId: materialized.runId,
2260
+ checkoutRoot: materialized.checkoutRoot,
2261
+ repository: materialized.repository,
2262
+ ref: materialized.ref,
2263
+ resolvedSha: materialized.resolvedSha,
2264
+ runBranch: materialized.runBranch,
1189
2265
  gitSha: ciIdentity?.gitSha,
1190
2266
  ciPrUrl: ciIdentity?.ciPrUrl,
1191
2267
  ciJobUrl: ciIdentity?.ciJobUrl,
@@ -1195,15 +2271,44 @@ export class RunManager {
1195
2271
  stageProcessLauncher: this.stageProcessLauncher,
1196
2272
  operatorCatalog: this.options.operatorCatalog,
1197
2273
  skipGates,
2274
+ schedulingHalt,
2275
+ callerId,
2276
+ ...(hasSkills ? { skills: validatedSkills } : {}),
1198
2277
  });
1199
- this.track(reserved.provisionalId, started.runId, started.done);
2278
+ this.track(admitted.provisionalId, started.runId, started.done);
1200
2279
  return { ok: true, runId: started.runId, done: started.done };
1201
2280
  }
1202
2281
  catch (err) {
1203
- this.clearReservation(reserved.provisionalId);
1204
- if (err instanceof PipelineValidationError) {
2282
+ await rollback().catch(() => undefined);
2283
+ this.clearReservation(admitted.provisionalId);
2284
+ if (err instanceof InlinePipelineTooLargeError) {
2285
+ return {
2286
+ ok: false,
2287
+ reason: err.message,
2288
+ status: 400,
2289
+ code: err.code,
2290
+ };
2291
+ }
2292
+ if (err instanceof PipelinePreflightError) {
2293
+ return {
2294
+ ok: false,
2295
+ reason: err.message,
2296
+ status: 400,
2297
+ code: err.code,
2298
+ };
2299
+ }
2300
+ if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
1205
2301
  throw err;
1206
2302
  }
2303
+ if (err instanceof StartLinkError) {
2304
+ return {
2305
+ ok: false,
2306
+ reason: err.message,
2307
+ status: err.status,
2308
+ code: err.code,
2309
+ ...(err.stderr !== undefined ? { stderr: err.stderr } : {}),
2310
+ };
2311
+ }
1207
2312
  return {
1208
2313
  ok: false,
1209
2314
  reason: err instanceof Error ? err.message : String(err),
@@ -1213,24 +2318,87 @@ export class RunManager {
1213
2318
  }
1214
2319
  busyFailure(code, extras) {
1215
2320
  const activeRunIds = this.getActiveRunIds();
1216
- const reason = code === "busy_capacity"
1217
- ? `Capacity full: ${this.active.size}/${this.maxConcurrent} active runs`
1218
- : `Checkout in use by run ${extras?.conflictingRunId ?? "unknown"}`;
2321
+ const scope = extras?.scope ?? (code === "busy_caller_quota" ? "caller" : "global");
2322
+ const activeCount = extras?.activeCount ?? this.active.size;
2323
+ const maxConcurrent = extras?.maxConcurrent ?? this.maxConcurrent;
2324
+ const reason = code === "busy_caller_quota"
2325
+ ? `Caller quota full: ${activeCount}/${maxConcurrent} active runs for caller ${extras?.caller_id ?? "unknown"}`
2326
+ : code === "busy_capacity"
2327
+ ? scope === "project"
2328
+ ? `Project capacity full: ${activeCount}/${maxConcurrent} active runs for ${extras?.project_root ?? "project"}`
2329
+ : this.active.size >= this.maxConcurrent &&
2330
+ this.admissionQueue.size >= this.maxQueued &&
2331
+ this.maxQueued > 0
2332
+ ? `Admission queue full: ${this.admissionQueue.size}/${this.maxQueued} queued runs`
2333
+ : `Capacity full: ${this.active.size}/${this.maxConcurrent} active runs`
2334
+ : `Checkout in use by run ${extras?.conflictingRunId ?? "unknown"}`;
1219
2335
  return {
1220
2336
  ok: false,
1221
2337
  reason,
1222
2338
  status: 409,
1223
2339
  code,
1224
- activeCount: this.active.size,
1225
- maxConcurrent: this.maxConcurrent,
2340
+ activeCount,
2341
+ maxConcurrent,
1226
2342
  activeRunIds,
1227
- ...extras,
2343
+ scope,
2344
+ ...(extras?.project_root !== undefined
2345
+ ? { project_root: extras.project_root }
2346
+ : {}),
2347
+ ...(extras?.caller_id !== undefined ? { caller_id: extras.caller_id } : {}),
2348
+ ...(extras?.conflictingRunId !== undefined
2349
+ ? { conflictingRunId: extras.conflictingRunId }
2350
+ : {}),
2351
+ ...(extras?.conflictingCheckout !== undefined
2352
+ ? { conflictingCheckout: extras.conflictingCheckout }
2353
+ : {}),
1228
2354
  };
1229
2355
  }
1230
- tryReserve(checkoutKey) {
1231
- if (this.active.size >= this.maxConcurrent) {
1232
- return { ok: false, failure: this.busyFailure("busy_capacity") };
2356
+ insufficientDiskFailure(freeBytes, minFreeBytes) {
2357
+ return {
2358
+ ok: false,
2359
+ reason: `Insufficient free disk: ${freeBytes} bytes free, floor ${minFreeBytes} bytes`,
2360
+ status: 409,
2361
+ code: "insufficient_disk",
2362
+ freeBytes,
2363
+ minFreeBytes,
2364
+ };
2365
+ }
2366
+ async checkDiskFloorAdmission() {
2367
+ try {
2368
+ const readFree = this.options.freeSpaceReader ?? readFilesystemSize;
2369
+ const size = await readFree(globalStageflowHome());
2370
+ const minFreeBytes = resolveMinFreeDiskFloor(process.env.STAGEFLOW_MIN_FREE_DISK_BYTES, size.totalBytes);
2371
+ if (size.freeBytes < minFreeBytes) {
2372
+ return this.insufficientDiskFailure(size.freeBytes, minFreeBytes);
2373
+ }
2374
+ return undefined;
2375
+ }
2376
+ catch (err) {
2377
+ // Default floor is always active via resolveMinFreeDiskFloor — fail closed.
2378
+ return {
2379
+ ok: false,
2380
+ reason: `Disk free-space check failed: ${err instanceof Error ? err.message : String(err)}`,
2381
+ status: 503,
2382
+ code: "disk_check_failed",
2383
+ };
2384
+ }
2385
+ }
2386
+ async isProjectRootAvailable(projectRoot) {
2387
+ const provider = this.options.knownWritableProjectRoots;
2388
+ if (provider !== undefined) {
2389
+ const roots = [...(await provider())].map((r) => normalizeCatalogPath(r));
2390
+ const normalized = normalizeCatalogPath(projectRoot);
2391
+ return roots.includes(normalized);
2392
+ }
2393
+ try {
2394
+ await access(projectRoot, constants.W_OK);
2395
+ return true;
2396
+ }
2397
+ catch {
2398
+ return false;
1233
2399
  }
2400
+ }
2401
+ tryAdmitOrEnqueue(checkoutKey, projectRoot = this.projectRoot, callerId) {
1234
2402
  if (checkoutKey !== undefined) {
1235
2403
  const holder = this.checkoutLeases.get(checkoutKey);
1236
2404
  if (holder !== undefined) {
@@ -1238,7 +2406,7 @@ export class RunManager {
1238
2406
  ? undefined
1239
2407
  : holder;
1240
2408
  return {
1241
- ok: false,
2409
+ action: "reject",
1242
2410
  failure: this.busyFailure("busy_checkout", {
1243
2411
  conflictingRunId,
1244
2412
  conflictingCheckout: checkoutKey,
@@ -1246,16 +2414,378 @@ export class RunManager {
1246
2414
  };
1247
2415
  }
1248
2416
  }
1249
- const provisionalId = randomUUID();
1250
- this.provisionalIds.add(provisionalId);
1251
- this.active.set(provisionalId, {
1252
- checkoutKey,
1253
- generation: ++this.trackingGeneration,
2417
+ const normalizedRoot = normalizeCatalogPath(projectRoot);
2418
+ if (this.maxConcurrentPerProject !== undefined) {
2419
+ const projectActive = this.countActiveForProject(normalizedRoot);
2420
+ if (projectActive >= this.maxConcurrentPerProject) {
2421
+ return {
2422
+ action: "reject",
2423
+ failure: this.busyFailure("busy_capacity", {
2424
+ scope: "project",
2425
+ project_root: normalizedRoot,
2426
+ activeCount: projectActive,
2427
+ maxConcurrent: this.maxConcurrentPerProject,
2428
+ }),
2429
+ };
2430
+ }
2431
+ }
2432
+ if (this.active.size < this.maxConcurrent) {
2433
+ const quota = this.callerQuotaLimit(callerId);
2434
+ if (quota !== undefined &&
2435
+ callerId !== undefined &&
2436
+ callerId !== null &&
2437
+ this.countActiveForCaller(callerId) >= quota) {
2438
+ if (this.admissionQueue.size < this.maxQueued) {
2439
+ return { action: "enqueue", reason: "caller_quota" };
2440
+ }
2441
+ return {
2442
+ action: "reject",
2443
+ failure: this.busyFailure("busy_caller_quota", {
2444
+ scope: "caller",
2445
+ caller_id: callerId,
2446
+ activeCount: this.countActiveForCaller(callerId),
2447
+ maxConcurrent: quota,
2448
+ }),
2449
+ };
2450
+ }
2451
+ const provisionalId = randomUUID();
2452
+ this.provisionalIds.add(provisionalId);
2453
+ this.active.set(provisionalId, {
2454
+ checkoutKey,
2455
+ projectRoot: normalizedRoot,
2456
+ callerId: callerId ?? null,
2457
+ generation: ++this.trackingGeneration,
2458
+ });
2459
+ if (checkoutKey !== undefined) {
2460
+ this.checkoutLeases.set(checkoutKey, provisionalId);
2461
+ }
2462
+ return { action: "reserve", provisionalId };
2463
+ }
2464
+ if (this.admissionQueue.size < this.maxQueued) {
2465
+ return { action: "enqueue", reason: "capacity" };
2466
+ }
2467
+ return {
2468
+ action: "reject",
2469
+ failure: this.busyFailure("busy_capacity", { scope: "global" }),
2470
+ };
2471
+ }
2472
+ tryReserve(checkoutKey, projectRoot = this.projectRoot, callerId) {
2473
+ const admitted = this.tryAdmitOrEnqueue(checkoutKey, projectRoot, callerId);
2474
+ if (admitted.action === "reserve") {
2475
+ return { ok: true, provisionalId: admitted.provisionalId };
2476
+ }
2477
+ if (admitted.action === "enqueue") {
2478
+ if (admitted.reason === "caller_quota") {
2479
+ const quota = this.callerQuotaLimit(callerId) ?? 0;
2480
+ return {
2481
+ ok: false,
2482
+ failure: this.busyFailure("busy_caller_quota", {
2483
+ scope: "caller",
2484
+ caller_id: callerId ?? undefined,
2485
+ activeCount: callerId !== undefined && callerId !== null
2486
+ ? this.countActiveForCaller(callerId)
2487
+ : 0,
2488
+ maxConcurrent: quota,
2489
+ }),
2490
+ };
2491
+ }
2492
+ return { ok: false, failure: this.busyFailure("busy_capacity") };
2493
+ }
2494
+ return { ok: false, failure: admitted.failure };
2495
+ }
2496
+ ensureQueuedDone(runId) {
2497
+ const existing = this.queuedDone.get(runId);
2498
+ if (existing !== undefined)
2499
+ return existing.promise;
2500
+ let resolve;
2501
+ const promise = new Promise((res) => {
2502
+ resolve = res;
1254
2503
  });
1255
- if (checkoutKey !== undefined) {
1256
- this.checkoutLeases.set(checkoutKey, provisionalId);
2504
+ this.queuedDone.set(runId, { promise, resolve });
2505
+ return promise;
2506
+ }
2507
+ resolveQueuedDone(runId, result) {
2508
+ const deferred = this.queuedDone.get(runId);
2509
+ if (deferred === undefined)
2510
+ return;
2511
+ this.queuedDone.delete(runId);
2512
+ deferred.resolve(result);
2513
+ }
2514
+ attachQueuedDone(runId, done) {
2515
+ void done.then((result) => this.resolveQueuedDone(runId, result), (err) => this.resolveQueuedDone(runId, {
2516
+ ok: false,
2517
+ outcome: "failed",
2518
+ runDir: this.options.store.getWorkspaceDir(runId),
2519
+ runId,
2520
+ reason: err instanceof Error ? err.message : String(err),
2521
+ }));
2522
+ }
2523
+ async drainAdmissionQueue() {
2524
+ if (!this.acceptingWork)
2525
+ return;
2526
+ if (this.admissionDrainInFlight)
2527
+ return;
2528
+ this.admissionDrainInFlight = true;
2529
+ try {
2530
+ const blockedRoots = new Set();
2531
+ const blockedCallers = new Set();
2532
+ while (this.acceptingWork && this.active.size < this.maxConcurrent) {
2533
+ const next = this.admissionQueue.dequeueNext(blockedRoots, blockedCallers);
2534
+ if (next === undefined)
2535
+ break;
2536
+ const outcome = await this.startDequeuedAdmission(next, blockedCallers);
2537
+ if (outcome === "checkout_busy") {
2538
+ this.admissionQueue.requeueFront(next.projectRoot, next.entry);
2539
+ blockedRoots.add(next.projectRoot);
2540
+ continue;
2541
+ }
2542
+ if (outcome === "project_capacity_busy") {
2543
+ blockedRoots.add(next.projectRoot);
2544
+ continue;
2545
+ }
2546
+ if (outcome === "caller_quota_busy") {
2547
+ continue;
2548
+ }
2549
+ if (outcome === "capacity_busy") {
2550
+ break;
2551
+ }
2552
+ }
2553
+ }
2554
+ finally {
2555
+ this.admissionDrainInFlight = false;
2556
+ }
2557
+ }
2558
+ async startDequeuedAdmission(next, blockedCallers) {
2559
+ if (!this.acceptingWork) {
2560
+ this.admissionQueue.requeueFront(next.projectRoot, next.entry);
2561
+ return "capacity_busy";
2562
+ }
2563
+ const { runId } = next.entry;
2564
+ let meta;
2565
+ try {
2566
+ meta = await this.options.store.readRunMeta(runId);
2567
+ }
2568
+ catch {
2569
+ this.pendingQueuedStarts.delete(runId);
2570
+ return "cancelled";
2571
+ }
2572
+ if (meta.status !== "queued") {
2573
+ this.pendingQueuedStarts.delete(runId);
2574
+ return "cancelled";
2575
+ }
2576
+ if (!(await this.isProjectRootAvailable(next.projectRoot))) {
2577
+ await this.cancelRun(runId, PROJECT_ROOT_UNAVAILABLE_REASON);
2578
+ return "cancelled";
2579
+ }
2580
+ const diskGate = await this.checkDiskFloorAdmission();
2581
+ if (diskGate !== undefined) {
2582
+ await this.cancelRun(runId, INSUFFICIENT_DISK_CANCEL_REASON);
2583
+ return "cancelled";
2584
+ }
2585
+ const pending = this.pendingQueuedStarts.get(runId);
2586
+ let taskYaml;
2587
+ let task;
2588
+ let binding;
2589
+ let pathCheckoutRoot;
2590
+ let checkoutKey;
2591
+ let pipeline;
2592
+ let cwd;
2593
+ let projectRoot;
2594
+ let checkoutOverride;
2595
+ let skipGates;
2596
+ let ciIdentity;
2597
+ let taskPath;
2598
+ let submission;
2599
+ let pinned;
2600
+ let callerId = pending?.callerId ?? meta.caller_id ?? null;
2601
+ let skills = pending?.skills;
2602
+ if (callerId !== undefined &&
2603
+ callerId !== null &&
2604
+ blockedCallers.has(callerId.toLowerCase())) {
2605
+ next.entry.callerId = next.entry.callerId ?? callerId;
2606
+ this.admissionQueue.requeueFront(next.projectRoot, next.entry);
2607
+ return "caller_quota_busy";
2608
+ }
2609
+ try {
2610
+ if (pending !== undefined) {
2611
+ taskYaml = pending.taskYaml;
2612
+ binding = pending.binding;
2613
+ pathCheckoutRoot = pending.pathCheckoutRoot;
2614
+ checkoutKey = pending.checkoutKey;
2615
+ pipeline = pending.pipeline;
2616
+ cwd = pending.cwd;
2617
+ projectRoot = pending.projectRoot;
2618
+ checkoutOverride = pending.checkoutOverride;
2619
+ skipGates = pending.skipGates;
2620
+ ciIdentity = pending.ciIdentity;
2621
+ taskPath = pending.taskPath;
2622
+ submission = pending.submission;
2623
+ pinned = pending.pinned;
2624
+ callerId = pending.callerId ?? meta.caller_id ?? null;
2625
+ const loadedTask = loadTaskFromYamlOutcome(taskYaml, pending.taskLabel);
2626
+ if (!loadedTask.ok) {
2627
+ await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
2628
+ return "cancelled";
2629
+ }
2630
+ task = loadedTask.value;
2631
+ }
2632
+ else {
2633
+ taskYaml = await this.options.store.readTaskYaml(runId);
2634
+ const loadedTask = loadTaskFromYamlOutcome(taskYaml, `run ${runId} task`);
2635
+ if (!loadedTask.ok) {
2636
+ await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
2637
+ return "cancelled";
2638
+ }
2639
+ task = loadedTask.value;
2640
+ const bindingOutcome = resolveWorkspaceBinding(task, {});
2641
+ if (!bindingOutcome.ok) {
2642
+ await this.cancelRun(runId, bindingOutcome.issues[0]?.message ?? "Invalid workspace binding");
2643
+ return "cancelled";
2644
+ }
2645
+ binding = bindingOutcome.value;
2646
+ cwd = meta.project_root ?? this.cwd;
2647
+ projectRoot = normalizeCatalogPath(meta.project_root ?? this.projectRoot);
2648
+ if (binding.kind === "checkout") {
2649
+ pathCheckoutRoot = await resolveAndValidateCheckout(task, undefined, cwd);
2650
+ checkoutKey =
2651
+ pathCheckoutRoot !== undefined
2652
+ ? await toCheckoutLeaseKey(pathCheckoutRoot)
2653
+ : undefined;
2654
+ }
2655
+ if (meta.pipeline_source === "inline") {
2656
+ const body = await this.options.store.readPipelineBody(runId);
2657
+ if (body === null || body === "") {
2658
+ await this.cancelRun(runId, "pipeline_body unavailable after restart");
2659
+ return "cancelled";
2660
+ }
2661
+ try {
2662
+ pipeline = JSON.parse(body);
2663
+ }
2664
+ catch {
2665
+ await this.cancelRun(runId, "pipeline_body invalid after restart");
2666
+ return "cancelled";
2667
+ }
2668
+ }
2669
+ else if (meta.pipeline_path) {
2670
+ pipeline = normalizeCatalogPath(meta.pipeline_path);
2671
+ }
2672
+ else {
2673
+ await this.cancelRun(runId, "pipeline_path unavailable after restart");
2674
+ return "cancelled";
2675
+ }
2676
+ taskPath = meta.task_path;
2677
+ skipGates = meta.skip_gates;
2678
+ ciIdentity = {
2679
+ gitSha: meta.git_sha,
2680
+ ciPrUrl: meta.ci_pr_url,
2681
+ ciJobUrl: meta.ci_job_url,
2682
+ };
2683
+ }
2684
+ }
2685
+ catch (err) {
2686
+ await this.cancelRun(runId, err instanceof Error ? err.message : String(err));
2687
+ return "cancelled";
2688
+ }
2689
+ const reserved = this.tryReserve(checkoutKey, projectRoot, callerId);
2690
+ if (!reserved.ok) {
2691
+ if (reserved.failure.code === "busy_checkout") {
2692
+ return "checkout_busy";
2693
+ }
2694
+ this.admissionQueue.requeueFront(next.projectRoot, next.entry);
2695
+ if (reserved.failure.code === "busy_capacity" &&
2696
+ reserved.failure.scope === "project") {
2697
+ return "project_capacity_busy";
2698
+ }
2699
+ if (reserved.failure.code === "busy_caller_quota") {
2700
+ if (callerId !== undefined && callerId !== null) {
2701
+ blockedCallers.add(callerId.toLowerCase());
2702
+ next.entry.callerId = next.entry.callerId ?? callerId;
2703
+ }
2704
+ return "caller_quota_busy";
2705
+ }
2706
+ return "capacity_busy";
2707
+ }
2708
+ let rollback = async () => { };
2709
+ try {
2710
+ if (this.options.onBeforeQueuedMaterialize !== undefined) {
2711
+ await this.options.onBeforeQueuedMaterialize(runId);
2712
+ }
2713
+ const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
2714
+ runId,
2715
+ task,
2716
+ binding,
2717
+ checkoutRoot: pathCheckoutRoot,
2718
+ pinned,
2719
+ });
2720
+ rollback = linkRollback;
2721
+ let metaAfterMaterialize;
2722
+ try {
2723
+ metaAfterMaterialize = await this.options.store.readRunMeta(runId);
2724
+ }
2725
+ catch {
2726
+ await rollback().catch(() => undefined);
2727
+ this.clearReservation(reserved.provisionalId);
2728
+ this.pendingQueuedStarts.delete(runId);
2729
+ return "cancelled";
2730
+ }
2731
+ if (metaAfterMaterialize.status !== "queued") {
2732
+ await rollback().catch(() => undefined);
2733
+ this.clearReservation(reserved.provisionalId);
2734
+ this.pendingQueuedStarts.delete(runId);
2735
+ this.schedulingHalts.delete(runId);
2736
+ return "cancelled";
2737
+ }
2738
+ const schedulingHalt = this.ensureSchedulingHalt(runId);
2739
+ const started = await startPipeline({
2740
+ submission,
2741
+ agent: this.options.agent,
2742
+ store: this.options.store,
2743
+ taskYaml,
2744
+ taskPath,
2745
+ pipeline,
2746
+ cwd,
2747
+ projectRoot,
2748
+ checkoutOverride,
2749
+ runId: materialized.runId,
2750
+ reuseExistingRun: true,
2751
+ checkoutRoot: materialized.checkoutRoot,
2752
+ repository: materialized.repository,
2753
+ ref: materialized.ref,
2754
+ resolvedSha: materialized.resolvedSha,
2755
+ runBranch: materialized.runBranch,
2756
+ gitSha: ciIdentity?.gitSha,
2757
+ ciPrUrl: ciIdentity?.ciPrUrl,
2758
+ ciJobUrl: ciIdentity?.ciJobUrl,
2759
+ hitl: this.hitl,
2760
+ maxActiveStagesPerRun: this.maxActiveStagesPerRun,
2761
+ executionMode: this.executionMode,
2762
+ stageProcessLauncher: this.stageProcessLauncher,
2763
+ operatorCatalog: this.options.operatorCatalog,
2764
+ skipGates,
2765
+ schedulingHalt,
2766
+ ...(skills !== undefined ? { skills } : {}),
2767
+ });
2768
+ this.pendingQueuedStarts.delete(runId);
2769
+ this.track(reserved.provisionalId, started.runId, started.done);
2770
+ this.attachQueuedDone(started.runId, started.done);
2771
+ return "started";
2772
+ }
2773
+ catch (err) {
2774
+ await rollback().catch(() => undefined);
2775
+ this.clearReservation(reserved.provisionalId);
2776
+ this.pendingQueuedStarts.delete(runId);
2777
+ this.schedulingHalts.delete(runId);
2778
+ if (err instanceof QueuedRunActivationAborted) {
2779
+ return "cancelled";
2780
+ }
2781
+ const reason = err instanceof StartLinkError
2782
+ ? err.code
2783
+ : err instanceof Error
2784
+ ? err.message
2785
+ : String(err);
2786
+ await this.cancelRun(runId, reason);
2787
+ return "cancelled";
1257
2788
  }
1258
- return { ok: true, provisionalId };
1259
2789
  }
1260
2790
  clearReservation(provisionalId) {
1261
2791
  this.removeActiveEntry(provisionalId, true);
@@ -1292,7 +2822,9 @@ export class RunManager {
1292
2822
  return false;
1293
2823
  const tracked = await this.ensureResumeTracked(runId);
1294
2824
  if (!tracked.ok) {
1295
- console.error(`resumeStalledSchedules: ${tracked.reason}`);
2825
+ log
2826
+ .child({ run_id: runId })
2827
+ .error("resume.track_failed", `resumeStalledSchedules: ${tracked.reason}`);
1296
2828
  return false;
1297
2829
  }
1298
2830
  try {
@@ -1315,6 +2847,7 @@ export class RunManager {
1315
2847
  executionMode: this.executionMode,
1316
2848
  stageProcessLauncher: this.stageProcessLauncher,
1317
2849
  initialSchedule: hydrated,
2850
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1318
2851
  }).finally(() => {
1319
2852
  void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
1320
2853
  });
@@ -1336,16 +2869,27 @@ export class RunManager {
1336
2869
  }
1337
2870
  let checkoutKey;
1338
2871
  let durableCheckoutRoot;
2872
+ let resumeCallerId = null;
2873
+ let resumeProjectRoot;
1339
2874
  try {
1340
2875
  const meta = await this.options.store.readRunMeta(runId);
1341
- const checkoutRoot = meta.checkout_root;
1342
- if (checkoutRoot !== undefined && checkoutRoot !== "") {
1343
- durableCheckoutRoot = checkoutRoot;
1344
- checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
2876
+ resumeCallerId = meta.caller_id ?? null;
2877
+ resumeProjectRoot = meta.project_root
2878
+ ? normalizeCatalogPath(meta.project_root)
2879
+ : undefined;
2880
+ const kind = derivedBindingKindFromMeta(meta);
2881
+ if (kind === "checkout") {
2882
+ const checkoutRoot = meta.checkout_root;
2883
+ if (checkoutRoot !== undefined && checkoutRoot !== "") {
2884
+ durableCheckoutRoot = checkoutRoot;
2885
+ checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
2886
+ }
1345
2887
  }
1346
2888
  }
1347
2889
  catch (err) {
1348
- console.error(`invariant: trackResume failed reading checkout_root for run ${runId}; continuing without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
2890
+ log
2891
+ .child({ run_id: runId })
2892
+ .error("resume.checkout_root_failed", `invariant: trackResume failed reading checkout_root for run ${runId}; continuing without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
1349
2893
  }
1350
2894
  const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
1351
2895
  if (conflictHolder !== undefined) {
@@ -1360,6 +2904,8 @@ export class RunManager {
1360
2904
  this.active.set(runId, {
1361
2905
  checkoutKey,
1362
2906
  durableCheckoutRoot,
2907
+ projectRoot: resumeProjectRoot,
2908
+ callerId: resumeCallerId,
1363
2909
  generation: ++this.trackingGeneration,
1364
2910
  });
1365
2911
  return { ok: true };
@@ -1416,7 +2962,7 @@ export class RunManager {
1416
2962
  catch {
1417
2963
  // ignore secondary failures — still fail-closed in memory
1418
2964
  }
1419
- console.error(reason);
2965
+ log.child({ run_id: runId }).error("attach.quarantined", reason);
1420
2966
  }
1421
2967
  untrackIfGeneration(runId, generation) {
1422
2968
  const entry = this.active.get(runId);
@@ -1429,6 +2975,7 @@ export class RunManager {
1429
2975
  if (!entry)
1430
2976
  return;
1431
2977
  this.active.delete(id);
2978
+ this.schedulingHalts.delete(id);
1432
2979
  if (isProvisional) {
1433
2980
  this.provisionalIds.delete(id);
1434
2981
  }
@@ -1436,5 +2983,19 @@ export class RunManager {
1436
2983
  this.checkoutLeases.get(entry.checkoutKey) === id) {
1437
2984
  this.checkoutLeases.delete(entry.checkoutKey);
1438
2985
  }
2986
+ if (!isProvisional) {
2987
+ void this.drainAdmissionQueue();
2988
+ }
2989
+ }
2990
+ ensureSchedulingHalt(runId) {
2991
+ let halt = this.schedulingHalts.get(runId);
2992
+ if (halt === undefined) {
2993
+ halt = {
2994
+ halted: !this.acceptingWork,
2995
+ hostShutdown: !this.acceptingWork,
2996
+ };
2997
+ this.schedulingHalts.set(runId, halt);
2998
+ }
2999
+ return halt;
1439
3000
  }
1440
3001
  }