stageflow 0.25.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 (501) hide show
  1. package/README.md +10 -3
  2. package/dist/a2a/contracts.d.ts +1 -0
  3. package/dist/a2a/contracts.js +1 -0
  4. package/dist/a2a/registry.d.ts +2 -0
  5. package/dist/a2a/registry.js +20 -1
  6. package/dist/a2a/server.d.ts +7 -0
  7. package/dist/a2a/server.js +5 -2
  8. package/dist/a2a/service.d.ts +1 -1
  9. package/dist/a2a/service.js +57 -8
  10. package/dist/a2a/store.d.ts +8 -5
  11. package/dist/a2a/store.js +24 -67
  12. package/dist/agent/activity.d.ts +3 -0
  13. package/dist/agent/bootProviderConfig.d.ts +21 -0
  14. package/dist/agent/bootProviderConfig.js +74 -0
  15. package/dist/agent/piAdapter.d.ts +9 -1
  16. package/dist/agent/piAdapter.js +58 -3
  17. package/dist/agent/piIsolatedMcp.d.ts +2 -0
  18. package/dist/agent/piIsolatedMcp.js +10 -2
  19. package/dist/agent/port.d.ts +6 -0
  20. package/dist/agent/streamLogRedact.d.ts +1 -1
  21. package/dist/agent/streamLogRedact.js +4 -11
  22. package/dist/cli/artifactCommand.js +9 -16
  23. package/dist/cli/backupCommand.d.ts +9 -0
  24. package/dist/cli/backupCommand.js +111 -0
  25. package/dist/cli/debugRunCommand.d.ts +10 -0
  26. package/dist/cli/debugRunCommand.js +109 -0
  27. package/dist/cli/doctorCommand.d.ts +32 -0
  28. package/dist/cli/doctorCommand.js +453 -0
  29. package/dist/cli/envelopeCommand.js +9 -2
  30. package/dist/cli/exportAllCommand.d.ts +55 -0
  31. package/dist/cli/exportAllCommand.js +218 -0
  32. package/dist/cli/exportRunCommand.js +23 -21
  33. package/dist/cli/hostClient.d.ts +21 -3
  34. package/dist/cli/hostClient.js +93 -12
  35. package/dist/cli/initTemplates.d.ts +1 -1
  36. package/dist/cli/initTemplates.js +8 -1
  37. package/dist/cli/resolveSafeOutPath.d.ts +2 -0
  38. package/dist/cli/resolveSafeOutPath.js +40 -0
  39. package/dist/cli/restoreCommand.d.ts +10 -0
  40. package/dist/cli/restoreCommand.js +90 -0
  41. package/dist/cli/runCommand.d.ts +3 -2
  42. package/dist/cli/runCommand.js +93 -11
  43. package/dist/cli/runOutput.d.ts +4 -0
  44. package/dist/cli/runOutput.js +33 -7
  45. package/dist/cli/runStageCommand.d.ts +1 -0
  46. package/dist/cli/runStageCommand.js +44 -25
  47. package/dist/cli/runsCommand.d.ts +1 -1
  48. package/dist/cli/runsCommand.js +162 -8
  49. package/dist/cli.js +171 -30
  50. package/dist/config/catalogRelativePath.d.ts +62 -0
  51. package/dist/config/catalogRelativePath.js +118 -0
  52. package/dist/config/configOrigin.d.ts +27 -0
  53. package/dist/config/configOrigin.js +59 -0
  54. package/dist/config/hostConfig.d.ts +55 -0
  55. package/dist/config/hostConfig.js +333 -0
  56. package/dist/config/listSkills.d.ts +26 -0
  57. package/dist/config/listSkills.js +190 -3
  58. package/dist/config/loadOutcome.d.ts +1 -1
  59. package/dist/config/loadPipeline.js +23 -2
  60. package/dist/config/loadStage.js +18 -0
  61. package/dist/config/loadTask.d.ts +2 -0
  62. package/dist/config/loadTask.js +57 -18
  63. package/dist/config/materializeStageModels.js +5 -1
  64. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  65. package/dist/config/mergePipelineIncludes.js +19 -0
  66. package/dist/config/migrateYaml.js +6 -11
  67. package/dist/config/multiProjectCatalog.d.ts +30 -0
  68. package/dist/config/multiProjectCatalog.js +142 -0
  69. package/dist/config/normalizePipelineStageEntry.js +20 -1
  70. package/dist/config/pipelineStageKeys.js +2 -0
  71. package/dist/config/resolveCatalogRoots.d.ts +27 -0
  72. package/dist/config/resolveCatalogRoots.js +76 -0
  73. package/dist/config/resolveModel.d.ts +7 -1
  74. package/dist/config/resolveModel.js +11 -1
  75. package/dist/config/resolveStageMcpServers.d.ts +9 -1
  76. package/dist/config/resolveStageMcpServers.js +36 -9
  77. package/dist/config/secretFromEnvOrFile.d.ts +9 -0
  78. package/dist/config/secretFromEnvOrFile.js +34 -0
  79. package/dist/config/seededCatalog.d.ts +11 -0
  80. package/dist/config/seededCatalog.js +29 -0
  81. package/dist/config/toolRequires.d.ts +24 -0
  82. package/dist/config/toolRequires.js +115 -0
  83. package/dist/config/validateCatalog.d.ts +5 -1
  84. package/dist/config/validateCatalog.js +140 -1
  85. package/dist/diagnostics/checks.d.ts +39 -0
  86. package/dist/diagnostics/checks.js +135 -0
  87. package/dist/errors/codes.d.ts +14 -0
  88. package/dist/errors/codes.js +59 -0
  89. package/dist/git/cache.d.ts +14 -0
  90. package/dist/git/cache.js +150 -0
  91. package/dist/git/credentials.d.ts +9 -0
  92. package/dist/git/credentials.js +88 -0
  93. package/dist/git/errors.d.ts +20 -0
  94. package/dist/git/errors.js +94 -0
  95. package/dist/git/exec.d.ts +16 -0
  96. package/dist/git/exec.js +135 -0
  97. package/dist/git/operations.d.ts +29 -0
  98. package/dist/git/operations.js +153 -0
  99. package/dist/git/version.d.ts +1 -0
  100. package/dist/git/version.js +10 -0
  101. package/dist/logging/audit.d.ts +16 -0
  102. package/dist/logging/audit.js +19 -0
  103. package/dist/logging/commonWords.d.ts +2 -0
  104. package/dist/logging/commonWords.js +13 -0
  105. package/dist/logging/logger.d.ts +49 -0
  106. package/dist/logging/logger.js +193 -0
  107. package/dist/logging/namedSecrets.d.ts +6 -0
  108. package/dist/logging/namedSecrets.js +58 -0
  109. package/dist/logging/redact.d.ts +13 -0
  110. package/dist/logging/redact.js +88 -0
  111. package/dist/mcp/catalogTools.js +456 -80
  112. package/dist/mcp/checkoutTools.d.ts +52 -0
  113. package/dist/mcp/checkoutTools.js +332 -0
  114. package/dist/mcp/controlTools.js +179 -5
  115. package/dist/mcp/deps.d.ts +1 -0
  116. package/dist/mcp/getStartedTool.d.ts +3 -0
  117. package/dist/mcp/getStartedTool.js +62 -0
  118. package/dist/mcp/readArtifact.d.ts +11 -0
  119. package/dist/mcp/readArtifact.js +46 -30
  120. package/dist/mcp/server.js +1 -0
  121. package/dist/mcp/tools.js +4 -0
  122. package/dist/net/proxy.d.ts +14 -0
  123. package/dist/net/proxy.js +78 -0
  124. package/dist/package-meta.d.ts +2 -1
  125. package/dist/package-meta.js +2 -1
  126. package/dist/preflight/bash.d.ts +8 -0
  127. package/dist/preflight/bash.js +34 -0
  128. package/dist/preflight/claudeRoot.d.ts +11 -0
  129. package/dist/preflight/claudeRoot.js +23 -0
  130. package/dist/preflight/mcpCommands.d.ts +8 -0
  131. package/dist/preflight/mcpCommands.js +36 -0
  132. package/dist/preflight/pipelinePreflight.d.ts +40 -0
  133. package/dist/preflight/pipelinePreflight.js +192 -0
  134. package/dist/preflight/tls.d.ts +6 -0
  135. package/dist/preflight/tls.js +18 -0
  136. package/dist/preflight/toolchain.d.ts +45 -0
  137. package/dist/preflight/toolchain.js +219 -0
  138. package/dist/project/findProjectRoot.js +3 -4
  139. package/dist/project/globalHome.d.ts +7 -0
  140. package/dist/project/globalHome.js +63 -2
  141. package/dist/project/resolveProjectContext.js +2 -2
  142. package/dist/project/resolveStageflowContext.js +2 -2
  143. package/dist/projection/projectRun.d.ts +2 -1
  144. package/dist/projection/projectRun.js +1 -0
  145. package/dist/runstore/assertTmpdir.d.ts +1 -0
  146. package/dist/runstore/assertTmpdir.js +21 -0
  147. package/dist/runstore/backup.d.ts +52 -0
  148. package/dist/runstore/backup.js +292 -0
  149. package/dist/runstore/createStore.d.ts +18 -1
  150. package/dist/runstore/createStore.js +64 -5
  151. package/dist/runstore/debugBundle.d.ts +60 -0
  152. package/dist/runstore/debugBundle.js +143 -0
  153. package/dist/runstore/diskUsage.d.ts +50 -0
  154. package/dist/runstore/diskUsage.js +214 -0
  155. package/dist/runstore/exportRunPayload.d.ts +8 -0
  156. package/dist/runstore/exportRunPayload.js +23 -0
  157. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  158. package/dist/runstore/normalizeCatalogPath.js +10 -0
  159. package/dist/runstore/paths.d.ts +5 -0
  160. package/dist/runstore/paths.js +145 -1
  161. package/dist/runstore/port.d.ts +111 -13
  162. package/dist/runstore/port.js +8 -1
  163. package/dist/runstore/restore.d.ts +81 -0
  164. package/dist/runstore/restore.js +355 -0
  165. package/dist/runstore/retention.d.ts +27 -0
  166. package/dist/runstore/retention.js +74 -0
  167. package/dist/runstore/runManifest.d.ts +146 -0
  168. package/dist/runstore/runManifest.js +288 -0
  169. package/dist/runstore/runProjection.d.ts +3 -1
  170. package/dist/runstore/runProjection.js +63 -1
  171. package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -3
  172. package/dist/runstore/sqlite/SqliteRunStore.js +352 -219
  173. package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
  174. package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
  175. package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
  176. package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
  177. package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
  178. package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
  179. package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
  180. package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
  181. package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
  182. package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
  183. package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
  184. package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
  185. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
  186. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
  187. package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
  188. package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
  189. package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
  190. package/dist/runstore/sqlite/migrations/index.js +72 -0
  191. package/dist/runstore/sqlite/schema.d.ts +20 -1
  192. package/dist/runstore/sqlite/schema.js +277 -1
  193. package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
  194. package/dist/runstore/sqlite/storeOpenError.js +11 -0
  195. package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
  196. package/dist/runstore/sqlite/storeSchemaError.js +8 -0
  197. package/dist/runstore/stageExecution.js +3 -0
  198. package/dist/runstore/storeFilesystem.d.ts +32 -0
  199. package/dist/runstore/storeFilesystem.js +148 -0
  200. package/dist/runstore/trackProjection.js +2 -0
  201. package/dist/runstore/workspaceLayout.d.ts +13 -0
  202. package/dist/runstore/workspaceLayout.js +76 -0
  203. package/dist/runtime/completionCheckRunner.d.ts +3 -0
  204. package/dist/runtime/completionCheckRunner.js +45 -7
  205. package/dist/runtime/containerLimits.d.ts +16 -0
  206. package/dist/runtime/containerLimits.js +84 -0
  207. package/dist/runtime/credentialMaterialisation.d.ts +28 -0
  208. package/dist/runtime/credentialMaterialisation.js +128 -0
  209. package/dist/runtime/gitCheckoutCapability.js +7 -13
  210. package/dist/runtime/pipelineRunner.d.ts +36 -1
  211. package/dist/runtime/pipelineRunner.js +178 -17
  212. package/dist/runtime/pipelineScheduler.d.ts +15 -0
  213. package/dist/runtime/pipelineScheduler.js +135 -27
  214. package/dist/runtime/reloadRunCatalog.js +6 -1
  215. package/dist/runtime/repositoryMaterialize.d.ts +47 -0
  216. package/dist/runtime/repositoryMaterialize.js +235 -0
  217. package/dist/runtime/resumeTimedOut.d.ts +1 -1
  218. package/dist/runtime/resumeTimedOut.js +5 -2
  219. package/dist/runtime/runChangeBus.js +9 -0
  220. package/dist/runtime/runDeletion.d.ts +7 -0
  221. package/dist/runtime/runDeletion.js +37 -0
  222. package/dist/runtime/runManager.d.ts +158 -4
  223. package/dist/runtime/runManager.js +1596 -79
  224. package/dist/runtime/runRetentionSweep.d.ts +31 -0
  225. package/dist/runtime/runRetentionSweep.js +328 -0
  226. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  227. package/dist/runtime/runRetryCoordinator.js +19 -2
  228. package/dist/runtime/runSkills.d.ts +40 -0
  229. package/dist/runtime/runSkills.js +173 -0
  230. package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
  231. package/dist/runtime/stageAttemptBootstrap.js +195 -24
  232. package/dist/runtime/stageCacheEnv.d.ts +6 -0
  233. package/dist/runtime/stageCacheEnv.js +34 -0
  234. package/dist/runtime/stageConcurrency.d.ts +2 -1
  235. package/dist/runtime/stageConcurrency.js +4 -2
  236. package/dist/runtime/stageEnvironment.d.ts +38 -0
  237. package/dist/runtime/stageEnvironment.js +193 -0
  238. package/dist/runtime/stageProcessLauncher.d.ts +18 -0
  239. package/dist/runtime/stageProcessLauncher.js +194 -38
  240. package/dist/runtime/stageRecovery.d.ts +3 -1
  241. package/dist/runtime/stageRecovery.js +19 -4
  242. package/dist/runtime/stageRoots.d.ts +38 -1
  243. package/dist/runtime/stageRoots.js +118 -0
  244. package/dist/runtime/stageRunner.d.ts +1 -0
  245. package/dist/runtime/stageRunner.js +3 -1
  246. package/dist/runtime/stageSecretDecl.d.ts +9 -0
  247. package/dist/runtime/stageSecretDecl.js +102 -0
  248. package/dist/runtime/stageSecrets.d.ts +40 -0
  249. package/dist/runtime/stageSecrets.js +181 -0
  250. package/dist/runtime/stageWorker.d.ts +3 -2
  251. package/dist/runtime/stageWorker.js +58 -10
  252. package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
  253. package/dist/runtime/stageWorkerProtocol.js +8 -0
  254. package/dist/runtime/startPayload.d.ts +32 -0
  255. package/dist/runtime/startPayload.js +59 -0
  256. package/dist/runtime/taskInput.js +14 -5
  257. package/dist/runtime/verifiedStageExecution.d.ts +1 -0
  258. package/dist/runtime/verifiedStageExecution.js +2 -1
  259. package/dist/runtime/workspaceBinding.d.ts +20 -0
  260. package/dist/runtime/workspaceBinding.js +60 -0
  261. package/dist/server/allowedHosts.d.ts +19 -0
  262. package/dist/server/allowedHosts.js +179 -0
  263. package/dist/server/bootstrap.d.ts +36 -2
  264. package/dist/server/bootstrap.js +209 -5
  265. package/dist/server/controlToken.d.ts +47 -0
  266. package/dist/server/controlToken.js +241 -0
  267. package/dist/server/createHttpHost.d.ts +11 -2
  268. package/dist/server/createHttpHost.js +103 -10
  269. package/dist/server/ensureGlobalService.d.ts +5 -1
  270. package/dist/server/ensureGlobalService.js +35 -1
  271. package/dist/server/healthSurfaces.d.ts +6 -0
  272. package/dist/server/healthSurfaces.js +98 -0
  273. package/dist/server/http.d.ts +12 -1
  274. package/dist/server/http.js +1085 -485
  275. package/dist/server/listenHost.d.ts +5 -0
  276. package/dist/server/listenHost.js +56 -0
  277. package/dist/server/mcpHost.d.ts +10 -1
  278. package/dist/server/mcpHost.js +36 -6
  279. package/dist/server/operatorResults.d.ts +2 -2
  280. package/dist/server/operatorResults.js +40 -29
  281. package/dist/server/requestAuthContext.d.ts +12 -0
  282. package/dist/server/requestAuthContext.js +21 -0
  283. package/dist/server/shutdown.d.ts +69 -0
  284. package/dist/server/shutdown.js +219 -0
  285. package/dist/types/pipeline.d.ts +7 -0
  286. package/dist/types/stage.d.ts +8 -0
  287. package/dist/types/task.d.ts +8 -0
  288. package/dist/ui/assets/index-B6hlSXXn.js +120 -0
  289. package/dist/ui/index.html +1 -1
  290. package/examples/README.md +98 -0
  291. package/examples/archify-on-pr/README.md +213 -0
  292. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
  293. package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
  294. package/examples/archify-on-pr/author-diagrams.yaml +106 -0
  295. package/examples/archify-on-pr/detect-changes.yaml +89 -0
  296. package/examples/ci-validate/README.md +35 -0
  297. package/examples/ci-validate/check.yaml +13 -0
  298. package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
  299. package/examples/ci-validate/validate.sh +5 -0
  300. package/examples/compliance-review-demo/README.md +125 -0
  301. package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
  302. package/examples/compliance-review-demo/connect-account.yaml +47 -0
  303. package/examples/compliance-review-demo/map-to-control.yaml +123 -0
  304. package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
  305. package/examples/compliance-review-demo/publish-report.yaml +79 -0
  306. package/examples/compliance-review-demo/review-verdict.yaml +122 -0
  307. package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
  308. package/examples/compliance-review-demo/store-evidence.yaml +143 -0
  309. package/examples/conditional-fork/README.md +89 -0
  310. package/examples/conditional-fork/branch-a.yaml +13 -0
  311. package/examples/conditional-fork/branch-b.yaml +13 -0
  312. package/examples/conditional-fork/decide.yaml +35 -0
  313. package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
  314. package/examples/conditional-fork/fork-demo.task.yaml +8 -0
  315. package/examples/context7-mcp/README.md +42 -0
  316. package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
  317. package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
  318. package/examples/context7-mcp/fetch-docs.yaml +52 -0
  319. package/examples/context7-mcp/resolve-library.yaml +31 -0
  320. package/examples/context7-mcp/write-brief.yaml +52 -0
  321. package/examples/feature-loop/README.md +104 -0
  322. package/examples/feature-loop/address-feedback.yaml +148 -0
  323. package/examples/feature-loop/align.yaml +116 -0
  324. package/examples/feature-loop/decompose.yaml +86 -0
  325. package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
  326. package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
  327. package/examples/feature-loop/feature-loop.task.yaml +25 -0
  328. package/examples/feature-loop/implement.yaml +128 -0
  329. package/examples/feature-loop/plan.yaml +111 -0
  330. package/examples/feature-loop/publish.yaml +167 -0
  331. package/examples/feature-loop/review.yaml +146 -0
  332. package/examples/feature-loop/verify.yaml +106 -0
  333. package/examples/feedback-loop/README.md +72 -0
  334. package/examples/feedback-loop/VERIFY.md +208 -0
  335. package/examples/feedback-loop/continue-only.task.yaml +4 -0
  336. package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
  337. package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
  338. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
  339. package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
  340. package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
  341. package/examples/feedback-loop/implement.yaml +14 -0
  342. package/examples/feedback-loop/plan.yaml +14 -0
  343. package/examples/feedback-loop/review.yaml +15 -0
  344. package/examples/feedback-loop/send-back-once.task.yaml +6 -0
  345. package/examples/feedback-loop/submit.yaml +12 -0
  346. package/examples/generic-fan-in/README.md +151 -0
  347. package/examples/generic-fan-in/clarify.yaml +18 -0
  348. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
  349. package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
  350. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
  351. package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
  352. package/examples/generic-fan-in/research.yaml +45 -0
  353. package/examples/generic-fan-in/synthesize.yaml +66 -0
  354. package/examples/generic-fan-in/validation.yaml +45 -0
  355. package/examples/github-release/README.md +78 -0
  356. package/examples/github-release/draft-release-notes.yaml +64 -0
  357. package/examples/github-release/github-release.pipeline.yaml +11 -0
  358. package/examples/github-release/github-release.task.yaml +13 -0
  359. package/examples/github-release/publish-github-release.yaml +64 -0
  360. package/examples/hello-world/README.md +28 -0
  361. package/examples/hello-world/hello.pipeline.yaml +4 -0
  362. package/examples/hello-world/my-task.task.yaml +6 -0
  363. package/examples/hello-world/research.yaml +18 -0
  364. package/examples/mcp-hitl-tour/README.md +113 -0
  365. package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
  366. package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
  367. package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
  368. package/examples/mcp-hitl-tour/gather.yaml +31 -0
  369. package/examples/mcp-hitl-tour/intake.yaml +62 -0
  370. package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
  371. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
  372. package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
  373. package/examples/mcp-hitl-tour/polish.yaml +33 -0
  374. package/examples/mcp-hitl-tour/review.yaml +17 -0
  375. package/examples/mcp-hitl-tour/ship.yaml +25 -0
  376. package/examples/oss-issue-contribution/README.md +218 -0
  377. package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
  378. package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
  379. package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
  380. package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
  381. package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
  382. package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
  383. package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
  384. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
  385. package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
  386. package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
  387. package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
  388. package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
  389. package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
  390. package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
  391. package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
  392. package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
  393. package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
  394. package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
  395. package/examples/oss-quick-fix/README.md +65 -0
  396. package/examples/oss-quick-fix/implement-fix.yaml +84 -0
  397. package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
  398. package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
  399. package/examples/oss-quick-fix/ship-fix.yaml +81 -0
  400. package/examples/oss-quick-fix/understand-issue.yaml +67 -0
  401. package/examples/oss-quick-fix/verify-fix.yaml +73 -0
  402. package/examples/plan-review/README.md +37 -0
  403. package/examples/plan-review/plan-review-followup.yaml +13 -0
  404. package/examples/plan-review/plan-review.pipeline.yaml +9 -0
  405. package/examples/plan-review/plan-review.task.yaml +9 -0
  406. package/examples/plan-review/plan-review.yaml +25 -0
  407. package/examples/playwright-mcp/README.md +39 -0
  408. package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
  409. package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
  410. package/examples/playwright-mcp/screenshot.yaml +40 -0
  411. package/examples/retry-tour/README.md +75 -0
  412. package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
  413. package/examples/retry-tour/retry-tour.task.yaml +9 -0
  414. package/examples/route-if-tour/README.md +202 -0
  415. package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
  416. package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
  417. package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
  418. package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
  419. package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
  420. package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
  421. package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
  422. package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
  423. package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
  424. package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
  425. package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
  426. package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
  427. package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
  428. package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
  429. package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
  430. package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
  431. package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
  432. package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
  433. package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
  434. package/examples/route-wiring-smoke-test/README.md +377 -0
  435. package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
  436. package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
  437. package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
  438. package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
  439. package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
  440. package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
  441. package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
  442. package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
  443. package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
  444. package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
  445. package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
  446. package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
  447. package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
  448. package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
  449. package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
  450. package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
  451. package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
  452. package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
  453. package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
  454. package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
  455. package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
  456. package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
  457. package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
  458. package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
  459. package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
  460. package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
  461. package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
  462. package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
  463. package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
  464. package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
  465. package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
  466. package/examples/ship-feature/README.md +190 -0
  467. package/examples/ship-feature/feat-address-feedback.yaml +119 -0
  468. package/examples/ship-feature/feat-implement.yaml +180 -0
  469. package/examples/ship-feature/feat-plan.yaml +120 -0
  470. package/examples/ship-feature/feat-review.yaml +109 -0
  471. package/examples/ship-feature/feat-ship.yaml +203 -0
  472. package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
  473. package/examples/ship-feature/ship-feature.task.yaml +78 -0
  474. package/examples/stage-mcp/README.md +144 -0
  475. package/examples/stage-mcp/echo-mcp.mjs +20 -0
  476. package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
  477. package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
  478. package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
  479. package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
  480. package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
  481. package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
  482. package/examples/stage-mcp/use-echo.yaml +20 -0
  483. package/examples/verify-tour/README.md +80 -0
  484. package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
  485. package/examples/verify-tour/verify-tour.task.yaml +11 -0
  486. package/package.json +5 -1
  487. package/skills/stageflow/SKILL.md +6 -6
  488. package/skills/stageflow/references/control-surface.md +11 -5
  489. package/skills/stageflow/scripts/detect-host.mjs +14 -1
  490. package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
  491. package/skills/stageflow-author/SKILL.md +4 -3
  492. package/skills/stageflow-author/references/catalog-mapping.md +16 -0
  493. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  494. package/skills/stageflow-run/SKILL.md +20 -12
  495. package/skills/stageflow-run/references/mcp-call.md +9 -1
  496. package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
  497. package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
  498. package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
  499. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
  500. package/skills/stageflow-setup/SKILL.md +40 -8
  501. package/dist/ui/assets/index-Da5EHU6M.js +0 -119
