stageflow 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. package/README.md +10 -3
  2. package/dist/a2a/contracts.d.ts +1 -0
  3. package/dist/a2a/contracts.js +1 -0
  4. package/dist/a2a/registry.d.ts +2 -0
  5. package/dist/a2a/registry.js +20 -1
  6. package/dist/a2a/server.d.ts +7 -0
  7. package/dist/a2a/server.js +5 -2
  8. package/dist/a2a/service.d.ts +1 -1
  9. package/dist/a2a/service.js +57 -8
  10. package/dist/a2a/store.d.ts +8 -5
  11. package/dist/a2a/store.js +24 -67
  12. package/dist/agent/activity.d.ts +3 -0
  13. package/dist/agent/bootProviderConfig.d.ts +21 -0
  14. package/dist/agent/bootProviderConfig.js +74 -0
  15. package/dist/agent/piAdapter.d.ts +9 -1
  16. package/dist/agent/piAdapter.js +58 -3
  17. package/dist/agent/piIsolatedMcp.d.ts +2 -0
  18. package/dist/agent/piIsolatedMcp.js +10 -2
  19. package/dist/agent/port.d.ts +6 -0
  20. package/dist/agent/streamLogRedact.d.ts +1 -1
  21. package/dist/agent/streamLogRedact.js +4 -11
  22. package/dist/cli/artifactCommand.js +9 -16
  23. package/dist/cli/backupCommand.d.ts +9 -0
  24. package/dist/cli/backupCommand.js +111 -0
  25. package/dist/cli/debugRunCommand.d.ts +10 -0
  26. package/dist/cli/debugRunCommand.js +109 -0
  27. package/dist/cli/doctorCommand.d.ts +32 -0
  28. package/dist/cli/doctorCommand.js +453 -0
  29. package/dist/cli/envelopeCommand.js +9 -2
  30. package/dist/cli/exportAllCommand.d.ts +55 -0
  31. package/dist/cli/exportAllCommand.js +218 -0
  32. package/dist/cli/exportRunCommand.js +23 -21
  33. package/dist/cli/hostClient.d.ts +21 -3
  34. package/dist/cli/hostClient.js +93 -12
  35. package/dist/cli/initTemplates.d.ts +1 -1
  36. package/dist/cli/initTemplates.js +8 -1
  37. package/dist/cli/resolveSafeOutPath.d.ts +2 -0
  38. package/dist/cli/resolveSafeOutPath.js +40 -0
  39. package/dist/cli/restoreCommand.d.ts +10 -0
  40. package/dist/cli/restoreCommand.js +90 -0
  41. package/dist/cli/runCommand.d.ts +3 -2
  42. package/dist/cli/runCommand.js +93 -11
  43. package/dist/cli/runOutput.d.ts +4 -0
  44. package/dist/cli/runOutput.js +33 -7
  45. package/dist/cli/runStageCommand.d.ts +1 -0
  46. package/dist/cli/runStageCommand.js +44 -25
  47. package/dist/cli/runsCommand.d.ts +1 -1
  48. package/dist/cli/runsCommand.js +162 -8
  49. package/dist/cli.js +171 -30
  50. package/dist/config/catalogRelativePath.d.ts +62 -0
  51. package/dist/config/catalogRelativePath.js +118 -0
  52. package/dist/config/configOrigin.d.ts +27 -0
  53. package/dist/config/configOrigin.js +59 -0
  54. package/dist/config/hostConfig.d.ts +55 -0
  55. package/dist/config/hostConfig.js +333 -0
  56. package/dist/config/listSkills.d.ts +26 -0
  57. package/dist/config/listSkills.js +190 -3
  58. package/dist/config/loadOutcome.d.ts +1 -1
  59. package/dist/config/loadPipeline.js +23 -2
  60. package/dist/config/loadStage.js +18 -0
  61. package/dist/config/loadTask.d.ts +2 -0
  62. package/dist/config/loadTask.js +57 -18
  63. package/dist/config/materializeStageModels.js +5 -1
  64. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  65. package/dist/config/mergePipelineIncludes.js +19 -0
  66. package/dist/config/migrateYaml.js +6 -11
  67. package/dist/config/multiProjectCatalog.d.ts +30 -0
  68. package/dist/config/multiProjectCatalog.js +142 -0
  69. package/dist/config/normalizePipelineStageEntry.js +20 -1
  70. package/dist/config/pipelineStageKeys.js +2 -0
  71. package/dist/config/resolveCatalogRoots.d.ts +27 -0
  72. package/dist/config/resolveCatalogRoots.js +76 -0
  73. package/dist/config/resolveModel.d.ts +7 -1
  74. package/dist/config/resolveModel.js +11 -1
  75. package/dist/config/resolveStageMcpServers.d.ts +9 -1
  76. package/dist/config/resolveStageMcpServers.js +36 -9
  77. package/dist/config/secretFromEnvOrFile.d.ts +9 -0
  78. package/dist/config/secretFromEnvOrFile.js +34 -0
  79. package/dist/config/seededCatalog.d.ts +11 -0
  80. package/dist/config/seededCatalog.js +29 -0
  81. package/dist/config/toolRequires.d.ts +24 -0
  82. package/dist/config/toolRequires.js +115 -0
  83. package/dist/config/validateCatalog.d.ts +5 -1
  84. package/dist/config/validateCatalog.js +140 -1
  85. package/dist/diagnostics/checks.d.ts +39 -0
  86. package/dist/diagnostics/checks.js +135 -0
  87. package/dist/errors/codes.d.ts +14 -0
  88. package/dist/errors/codes.js +59 -0
  89. package/dist/git/cache.d.ts +14 -0
  90. package/dist/git/cache.js +150 -0
  91. package/dist/git/credentials.d.ts +9 -0
  92. package/dist/git/credentials.js +88 -0
  93. package/dist/git/errors.d.ts +20 -0
  94. package/dist/git/errors.js +94 -0
  95. package/dist/git/exec.d.ts +16 -0
  96. package/dist/git/exec.js +135 -0
  97. package/dist/git/operations.d.ts +29 -0
  98. package/dist/git/operations.js +153 -0
  99. package/dist/git/version.d.ts +1 -0
  100. package/dist/git/version.js +10 -0
  101. package/dist/logging/audit.d.ts +16 -0
  102. package/dist/logging/audit.js +19 -0
  103. package/dist/logging/commonWords.d.ts +2 -0
  104. package/dist/logging/commonWords.js +13 -0
  105. package/dist/logging/logger.d.ts +49 -0
  106. package/dist/logging/logger.js +193 -0
  107. package/dist/logging/namedSecrets.d.ts +6 -0
  108. package/dist/logging/namedSecrets.js +58 -0
  109. package/dist/logging/redact.d.ts +13 -0
  110. package/dist/logging/redact.js +88 -0
  111. package/dist/mcp/catalogTools.js +456 -80
  112. package/dist/mcp/checkoutTools.d.ts +52 -0
  113. package/dist/mcp/checkoutTools.js +332 -0
  114. package/dist/mcp/controlTools.js +179 -5
  115. package/dist/mcp/deps.d.ts +1 -0
  116. package/dist/mcp/getStartedTool.d.ts +3 -0
  117. package/dist/mcp/getStartedTool.js +62 -0
  118. package/dist/mcp/readArtifact.d.ts +11 -0
  119. package/dist/mcp/readArtifact.js +46 -30
  120. package/dist/mcp/server.js +1 -0
  121. package/dist/mcp/tools.js +4 -0
  122. package/dist/net/proxy.d.ts +14 -0
  123. package/dist/net/proxy.js +78 -0
  124. package/dist/package-meta.d.ts +2 -1
  125. package/dist/package-meta.js +2 -1
  126. package/dist/preflight/bash.d.ts +8 -0
  127. package/dist/preflight/bash.js +34 -0
  128. package/dist/preflight/claudeRoot.d.ts +11 -0
  129. package/dist/preflight/claudeRoot.js +23 -0
  130. package/dist/preflight/mcpCommands.d.ts +8 -0
  131. package/dist/preflight/mcpCommands.js +36 -0
  132. package/dist/preflight/pipelinePreflight.d.ts +40 -0
  133. package/dist/preflight/pipelinePreflight.js +192 -0
  134. package/dist/preflight/tls.d.ts +6 -0
  135. package/dist/preflight/tls.js +18 -0
  136. package/dist/preflight/toolchain.d.ts +45 -0
  137. package/dist/preflight/toolchain.js +219 -0
  138. package/dist/project/findProjectRoot.js +3 -4
  139. package/dist/project/globalHome.d.ts +7 -0
  140. package/dist/project/globalHome.js +63 -2
  141. package/dist/project/resolveProjectContext.js +2 -2
  142. package/dist/project/resolveStageflowContext.js +2 -2
  143. package/dist/projection/projectRun.d.ts +2 -1
  144. package/dist/projection/projectRun.js +1 -0
  145. package/dist/runstore/assertTmpdir.d.ts +1 -0
  146. package/dist/runstore/assertTmpdir.js +21 -0
  147. package/dist/runstore/backup.d.ts +52 -0
  148. package/dist/runstore/backup.js +292 -0
  149. package/dist/runstore/createStore.d.ts +18 -1
  150. package/dist/runstore/createStore.js +64 -5
  151. package/dist/runstore/debugBundle.d.ts +60 -0
  152. package/dist/runstore/debugBundle.js +143 -0
  153. package/dist/runstore/diskUsage.d.ts +50 -0
  154. package/dist/runstore/diskUsage.js +214 -0
  155. package/dist/runstore/exportRunPayload.d.ts +8 -0
  156. package/dist/runstore/exportRunPayload.js +23 -0
  157. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  158. package/dist/runstore/normalizeCatalogPath.js +10 -0
  159. package/dist/runstore/paths.d.ts +5 -0
  160. package/dist/runstore/paths.js +145 -1
  161. package/dist/runstore/port.d.ts +111 -13
  162. package/dist/runstore/port.js +8 -1
  163. package/dist/runstore/restore.d.ts +81 -0
  164. package/dist/runstore/restore.js +355 -0
  165. package/dist/runstore/retention.d.ts +27 -0
  166. package/dist/runstore/retention.js +74 -0
  167. package/dist/runstore/runManifest.d.ts +146 -0
  168. package/dist/runstore/runManifest.js +288 -0
  169. package/dist/runstore/runProjection.d.ts +3 -1
  170. package/dist/runstore/runProjection.js +63 -1
  171. package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -3
  172. package/dist/runstore/sqlite/SqliteRunStore.js +352 -219
  173. package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
  174. package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
  175. package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
  176. package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
  177. package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
  178. package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
  179. package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
  180. package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
  181. package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
  182. package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
  183. package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
  184. package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
  185. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
  186. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
  187. package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
  188. package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
  189. package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
  190. package/dist/runstore/sqlite/migrations/index.js +72 -0
  191. package/dist/runstore/sqlite/schema.d.ts +20 -1
  192. package/dist/runstore/sqlite/schema.js +277 -1
  193. package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
  194. package/dist/runstore/sqlite/storeOpenError.js +11 -0
  195. package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
  196. package/dist/runstore/sqlite/storeSchemaError.js +8 -0
  197. package/dist/runstore/stageExecution.js +3 -0
  198. package/dist/runstore/storeFilesystem.d.ts +32 -0
  199. package/dist/runstore/storeFilesystem.js +148 -0
  200. package/dist/runstore/trackProjection.js +2 -0
  201. package/dist/runstore/workspaceLayout.d.ts +13 -0
  202. package/dist/runstore/workspaceLayout.js +76 -0
  203. package/dist/runtime/completionCheckRunner.d.ts +3 -0
  204. package/dist/runtime/completionCheckRunner.js +45 -7
  205. package/dist/runtime/containerLimits.d.ts +16 -0
  206. package/dist/runtime/containerLimits.js +84 -0
  207. package/dist/runtime/credentialMaterialisation.d.ts +28 -0
  208. package/dist/runtime/credentialMaterialisation.js +128 -0
  209. package/dist/runtime/gitCheckoutCapability.js +7 -13
  210. package/dist/runtime/pipelineRunner.d.ts +36 -1
  211. package/dist/runtime/pipelineRunner.js +178 -17
  212. package/dist/runtime/pipelineScheduler.d.ts +15 -0
  213. package/dist/runtime/pipelineScheduler.js +135 -27
  214. package/dist/runtime/reloadRunCatalog.js +6 -1
  215. package/dist/runtime/repositoryMaterialize.d.ts +47 -0
  216. package/dist/runtime/repositoryMaterialize.js +235 -0
  217. package/dist/runtime/resumeTimedOut.d.ts +1 -1
  218. package/dist/runtime/resumeTimedOut.js +5 -2
  219. package/dist/runtime/runChangeBus.js +9 -0
  220. package/dist/runtime/runDeletion.d.ts +7 -0
  221. package/dist/runtime/runDeletion.js +37 -0
  222. package/dist/runtime/runManager.d.ts +158 -4
  223. package/dist/runtime/runManager.js +1596 -79
  224. package/dist/runtime/runRetentionSweep.d.ts +31 -0
  225. package/dist/runtime/runRetentionSweep.js +328 -0
  226. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  227. package/dist/runtime/runRetryCoordinator.js +19 -2
  228. package/dist/runtime/runSkills.d.ts +40 -0
  229. package/dist/runtime/runSkills.js +173 -0
  230. package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
  231. package/dist/runtime/stageAttemptBootstrap.js +195 -24
  232. package/dist/runtime/stageCacheEnv.d.ts +6 -0
  233. package/dist/runtime/stageCacheEnv.js +34 -0
  234. package/dist/runtime/stageConcurrency.d.ts +2 -1
  235. package/dist/runtime/stageConcurrency.js +4 -2
  236. package/dist/runtime/stageEnvironment.d.ts +38 -0
  237. package/dist/runtime/stageEnvironment.js +193 -0
  238. package/dist/runtime/stageProcessLauncher.d.ts +18 -0
  239. package/dist/runtime/stageProcessLauncher.js +194 -38
  240. package/dist/runtime/stageRecovery.d.ts +3 -1
  241. package/dist/runtime/stageRecovery.js +19 -4
  242. package/dist/runtime/stageRoots.d.ts +38 -1
  243. package/dist/runtime/stageRoots.js +118 -0
  244. package/dist/runtime/stageRunner.d.ts +1 -0
  245. package/dist/runtime/stageRunner.js +3 -1
  246. package/dist/runtime/stageSecretDecl.d.ts +9 -0
  247. package/dist/runtime/stageSecretDecl.js +102 -0
  248. package/dist/runtime/stageSecrets.d.ts +40 -0
  249. package/dist/runtime/stageSecrets.js +181 -0
  250. package/dist/runtime/stageWorker.d.ts +3 -2
  251. package/dist/runtime/stageWorker.js +58 -10
  252. package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
  253. package/dist/runtime/stageWorkerProtocol.js +8 -0
  254. package/dist/runtime/startPayload.d.ts +32 -0
  255. package/dist/runtime/startPayload.js +59 -0
  256. package/dist/runtime/taskInput.js +14 -5
  257. package/dist/runtime/verifiedStageExecution.d.ts +1 -0
  258. package/dist/runtime/verifiedStageExecution.js +2 -1
  259. package/dist/runtime/workspaceBinding.d.ts +20 -0
  260. package/dist/runtime/workspaceBinding.js +60 -0
  261. package/dist/server/allowedHosts.d.ts +19 -0
  262. package/dist/server/allowedHosts.js +179 -0
  263. package/dist/server/bootstrap.d.ts +36 -2
  264. package/dist/server/bootstrap.js +209 -5
  265. package/dist/server/controlToken.d.ts +47 -0
  266. package/dist/server/controlToken.js +241 -0
  267. package/dist/server/createHttpHost.d.ts +11 -2
  268. package/dist/server/createHttpHost.js +103 -10
  269. package/dist/server/ensureGlobalService.d.ts +5 -1
  270. package/dist/server/ensureGlobalService.js +35 -1
  271. package/dist/server/healthSurfaces.d.ts +6 -0
  272. package/dist/server/healthSurfaces.js +98 -0
  273. package/dist/server/http.d.ts +12 -1
  274. package/dist/server/http.js +1085 -485
  275. package/dist/server/listenHost.d.ts +5 -0
  276. package/dist/server/listenHost.js +56 -0
  277. package/dist/server/mcpHost.d.ts +10 -1
  278. package/dist/server/mcpHost.js +36 -6
  279. package/dist/server/operatorResults.d.ts +2 -2
  280. package/dist/server/operatorResults.js +40 -29
  281. package/dist/server/requestAuthContext.d.ts +12 -0
  282. package/dist/server/requestAuthContext.js +21 -0
  283. package/dist/server/shutdown.d.ts +69 -0
  284. package/dist/server/shutdown.js +219 -0
  285. package/dist/types/pipeline.d.ts +7 -0
  286. package/dist/types/stage.d.ts +8 -0
  287. package/dist/types/task.d.ts +8 -0
  288. package/dist/ui/assets/index-B6hlSXXn.js +120 -0
  289. package/dist/ui/index.html +1 -1
  290. package/examples/README.md +98 -0
  291. package/examples/archify-on-pr/README.md +213 -0
  292. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
  293. package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
  294. package/examples/archify-on-pr/author-diagrams.yaml +106 -0
  295. package/examples/archify-on-pr/detect-changes.yaml +89 -0
  296. package/examples/ci-validate/README.md +35 -0
  297. package/examples/ci-validate/check.yaml +13 -0
  298. package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
  299. package/examples/ci-validate/validate.sh +5 -0
  300. package/examples/compliance-review-demo/README.md +125 -0
  301. package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
  302. package/examples/compliance-review-demo/connect-account.yaml +47 -0
  303. package/examples/compliance-review-demo/map-to-control.yaml +123 -0
  304. package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
  305. package/examples/compliance-review-demo/publish-report.yaml +79 -0
  306. package/examples/compliance-review-demo/review-verdict.yaml +122 -0
  307. package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
  308. package/examples/compliance-review-demo/store-evidence.yaml +143 -0
  309. package/examples/conditional-fork/README.md +89 -0
  310. package/examples/conditional-fork/branch-a.yaml +13 -0
  311. package/examples/conditional-fork/branch-b.yaml +13 -0
  312. package/examples/conditional-fork/decide.yaml +35 -0
  313. package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
  314. package/examples/conditional-fork/fork-demo.task.yaml +8 -0
  315. package/examples/context7-mcp/README.md +42 -0
  316. package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
  317. package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
  318. package/examples/context7-mcp/fetch-docs.yaml +52 -0
  319. package/examples/context7-mcp/resolve-library.yaml +31 -0
  320. package/examples/context7-mcp/write-brief.yaml +52 -0
  321. package/examples/feature-loop/README.md +104 -0
  322. package/examples/feature-loop/address-feedback.yaml +148 -0
  323. package/examples/feature-loop/align.yaml +116 -0
  324. package/examples/feature-loop/decompose.yaml +86 -0
  325. package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
  326. package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
  327. package/examples/feature-loop/feature-loop.task.yaml +25 -0
  328. package/examples/feature-loop/implement.yaml +128 -0
  329. package/examples/feature-loop/plan.yaml +111 -0
  330. package/examples/feature-loop/publish.yaml +167 -0
  331. package/examples/feature-loop/review.yaml +146 -0
  332. package/examples/feature-loop/verify.yaml +106 -0
  333. package/examples/feedback-loop/README.md +72 -0
  334. package/examples/feedback-loop/VERIFY.md +208 -0
  335. package/examples/feedback-loop/continue-only.task.yaml +4 -0
  336. package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
  337. package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
  338. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
  339. package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
  340. package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
  341. package/examples/feedback-loop/implement.yaml +14 -0
  342. package/examples/feedback-loop/plan.yaml +14 -0
  343. package/examples/feedback-loop/review.yaml +15 -0
  344. package/examples/feedback-loop/send-back-once.task.yaml +6 -0
  345. package/examples/feedback-loop/submit.yaml +12 -0
  346. package/examples/generic-fan-in/README.md +151 -0
  347. package/examples/generic-fan-in/clarify.yaml +18 -0
  348. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
  349. package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
  350. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
  351. package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
  352. package/examples/generic-fan-in/research.yaml +45 -0
  353. package/examples/generic-fan-in/synthesize.yaml +66 -0
  354. package/examples/generic-fan-in/validation.yaml +45 -0
  355. package/examples/github-release/README.md +78 -0
  356. package/examples/github-release/draft-release-notes.yaml +64 -0
  357. package/examples/github-release/github-release.pipeline.yaml +11 -0
  358. package/examples/github-release/github-release.task.yaml +13 -0
  359. package/examples/github-release/publish-github-release.yaml +64 -0
  360. package/examples/hello-world/README.md +28 -0
  361. package/examples/hello-world/hello.pipeline.yaml +4 -0
  362. package/examples/hello-world/my-task.task.yaml +6 -0
  363. package/examples/hello-world/research.yaml +18 -0
  364. package/examples/mcp-hitl-tour/README.md +113 -0
  365. package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
  366. package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
  367. package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
  368. package/examples/mcp-hitl-tour/gather.yaml +31 -0
  369. package/examples/mcp-hitl-tour/intake.yaml +62 -0
  370. package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
  371. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
  372. package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
  373. package/examples/mcp-hitl-tour/polish.yaml +33 -0
  374. package/examples/mcp-hitl-tour/review.yaml +17 -0
  375. package/examples/mcp-hitl-tour/ship.yaml +25 -0
  376. package/examples/oss-issue-contribution/README.md +218 -0
  377. package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
  378. package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
  379. package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
  380. package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
  381. package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
  382. package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
  383. package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
  384. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
  385. package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
  386. package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
  387. package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
  388. package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
  389. package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
  390. package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
  391. package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
  392. package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
  393. package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
  394. package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
  395. package/examples/oss-quick-fix/README.md +65 -0
  396. package/examples/oss-quick-fix/implement-fix.yaml +84 -0
  397. package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
  398. package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
  399. package/examples/oss-quick-fix/ship-fix.yaml +81 -0
  400. package/examples/oss-quick-fix/understand-issue.yaml +67 -0
  401. package/examples/oss-quick-fix/verify-fix.yaml +73 -0
  402. package/examples/plan-review/README.md +37 -0
  403. package/examples/plan-review/plan-review-followup.yaml +13 -0
  404. package/examples/plan-review/plan-review.pipeline.yaml +9 -0
  405. package/examples/plan-review/plan-review.task.yaml +9 -0
  406. package/examples/plan-review/plan-review.yaml +25 -0
  407. package/examples/playwright-mcp/README.md +39 -0
  408. package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
  409. package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
  410. package/examples/playwright-mcp/screenshot.yaml +40 -0
  411. package/examples/retry-tour/README.md +75 -0
  412. package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
  413. package/examples/retry-tour/retry-tour.task.yaml +9 -0
  414. package/examples/route-if-tour/README.md +202 -0
  415. package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
  416. package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
  417. package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
  418. package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
  419. package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
  420. package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
  421. package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
  422. package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
  423. package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
  424. package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
  425. package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
  426. package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
  427. package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
  428. package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
  429. package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
  430. package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
  431. package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
  432. package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
  433. package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
  434. package/examples/route-wiring-smoke-test/README.md +377 -0
  435. package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
  436. package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
  437. package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
  438. package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
  439. package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
  440. package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
  441. package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
  442. package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
  443. package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
  444. package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
  445. package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
  446. package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
  447. package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
  448. package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
  449. package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
  450. package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
  451. package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
  452. package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
  453. package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
  454. package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
  455. package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
  456. package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
  457. package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
  458. package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
  459. package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
  460. package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
  461. package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
  462. package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
  463. package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
  464. package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
  465. package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
  466. package/examples/ship-feature/README.md +190 -0
  467. package/examples/ship-feature/feat-address-feedback.yaml +119 -0
  468. package/examples/ship-feature/feat-implement.yaml +180 -0
  469. package/examples/ship-feature/feat-plan.yaml +120 -0
  470. package/examples/ship-feature/feat-review.yaml +109 -0
  471. package/examples/ship-feature/feat-ship.yaml +203 -0
  472. package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
  473. package/examples/ship-feature/ship-feature.task.yaml +78 -0
  474. package/examples/stage-mcp/README.md +144 -0
  475. package/examples/stage-mcp/echo-mcp.mjs +20 -0
  476. package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
  477. package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
  478. package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
  479. package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
  480. package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
  481. package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
  482. package/examples/stage-mcp/use-echo.yaml +20 -0
  483. package/examples/verify-tour/README.md +80 -0
  484. package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
  485. package/examples/verify-tour/verify-tour.task.yaml +11 -0
  486. package/package.json +5 -1
  487. package/skills/stageflow/SKILL.md +6 -6
  488. package/skills/stageflow/references/control-surface.md +11 -5
  489. package/skills/stageflow/scripts/detect-host.mjs +14 -1
  490. package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
  491. package/skills/stageflow-author/SKILL.md +4 -3
  492. package/skills/stageflow-author/references/catalog-mapping.md +16 -0
  493. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  494. package/skills/stageflow-run/SKILL.md +20 -12
  495. package/skills/stageflow-run/references/mcp-call.md +9 -1
  496. package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
  497. package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
  498. package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
  499. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
  500. package/skills/stageflow-setup/SKILL.md +40 -8
  501. package/dist/ui/assets/index-Da5EHU6M.js +0 -119
