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