@@ -1,52 +1,250 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
2
3
  import { RunSubmissionExistsError } from "../runstore/submission.js";
3
- import { readFile, realpath } from "node:fs/promises";
4
+ import { access, readFile, realpath } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { findProjectRoot } from "../project/findProjectRoot.js";
6
- 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";
7
11
  import { loadRunContext } from "./resumeReconstruct.js";
8
- import { assertTimedOutStageEligible, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
9
- 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";
10
15
  import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
11
- 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";
12
20
  import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
13
21
  import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
14
22
  import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
15
23
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
16
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";
17
37
  import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
18
38
  import { StageHitlController, waitKey, } from "./stageHitl.js";
19
- import { resolveAndValidateCheckout, } from "./stageRoots.js";
39
+ import { resolveAndValidateCheckout, resolveEffectiveGitIdentity, stageBindingEnvFromRun, } from "./stageRoots.js";
20
40
  import { orchestrateAnswerResume } from "./answerResume.js";
21
41
  import { reconstructAndContinue as resumeReconstructAndContinue } from "./resumeReconstruct.js";
22
- import { resumeSessionFilePath } from "./stageAttemptContext.js";
23
- import { resolveStartTaskInput } from "./taskInput.js";
24
- 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";
25
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";
26
52
  function existingSubmissionResult(existing, request) {
27
53
  if (existing.requestHash !== request.requestHash) {
28
54
  return { ok: false, status: 409, reason: "Submission key was already used for different input" };
29
55
  }
30
56
  return { ok: true, runId: existing.runId, reused: true };
31
57
  }
58
+ export const FORCE_DELETE_CANCEL_REASON = "delete_run: force";
32
59
  const DEFAULT_MAX_CONCURRENT = 3;
33
- 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";
34
66
  const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
67
+ const log = rootLogger.child({ component: "runtime" });
35
68
  function parseMaxConcurrent(raw) {
36
69
  if (raw === undefined || raw.trim() === "")
37
70
  return DEFAULT_MAX_CONCURRENT;
38
71
  const n = Number.parseInt(raw, 10);
39
- if (!Number.isFinite(n) || n < 1)
40
- 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
+ }
75
+ return n;
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;
41
99
  return n;
42
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
+ }
43
241
  async function toCheckoutLeaseKey(absPath) {
44
242
  try {
45
243
  return await realpath(absPath);
46
244
  }
47
245
  catch {
48
246
  const fallback = path.resolve(absPath);
49
- 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 });
50
248
  return fallback;