@@ -1,4 +1,5 @@
1
1
  import { readFile, access } from "node:fs/promises";
2
+ import { createReadStream, statSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
@@ -7,19 +8,38 @@ import { handleProviderRoutes } from "./providerRoutes.js";
7
8
  import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
8
9
  import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
9
10
  import { createStage, parseCreateStageBody } from "../config/createStage.js";
10
- import { browseCatalog } from "../config/browseCatalog.js";
11
+ import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
12
+ import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
11
13
  import { listExtensions } from "../config/listExtensions.js";
12
14
  import { listSkills } from "../config/listSkills.js";
13
- import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
15
+ import { artifactMediaType, classifyArtifactContent, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
16
+ import { getRunDiff, listCheckoutChanges, readCheckoutFileBytes, } from "../mcp/checkoutTools.js";
14
17
  import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
18
+ import { BackupError, createBackup, resolveBackupDownloadPath, } from "../runstore/backup.js";
19
+ import { RestoreError, resolveBackupNameForRestore, stageRestoreForBoot, } from "../runstore/restore.js";
20
+ import { iterateExportNdjson } from "../cli/exportAllCommand.js";
21
+ import { buildRunExportPayload, runDetailWithRedactedManifest, } from "../runstore/exportRunPayload.js";
22
+ import { buildDebugBundle } from "../runstore/debugBundle.js";
23
+ import { parseShutdownGraceMs } from "./shutdown.js";
24
+ import { globalStageflowHome } from "../project/globalHome.js";
15
25
  import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
16
26
  import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
27
+ import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
17
28
  import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
18
29
  import { isTaskFile } from "../runtime/taskInput.js";
30
+ import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
19
31
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
20
32
  import { bootstrapStageflowHost, } from "./bootstrap.js";
33
+ import { assertAllowedHttpAccess, isTrustedLocalHttpRequest, resolveAllowedHosts, } from "./allowedHosts.js";
34
+ import { enforceBearerAuth, loadControlTokens, requiredScopeFor, } from "./controlToken.js";
21
35
  import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
36
+ import { handleApiHealth } from "./healthSurfaces.js";
22
37
  import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "./operatorResults.js";
38
+ import { installShutdownController, makeDrainableHostFromOptional, } from "./shutdown.js";
39
+ import { writeAudit } from "../logging/audit.js";
40
+ import { logger as rootLogger } from "../logging/logger.js";
41
+ import { callerIdFromRequestAuth, getRequestAuth, requestAuthFromBearer, runWithRequestAuth, } from "./requestAuthContext.js";
42
+ const auditLog = rootLogger.child({ component: "audit" });
23
43
  function textPlain(res, status, body) {
24
44
  res.writeHead(status, {
25
45
  "Content-Type": "text/plain; charset=utf-8",
@@ -82,27 +102,6 @@ async function serveStatic(res, uiDistDir, urlPath) {
82
102
  res.end(data);
83
103
  return true;
84
104
  }
85
- function isMutatingApi(method, pathname) {
86
- if (method !== "POST")
87
- return false;
88
- return (pathname === "/api/runs" ||
89
- pathname === "/api/settings" ||
90
- pathname === "/api/stages" ||
91
- pathname === "/api/pipelines" ||
92
- /^\/api\/runs\/[^/]+\/rerun$/.test(pathname) ||
93
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/answer$/.test(pathname) ||
94
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/feedback-decision$/.test(pathname) ||
95
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/retry$/.test(pathname) ||
96
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/resume$/.test(pathname) ||
97
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery$/.test(pathname) ||
98
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery\/stop$/.test(pathname) ||
99
- /^\/api\/runs\/[^/]+\/stages\/[^/]+\/abandon$/.test(pathname) ||
100
- /^\/api\/providers\/[^/]+\/login$/.test(pathname) ||
101
- /^\/api\/providers\/[^/]+\/login\/[^/]+\/answer$/.test(pathname) ||
102
- /^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
103
- /^\/api\/providers\/[^/]+\/logout$/.test(pathname) ||
104
- /^\/api\/project-mcp\/[^/]+\/probe$/.test(pathname));
105
- }
106
105
  function isCredentialMutatingApi(method, pathname) {
107
106
  if (method !== "POST")
108
107
  return false;
@@ -111,68 +110,6 @@ function isCredentialMutatingApi(method, pathname) {
111
110
  /^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
112
111
  /^\/api\/providers\/[^/]+\/logout$/.test(pathname));
113
112
  }
114
- /** Login/logout require a non-empty loopback Origin (stricter than other mutating APIs). */
115
- function assertCredentialMutatingOrigin(req, res) {
116
- const origin = req.headers.origin;
117
- if (typeof origin !== "string" || origin.length === 0) {
118
- json(res, 403, { error: "Origin required" });
119
- return false;
120
- }
121
- try {
122
- const originHost = new URL(origin).hostname;
123
- if (!isLoopbackHostname(originHost)) {
124
- json(res, 403, { error: "Forbidden origin" });
125
- return false;
126
- }
127
- }
128
- catch {
129
- json(res, 403, { error: "Forbidden origin" });
130
- return false;
131
- }
132
- return true;
133
- }
134
- function hostnameFromHostHeader(hostHeader) {
135
- if (hostHeader.startsWith("[")) {
136
- const end = hostHeader.indexOf("]");
137
- return end === -1 ? hostHeader : hostHeader.slice(1, end);
138
- }
139
- const colon = hostHeader.lastIndexOf(":");
140
- if (colon > 0 && /^\d+$/.test(hostHeader.slice(colon + 1))) {
141
- return hostHeader.slice(0, colon);
142
- }
143
- return hostHeader;
144
- }
145
- function isLoopbackHostname(hostname) {
146
- const h = hostname.toLowerCase();
147
- return h === "localhost" || h === "127.0.0.1" || h === "::1";
148
- }
149
- /** REST-friendly Host/Origin gate (plain JSON 403). Absent Origin is allowed. */
150
- function assertLoopbackHttpAccess(req, res) {
151
- const hostHeader = req.headers.host;
152
- if (typeof hostHeader !== "string" || hostHeader.length === 0) {
153
- json(res, 403, { error: "Forbidden host" });
154
- return false;
155
- }
156
- if (!isLoopbackHostname(hostnameFromHostHeader(hostHeader))) {
157
- json(res, 403, { error: "Forbidden host" });
158
- return false;
159
- }
160
- const origin = req.headers.origin;
161
- if (typeof origin === "string" && origin.length > 0) {
162
- try {
163
- const originHost = new URL(origin).hostname;
164
- if (!isLoopbackHostname(originHost)) {
165
- json(res, 403, { error: "Forbidden origin" });
166
- return false;
167
- }
168
- }
169
- catch {
170
- json(res, 403, { error: "Forbidden origin" });
171
- return false;
172
- }
173
- }
174
- return true;
175
- }
176
113
  export function defaultUiDistDir() {
177
114
  const here = path.dirname(fileURLToPath(import.meta.url));
178
115
  if (path.basename(path.dirname(here)) === "src") {
@@ -189,494 +126,1136 @@ export function defaultUiDistDir() {
189
126
  */
190
127
  export function createOperatorRoutes(deps) {
191
128
  const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
192
- return async ({ req, res, url, pathname, method }) => {
193
- if (isMutatingApi(method, pathname)) {
194
- if (!assertLoopbackHttpAccess(req, res)) {
129
+ const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
130
+ const controlTokens = deps.controlTokens ?? loadControlTokens();
131
+ return async ({ req, res, url, pathname, method, boot }) => {
132
+ if (boot.serveBlocked !== undefined && pathname.startsWith("/api/")) {
133
+ json(res, 503, {
134
+ error: boot.serveBlocked.reason,
135
+ code: boot.serveBlocked.code,
136
+ });
137
+ return true;
138
+ }
139
+ if (pathname.startsWith("/api/")) {
140
+ if (!assertAllowedHttpAccess(allowedHosts, req, res, {
141
+ requireOrigin: isCredentialMutatingApi(method, pathname),
142
+ })) {
195
143
  return true;
196
144
  }
197
- if (isCredentialMutatingApi(method, pathname) &&
198
- !assertCredentialMutatingOrigin(req, res)) {
199
- return true;
145
+ const scope = requiredScopeFor(method, pathname);
146
+ if (scope !== null) {
147
+ const authResult = enforceBearerAuth(controlTokens, req, res, scope);
148
+ if (!authResult.ok)
149
+ return true;
150
+ return runWithRequestAuth(requestAuthFromBearer(authResult.auth, "rest"), () => handleOperatorRequest());
200
151
  }
201
152
  }
202
- try {
203
- if (method === "GET" && pathname === "/api/runs") {
204
- json(res, 200, { runs: await store.listRuns() });
205
- return true;
206
- }
207
- const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
208
- if (method === "GET" && artifactMatch) {
209
- const runId = decodeURIComponent(artifactMatch[1] ?? "");
210
- const artifactPath = url.searchParams.get("path");
211
- if (artifactPath === null || artifactPath.trim().length === 0) {
212
- json(res, 400, { error: "path query parameter is required" });
213
- return true;
214
- }
215
- try {
216
- const mediaType = artifactMediaType(artifactPath);
217
- if (mediaType !== undefined) {
218
- const bytes = await readRunArtifactBytes(store, runId, artifactPath);
153
+ return handleOperatorRequest();
154
+ async function handleOperatorRequest() {
155
+ try {
156
+ if (method === "GET" && pathname === "/api/runs") {
157
+ const filter = {};
158
+ const status = url.searchParams.get("status");
159
+ const since = url.searchParams.get("since");
160
+ const pipeline = url.searchParams.get("pipeline");
161
+ const callerId = url.searchParams.get("caller_id");
162
+ if (status !== null)
163
+ filter.status = status;
164
+ if (since !== null)
165
+ filter.since = since;
166
+ if (pipeline !== null)
167
+ filter.pipeline = pipeline;
168
+ if (callerId !== null)
169
+ filter.caller_id = callerId;
170
+ json(res, 200, {
171
+ runs: await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined),
172
+ });
173
+ return true;
174
+ }
175
+ if (method === "POST" && pathname === "/api/backup") {
176
+ const body = (await readJsonBody(req));
177
+ const callerId = callerIdFromRequestAuth();
178
+ try {
179
+ const result = await createBackup({
180
+ store,
181
+ homeDir: globalStageflowHome(),
182
+ outPath: typeof body.out === "string" ? body.out : undefined,
183
+ dbOnly: body.db_only === true,
184
+ noCredentials: body.no_credentials === true,
185
+ includeA2aArtifacts: body.include_a2a_artifacts === true,
186
+ });
187
+ writeAudit(auditLog, {
188
+ caller_id: callerId,
189
+ surface: getRequestAuth()?.surface ?? "rest",
190
+ action: "backup",
191
+ outcome: "ok",
192
+ });
193
+ json(res, 200, result);
194
+ }
195
+ catch (err) {
196
+ if (err instanceof BackupError) {
197
+ writeAudit(auditLog, {
198
+ caller_id: callerId,
199
+ surface: getRequestAuth()?.surface ?? "rest",
200
+ action: "backup",
201
+ outcome: "error",
202
+ error_code: err.code,
203
+ });
204
+ const status = err.code === "backup_insufficient_disk" ? 507 : 400;
205
+ json(res, status, { error: err.message, code: err.code });
206
+ return true;
207
+ }
208
+ throw err;
209
+ }
210
+ return true;
211
+ }
212
+ const backupGetMatch = pathname.match(/^\/api\/backup\/([^/]+)$/);
213
+ if (method === "GET" && backupGetMatch) {
214
+ const name = decodeURIComponent(backupGetMatch[1] ?? "");
215
+ try {
216
+ const filePath = await resolveBackupDownloadPath(name, globalStageflowHome());
217
+ const st = statSync(filePath);
219
218
  res.writeHead(200, {
220
- "Content-Type": mediaType,
221
- "Content-Length": bytes.length,
219
+ "Content-Type": "application/octet-stream",
220
+ "Content-Length": st.size,
221
+ "Content-Disposition": `attachment; filename="${name}"`,
222
222
  });
223
- res.end(bytes);
223
+ createReadStream(filePath).pipe(res);
224
224
  }
225
- else {
226
- const content = await readRunArtifact(store, runId, artifactPath);
227
- textPlain(res, 200, content);
225
+ catch (err) {
226
+ if (err instanceof BackupError) {
227
+ json(res, 400, { error: err.message, code: err.code });
228
+ return true;
229
+ }
230
+ throw err;
228
231
  }
232
+ return true;
229
233
  }
230
- catch (err) {
231
- const mapped = mapStoreLookupError(err, { policy: "artifact" });
232
- const status = mapped.kind === "denied" ? 403 : mapped.status;
233
- json(res, status, { error: mapped.error });
234
+ if (method === "POST" && pathname === "/api/restore") {
235
+ const body = (await readJsonBody(req));
236
+ const backupName = typeof body.backup === "string"
237
+ ? body.backup
238
+ : typeof body.path === "string"
239
+ ? body.path
240
+ : undefined;
241
+ if (backupName === undefined) {
242
+ json(res, 400, {
243
+ error: "body.backup (backup name under backups/) is required",
244
+ code: "restore_not_found",
245
+ });
246
+ return true;
247
+ }
248
+ const callerId = callerIdFromRequestAuth();
249
+ try {
250
+ const archivePath = await resolveBackupNameForRestore(path.basename(backupName), globalStageflowHome());
251
+ const graceMs = parseShutdownGraceMs();
252
+ const staged = await stageRestoreForBoot({
253
+ archivePath,
254
+ homeDir: globalStageflowHome(),
255
+ graceMs,
256
+ });
257
+ writeAudit(auditLog, {
258
+ caller_id: callerId,
259
+ surface: getRequestAuth()?.surface ?? "rest",
260
+ action: "restore",
261
+ outcome: "ok",
262
+ });
263
+ json(res, 202, {
264
+ ok: true,
265
+ staged: staged.stagedPath,
266
+ drain_deadline: staged.marker.drain_deadline,
267
+ });
268
+ const shutdown = deps.getShutdown?.();
269
+ if (shutdown !== undefined) {
270
+ void shutdown.beginDrain();
271
+ }
272
+ }
273
+ catch (err) {
274
+ if (err instanceof RestoreError || err instanceof BackupError) {
275
+ writeAudit(auditLog, {
276
+ caller_id: callerId,
277
+ surface: getRequestAuth()?.surface ?? "rest",
278
+ action: "restore",
279
+ outcome: "error",
280
+ error_code: err.code,
281
+ });
282
+ json(res, 400, {
283
+ error: err.message,
284
+ code: err.code,
285
+ });
286
+ return true;
287
+ }
288
+ throw err;
289
+ }
290
+ return true;
234
291
  }
235
- return true;
236
- }
237
- const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
238
- if (method === "GET" && verificationMatch) {
239
- const runId = decodeURIComponent(verificationMatch[1] ?? "");
240
- const stageId = decodeURIComponent(verificationMatch[2] ?? "");
241
- try {
242
- json(res, 200, await readStageVerificationHistory(store, runId, stageId));
292
+ if (method === "GET" && pathname === "/api/export") {
293
+ const filter = {};
294
+ const status = url.searchParams.get("status");
295
+ const since = url.searchParams.get("since");
296
+ const pipeline = url.searchParams.get("pipeline");
297
+ const callerId = url.searchParams.get("caller_id");
298
+ if (status)
299
+ filter.status = status;
300
+ if (since)
301
+ filter.since = since;
302
+ if (pipeline)
303
+ filter.pipeline = pipeline;
304
+ if (callerId)
305
+ filter.caller_id = callerId;
306
+ res.writeHead(200, {
307
+ "Content-Type": "application/x-ndjson; charset=utf-8",
308
+ });
309
+ try {
310
+ for await (const line of iterateExportNdjson({
311
+ store,
312
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
313
+ })) {
314
+ if (req.aborted || res.writableEnded || res.destroyed) {
315
+ break;
316
+ }
317
+ if (!res.write(line)) {
318
+ await new Promise((resolve) => res.once("drain", resolve));
319
+ }
320
+ }
321
+ if (!res.writableEnded && !res.destroyed) {
322
+ res.end();
323
+ }
324
+ }
325
+ catch (err) {
326
+ if (res.headersSent) {
327
+ res.destroy(err instanceof Error ? err : new Error(String(err)));
328
+ }
329
+ else {
330
+ throw err;
331
+ }
332
+ }
333
+ return true;
243
334
  }
244
- catch (err) {
245
- const mapped = mapStoreLookupError(err, { policy: "run" });
246
- json(res, mapped.status, { error: mapped.error });
335
+ const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
336
+ if (method === "GET" && artifactMatch) {
337
+ const runId = decodeURIComponent(artifactMatch[1] ?? "");
338
+ const artifactPath = url.searchParams.get("path");
339
+ if (artifactPath === null || artifactPath.trim().length === 0) {
340
+ json(res, 400, { error: "path query parameter is required" });
341
+ return true;
342
+ }
343
+ try {
344
+ const mediaType = artifactMediaType(artifactPath);
345
+ if (mediaType !== undefined) {
346
+ const bytes = await readRunArtifactBytes(store, runId, artifactPath);
347
+ res.writeHead(200, {
348
+ "Content-Type": mediaType,
349
+ "Content-Length": bytes.length,
350
+ });
351
+ res.end(bytes);
352
+ }
353
+ else {
354
+ const content = await readRunArtifact(store, runId, artifactPath);
355
+ textPlain(res, 200, content);
356
+ }
357
+ }
358
+ catch (err) {
359
+ const mapped = mapStoreLookupError(err, { policy: "artifact" });
360
+ const status = mapped.kind === "denied" ? 403 : mapped.status;
361
+ json(res, status, { error: mapped.error });
362
+ }
363
+ return true;
247
364
  }
248
- return true;
249
- }
250
- if (method === "GET" && pathname.startsWith("/api/runs/")) {
251
- const rest = pathname.slice("/api/runs/".length);
252
- if (rest && !rest.includes("/")) {
365
+ const changesMatch = pathname.match(/^\/api\/runs\/([^/]+)\/changes$/);
366
+ if (method === "GET" && changesMatch) {
367
+ const runId = decodeURIComponent(changesMatch[1] ?? "");
253
368
  try {
254
- json(res, 200, await store.readRun(decodeURIComponent(rest)));
369
+ const result = await listCheckoutChanges(store, runId);
370
+ if (!result.ok) {
371
+ json(res, result.status, {
372
+ error: result.error,
373
+ code: result.code,
374
+ });
375
+ return true;
376
+ }
377
+ json(res, 200, {
378
+ runId,
379
+ changes: result.changes,
380
+ truncated: result.truncated,
381
+ });
255
382
  }
256
383
  catch (err) {
257
384
  const mapped = mapStoreLookupError(err, { policy: "run" });
258
- json(res, 404, { error: mapped.error });
385
+ json(res, mapped.status, { error: mapped.error });
259
386
  }
260
387
  return true;
261
388
  }
262
- }
263
- if (method === "POST" && pathname === "/api/runs") {
264
- const body = (await readJsonBody(req));
265
- if (typeof body.pipeline !== "string" ||
266
- !body.pipeline.trim() ||
267
- body.task === undefined) {
268
- json(res, 400, { error: "task and pipeline are required" });
389
+ const diffMatch = pathname.match(/^\/api\/runs\/([^/]+)\/diff$/);
390
+ if (method === "GET" && diffMatch) {
391
+ const runId = decodeURIComponent(diffMatch[1] ?? "");
392
+ const modeParam = url.searchParams.get("mode");
393
+ const mode = modeParam === "patch" || modeParam === "stat" ? modeParam : undefined;
394
+ const filterPath = url.searchParams.get("path") ?? undefined;
395
+ const maxBytesRaw = url.searchParams.get("maxBytes");
396
+ const maxBytes = maxBytesRaw !== null && maxBytesRaw.trim() !== ""
397
+ ? Number(maxBytesRaw)
398
+ : undefined;
399
+ try {
400
+ const result = await getRunDiff(store, runId, {
401
+ mode,
402
+ path: filterPath ?? undefined,
403
+ maxBytes: maxBytes !== undefined && Number.isFinite(maxBytes)
404
+ ? maxBytes
405
+ : undefined,
406
+ });
407
+ if (!result.ok) {
408
+ json(res, result.status, {
409
+ error: result.error,
410
+ code: result.code,
411
+ });
412
+ return true;
413
+ }
414
+ json(res, 200, {
415
+ runId,
416
+ mode: result.mode,
417
+ base_sha: result.base_sha,
418
+ base_source: result.base_source,
419
+ content: result.content,
420
+ truncated: result.truncated,
421
+ untracked: result.untracked,
422
+ });
423
+ }
424
+ catch (err) {
425
+ const mapped = mapStoreLookupError(err, { policy: "run" });
426
+ json(res, mapped.status, { error: mapped.error });
427
+ }
269
428
  return true;
270
429
  }
271
- if (typeof body.task !== "string" && !isTaskFile(body.task)) {
272
- json(res, 400, {
273
- error: "task must be a path string or TaskFile object (id and goal required)",
274
- });
430
+ const checkoutFileMatch = pathname.match(/^\/api\/runs\/([^/]+)\/file$/);
431
+ if (method === "GET" && checkoutFileMatch) {
432
+ const runId = decodeURIComponent(checkoutFileMatch[1] ?? "");
433
+ const filePath = url.searchParams.get("path");
434
+ if (filePath === null || filePath.trim().length === 0) {
435
+ json(res, 400, { error: "path query parameter is required" });
436
+ return true;
437
+ }
438
+ try {
439
+ const result = await readCheckoutFileBytes(store, runId, filePath);
440
+ if (!result.ok) {
441
+ json(res, result.status, {
442
+ error: result.error,
443
+ code: result.code,
444
+ });
445
+ return true;
446
+ }
447
+ const mediaType = artifactMediaType(filePath);
448
+ if (mediaType !== undefined) {
449
+ res.writeHead(200, {
450
+ "Content-Type": mediaType,
451
+ "Content-Length": result.bytes.length,
452
+ });
453
+ res.end(result.bytes);
454
+ }
455
+ else {
456
+ const classified = classifyArtifactContent(filePath, result.bytes);
457
+ if (classified.kind !== "utf8") {
458
+ json(res, 400, {
459
+ error: "Checkout file is not valid UTF-8 text",
460
+ });
461
+ return true;
462
+ }
463
+ textPlain(res, 200, result.bytes.toString("utf8"));
464
+ }
465
+ }
466
+ catch (err) {
467
+ const mapped = mapStoreLookupError(err, { policy: "artifact" });
468
+ const status = mapped.kind === "denied" ? 403 : mapped.status;
469
+ json(res, status, { error: mapped.error });
470
+ }
275
471
  return true;
276
472
  }
277
- if (body.checkoutOverride !== undefined &&
278
- typeof body.checkoutOverride !== "string") {
279
- json(res, 400, { error: "checkoutOverride must be a string" });
473
+ const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
474
+ if (method === "GET" && verificationMatch) {
475
+ const runId = decodeURIComponent(verificationMatch[1] ?? "");
476
+ const stageId = decodeURIComponent(verificationMatch[2] ?? "");
477
+ try {
478
+ json(res, 200, await readStageVerificationHistory(store, runId, stageId));
479
+ }
480
+ catch (err) {
481
+ const mapped = mapStoreLookupError(err, { policy: "run" });
482
+ json(res, mapped.status, { error: mapped.error });
483
+ }
280
484
  return true;
281
485
  }
282
- if (body.skipGates !== undefined && typeof body.skipGates !== "boolean") {
283
- json(res, 400, { error: "skipGates must be a boolean" });
486
+ const exportMatch = pathname.match(/^\/api\/runs\/([^/]+)\/export$/);
487
+ if (method === "GET" && exportMatch) {
488
+ const runId = decodeURIComponent(exportMatch[1] ?? "");
489
+ try {
490
+ const detail = await store.readRun(runId);
491
+ json(res, 200, buildRunExportPayload(detail));
492
+ }
493
+ catch (err) {
494
+ const mapped = mapStoreLookupError(err, { policy: "run" });
495
+ json(res, mapped.status, { error: mapped.error });
496
+ }
284
497
  return true;
285
498
  }
286
- for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
287
- if (body[field] !== undefined && typeof body[field] !== "string") {
288
- json(res, 400, { error: `${field} must be a string` });
289
- return true;
499
+ const debugBundleMatch = pathname.match(/^\/api\/runs\/([^/]+)\/debug-bundle$/);
500
+ if (method === "GET" && debugBundleMatch) {
501
+ const runId = decodeURIComponent(debugBundleMatch[1] ?? "");
502
+ try {
503
+ json(res, 200, await buildDebugBundle(store, runId));
504
+ }
505
+ catch (err) {
506
+ const mapped = mapStoreLookupError(err, { policy: "run" });
507
+ json(res, mapped.status, { error: mapped.error });
290
508
  }
509
+ return true;
291
510
  }
292
- let result;
293
- try {
294
- result = await manager.startRun({
295
- task: body.task,
296
- pipeline: body.pipeline.trim(),
297
- ...(body.checkoutOverride !== undefined
298
- ? { checkoutOverride: body.checkoutOverride }
299
- : {}),
300
- ...(body.skipGates !== undefined ? { skipGates: body.skipGates } : {}),
301
- ...(body.gitSha !== undefined ? { gitSha: body.gitSha } : {}),
302
- ...(body.ciPrUrl !== undefined ? { ciPrUrl: body.ciPrUrl } : {}),
303
- ...(body.ciJobUrl !== undefined ? { ciJobUrl: body.ciJobUrl } : {}),
304
- });
511
+ if (method === "GET" && pathname.startsWith("/api/runs/")) {
512
+ const rest = pathname.slice("/api/runs/".length);
513
+ if (rest && !rest.includes("/")) {
514
+ try {
515
+ const detail = await store.readRun(decodeURIComponent(rest));
516
+ json(res, 200, runDetailWithRedactedManifest(detail));
517
+ }
518
+ catch (err) {
519
+ const mapped = mapStoreLookupError(err, { policy: "run" });
520
+ json(res, 404, { error: mapped.error });
521
+ }
522
+ return true;
523
+ }
305
524
  }
306
- catch (err) {
307
- if (err instanceof PipelineValidationError) {
525
+ if (method === "POST" && pathname === "/api/projects") {
526
+ if (!isTrustedLocalHttpRequest(req)) {
527
+ json(res, 403, {
528
+ error: "ensure_project is only allowed from trusted local clients (loopback peer and Host)",
529
+ code: "ensure_project_not_allowed",
530
+ });
531
+ return true;
532
+ }
533
+ let body;
534
+ try {
535
+ body = await readJsonBody(req);
536
+ }
537
+ catch {
538
+ json(res, 400, { error: "Invalid JSON body" });
539
+ return true;
540
+ }
541
+ const projectRoot = body !== null &&
542
+ typeof body === "object" &&
543
+ !Array.isArray(body) &&
544
+ typeof body.project_root === "string"
545
+ ? body.project_root.trim()
546
+ : "";
547
+ if (!projectRoot) {
548
+ json(res, 400, { error: "project_root is required" });
549
+ return true;
550
+ }
551
+ if (!path.isAbsolute(projectRoot)) {
308
552
  json(res, 400, {
309
- error: "Pipeline validation failed",
310
- validation: err.result,
553
+ error: "project_root must be an absolute path",
311
554
  });
312
555
  return true;
313
556
  }
314
- throw err;
315
- }
316
- if (!result.ok) {
317
- json(res, result.status ?? 500, mapStartFailure(result));
557
+ try {
558
+ const registered = await store.ensureProject(projectRoot);
559
+ json(res, 200, { project_root: registered });
560
+ }
561
+ catch (err) {
562
+ const message = err instanceof Error ? err.message : String(err);
563
+ json(res, 400, {
564
+ error: message,
565
+ code: "invalid_project_root",
566
+ });
567
+ }
318
568
  return true;
319
569
  }
320
- json(res, 202, { runId: result.runId });
321
- return true;
322
- }
323
- if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
324
- const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
325
- const result = await manager.rerun(runId);
326
- if (!result.ok) {
327
- json(res, result.status ?? 500, mapStartFailure(result));
570
+ if (method === "POST" && pathname === "/api/runs") {
571
+ const body = (await readJsonBody(req));
572
+ const tokenField = findTokenShapedField(body);
573
+ if (tokenField !== undefined) {
574
+ json(res, 400, tokenRejectedPayload(tokenField));
575
+ return true;
576
+ }
577
+ const typed = body;
578
+ if (typeof typed.pipeline !== "string" ||
579
+ !typed.pipeline.trim() ||
580
+ typed.task === undefined) {
581
+ json(res, 400, { error: "task and pipeline are required" });
582
+ return true;
583
+ }
584
+ if (typeof typed.task !== "string" && !isTaskFile(typed.task)) {
585
+ json(res, 400, {
586
+ error: "task must be a path string or TaskFile object (id and goal required)",
587
+ });
588
+ return true;
589
+ }
590
+ if (typed.checkoutOverride !== undefined &&
591
+ typeof typed.checkoutOverride !== "string") {
592
+ json(res, 400, { error: "checkoutOverride must be a string" });
593
+ return true;
594
+ }
595
+ if (typed.skipGates !== undefined && typeof typed.skipGates !== "boolean") {
596
+ json(res, 400, { error: "skipGates must be a boolean" });
597
+ return true;
598
+ }
599
+ for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
600
+ if (typed[field] !== undefined && typeof typed[field] !== "string") {
601
+ json(res, 400, { error: `${field} must be a string` });
602
+ return true;
603
+ }
604
+ }
605
+ let wireRoot;
606
+ let roots;
607
+ try {
608
+ ({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, typed.project_root));
609
+ }
610
+ catch (err) {
611
+ if (err instanceof CatalogPathError) {
612
+ json(res, 400, catalogPathErrorBody(err));
613
+ return true;
614
+ }
615
+ throw err;
616
+ }
617
+ const wireProjectRoot = wireRoot.project_root;
618
+ let pipelinePath = typed.pipeline.trim();
619
+ let taskInput = typed.task;
620
+ let checkoutOverride;
621
+ try {
622
+ pipelinePath = resolveCatalogRelativePath({
623
+ inputPath: pipelinePath,
624
+ projectRoot: wireProjectRoot,
625
+ roots,
626
+ fieldName: "pipeline",
627
+ }).absolutePath;
628
+ if (typeof typed.task === "string") {
629
+ taskInput = resolveCatalogRelativePath({
630
+ inputPath: typed.task,
631
+ projectRoot: wireProjectRoot,
632
+ roots,
633
+ fieldName: "task",
634
+ }).absolutePath;
635
+ }
636
+ if (typed.checkoutOverride !== undefined) {
637
+ checkoutOverride = resolveCatalogRelativePath({
638
+ inputPath: typed.checkoutOverride,
639
+ projectRoot: wireProjectRoot,
640
+ roots,
641
+ fieldName: "checkout",
642
+ }).absolutePath;
643
+ }
644
+ }
645
+ catch (err) {
646
+ if (err instanceof CatalogPathError) {
647
+ json(res, 400, catalogPathErrorBody(err));
648
+ return true;
649
+ }
650
+ throw err;
651
+ }
652
+ let result;
653
+ const callerId = callerIdFromRequestAuth();
654
+ try {
655
+ result = await manager.startRun({
656
+ task: taskInput,
657
+ pipeline: pipelinePath,
658
+ projectRoot: wireRoot.path,
659
+ ...(checkoutOverride !== undefined
660
+ ? { checkoutOverride }
661
+ : {}),
662
+ ...(typed.skipGates !== undefined ? { skipGates: typed.skipGates } : {}),
663
+ ...(typed.gitSha !== undefined ? { gitSha: typed.gitSha } : {}),
664
+ ...(typed.ciPrUrl !== undefined ? { ciPrUrl: typed.ciPrUrl } : {}),
665
+ ...(typed.ciJobUrl !== undefined ? { ciJobUrl: typed.ciJobUrl } : {}),
666
+ ...(typed.skills !== undefined ? { skills: typed.skills } : {}),
667
+ callerId,
668
+ });
669
+ }
670
+ catch (err) {
671
+ if (err instanceof PipelineValidationError) {
672
+ writeAudit(auditLog, {
673
+ caller_id: callerId,
674
+ surface: getRequestAuth()?.surface ?? "rest",
675
+ action: "start_run",
676
+ outcome: "error",
677
+ error_code: "pipeline_validation",
678
+ });
679
+ json(res, 400, {
680
+ error: "Pipeline validation failed",
681
+ validation: err.result,
682
+ });
683
+ return true;
684
+ }
685
+ if (err instanceof PipelinePreflightError) {
686
+ writeAudit(auditLog, {
687
+ caller_id: callerId,
688
+ surface: getRequestAuth()?.surface ?? "rest",
689
+ action: "start_run",
690
+ outcome: "error",
691
+ error_code: err.code,
692
+ });
693
+ json(res, 400, err.toNetworkBody());
694
+ return true;
695
+ }
696
+ throw err;
697
+ }
698
+ if (!result.ok) {
699
+ writeAudit(auditLog, {
700
+ caller_id: callerId,
701
+ surface: getRequestAuth()?.surface ?? "rest",
702
+ action: "start_run",
703
+ outcome: "error",
704
+ error_code: result.code,
705
+ });
706
+ json(res, result.status ?? 500, mapStartFailure(result));
707
+ return true;
708
+ }
709
+ writeAudit(auditLog, {
710
+ caller_id: callerId,
711
+ surface: getRequestAuth()?.surface ?? "rest",
712
+ action: "start_run",
713
+ target_run_id: result.runId,
714
+ outcome: "ok",
715
+ });
716
+ json(res, 202, {
717
+ runId: result.runId,
718
+ ...(result.queued === true
719
+ ? {
720
+ queued: true,
721
+ queuePosition: result.queuePosition,
722
+ ...(result.queuedCode !== undefined
723
+ ? { queuedCode: result.queuedCode }
724
+ : {}),
725
+ }
726
+ : {}),
727
+ });
328
728
  return true;
329
729
  }
330
- json(res, 202, { runId: result.runId });
331
- return true;
332
- }
333
- const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
334
- if (method === "POST" && answerMatch) {
335
- const runId = decodeURIComponent(answerMatch[1] ?? "");
336
- const stageId = decodeURIComponent(answerMatch[2] ?? "");
337
- let answer;
338
- try {
339
- answer = parseAskOperatorAnswer(await readJsonBody(req));
340
- }
341
- catch (err) {
342
- json(res, 400, {
343
- error: err instanceof Error ? err.message : String(err),
730
+ if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
731
+ const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
732
+ const body = (await readJsonBody(req));
733
+ if (body.pinned !== undefined && typeof body.pinned !== "boolean") {
734
+ json(res, 400, { error: "pinned must be a boolean" });
735
+ return true;
736
+ }
737
+ const callerId = callerIdFromRequestAuth();
738
+ const result = await manager.rerun(runId, {
739
+ ...(body.pinned !== undefined ? { pinned: body.pinned } : {}),
740
+ callerId,
741
+ });
742
+ if (!result.ok) {
743
+ writeAudit(auditLog, {
744
+ caller_id: callerId,
745
+ surface: getRequestAuth()?.surface ?? "rest",
746
+ action: "rerun",
747
+ target_run_id: runId,
748
+ outcome: "error",
749
+ error_code: result.code,
750
+ });
751
+ json(res, result.status ?? 500, mapStartFailure(result));
752
+ return true;
753
+ }
754
+ writeAudit(auditLog, {
755
+ caller_id: callerId,
756
+ surface: getRequestAuth()?.surface ?? "rest",
757
+ action: "rerun",
758
+ target_run_id: result.runId,
759
+ outcome: "ok",
344
760
  });
761
+ json(res, 202, { runId: result.runId });
345
762
  return true;
346
763
  }
347
- const result = await manager.deliverAnswer(runId, stageId, answer);
348
- if (!result.ok) {
349
- json(res, result.status, { error: result.reason });
764
+ const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
765
+ if (method === "POST" && answerMatch) {
766
+ const runId = decodeURIComponent(answerMatch[1] ?? "");
767
+ const stageId = decodeURIComponent(answerMatch[2] ?? "");
768
+ let answer;
769
+ try {
770
+ answer = parseAskOperatorAnswer(await readJsonBody(req));
771
+ }
772
+ catch (err) {
773
+ json(res, 400, {
774
+ error: err instanceof Error ? err.message : String(err),
775
+ });
776
+ return true;
777
+ }
778
+ const result = await manager.deliverAnswer(runId, stageId, answer);
779
+ if (!result.ok) {
780
+ json(res, result.status, { error: result.reason });
781
+ return true;
782
+ }
783
+ json(res, 202, { ok: true });
350
784
  return true;
351
785
  }
352
- json(res, 202, { ok: true });
353
- return true;
354
- }
355
- const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
356
- if (method === "POST" && feedbackDecisionMatch) {
357
- const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
358
- const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
359
- let body;
360
- try {
361
- body = await readJsonBody(req);
362
- }
363
- catch {
364
- json(res, 400, { error: "Invalid JSON body" });
786
+ const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
787
+ if (method === "POST" && feedbackDecisionMatch) {
788
+ const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
789
+ const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
790
+ let body;
791
+ try {
792
+ body = await readJsonBody(req);
793
+ }
794
+ catch {
795
+ json(res, 400, { error: "Invalid JSON body" });
796
+ return true;
797
+ }
798
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
799
+ json(res, 400, { error: "Body must be a JSON object" });
800
+ return true;
801
+ }
802
+ const raw = body;
803
+ if (raw.decision !== "extend" &&
804
+ raw.decision !== "continue" &&
805
+ raw.decision !== "abandon") {
806
+ json(res, 400, {
807
+ error: "decision must be extend, continue, or abandon",
808
+ });
809
+ return true;
810
+ }
811
+ if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
812
+ json(res, 400, { error: "loopId must be a string" });
813
+ return true;
814
+ }
815
+ if (raw.reason !== undefined && typeof raw.reason !== "string") {
816
+ json(res, 400, { error: "reason must be a string" });
817
+ return true;
818
+ }
819
+ const result = await manager.decideFeedbackLoop(runId, stageId, {
820
+ decision: raw.decision,
821
+ ...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
822
+ ...(raw.reason !== undefined ? { reason: raw.reason } : {}),
823
+ });
824
+ if (!result.ok) {
825
+ json(res, result.status ?? 500, { error: result.reason });
826
+ return true;
827
+ }
828
+ json(res, 202, {
829
+ ok: true,
830
+ effect: result.effect,
831
+ loopId: result.loopId,
832
+ });
365
833
  return true;
366
834
  }
367
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
368
- json(res, 400, { error: "Body must be a JSON object" });
835
+ const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
836
+ if (method === "POST" && retryMatch) {
837
+ await readJsonBody(req);
838
+ const runId = decodeURIComponent(retryMatch[1] ?? "");
839
+ const stageId = decodeURIComponent(retryMatch[2] ?? "");
840
+ const result = await manager.retryStage(runId, stageId);
841
+ if (!result.ok) {
842
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
843
+ return true;
844
+ }
845
+ json(res, 202, {
846
+ runId: result.runId,
847
+ stageId: result.stageId,
848
+ attemptIndex: result.attemptIndex,
849
+ });
369
850
  return true;
370
851
  }
371
- const raw = body;
372
- if (raw.decision !== "extend" &&
373
- raw.decision !== "continue" &&
374
- raw.decision !== "abandon") {
375
- json(res, 400, {
376
- error: "decision must be extend, continue, or abandon",
852
+ const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
853
+ if (method === "POST" && resumeMatch) {
854
+ await readJsonBody(req);
855
+ const runId = decodeURIComponent(resumeMatch[1] ?? "");
856
+ const stageId = decodeURIComponent(resumeMatch[2] ?? "");
857
+ const result = await manager.resumeTimedOutStage(runId, stageId);
858
+ if (!result.ok) {
859
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
860
+ return true;
861
+ }
862
+ json(res, 202, {
863
+ runId: result.runId,
864
+ stageId: result.stageId,
865
+ attemptIndex: result.attemptIndex,
377
866
  });
378
867
  return true;
379
868
  }
380
- if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
381
- json(res, 400, { error: "loopId must be a string" });
869
+ const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
870
+ if (method === "POST" && recoverMatch) {
871
+ let body;
872
+ try {
873
+ body = await readJsonBody(req);
874
+ }
875
+ catch {
876
+ json(res, 400, { error: "Invalid JSON body" });
877
+ return true;
878
+ }
879
+ const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
880
+ ? body.guidance
881
+ : undefined;
882
+ if (guidance !== undefined && typeof guidance !== "string") {
883
+ json(res, 400, { error: "guidance must be a string" });
884
+ return true;
885
+ }
886
+ const runId = decodeURIComponent(recoverMatch[1] ?? "");
887
+ const stageId = decodeURIComponent(recoverMatch[2] ?? "");
888
+ const result = await manager.recoverManualStage(runId, stageId, guidance);
889
+ if (!result.ok) {
890
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
891
+ return true;
892
+ }
893
+ json(res, 202, {
894
+ runId: result.runId,
895
+ stageId: result.stageId,
896
+ attemptIndex: result.attemptIndex,
897
+ });
382
898
  return true;
383
899
  }
384
- if (raw.reason !== undefined && typeof raw.reason !== "string") {
385
- json(res, 400, { error: "reason must be a string" });
900
+ const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
901
+ if (method === "POST" && stopRecoveryMatch) {
902
+ await readJsonBody(req);
903
+ const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
904
+ const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
905
+ const result = await manager.stopManualRecovery(runId, stageId);
906
+ if (!result.ok) {
907
+ json(res, result.status ?? 500, { error: result.reason });
908
+ return true;
909
+ }
910
+ json(res, 202, result);
386
911
  return true;
387
912
  }
388
- const result = await manager.decideFeedbackLoop(runId, stageId, {
389
- decision: raw.decision,
390
- ...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
391
- ...(raw.reason !== undefined ? { reason: raw.reason } : {}),
392
- });
393
- if (!result.ok) {
394
- json(res, result.status ?? 500, { error: result.reason });
913
+ const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
914
+ if (method === "POST" && abandonMatch) {
915
+ await readJsonBody(req);
916
+ const runId = decodeURIComponent(abandonMatch[1] ?? "");
917
+ const stageId = decodeURIComponent(abandonMatch[2] ?? "");
918
+ const result = await manager.abandonStage(runId, stageId);
919
+ if (!result.ok) {
920
+ json(res, result.status ?? 500, {
921
+ error: result.reason,
922
+ });
923
+ return true;
924
+ }
925
+ json(res, 202, {
926
+ ok: true,
927
+ runId: result.runId,
928
+ stageId: result.stageId,
929
+ });
395
930
  return true;
396
931
  }
397
- json(res, 202, {
398
- ok: true,
399
- effect: result.effect,
400
- loopId: result.loopId,
401
- });
402
- return true;
403
- }
404
- const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
405
- if (method === "POST" && retryMatch) {
406
- await readJsonBody(req);
407
- const runId = decodeURIComponent(retryMatch[1] ?? "");
408
- const stageId = decodeURIComponent(retryMatch[2] ?? "");
409
- const result = await manager.retryStage(runId, stageId);
410
- if (!result.ok) {
411
- json(res, result.status ?? 500, mapRetryStageFailure(result));
412
- return true;
413
- }
414
- json(res, 202, {
415
- runId: result.runId,
416
- stageId: result.stageId,
417
- attemptIndex: result.attemptIndex,
418
- });
419
- return true;
420
- }
421
- const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
422
- if (method === "POST" && resumeMatch) {
423
- await readJsonBody(req);
424
- const runId = decodeURIComponent(resumeMatch[1] ?? "");
425
- const stageId = decodeURIComponent(resumeMatch[2] ?? "");
426
- const result = await manager.resumeTimedOutStage(runId, stageId);
427
- if (!result.ok) {
428
- json(res, result.status ?? 500, mapRetryStageFailure(result));
429
- return true;
430
- }
431
- json(res, 202, {
432
- runId: result.runId,
433
- stageId: result.stageId,
434
- attemptIndex: result.attemptIndex,
435
- });
436
- return true;
437
- }
438
- const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
439
- if (method === "POST" && recoverMatch) {
440
- let body;
441
- try {
442
- body = await readJsonBody(req);
443
- }
444
- catch {
445
- json(res, 400, { error: "Invalid JSON body" });
932
+ const cancelMatch = pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/);
933
+ if (method === "POST" && cancelMatch) {
934
+ const body = (await readJsonBody(req));
935
+ const runId = decodeURIComponent(cancelMatch[1] ?? "");
936
+ if (typeof body.reason !== "string" || body.reason.trim().length === 0) {
937
+ json(res, 400, { error: "Cancel reason is required" });
938
+ return true;
939
+ }
940
+ const result = await manager.cancelRun(runId, body.reason);
941
+ if (!result.ok) {
942
+ json(res, result.status ?? 500, {
943
+ error: result.reason,
944
+ });
945
+ return true;
946
+ }
947
+ json(res, 202, {
948
+ ok: true,
949
+ runId: result.runId,
950
+ });
446
951
  return true;
447
952
  }
448
- const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
449
- ? body.guidance
450
- : undefined;
451
- if (guidance !== undefined && typeof guidance !== "string") {
452
- json(res, 400, { error: "guidance must be a string" });
953
+ if (method === "POST" && pathname === "/api/runs/gc") {
954
+ const body = (await readJsonBody(req));
955
+ const execute = body.execute === true;
956
+ const result = await manager.gcRuns({ execute, channel: "rest" });
957
+ if (!result.ok) {
958
+ json(res, result.status ?? 500, {
959
+ error: result.reason,
960
+ });
961
+ return true;
962
+ }
963
+ json(res, 200, {
964
+ slimmed: result.slimmed,
965
+ purged: result.purged,
966
+ bareCachesEvicted: result.bareCachesEvicted,
967
+ });
453
968
  return true;
454
969
  }
455
- const runId = decodeURIComponent(recoverMatch[1] ?? "");
456
- const stageId = decodeURIComponent(recoverMatch[2] ?? "");
457
- const result = await manager.recoverManualStage(runId, stageId, guidance);
458
- if (!result.ok) {
459
- json(res, result.status ?? 500, mapRetryStageFailure(result));
970
+ const deleteMatch = pathname.match(/^\/api\/runs\/([^/]+)$/);
971
+ if (method === "DELETE" && deleteMatch) {
972
+ const runId = decodeURIComponent(deleteMatch[1] ?? "");
973
+ const forceParam = url.searchParams.get("force");
974
+ const force = forceParam === "true" || forceParam === "1";
975
+ const result = await manager.deleteRun(runId, {
976
+ force,
977
+ channel: "rest",
978
+ });
979
+ if (!result.ok) {
980
+ json(res, result.status ?? 500, {
981
+ error: result.reason,
982
+ });
983
+ return true;
984
+ }
985
+ json(res, 200, {
986
+ ok: true,
987
+ runId: result.runId,
988
+ });
460
989
  return true;
461
990
  }
462
- json(res, 202, {
463
- runId: result.runId,
464
- stageId: result.stageId,
465
- attemptIndex: result.attemptIndex,
466
- });
467
- return true;
468
- }
469
- const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
470
- if (method === "POST" && stopRecoveryMatch) {
471
- await readJsonBody(req);
472
- const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
473
- const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
474
- const result = await manager.stopManualRecovery(runId, stageId);
475
- if (!result.ok) {
476
- json(res, result.status ?? 500, { error: result.reason });
991
+ if (method === "GET" && pathname === "/api/tasks") {
992
+ const filter = url.searchParams.get("project_root") ?? undefined;
993
+ const result = await listTasksMultiProject({
994
+ store,
995
+ bootCwd: cwd,
996
+ projectRootFilter: filter,
997
+ });
998
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
999
+ result.items.length === 0) {
1000
+ json(res, 400, {
1001
+ error: result.root_errors[0].message,
1002
+ code: "unknown_project_root",
1003
+ root_errors: result.root_errors,
1004
+ });
1005
+ return true;
1006
+ }
1007
+ json(res, 200, {
1008
+ tasks: result.items,
1009
+ root_errors: result.root_errors,
1010
+ });
477
1011
  return true;
478
1012
  }
479
- json(res, 202, result);
480
- return true;
481
- }
482
- const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
483
- if (method === "POST" && abandonMatch) {
484
- await readJsonBody(req);
485
- const runId = decodeURIComponent(abandonMatch[1] ?? "");
486
- const stageId = decodeURIComponent(abandonMatch[2] ?? "");
487
- const result = await manager.abandonStage(runId, stageId);
488
- if (!result.ok) {
489
- json(res, result.status ?? 500, {
490
- error: result.reason,
1013
+ if (method === "GET" && pathname === "/api/pipelines") {
1014
+ const filter = url.searchParams.get("project_root") ?? undefined;
1015
+ const result = await listPipelinesMultiProject({
1016
+ store,
1017
+ bootCwd: cwd,
1018
+ projectRootFilter: filter,
1019
+ });
1020
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1021
+ result.items.length === 0) {
1022
+ json(res, 400, {
1023
+ error: result.root_errors[0].message,
1024
+ code: "unknown_project_root",
1025
+ root_errors: result.root_errors,
1026
+ });
1027
+ return true;
1028
+ }
1029
+ json(res, 200, {
1030
+ pipelines: result.items,
1031
+ root_errors: result.root_errors,
491
1032
  });
492
1033
  return true;
493
1034
  }
494
- json(res, 202, {
495
- ok: true,
496
- runId: result.runId,
497
- stageId: result.stageId,
498
- });
499
- return true;
500
- }
501
- if (method === "GET" && pathname === "/api/tasks") {
502
- const catalog = await browseCatalog(cwd);
503
- json(res, 200, { tasks: catalog.tasks });
504
- return true;
505
- }
506
- if (method === "GET" && pathname === "/api/pipelines") {
507
- const catalog = await browseCatalog(cwd);
508
- json(res, 200, { pipelines: catalog.pipelines });
509
- return true;
510
- }
511
- if (method === "GET" && pathname === "/api/stages") {
512
- json(res, 404, {
513
- error: "Global stage library removed; stages are pipeline-scoped",
514
- });
515
- return true;
516
- }
517
- if (method === "POST" && pathname === "/api/stages") {
518
- let body;
519
- try {
520
- body = await readJsonBody(req);
1035
+ if (method === "GET" && pathname === "/api/stages") {
1036
+ json(res, 404, {
1037
+ error: "Global stage library removed; stages are pipeline-scoped",
1038
+ });
1039
+ return true;
521
1040
  }
522
- catch {
523
- json(res, 400, { error: "Invalid JSON body" });
1041
+ if (method === "POST" && pathname === "/api/stages") {
1042
+ let body;
1043
+ try {
1044
+ body = await readJsonBody(req);
1045
+ }
1046
+ catch {
1047
+ json(res, 400, { error: "Invalid JSON body" });
1048
+ return true;
1049
+ }
1050
+ const writeRoot = body !== null &&
1051
+ typeof body === "object" &&
1052
+ !Array.isArray(body) &&
1053
+ typeof body.project_root === "string"
1054
+ ? body.project_root
1055
+ : undefined;
1056
+ let stageWriteRoot;
1057
+ try {
1058
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1059
+ stageWriteRoot = selected.path;
1060
+ }
1061
+ catch (err) {
1062
+ if (err instanceof CatalogPathError) {
1063
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1064
+ return true;
1065
+ }
1066
+ throw err;
1067
+ }
1068
+ const parsed = parseCreateStageBody(body);
1069
+ if ("ok" in parsed) {
1070
+ json(res, parsed.status, { error: parsed.error });
1071
+ return true;
1072
+ }
1073
+ const ctx = await resolveStageflowContext(stageWriteRoot);
1074
+ if (!ctx.isGitProject) {
1075
+ json(res, 400, {
1076
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
1077
+ });
1078
+ return true;
1079
+ }
1080
+ const result = await createStage(ctx.projectRoot, parsed);
1081
+ if (!result.ok) {
1082
+ json(res, result.status, { error: result.error });
1083
+ return true;
1084
+ }
1085
+ json(res, 201, result.stage);
524
1086
  return true;
525
1087
  }
526
- const parsed = parseCreateStageBody(body);
527
- if ("ok" in parsed) {
528
- json(res, parsed.status, { error: parsed.error });
1088
+ if (method === "POST" && pathname === "/api/pipelines") {
1089
+ let body;
1090
+ try {
1091
+ body = await readJsonBody(req);
1092
+ }
1093
+ catch {
1094
+ json(res, 400, { error: "Invalid JSON body" });
1095
+ return true;
1096
+ }
1097
+ const writeRoot = body !== null &&
1098
+ typeof body === "object" &&
1099
+ !Array.isArray(body) &&
1100
+ typeof body.project_root === "string"
1101
+ ? body.project_root
1102
+ : undefined;
1103
+ let pipelineWriteRoot;
1104
+ try {
1105
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1106
+ pipelineWriteRoot = selected.path;
1107
+ }
1108
+ catch (err) {
1109
+ if (err instanceof CatalogPathError) {
1110
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1111
+ return true;
1112
+ }
1113
+ throw err;
1114
+ }
1115
+ const parsed = parseCreatePipelineBody(body);
1116
+ if ("ok" in parsed) {
1117
+ json(res, parsed.status, { error: parsed.error });
1118
+ return true;
1119
+ }
1120
+ const ctx = await resolveStageflowContext(pipelineWriteRoot);
1121
+ if (!ctx.isGitProject) {
1122
+ json(res, 400, {
1123
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
1124
+ });
1125
+ return true;
1126
+ }
1127
+ const result = await createPipeline(ctx.projectRoot, parsed);
1128
+ if (!result.ok) {
1129
+ json(res, result.status, { error: result.error });
1130
+ return true;
1131
+ }
1132
+ json(res, 201, result.pipeline);
529
1133
  return true;
530
1134
  }
531
- const ctx = await resolveStageflowContext(cwd);
532
- if (!ctx.isGitProject) {
533
- json(res, 400, {
534
- error: "Project root not found; initialize stageflow.yaml in a git repo",
1135
+ if (method === "GET" && pathname === "/api/models") {
1136
+ const filter = url.searchParams.get("project_root") ?? undefined;
1137
+ const result = await listModelsMultiProject({
1138
+ store,
1139
+ bootCwd: cwd,
1140
+ projectRootFilter: filter,
1141
+ });
1142
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1143
+ result.items.length === 0) {
1144
+ json(res, 400, {
1145
+ error: result.root_errors[0].message,
1146
+ code: "unknown_project_root",
1147
+ root_errors: result.root_errors,
1148
+ });
1149
+ return true;
1150
+ }
1151
+ json(res, 200, {
1152
+ models: result.models,
1153
+ entries: result.items,
1154
+ root_errors: result.root_errors,
535
1155
  });
536
1156
  return true;
537
1157
  }
538
- const result = await createStage(ctx.projectRoot, parsed);
539
- if (!result.ok) {
540
- json(res, result.status, { error: result.error });
1158
+ if (await handleProviderRoutes(req, res, {
1159
+ cwd,
1160
+ readJsonBody,
1161
+ json,
1162
+ providerAuthContext,
1163
+ })) {
541
1164
  return true;
542
1165
  }
543
- json(res, 201, result.stage);
544
- return true;
545
- }
546
- if (method === "POST" && pathname === "/api/pipelines") {
547
- let body;
548
- try {
549
- body = await readJsonBody(req);
550
- }
551
- catch {
552
- json(res, 400, { error: "Invalid JSON body" });
1166
+ if (await handleProjectMcpRoutes(req, res, {
1167
+ projectRoot: rootDir,
1168
+ json,
1169
+ })) {
553
1170
  return true;
554
1171
  }
555
- const parsed = parseCreatePipelineBody(body);
556
- if ("ok" in parsed) {
557
- json(res, parsed.status, { error: parsed.error });
1172
+ if (method === "GET" && pathname === "/api/skills") {
1173
+ json(res, 200, await listSkills({ cwd, agentDir }));
558
1174
  return true;
559
1175
  }
560
- const ctx = await resolveStageflowContext(cwd);
561
- if (!ctx.isGitProject) {
562
- json(res, 400, {
563
- error: "Project root not found; initialize stageflow.yaml in a git repo",
564
- });
1176
+ if (method === "GET" && pathname === "/api/extensions") {
1177
+ json(res, 200, await listExtensions({ cwd, agentDir }));
565
1178
  return true;
566
1179
  }
567
- const result = await createPipeline(ctx.projectRoot, parsed);
568
- if (!result.ok) {
569
- json(res, result.status, { error: result.error });
1180
+ if (method === "GET" && pathname === "/api/health") {
1181
+ await handleApiHealth(req, res, boot);
570
1182
  return true;
571
1183
  }
572
- json(res, 201, result.pipeline);
573
- return true;
574
- }
575
- if (method === "GET" && pathname === "/api/models") {
576
- const catalog = await browseCatalog(cwd);
577
- json(res, 200, { models: catalog.models });
578
- return true;
579
- }
580
- if (await handleProviderRoutes(req, res, {
581
- cwd,
582
- readJsonBody,
583
- json,
584
- providerAuthContext,
585
- })) {
586
- return true;
587
- }
588
- if (await handleProjectMcpRoutes(req, res, {
589
- projectRoot: rootDir,
590
- json,
591
- })) {
592
- return true;
593
- }
594
- if (method === "GET" && pathname === "/api/skills") {
595
- json(res, 200, await listSkills({ cwd, agentDir }));
596
- return true;
597
- }
598
- if (method === "GET" && pathname === "/api/extensions") {
599
- json(res, 200, await listExtensions({ cwd, agentDir }));
600
- return true;
601
- }
602
- if (method === "GET" && pathname === "/api/health") {
603
- json(res, 200, manager.getHealth());
604
- return true;
605
- }
606
- if (method === "GET" && pathname === "/api/settings") {
607
- const health = manager.getHealth();
608
- const credential = getCredentialSourceSettings(cwd);
609
- json(res, 200, {
610
- maxConcurrent: health.maxConcurrent,
611
- ...credential,
612
- });
613
- return true;
614
- }
615
- if (method === "POST" && pathname === "/api/settings") {
616
- let body;
617
- try {
618
- body = await readJsonBody(req);
619
- }
620
- catch {
621
- json(res, 400, { error: "Invalid JSON body" });
622
- return true;
623
- }
624
- const record = body !== null && typeof body === "object" && !Array.isArray(body)
625
- ? body
626
- : {};
627
- const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
628
- const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
629
- if (!hasMax && !hasCredentialSource) {
630
- json(res, 400, {
631
- error: "maxConcurrent or credentialSource is required",
1184
+ if (method === "GET" && pathname === "/api/settings") {
1185
+ const health = manager.getHealth();
1186
+ const credential = getCredentialSourceSettings(cwd);
1187
+ json(res, 200, {
1188
+ maxConcurrent: health.maxConcurrent,
1189
+ ...credential,
632
1190
  });
633
1191
  return true;
634
1192
  }
635
- let health = manager.getHealth();
636
- if (hasMax) {
637
- const n = parseSlotCount(record.maxConcurrent);
638
- if (n === undefined) {
639
- json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
640
- return true;
641
- }
642
- health = manager.setMaxConcurrent(n);
643
- }
644
- let credential = getCredentialSourceSettings(cwd);
645
- if (hasCredentialSource) {
1193
+ if (method === "POST" && pathname === "/api/settings") {
1194
+ let body;
646
1195
  try {
647
- credential = setCredentialSource(cwd, record.credentialSource);
1196
+ body = await readJsonBody(req);
648
1197
  }
649
- catch (err) {
650
- const mapped = mapProviderAuthError(err);
651
- json(res, mapped.status, mapped.body);
1198
+ catch {
1199
+ json(res, 400, { error: "Invalid JSON body" });
652
1200
  return true;
653
1201
  }
1202
+ const record = body !== null && typeof body === "object" && !Array.isArray(body)
1203
+ ? body
1204
+ : {};
1205
+ const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
1206
+ const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
1207
+ if (!hasMax && !hasCredentialSource) {
1208
+ json(res, 400, {
1209
+ error: "maxConcurrent or credentialSource is required",
1210
+ });
1211
+ return true;
1212
+ }
1213
+ let health = manager.getHealth();
1214
+ if (hasMax) {
1215
+ const n = parseSlotCount(record.maxConcurrent);
1216
+ if (n === undefined) {
1217
+ json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
1218
+ return true;
1219
+ }
1220
+ health = manager.setMaxConcurrent(n);
1221
+ }
1222
+ let credential = getCredentialSourceSettings(cwd);
1223
+ if (hasCredentialSource) {
1224
+ try {
1225
+ credential = setCredentialSource(cwd, record.credentialSource);
1226
+ }
1227
+ catch (err) {
1228
+ const mapped = mapProviderAuthError(err);
1229
+ json(res, mapped.status, mapped.body);
1230
+ return true;
1231
+ }
1232
+ }
1233
+ json(res, 200, {
1234
+ ...health,
1235
+ ...credential,
1236
+ });
1237
+ return true;
654
1238
  }
655
- json(res, 200, {
656
- ...health,
657
- ...credential,
658
- });
659
- return true;
660
- }
661
- if (method === "GET" && uiDistDir !== undefined) {
662
- const served = await serveStatic(res, uiDistDir, pathname);
663
- if (served)
1239
+ if (method === "GET" && uiDistDir !== undefined) {
1240
+ const served = await serveStatic(res, uiDistDir, pathname);
1241
+ if (served)
1242
+ return true;
1243
+ json(res, 404, {
1244
+ error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
1245
+ });
664
1246
  return true;
665
- json(res, 404, {
666
- error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
667
- });
1247
+ }
1248
+ json(res, 404, { error: "Not found" });
668
1249
  return true;
669
1250
  }
670
- json(res, 404, { error: "Not found" });
671
- return true;
672
- }
673
- catch (err) {
674
- if (!res.headersSent) {
675
- json(res, 500, {
676
- error: err instanceof Error ? err.message : String(err),
677
- });
1251
+ catch (err) {
1252
+ if (!res.headersSent) {
1253
+ json(res, 500, {
1254
+ error: err instanceof Error ? err.message : String(err),
1255
+ });
1256
+ }
1257
+ return true;
678
1258
  }
679
- return true;
680
1259
  }
681
1260
  };
682
1261
  }
@@ -685,21 +1264,42 @@ export async function startUiServer(options) {
685
1264
  const port = options.port ?? DEFAULT_PORT;
686
1265
  const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
687
1266
  const boot = await bootstrapStageflowHost(options);
688
- const { manager, store, cwd, agentDir, rootDir } = boot;
1267
+ const { cwd, agentDir, rootDir } = boot;
689
1268
  const providerAuthContext = boot.providerAuthContext;
690
- return createHttpHost({
691
- boot,
692
- host,
693
- port,
694
- routes: createOperatorRoutes({
695
- manager,
696
- store,
1269
+ const allowedHosts = options.allowedHosts ?? resolveAllowedHosts();
1270
+ const controlTokens = options.controlTokens ?? loadControlTokens();
1271
+ let shutdown;
1272
+ const routes = boot.serveBlocked !== undefined ||
1273
+ boot.manager === undefined ||
1274
+ boot.store === undefined
1275
+ ? async () => false
1276
+ : createOperatorRoutes({
1277
+ manager: boot.manager,
1278
+ store: boot.store,
697
1279
  cwd,
698
1280
  agentDir,
699
1281
  rootDir,
700
1282
  providerAuthContext,
701
1283
  uiDistDir,
702
- }),
1284
+ allowedHosts,
1285
+ controlTokens,
1286
+ getShutdown: () => shutdown,
1287
+ });
1288
+ const envelope = await createHttpHost({
1289
+ boot,
1290
+ host,
1291
+ port,
1292
+ allowedHosts,
1293
+ controlTokens,
1294
+ routes,
1295
+ });
1296
+ shutdown = installShutdownController({
1297
+ server: envelope.server,
1298
+ host: makeDrainableHostFromOptional(envelope.manager, envelope.store),
1299
+ });
1300
+ envelope.server.on("close", () => {
1301
+ shutdown?.uninstall();
703
1302
  });
1303
+ return { ...envelope, shutdown };
704
1304
  }
705
1305
  export { DEFAULT_PORT };