51
249
  }
52
250
  }
@@ -54,17 +252,26 @@ export class RunManager {
54
252
  options;
55
253
  submissionsInFlight = new Map();
56
254
  active = new Map();
255
+ schedulingHalts = new Map();
57
256
  checkoutLeases = new Map();
58
257
  provisionalIds = new Set();
258
+ admissionQueue = new AdmissionQueue();
259
+ pendingQueuedStarts = new Map();
260
+ queuedDone = new Map();
261
+ admissionDrainInFlight = false;
59
262
  resumeInFlight = new Set();
60
263
  retryInFlight = new Set();
61
264
  retryStartOwner = new Map();
62
265
  retryStartWaiters = new Map();
63
266
  trackingGeneration = 0;
64
267
  maxConcurrent;
268
+ maxQueued;
269
+ maxConcurrentPerProject;
270
+ callerQuotas;
65
271
  maxActiveStagesPerRun;
66
272
  executionMode;
67
273
  stageProcessLauncher;
274
+ a2aStore;
68
275
  hitl;
69
276
  attachedWaiting = new Set();
70
277
  retryCoordinator = new RunRetryCoordinator();
@@ -98,15 +305,24 @@ export class RunManager {
98
305
  cwd;
99
306
  projectRoot;
100
307
  isGitProject;
308
+ acceptingWork = true;
101
309
  constructor(options) {
102
310
  this.options = options;
103
311
  this.cwd = options.cwd ?? process.cwd();
104
312
  this.projectRoot = options.projectRoot ?? this.cwd;
105
313
  this.isGitProject = options.isGitProject ?? false;
314
+ this.a2aStore = options.a2aStore;
106
315
  this.maxConcurrent =
107
316
  options.maxConcurrent ??
108
317
  readMaxConcurrentFromGlobal() ??
109
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
+ ]));
110
326
  this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
111
327
  this.executionMode = readStageExecutionMode(process.env, options.executionMode);
112
328
  this.stageProcessLauncher =
@@ -133,6 +349,57 @@ export class RunManager {
133
349
  getActiveCount() {
134
350
  return this.active.size;
135
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
+ }
136
403
  getActiveRunIds() {
137
404
  return [...this.active.keys()];
138
405
  }
@@ -142,6 +409,7 @@ export class RunManager {
142
409
  const maxConcurrent = this.getMaxConcurrent();
143
410
  const maxActiveStageProcessesRaw = readMaxActiveStageProcesses(process.env);
144
411
  const activeStageProcesses = this.stageProcessLauncher?.activeCount() ?? 0;
412
+ const limits = getContainerLimits();
145
413
  return {
146
414
  ok: true,
147
415
  activeRunIds,
@@ -152,8 +420,83 @@ export class RunManager {
152
420
  maxActiveStageProcesses: Number.isFinite(maxActiveStageProcessesRaw)
153
421
  ? maxActiveStageProcessesRaw
154
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() },
155
432
  };
156
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
+ })),
447
+ };
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
+ }
157
500
  getHitlController() {
158
501
  return this.hitl;
159
502
  }
@@ -188,16 +531,27 @@ export class RunManager {
188
531
  }
189
532
  let checkoutKey;
190
533
  let durableCheckoutRoot;
534
+ let attachCallerId = null;
535
+ let attachProjectRoot;
191
536
  try {
192
537
  const meta = await this.options.store.readRunMeta(runId);
193
- const checkoutRoot = meta.checkout_root;
194
- if (checkoutRoot !== undefined && checkoutRoot !== "") {
195
- durableCheckoutRoot = checkoutRoot;
196
- 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
+ }
197
549
  }
198
550
  }
199
551
  catch (err) {
200
- 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)}`);
201
555
  }
202
556
  const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
203
557
  if (conflictHolder !== undefined) {
@@ -211,6 +565,8 @@ export class RunManager {
211
565
  this.active.set(runId, {
212
566
  checkoutKey,
213
567
  durableCheckoutRoot,
568
+ projectRoot: attachProjectRoot,
569
+ callerId: attachCallerId,
214
570
  generation: ++this.trackingGeneration,
215
571
  });
216
572
  for (const stage of waitingStages) {
@@ -232,27 +588,74 @@ export class RunManager {
232
588
  const reconciled = [];
233
589
  const runs = await this.options.store.listRuns();
234
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;
235
603
  let detail;
236
604
  try {
237
605
  detail = await this.options.store.readRun(summary.run_id);
238
606
  }
239
607
  catch (err) {
240
- 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)}`);
241
611
  continue;
242
612
  }
243
613
  const runId = summary.run_id;
614
+ const cancelled = meta.status === "cancelled";
244
615
  let runChanged = false;
245
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
+ }
246
648
  if (stage.status !== "running")
247
649
  continue;
248
650
  if (this.hasActiveWorker(runId, stage.stage_id))
249
651
  continue;
250
652
  try {
251
- await failStageAsInterrupted({
653
+ await markStageInterrupted({
252
654
  store: this.options.store,
253
655
  runId,
254
656
  stageId: stage.stage_id,
255
657
  reason: STARTUP_RECONCILE_REASON,
658
+ status: "interrupted",
256
659
  });
257
660
  reconciled.push({
258
661
  runId,
@@ -260,26 +663,106 @@ export class RunManager {
260
663
  reason: STARTUP_RECONCILE_REASON,
261
664
  });
262
665
  runChanged = true;
263
- 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 });
264
669
  }
265
670
  catch (err) {
266
- 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)}`);
267
674
  }
268
675
  }
269
- if (runChanged) {
676
+ if (runChanged && !cancelled) {
270
677
  try {
271
678
  await syncRunStatusFromStages(this.options.store, runId);
272
679
  }
273
680
  catch (err) {
274
- 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)}`);
275
684
  }
276
685
  }
277
686
  }
278
687
  if (reconciled.length > 0) {
279
- console.error(`reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`);
688
+ log.error("reconcile.complete", `reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`, { count: reconciled.length });
280
689
  }
281
690
  return { reconciled };
282
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
+ }
283
766
  async resumeStalledSchedules() {
284
767
  const resumed = [];
285
768
  const runs = await this.options.store.listRuns();
@@ -295,11 +778,37 @@ export class RunManager {
295
778
  }
296
779
  }
297
780
  catch (err) {
298
- 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)}`);
299
784
  }
300
785
  }
301
786
  return resumed;
302
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
+ }
303
812
  async abandonStage(runId, stageId) {
304
813
  let detail;
305
814
  try {
@@ -338,11 +847,12 @@ export class RunManager {
338
847
  await this.stageProcessLauncher.cancelRun(runId);
339
848
  }
340
849
  }
341
- await failStageAsInterrupted({
850
+ await markStageInterrupted({
342
851
  store: this.options.store,
343
852
  runId,
344
853
  stageId,
345
854
  reason: OPERATOR_ABANDON_REASON,
855
+ status: "failed",
346
856
  });
347
857
  if (this.retryCoordinator.isActive(runId) &&
348
858
  this.retryCoordinator.getActiveRoots(runId)?.has(stageId)) {
@@ -356,6 +866,195 @@ export class RunManager {
356
866
  }
357
867
  return { ok: true, runId, stageId };
358
868
  }
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
+ }
359
1058
  async startRunOnce(input, submission) {
360
1059
  if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
361
1060
  return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
@@ -392,12 +1091,25 @@ export class RunManager {
392
1091
  }
393
1092
  }
394
1093
  async startRun(input, submission) {
395
- const cwd = this.options.cwd ?? process.cwd();
396
- // An inline pipeline has no filesystem anchor to derive a project root from.
397
- const derivedProjectRoot = typeof input.pipeline === "string"
398
- ? (findProjectRoot(path.dirname(path.resolve(cwd, input.pipeline))) ??
399
- this.projectRoot)
400
- : this.projectRoot;
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;
401
1113
  let resolved;
402
1114
  try {
403
1115
  resolved = resolveStartTaskInput(input, cwd);
@@ -432,24 +1144,57 @@ export class RunManager {
432
1144
  gitSha: input.gitSha,
433
1145
  ciPrUrl: input.ciPrUrl,
434
1146
  ciJobUrl: input.ciJobUrl,
435
- }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission);
1147
+ }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
436
1148
  }
437
- 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
+ }
438
1158
  const cwd = this.options.cwd ?? process.cwd();
439
1159
  let pipeline;
440
1160
  let taskYaml;
441
1161
  let rerunCwd = cwd;
442
1162
  let rerunProjectRoot;
1163
+ let skipGates;
1164
+ let ciIdentity;
1165
+ let pinned;
443
1166
  try {
444
1167
  const meta = await this.options.store.readRunMeta(runId);
445
- 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 {
446
1192
  return {
447
1193
  ok: false,
448
- 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.`,
449
1195
  status: 400,
450
1196
  };
451
1197
  }
452
- pipeline = normalizeCatalogPath(meta.pipeline_path);
453
1198
  rerunCwd = meta.project_root
454
1199
  ? normalizeCatalogPath(meta.project_root)
455
1200
  : cwd;
@@ -457,16 +1202,44 @@ export class RunManager {
457
1202
  ? normalizeCatalogPath(meta.project_root)
458
1203
  : undefined;
459
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
+ }
460
1225
  }
461
1226
  catch {
462
1227
  return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
463
1228
  }
464
- 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);
465
1230
  }
466
1231
  async retryStage(runId, stageId) {
467
1232
  return this.retryStageInternal(runId, stageId, true);
468
1233
  }
469
- 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
+ }
470
1243
  const resumeKey = waitKey(runId, stageId);
471
1244
  if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
472
1245
  return {
@@ -489,13 +1262,16 @@ export class RunManager {
489
1262
  this.resumeInFlight.delete(resumeKey);
490
1263
  return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
491
1264
  }
492
- const eligibility = assertTimedOutStageEligible(stageSnap.status, stageSnap.events);
1265
+ const eligibility = assertResumableStage(stageSnap.status, stageSnap.events);
493
1266
  if (!eligibility.ok) {
494
1267
  this.resumeInFlight.delete(resumeKey);
495
1268
  return eligibility;
496
1269
  }
497
1270
  const latest = await this.options.store.getLatestStageExecution(runId, stageId);
498
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
+ }
499
1275
  const wasActive = this.active.has(runId);
500
1276
  const tracked = await this.ensureResumeTracked(runId);
501
1277
  if (!tracked.ok) {
@@ -615,12 +1391,21 @@ export class RunManager {
615
1391
  }
616
1392
  }
617
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
+ }
618
1402
  const retryKey = waitKey(runId, stageId);
619
1403
  if (this.retryInFlight.has(retryKey)) {
620
1404
  return {
621
1405
  ok: false,
622
1406
  reason: `Retry already in progress for run ${runId} stage ${stageId}`,
623
1407
  status: 409,
1408
+ code: "retry_in_progress",
624
1409
  };
625
1410
  }
626
1411
  this.retryInFlight.add(retryKey);
@@ -631,6 +1416,7 @@ export class RunManager {
631
1416
  ok: false,
632
1417
  reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
633
1418
  status: 409,
1419
+ code: "manual_recovery_required",
634
1420
  };
635
1421
  }
636
1422
  try {
@@ -1059,6 +1845,7 @@ export class RunManager {
1059
1845
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1060
1846
  executionMode: this.executionMode,
1061
1847
  stageProcessLauncher: this.stageProcessLauncher,
1848
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1062
1849
  });
1063
1850
  this.registerResumeUntrack(runId, done);
1064
1851
  const rest = await done;
@@ -1103,6 +1890,13 @@ export class RunManager {
1103
1890
  try {
1104
1891
  const runMeta = await store.readRunMeta(runId);
1105
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
+ });
1106
1900
  const launchResult = await launcher.launch({
1107
1901
  runId,
1108
1902
  stageId,
@@ -1111,6 +1905,8 @@ export class RunManager {
1111
1905
  resumeAnswer: opaqueAnswer,
1112
1906
  attempt,
1113
1907
  sessionFilePath,
1908
+ env: stageBinding.env,
1909
+ bindingKind: stageBinding.kind,
1114
1910
  ...(this.options.operatorCatalog !== undefined
1115
1911
  ? { operatorCatalog: this.options.operatorCatalog }
1116
1912
  : {}),
@@ -1133,7 +1929,6 @@ export class RunManager {
1133
1929
  catch {
1134
1930
  // downstream resume may read envelope from store
1135
1931
  }
1136
- const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
1137
1932
  const rest = await resumeRun({
1138
1933
  prepared: {
1139
1934
  task,
@@ -1152,6 +1947,7 @@ export class RunManager {
1152
1947
  initialPrior,
1153
1948
  executionMode: this.executionMode,
1154
1949
  stageProcessLauncher: launcher,
1950
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1155
1951
  });
1156
1952
  if (rest.outcome === "failed") {
1157
1953
  return { ok: false, reason: rest.reason };
@@ -1199,27 +1995,257 @@ export class RunManager {
1199
1995
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
1200
1996
  }
1201
1997
  }
1202
- async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission) {
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;
1203
2023
  let checkoutKey;
2024
+ let pathCheckoutRoot;
2025
+ let binding;
2026
+ let pipelineId;
2027
+ let pipelineDag;
2028
+ let pipelinePath;
2029
+ let loadedPipeline;
2030
+ let toolchainChecks = [];
1204
2031
  try {
1205
- const task = loadTaskFromYaml(taskYaml, taskLabel);
1206
- const checkoutRoot = await resolveAndValidateCheckout(task, checkoutOverride, cwd);
1207
- checkoutKey =
1208
- checkoutRoot !== undefined
1209
- ? 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)
1210
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;
1211
2117
  }
1212
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
+ }
1213
2129
  return {
1214
2130
  ok: false,
1215
2131
  reason: err instanceof Error ? err.message : String(err),
1216
2132
  status: 400,
1217
2133
  };
1218
2134
  }
1219
- const reserved = this.tryReserve(checkoutKey);
1220
- if (!reserved.ok)
1221
- 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 () => { };
1222
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);
1223
2249
  const started = await startPipeline({
1224
2250
  submission,
1225
2251
  agent: this.options.agent,
@@ -1228,8 +2254,14 @@ export class RunManager {
1228
2254
  taskPath,
1229
2255
  pipeline,
1230
2256
  cwd,
1231
- projectRoot: projectRoot ?? this.projectRoot,
2257
+ projectRoot: resolvedProjectRoot,
1232
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,
1233
2265
  gitSha: ciIdentity?.gitSha,
1234
2266
  ciPrUrl: ciIdentity?.ciPrUrl,
1235
2267
  ciJobUrl: ciIdentity?.ciJobUrl,
@@ -1239,15 +2271,44 @@ export class RunManager {
1239
2271
  stageProcessLauncher: this.stageProcessLauncher,
1240
2272
  operatorCatalog: this.options.operatorCatalog,
1241
2273
  skipGates,
2274
+ schedulingHalt,
2275
+ callerId,
2276
+ ...(hasSkills ? { skills: validatedSkills } : {}),
1242
2277
  });
1243
- this.track(reserved.provisionalId, started.runId, started.done);
2278
+ this.track(admitted.provisionalId, started.runId, started.done);
1244
2279
  return { ok: true, runId: started.runId, done: started.done };
1245
2280
  }
1246
2281
  catch (err) {
1247
- this.clearReservation(reserved.provisionalId);
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
+ }
1248
2300
  if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
1249
2301
  throw err;
1250
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
+ }
1251
2312
  return {
1252
2313
  ok: false,
1253
2314
  reason: err instanceof Error ? err.message : String(err),
@@ -1257,24 +2318,87 @@ export class RunManager {
1257
2318
  }
1258
2319
  busyFailure(code, extras) {
1259
2320
  const activeRunIds = this.getActiveRunIds();
1260
- const reason = code === "busy_capacity"
1261
- ? `Capacity full: ${this.active.size}/${this.maxConcurrent} active runs`
1262
- : `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"}`;
1263
2335
  return {
1264
2336
  ok: false,
1265
2337
  reason,
1266
2338
  status: 409,
1267
2339
  code,
1268
- activeCount: this.active.size,
1269
- maxConcurrent: this.maxConcurrent,
2340
+ activeCount,
2341
+ maxConcurrent,
1270
2342
  activeRunIds,
1271
- ...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
+ : {}),
1272
2354
  };
1273
2355
  }
1274
- tryReserve(checkoutKey) {
1275
- if (this.active.size >= this.maxConcurrent) {
1276
- 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
+ };
1277
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;
2399
+ }
2400
+ }
2401
+ tryAdmitOrEnqueue(checkoutKey, projectRoot = this.projectRoot, callerId) {
1278
2402
  if (checkoutKey !== undefined) {
1279
2403
  const holder = this.checkoutLeases.get(checkoutKey);
1280
2404
  if (holder !== undefined) {
@@ -1282,7 +2406,7 @@ export class RunManager {
1282
2406
  ? undefined
1283
2407
  : holder;
1284
2408
  return {
1285
- ok: false,
2409
+ action: "reject",
1286
2410
  failure: this.busyFailure("busy_checkout", {
1287
2411
  conflictingRunId,
1288
2412
  conflictingCheckout: checkoutKey,
@@ -1290,16 +2414,378 @@ export class RunManager {
1290
2414
  };
1291
2415
  }
1292
2416
  }
1293
- const provisionalId = randomUUID();
1294
- this.provisionalIds.add(provisionalId);
1295
- this.active.set(provisionalId, {
1296
- checkoutKey,
1297
- 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;
1298
2503
  });
1299
- if (checkoutKey !== undefined) {
1300
- 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";
1301
2788
  }
1302
- return { ok: true, provisionalId };
1303
2789
  }
1304
2790
  clearReservation(provisionalId) {
1305
2791
  this.removeActiveEntry(provisionalId, true);
@@ -1336,7 +2822,9 @@ export class RunManager {
1336
2822
  return false;
1337
2823
  const tracked = await this.ensureResumeTracked(runId);
1338
2824
  if (!tracked.ok) {
1339
- console.error(`resumeStalledSchedules: ${tracked.reason}`);
2825
+ log
2826
+ .child({ run_id: runId })
2827
+ .error("resume.track_failed", `resumeStalledSchedules: ${tracked.reason}`);
1340
2828
  return false;
1341
2829
  }
1342
2830
  try {
@@ -1359,6 +2847,7 @@ export class RunManager {
1359
2847
  executionMode: this.executionMode,
1360
2848
  stageProcessLauncher: this.stageProcessLauncher,
1361
2849
  initialSchedule: hydrated,
2850
+ schedulingHalt: this.ensureSchedulingHalt(runId),
1362
2851
  }).finally(() => {
1363
2852
  void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
1364
2853
  });
@@ -1380,16 +2869,27 @@ export class RunManager {
1380
2869
  }
1381
2870
  let checkoutKey;
1382
2871
  let durableCheckoutRoot;
2872
+ let resumeCallerId = null;
2873
+ let resumeProjectRoot;
1383
2874
  try {
1384
2875
  const meta = await this.options.store.readRunMeta(runId);
1385
- const checkoutRoot = meta.checkout_root;
1386
- if (checkoutRoot !== undefined && checkoutRoot !== "") {
1387
- durableCheckoutRoot = checkoutRoot;
1388
- 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
+ }
1389
2887
  }
1390
2888
  }
1391
2889
  catch (err) {
1392
- 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)}`);
1393
2893
  }
1394
2894
  const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
1395
2895
  if (conflictHolder !== undefined) {
@@ -1404,6 +2904,8 @@ export class RunManager {
1404
2904
  this.active.set(runId, {
1405
2905
  checkoutKey,
1406
2906
  durableCheckoutRoot,
2907
+ projectRoot: resumeProjectRoot,
2908
+ callerId: resumeCallerId,
1407
2909
  generation: ++this.trackingGeneration,
1408
2910
  });
1409
2911
  return { ok: true };
@@ -1460,7 +2962,7 @@ export class RunManager {
1460
2962
  catch {
1461
2963
  // ignore secondary failures — still fail-closed in memory
1462
2964
  }
1463
- console.error(reason);
2965
+ log.child({ run_id: runId }).error("attach.quarantined", reason);
1464
2966
  }
1465
2967
  untrackIfGeneration(runId, generation) {
1466
2968
  const entry = this.active.get(runId);
@@ -1473,6 +2975,7 @@ export class RunManager {
1473
2975
  if (!entry)
1474
2976
  return;
1475
2977
  this.active.delete(id);
2978
+ this.schedulingHalts.delete(id);
1476
2979
  if (isProvisional) {
1477
2980
  this.provisionalIds.delete(id);
1478
2981
  }
@@ -1480,5 +2983,19 @@ export class RunManager {
1480
2983
  this.checkoutLeases.get(entry.checkoutKey) === id) {
1481
2984
  this.checkoutLeases.delete(entry.checkoutKey);
1482
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;
1483
3000
  }
1484
3001
  }