stageflow 0.25.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (681) hide show
  1. package/README.md +14 -3
  2. package/builtin-skills/browser/SKILL.md +42 -0
  3. package/dist/a2a/contracts.d.ts +1 -0
  4. package/dist/a2a/contracts.js +1 -0
  5. package/dist/a2a/registry.d.ts +2 -0
  6. package/dist/a2a/registry.js +20 -1
  7. package/dist/a2a/server.d.ts +7 -0
  8. package/dist/a2a/server.js +5 -2
  9. package/dist/a2a/service.d.ts +1 -1
  10. package/dist/a2a/service.js +57 -8
  11. package/dist/a2a/store.d.ts +8 -5
  12. package/dist/a2a/store.js +24 -67
  13. package/dist/agent/activity.d.ts +3 -0
  14. package/dist/agent/bootProviderConfig.d.ts +21 -0
  15. package/dist/agent/bootProviderConfig.js +74 -0
  16. package/dist/agent/claudeAdapter.js +2 -2
  17. package/dist/agent/cursorProvider.d.ts +2 -0
  18. package/dist/agent/cursorProvider.js +30 -5
  19. package/dist/agent/fakeAgent.d.ts +1 -0
  20. package/dist/agent/fakeAgent.js +13 -0
  21. package/dist/agent/piAdapter.d.ts +11 -24
  22. package/dist/agent/piAdapter.js +107 -50
  23. package/dist/agent/piIsolatedMcp.d.ts +2 -0
  24. package/dist/agent/piIsolatedMcp.js +10 -2
  25. package/dist/agent/piSessionFactory.d.ts +38 -0
  26. package/dist/agent/piSessionFactory.js +60 -0
  27. package/dist/agent/port.d.ts +8 -0
  28. package/dist/agent/streamLogRedact.d.ts +7 -1
  29. package/dist/agent/streamLogRedact.js +26 -11
  30. package/dist/browser/anchor.d.ts +27 -0
  31. package/dist/browser/anchor.js +127 -0
  32. package/dist/browser/auditSink.d.ts +38 -0
  33. package/dist/browser/auditSink.js +34 -0
  34. package/dist/browser/browserHost.d.ts +86 -0
  35. package/dist/browser/browserHost.js +7 -0
  36. package/dist/browser/browserSweep.d.ts +14 -0
  37. package/dist/browser/browserSweep.js +54 -0
  38. package/dist/browser/browserTeardown.d.ts +56 -0
  39. package/dist/browser/browserTeardown.js +235 -0
  40. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  41. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  42. package/dist/browser/gateHandoff.d.ts +25 -0
  43. package/dist/browser/gateHandoff.js +83 -0
  44. package/dist/browser/humanLogin.d.ts +18 -0
  45. package/dist/browser/humanLogin.js +34 -0
  46. package/dist/browser/keyProvider.d.ts +10 -0
  47. package/dist/browser/keyProvider.js +39 -0
  48. package/dist/browser/localBrowserHost.d.ts +10 -0
  49. package/dist/browser/localBrowserHost.js +152 -0
  50. package/dist/browser/localProfileLock.d.ts +2 -0
  51. package/dist/browser/localProfileLock.js +218 -0
  52. package/dist/browser/localProfileStore.d.ts +5 -0
  53. package/dist/browser/localProfileStore.js +91 -0
  54. package/dist/browser/loginCheck.d.ts +53 -0
  55. package/dist/browser/loginCheck.js +147 -0
  56. package/dist/browser/memoryProfileLock.d.ts +2 -0
  57. package/dist/browser/memoryProfileLock.js +53 -0
  58. package/dist/browser/memoryProfileStore.d.ts +2 -0
  59. package/dist/browser/memoryProfileStore.js +31 -0
  60. package/dist/browser/navigationAudit.d.ts +16 -0
  61. package/dist/browser/navigationAudit.js +53 -0
  62. package/dist/browser/persistedEnv.d.ts +3 -0
  63. package/dist/browser/persistedEnv.js +15 -0
  64. package/dist/browser/profileLock.d.ts +40 -0
  65. package/dist/browser/profileLock.js +10 -0
  66. package/dist/browser/profileStore.d.ts +23 -0
  67. package/dist/browser/profileStore.js +39 -0
  68. package/dist/browser/runLiveness.d.ts +13 -0
  69. package/dist/browser/runLiveness.js +39 -0
  70. package/dist/browser/sitePolicy.d.ts +10 -0
  71. package/dist/browser/sitePolicy.js +61 -0
  72. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  73. package/dist/browser/stageBrowserEnv.js +147 -0
  74. package/dist/browser/stageProfileLock.d.ts +17 -0
  75. package/dist/browser/stageProfileLock.js +39 -0
  76. package/dist/cli/artifactCommand.js +9 -16
  77. package/dist/cli/backupCommand.d.ts +9 -0
  78. package/dist/cli/backupCommand.js +111 -0
  79. package/dist/cli/browserCommand.d.ts +44 -0
  80. package/dist/cli/browserCommand.js +472 -0
  81. package/dist/cli/debugRunCommand.d.ts +10 -0
  82. package/dist/cli/debugRunCommand.js +109 -0
  83. package/dist/cli/doctorCommand.d.ts +32 -0
  84. package/dist/cli/doctorCommand.js +453 -0
  85. package/dist/cli/envelopeCommand.js +9 -2
  86. package/dist/cli/exportAllCommand.d.ts +55 -0
  87. package/dist/cli/exportAllCommand.js +218 -0
  88. package/dist/cli/exportRunCommand.js +23 -21
  89. package/dist/cli/hostClient.d.ts +41 -3
  90. package/dist/cli/hostClient.js +129 -14
  91. package/dist/cli/initCommand.d.ts +1 -0
  92. package/dist/cli/initCommand.js +23 -7
  93. package/dist/cli/initTemplates.d.ts +5 -2
  94. package/dist/cli/initTemplates.js +32 -2
  95. package/dist/cli/resolveSafeOutPath.d.ts +2 -0
  96. package/dist/cli/resolveSafeOutPath.js +40 -0
  97. package/dist/cli/restoreCommand.d.ts +10 -0
  98. package/dist/cli/restoreCommand.js +90 -0
  99. package/dist/cli/runCommand.d.ts +3 -2
  100. package/dist/cli/runCommand.js +93 -11
  101. package/dist/cli/runOutput.d.ts +4 -0
  102. package/dist/cli/runOutput.js +33 -7
  103. package/dist/cli/runStageCommand.d.ts +1 -0
  104. package/dist/cli/runStageCommand.js +44 -25
  105. package/dist/cli/runsCommand.d.ts +1 -1
  106. package/dist/cli/runsCommand.js +168 -10
  107. package/dist/cli/triggerCommand.d.ts +12 -0
  108. package/dist/cli/triggerCommand.js +183 -0
  109. package/dist/cli/validateCommand.d.ts +1 -1
  110. package/dist/cli/validateCommand.js +23 -6
  111. package/dist/cli.js +202 -32
  112. package/dist/config/browseCatalog.d.ts +1 -0
  113. package/dist/config/browseCatalog.js +5 -4
  114. package/dist/config/builtinSkills.d.ts +4 -0
  115. package/dist/config/builtinSkills.js +23 -0
  116. package/dist/config/catalogRelativePath.d.ts +63 -0
  117. package/dist/config/catalogRelativePath.js +126 -0
  118. package/dist/config/configOrigin.d.ts +27 -0
  119. package/dist/config/configOrigin.js +59 -0
  120. package/dist/config/createTrigger.d.ts +27 -0
  121. package/dist/config/createTrigger.js +232 -0
  122. package/dist/config/draftPackage.d.ts +117 -0
  123. package/dist/config/draftPackage.js +550 -0
  124. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  125. package/dist/config/ensureCatalogScanRoot.js +73 -0
  126. package/dist/config/hostConfig.d.ts +57 -0
  127. package/dist/config/hostConfig.js +360 -0
  128. package/dist/config/listSkills.d.ts +26 -0
  129. package/dist/config/listSkills.js +206 -5
  130. package/dist/config/loadOutcome.d.ts +7 -1
  131. package/dist/config/loadPipeline.js +39 -2
  132. package/dist/config/loadStage.js +25 -0
  133. package/dist/config/loadStageflowManifest.js +25 -0
  134. package/dist/config/loadTask.d.ts +2 -0
  135. package/dist/config/loadTask.js +57 -18
  136. package/dist/config/loadTrigger.d.ts +9 -0
  137. package/dist/config/loadTrigger.js +120 -0
  138. package/dist/config/materializeStageModels.js +5 -1
  139. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  140. package/dist/config/mergePipelineIncludes.js +19 -0
  141. package/dist/config/migrateYaml.js +6 -11
  142. package/dist/config/multiProjectCatalog.d.ts +32 -0
  143. package/dist/config/multiProjectCatalog.js +150 -0
  144. package/dist/config/normalizePipelineStageEntry.js +27 -1
  145. package/dist/config/parseCompletionContract.js +6 -0
  146. package/dist/config/pipelineStageKeys.js +3 -0
  147. package/dist/config/publishDraftPackage.d.ts +7 -0
  148. package/dist/config/publishDraftPackage.js +25 -0
  149. package/dist/config/resolveCatalogContext.js +1 -1
  150. package/dist/config/resolveCatalogRoots.d.ts +27 -0
  151. package/dist/config/resolveCatalogRoots.js +76 -0
  152. package/dist/config/resolveModel.d.ts +7 -1
  153. package/dist/config/resolveModel.js +11 -1
  154. package/dist/config/resolvePipelineDag.js +1 -1
  155. package/dist/config/resolveStageMcpServers.d.ts +9 -1
  156. package/dist/config/resolveStageMcpServers.js +36 -9
  157. package/dist/config/scanCatalogPaths.d.ts +1 -1
  158. package/dist/config/scanCatalogPaths.js +6 -2
  159. package/dist/config/secretFromEnvOrFile.d.ts +9 -0
  160. package/dist/config/secretFromEnvOrFile.js +34 -0
  161. package/dist/config/seededCatalog.d.ts +11 -0
  162. package/dist/config/seededCatalog.js +29 -0
  163. package/dist/config/stageBrowser.d.ts +12 -0
  164. package/dist/config/stageBrowser.js +118 -0
  165. package/dist/config/toolRequires.d.ts +24 -0
  166. package/dist/config/toolRequires.js +115 -0
  167. package/dist/config/triggerCatalog.d.ts +10 -0
  168. package/dist/config/triggerCatalog.js +37 -0
  169. package/dist/config/validateCatalog.d.ts +17 -3
  170. package/dist/config/validateCatalog.js +223 -3
  171. package/dist/config/validateCompletionContract.js +8 -0
  172. package/dist/diagnostics/checks.d.ts +39 -0
  173. package/dist/diagnostics/checks.js +135 -0
  174. package/dist/envelope/preEmitChecks.d.ts +1 -1
  175. package/dist/envelope/preEmitChecks.js +8 -0
  176. package/dist/errors/codes.d.ts +14 -0
  177. package/dist/errors/codes.js +59 -0
  178. package/dist/git/cache.d.ts +14 -0
  179. package/dist/git/cache.js +150 -0
  180. package/dist/git/credentials.d.ts +9 -0
  181. package/dist/git/credentials.js +88 -0
  182. package/dist/git/errors.d.ts +20 -0
  183. package/dist/git/errors.js +94 -0
  184. package/dist/git/exec.d.ts +16 -0
  185. package/dist/git/exec.js +135 -0
  186. package/dist/git/operations.d.ts +29 -0
  187. package/dist/git/operations.js +153 -0
  188. package/dist/git/version.d.ts +1 -0
  189. package/dist/git/version.js +10 -0
  190. package/dist/logging/audit.d.ts +16 -0
  191. package/dist/logging/audit.js +19 -0
  192. package/dist/logging/commonWords.d.ts +2 -0
  193. package/dist/logging/commonWords.js +13 -0
  194. package/dist/logging/logger.d.ts +49 -0
  195. package/dist/logging/logger.js +193 -0
  196. package/dist/logging/namedSecrets.d.ts +6 -0
  197. package/dist/logging/namedSecrets.js +58 -0
  198. package/dist/logging/redact.d.ts +17 -0
  199. package/dist/logging/redact.js +105 -0
  200. package/dist/mcp/catalogTools.js +492 -81
  201. package/dist/mcp/checkoutTools.d.ts +52 -0
  202. package/dist/mcp/checkoutTools.js +332 -0
  203. package/dist/mcp/controlTools.js +182 -6
  204. package/dist/mcp/deps.d.ts +3 -0
  205. package/dist/mcp/getStartedTool.d.ts +3 -0
  206. package/dist/mcp/getStartedTool.js +62 -0
  207. package/dist/mcp/readArtifact.d.ts +11 -0
  208. package/dist/mcp/readArtifact.js +46 -30
  209. package/dist/mcp/server.js +2 -0
  210. package/dist/mcp/tools.js +6 -0
  211. package/dist/mcp/triggerTools.d.ts +3 -0
  212. package/dist/mcp/triggerTools.js +176 -0
  213. package/dist/net/proxy.d.ts +14 -0
  214. package/dist/net/proxy.js +78 -0
  215. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  216. package/dist/operatorAgent/docsRetrieval.js +269 -0
  217. package/dist/operatorAgent/draftContext.d.ts +16 -0
  218. package/dist/operatorAgent/draftContext.js +68 -0
  219. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  220. package/dist/operatorAgent/fakeHost.js +92 -0
  221. package/dist/operatorAgent/host.d.ts +40 -0
  222. package/dist/operatorAgent/host.js +179 -0
  223. package/dist/operatorAgent/index.d.ts +9 -0
  224. package/dist/operatorAgent/index.js +8 -0
  225. package/dist/operatorAgent/piModel.d.ts +80 -0
  226. package/dist/operatorAgent/piModel.js +528 -0
  227. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  228. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  229. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  230. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  231. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  232. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  233. package/dist/operatorAgent/proposals.d.ts +32 -0
  234. package/dist/operatorAgent/proposals.js +144 -0
  235. package/dist/operatorAgent/types.d.ts +128 -0
  236. package/dist/operatorAgent/types.js +1 -0
  237. package/dist/package-meta.d.ts +2 -1
  238. package/dist/package-meta.js +2 -1
  239. package/dist/preflight/bash.d.ts +8 -0
  240. package/dist/preflight/bash.js +34 -0
  241. package/dist/preflight/claudeRoot.d.ts +11 -0
  242. package/dist/preflight/claudeRoot.js +23 -0
  243. package/dist/preflight/mcpCommands.d.ts +8 -0
  244. package/dist/preflight/mcpCommands.js +36 -0
  245. package/dist/preflight/pipelinePreflight.d.ts +40 -0
  246. package/dist/preflight/pipelinePreflight.js +192 -0
  247. package/dist/preflight/tls.d.ts +6 -0
  248. package/dist/preflight/tls.js +18 -0
  249. package/dist/preflight/toolchain.d.ts +45 -0
  250. package/dist/preflight/toolchain.js +219 -0
  251. package/dist/project/findManifestRoot.d.ts +3 -0
  252. package/dist/project/findManifestRoot.js +35 -0
  253. package/dist/project/findProjectRoot.js +3 -4
  254. package/dist/project/globalHome.d.ts +7 -0
  255. package/dist/project/globalHome.js +63 -2
  256. package/dist/project/resolveProjectContext.js +2 -2
  257. package/dist/project/resolveStageflowContext.d.ts +7 -0
  258. package/dist/project/resolveStageflowContext.js +46 -30
  259. package/dist/projection/projectRun.d.ts +2 -1
  260. package/dist/projection/projectRun.js +1 -0
  261. package/dist/runstore/assertTmpdir.d.ts +1 -0
  262. package/dist/runstore/assertTmpdir.js +25 -0
  263. package/dist/runstore/backup.d.ts +52 -0
  264. package/dist/runstore/backup.js +292 -0
  265. package/dist/runstore/createStore.d.ts +18 -1
  266. package/dist/runstore/createStore.js +64 -5
  267. package/dist/runstore/debugBundle.d.ts +60 -0
  268. package/dist/runstore/debugBundle.js +143 -0
  269. package/dist/runstore/diskUsage.d.ts +50 -0
  270. package/dist/runstore/diskUsage.js +214 -0
  271. package/dist/runstore/exportRunPayload.d.ts +8 -0
  272. package/dist/runstore/exportRunPayload.js +23 -0
  273. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  274. package/dist/runstore/normalizeCatalogPath.js +10 -0
  275. package/dist/runstore/paths.d.ts +5 -0
  276. package/dist/runstore/paths.js +145 -1
  277. package/dist/runstore/port.d.ts +139 -13
  278. package/dist/runstore/port.js +8 -1
  279. package/dist/runstore/restore.d.ts +81 -0
  280. package/dist/runstore/restore.js +355 -0
  281. package/dist/runstore/retention.d.ts +27 -0
  282. package/dist/runstore/retention.js +74 -0
  283. package/dist/runstore/runManifest.d.ts +146 -0
  284. package/dist/runstore/runManifest.js +288 -0
  285. package/dist/runstore/runProjection.d.ts +3 -1
  286. package/dist/runstore/runProjection.js +63 -1
  287. package/dist/runstore/sqlite/SqliteRunStore.d.ts +35 -3
  288. package/dist/runstore/sqlite/SqliteRunStore.js +442 -219
  289. package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
  290. package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
  291. package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
  292. package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
  293. package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
  294. package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
  295. package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
  296. package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
  297. package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
  298. package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
  299. package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
  300. package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
  301. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
  302. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
  303. package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
  304. package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
  305. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  306. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  307. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  308. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  309. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  310. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  311. package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
  312. package/dist/runstore/sqlite/migrations/index.js +78 -0
  313. package/dist/runstore/sqlite/schema.d.ts +20 -1
  314. package/dist/runstore/sqlite/schema.js +277 -1
  315. package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
  316. package/dist/runstore/sqlite/storeOpenError.js +11 -0
  317. package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
  318. package/dist/runstore/sqlite/storeSchemaError.js +8 -0
  319. package/dist/runstore/stageExecution.js +3 -0
  320. package/dist/runstore/storeFilesystem.d.ts +32 -0
  321. package/dist/runstore/storeFilesystem.js +148 -0
  322. package/dist/runstore/trackProjection.js +2 -0
  323. package/dist/runstore/workspaceLayout.d.ts +13 -0
  324. package/dist/runstore/workspaceLayout.js +76 -0
  325. package/dist/runtime/answerResume.d.ts +1 -0
  326. package/dist/runtime/answerResume.js +3 -1
  327. package/dist/runtime/completionCheckRunner.d.ts +15 -1
  328. package/dist/runtime/completionCheckRunner.js +67 -7
  329. package/dist/runtime/containerLimits.d.ts +16 -0
  330. package/dist/runtime/containerLimits.js +84 -0
  331. package/dist/runtime/credentialBinding.d.ts +2 -0
  332. package/dist/runtime/credentialBinding.js +3 -0
  333. package/dist/runtime/credentialMaterialisation.d.ts +28 -0
  334. package/dist/runtime/credentialMaterialisation.js +128 -0
  335. package/dist/runtime/emailSource.d.ts +72 -0
  336. package/dist/runtime/emailSource.js +304 -0
  337. package/dist/runtime/gitCheckoutCapability.js +7 -13
  338. package/dist/runtime/githubPollSource.d.ts +86 -0
  339. package/dist/runtime/githubPollSource.js +284 -0
  340. package/dist/runtime/pipelineRunner.d.ts +40 -1
  341. package/dist/runtime/pipelineRunner.js +181 -17
  342. package/dist/runtime/pipelineScheduler.d.ts +17 -0
  343. package/dist/runtime/pipelineScheduler.js +220 -28
  344. package/dist/runtime/reloadRunCatalog.js +6 -1
  345. package/dist/runtime/repositoryMaterialize.d.ts +47 -0
  346. package/dist/runtime/repositoryMaterialize.js +235 -0
  347. package/dist/runtime/resumeTimedOut.d.ts +1 -1
  348. package/dist/runtime/resumeTimedOut.js +5 -2
  349. package/dist/runtime/runChangeBus.js +9 -0
  350. package/dist/runtime/runDeletion.d.ts +7 -0
  351. package/dist/runtime/runDeletion.js +37 -0
  352. package/dist/runtime/runManager.d.ts +169 -4
  353. package/dist/runtime/runManager.js +1748 -83
  354. package/dist/runtime/runRetentionSweep.d.ts +31 -0
  355. package/dist/runtime/runRetentionSweep.js +328 -0
  356. package/dist/runtime/runRetryCoordinator.d.ts +4 -0
  357. package/dist/runtime/runRetryCoordinator.js +20 -2
  358. package/dist/runtime/runSkills.d.ts +40 -0
  359. package/dist/runtime/runSkills.js +173 -0
  360. package/dist/runtime/scheduleSource.d.ts +38 -0
  361. package/dist/runtime/scheduleSource.js +119 -0
  362. package/dist/runtime/settingsFile.d.ts +5 -0
  363. package/dist/runtime/settingsFile.js +61 -54
  364. package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
  365. package/dist/runtime/stageAttemptBootstrap.js +250 -27
  366. package/dist/runtime/stageCacheEnv.d.ts +6 -0
  367. package/dist/runtime/stageCacheEnv.js +34 -0
  368. package/dist/runtime/stageConcurrency.d.ts +2 -1
  369. package/dist/runtime/stageConcurrency.js +4 -2
  370. package/dist/runtime/stageEnvironment.d.ts +38 -0
  371. package/dist/runtime/stageEnvironment.js +194 -0
  372. package/dist/runtime/stageHitl.d.ts +1 -0
  373. package/dist/runtime/stageHitl.js +5 -1
  374. package/dist/runtime/stageProcessLauncher.d.ts +20 -0
  375. package/dist/runtime/stageProcessLauncher.js +236 -41
  376. package/dist/runtime/stageRecovery.d.ts +3 -1
  377. package/dist/runtime/stageRecovery.js +19 -4
  378. package/dist/runtime/stageRoots.d.ts +38 -1
  379. package/dist/runtime/stageRoots.js +118 -0
  380. package/dist/runtime/stageRunner.d.ts +6 -0
  381. package/dist/runtime/stageRunner.js +32 -6
  382. package/dist/runtime/stageSecretDecl.d.ts +9 -0
  383. package/dist/runtime/stageSecretDecl.js +102 -0
  384. package/dist/runtime/stageSecrets.d.ts +40 -0
  385. package/dist/runtime/stageSecrets.js +181 -0
  386. package/dist/runtime/stageStreamLog.d.ts +1 -0
  387. package/dist/runtime/stageStreamLog.js +2 -1
  388. package/dist/runtime/stageWorker.d.ts +3 -2
  389. package/dist/runtime/stageWorker.js +58 -10
  390. package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
  391. package/dist/runtime/stageWorkerProtocol.js +8 -0
  392. package/dist/runtime/startPayload.d.ts +32 -0
  393. package/dist/runtime/startPayload.js +59 -0
  394. package/dist/runtime/taskInput.js +14 -5
  395. package/dist/runtime/triggerCatalog.d.ts +6 -0
  396. package/dist/runtime/triggerCatalog.js +24 -0
  397. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  398. package/dist/runtime/triggerEventMatch.js +6 -0
  399. package/dist/runtime/triggerPort.d.ts +14 -0
  400. package/dist/runtime/triggerPort.js +1 -0
  401. package/dist/runtime/triggerRunner.d.ts +17 -0
  402. package/dist/runtime/triggerRunner.js +89 -0
  403. package/dist/runtime/verifiedStageExecution.d.ts +8 -0
  404. package/dist/runtime/verifiedStageExecution.js +16 -1
  405. package/dist/runtime/webhookSignature.d.ts +1 -0
  406. package/dist/runtime/webhookSignature.js +12 -0
  407. package/dist/runtime/workspaceBinding.d.ts +20 -0
  408. package/dist/runtime/workspaceBinding.js +60 -0
  409. package/dist/server/allowedHosts.d.ts +19 -0
  410. package/dist/server/allowedHosts.js +179 -0
  411. package/dist/server/bootstrap.d.ts +42 -2
  412. package/dist/server/bootstrap.js +273 -5
  413. package/dist/server/controlToken.d.ts +47 -0
  414. package/dist/server/controlToken.js +254 -0
  415. package/dist/server/createHttpHost.d.ts +11 -2
  416. package/dist/server/createHttpHost.js +106 -10
  417. package/dist/server/ensureGlobalService.d.ts +5 -1
  418. package/dist/server/ensureGlobalService.js +35 -1
  419. package/dist/server/healthSurfaces.d.ts +6 -0
  420. package/dist/server/healthSurfaces.js +123 -0
  421. package/dist/server/http.d.ts +27 -1
  422. package/dist/server/http.js +2254 -477
  423. package/dist/server/listenHost.d.ts +5 -0
  424. package/dist/server/listenHost.js +56 -0
  425. package/dist/server/mcpHost.d.ts +10 -1
  426. package/dist/server/mcpHost.js +36 -6
  427. package/dist/server/operatorResults.d.ts +2 -2
  428. package/dist/server/operatorResults.js +40 -29
  429. package/dist/server/requestAuthContext.d.ts +12 -0
  430. package/dist/server/requestAuthContext.js +21 -0
  431. package/dist/server/shutdown.d.ts +69 -0
  432. package/dist/server/shutdown.js +219 -0
  433. package/dist/server/triggerRoutes.d.ts +15 -0
  434. package/dist/server/triggerRoutes.js +268 -0
  435. package/dist/tools/askOperator.d.ts +3 -2
  436. package/dist/types/completion.d.ts +3 -0
  437. package/dist/types/pipeline.d.ts +10 -1
  438. package/dist/types/preEmitCheck.d.ts +7 -0
  439. package/dist/types/stage.d.ts +22 -0
  440. package/dist/types/stageflowManifest.d.ts +3 -0
  441. package/dist/types/task.d.ts +8 -0
  442. package/dist/types/trigger.d.ts +18 -0
  443. package/dist/types/trigger.js +1 -0
  444. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  445. package/dist/ui/assets/index-CntB4bZR.js +133 -0
  446. package/dist/ui/index.html +2 -2
  447. package/dist/workshop/autosave.d.ts +58 -0
  448. package/dist/workshop/autosave.js +222 -0
  449. package/dist/workshop/buildStore.d.ts +79 -0
  450. package/dist/workshop/buildStore.js +326 -0
  451. package/dist/workshop/chatTurn.d.ts +232 -0
  452. package/dist/workshop/chatTurn.js +475 -0
  453. package/dist/workshop/modelSettings.d.ts +18 -0
  454. package/dist/workshop/modelSettings.js +44 -0
  455. package/dist/workshop/sessionStore.d.ts +61 -0
  456. package/dist/workshop/sessionStore.js +254 -0
  457. package/dist/workshop/workshopPin.d.ts +72 -0
  458. package/dist/workshop/workshopPin.js +193 -0
  459. package/examples/README.md +99 -0
  460. package/examples/archify-on-pr/README.md +213 -0
  461. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
  462. package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
  463. package/examples/archify-on-pr/author-diagrams.yaml +106 -0
  464. package/examples/archify-on-pr/detect-changes.yaml +89 -0
  465. package/examples/browser-session/README.md +77 -0
  466. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  467. package/examples/browser-session/browser-session.task.yaml +6 -0
  468. package/examples/browser-session/check-login.yaml +22 -0
  469. package/examples/browser-session/fixture-server.mjs +64 -0
  470. package/examples/browser-session/human-login.yaml +31 -0
  471. package/examples/browser-session/merge.yaml +36 -0
  472. package/examples/browser-session/work-a.yaml +41 -0
  473. package/examples/browser-session/work-b.yaml +41 -0
  474. package/examples/ci-validate/README.md +35 -0
  475. package/examples/ci-validate/check.yaml +13 -0
  476. package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
  477. package/examples/ci-validate/validate.sh +5 -0
  478. package/examples/compliance-review-demo/README.md +125 -0
  479. package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
  480. package/examples/compliance-review-demo/connect-account.yaml +47 -0
  481. package/examples/compliance-review-demo/map-to-control.yaml +123 -0
  482. package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
  483. package/examples/compliance-review-demo/publish-report.yaml +79 -0
  484. package/examples/compliance-review-demo/review-verdict.yaml +122 -0
  485. package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
  486. package/examples/compliance-review-demo/store-evidence.yaml +143 -0
  487. package/examples/conditional-fork/README.md +89 -0
  488. package/examples/conditional-fork/branch-a.yaml +13 -0
  489. package/examples/conditional-fork/branch-b.yaml +13 -0
  490. package/examples/conditional-fork/decide.yaml +35 -0
  491. package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
  492. package/examples/conditional-fork/fork-demo.task.yaml +8 -0
  493. package/examples/context7-mcp/README.md +42 -0
  494. package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
  495. package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
  496. package/examples/context7-mcp/fetch-docs.yaml +52 -0
  497. package/examples/context7-mcp/resolve-library.yaml +31 -0
  498. package/examples/context7-mcp/write-brief.yaml +52 -0
  499. package/examples/feature-loop/README.md +104 -0
  500. package/examples/feature-loop/address-feedback.yaml +148 -0
  501. package/examples/feature-loop/align.yaml +116 -0
  502. package/examples/feature-loop/decompose.yaml +86 -0
  503. package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
  504. package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
  505. package/examples/feature-loop/feature-loop.task.yaml +25 -0
  506. package/examples/feature-loop/implement.yaml +128 -0
  507. package/examples/feature-loop/plan.yaml +111 -0
  508. package/examples/feature-loop/publish.yaml +167 -0
  509. package/examples/feature-loop/review.yaml +146 -0
  510. package/examples/feature-loop/verify.yaml +106 -0
  511. package/examples/feedback-loop/README.md +72 -0
  512. package/examples/feedback-loop/VERIFY.md +208 -0
  513. package/examples/feedback-loop/continue-only.task.yaml +4 -0
  514. package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
  515. package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
  516. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
  517. package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
  518. package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
  519. package/examples/feedback-loop/implement.yaml +14 -0
  520. package/examples/feedback-loop/plan.yaml +14 -0
  521. package/examples/feedback-loop/review.yaml +15 -0
  522. package/examples/feedback-loop/send-back-once.task.yaml +6 -0
  523. package/examples/feedback-loop/submit.yaml +12 -0
  524. package/examples/generic-fan-in/README.md +151 -0
  525. package/examples/generic-fan-in/clarify.yaml +18 -0
  526. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
  527. package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
  528. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
  529. package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
  530. package/examples/generic-fan-in/research.yaml +45 -0
  531. package/examples/generic-fan-in/synthesize.yaml +66 -0
  532. package/examples/generic-fan-in/validation.yaml +45 -0
  533. package/examples/github-release/README.md +78 -0
  534. package/examples/github-release/draft-release-notes.yaml +64 -0
  535. package/examples/github-release/github-release.pipeline.yaml +11 -0
  536. package/examples/github-release/github-release.task.yaml +13 -0
  537. package/examples/github-release/publish-github-release.yaml +64 -0
  538. package/examples/hello-world/README.md +28 -0
  539. package/examples/hello-world/hello.pipeline.yaml +4 -0
  540. package/examples/hello-world/my-task.task.yaml +6 -0
  541. package/examples/hello-world/research.yaml +18 -0
  542. package/examples/mcp-hitl-tour/README.md +113 -0
  543. package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
  544. package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
  545. package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
  546. package/examples/mcp-hitl-tour/gather.yaml +31 -0
  547. package/examples/mcp-hitl-tour/intake.yaml +62 -0
  548. package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
  549. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
  550. package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
  551. package/examples/mcp-hitl-tour/polish.yaml +33 -0
  552. package/examples/mcp-hitl-tour/review.yaml +17 -0
  553. package/examples/mcp-hitl-tour/ship.yaml +25 -0
  554. package/examples/oss-issue-contribution/README.md +218 -0
  555. package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
  556. package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
  557. package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
  558. package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
  559. package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
  560. package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
  561. package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
  562. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
  563. package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
  564. package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
  565. package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
  566. package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
  567. package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
  568. package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
  569. package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
  570. package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
  571. package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
  572. package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
  573. package/examples/oss-quick-fix/README.md +65 -0
  574. package/examples/oss-quick-fix/implement-fix.yaml +84 -0
  575. package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
  576. package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
  577. package/examples/oss-quick-fix/ship-fix.yaml +81 -0
  578. package/examples/oss-quick-fix/understand-issue.yaml +67 -0
  579. package/examples/oss-quick-fix/verify-fix.yaml +73 -0
  580. package/examples/plan-review/README.md +37 -0
  581. package/examples/plan-review/plan-review-followup.yaml +13 -0
  582. package/examples/plan-review/plan-review.pipeline.yaml +9 -0
  583. package/examples/plan-review/plan-review.task.yaml +9 -0
  584. package/examples/plan-review/plan-review.yaml +25 -0
  585. package/examples/playwright-mcp/README.md +39 -0
  586. package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
  587. package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
  588. package/examples/playwright-mcp/screenshot.yaml +40 -0
  589. package/examples/retry-tour/README.md +75 -0
  590. package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
  591. package/examples/retry-tour/retry-tour.task.yaml +9 -0
  592. package/examples/route-if-tour/README.md +202 -0
  593. package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
  594. package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
  595. package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
  596. package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
  597. package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
  598. package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
  599. package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
  600. package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
  601. package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
  602. package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
  603. package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
  604. package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
  605. package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
  606. package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
  607. package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
  608. package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
  609. package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
  610. package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
  611. package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
  612. package/examples/route-wiring-smoke-test/README.md +377 -0
  613. package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
  614. package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
  615. package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
  616. package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
  617. package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
  618. package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
  619. package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
  620. package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
  621. package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
  622. package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
  623. package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
  624. package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
  625. package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
  626. package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
  627. package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
  628. package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
  629. package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
  630. package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
  631. package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
  632. package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
  633. package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
  634. package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
  635. package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
  636. package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
  637. package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
  638. package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
  639. package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
  640. package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
  641. package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
  642. package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
  643. package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
  644. package/examples/ship-feature/README.md +190 -0
  645. package/examples/ship-feature/feat-address-feedback.yaml +119 -0
  646. package/examples/ship-feature/feat-implement.yaml +180 -0
  647. package/examples/ship-feature/feat-plan.yaml +120 -0
  648. package/examples/ship-feature/feat-review.yaml +109 -0
  649. package/examples/ship-feature/feat-ship.yaml +203 -0
  650. package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
  651. package/examples/ship-feature/ship-feature.task.yaml +78 -0
  652. package/examples/stage-mcp/README.md +145 -0
  653. package/examples/stage-mcp/echo-mcp.mjs +20 -0
  654. package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
  655. package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
  656. package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
  657. package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
  658. package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
  659. package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
  660. package/examples/stage-mcp/use-echo.yaml +20 -0
  661. package/examples/stageflow.yaml +51 -0
  662. package/examples/verify-tour/README.md +80 -0
  663. package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
  664. package/examples/verify-tour/verify-tour.task.yaml +11 -0
  665. package/package.json +9 -1
  666. package/skills/stageflow/SKILL.md +6 -6
  667. package/skills/stageflow/references/control-surface.md +13 -5
  668. package/skills/stageflow/scripts/detect-host.mjs +14 -1
  669. package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
  670. package/skills/stageflow-author/SKILL.md +4 -3
  671. package/skills/stageflow-author/references/catalog-mapping.md +16 -0
  672. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  673. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  674. package/skills/stageflow-run/SKILL.md +22 -12
  675. package/skills/stageflow-run/references/mcp-call.md +9 -1
  676. package/skills/stageflow-run/references/task-and-pipeline-selection.md +19 -6
  677. package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
  678. package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
  679. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
  680. package/skills/stageflow-setup/SKILL.md +51 -8
  681. package/dist/ui/assets/index-Da5EHU6M.js +0 -119
@@ -1,25 +1,54 @@
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";
5
6
  import { mapProviderAuthError } from "../agent/providerInspect.js";
6
7
  import { handleProviderRoutes } from "./providerRoutes.js";
7
8
  import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
9
+ import { handleTriggerRoutes } from "./triggerRoutes.js";
8
10
  import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
9
11
  import { createStage, parseCreateStageBody } from "../config/createStage.js";
10
- import { browseCatalog } from "../config/browseCatalog.js";
12
+ import { loadDraftPackage, loadTaskArtifact, parseAttachTaskBody, parseCreateDraftPackageBody, parseDraftPackageBody, parseOpenDraftPackageBody, parseOverwriteDraftPackageBody, validateDraftPackage, } from "../config/draftPackage.js";
13
+ import { publishDraftPackage } from "../config/publishDraftPackage.js";
14
+ import { clearWorkshopAutosave, detectDiskChange, draftPackageDiskRelativePaths, fingerprintPackageFiles, parseWorkshopAutosaveRecord, readWorkshopAutosave, resolveWorkshopAutosaveStoreRoot, workshopAutosaveSlotKey, writeWorkshopAutosave, } from "../workshop/autosave.js";
15
+ import { acceptWorkshopSessionMutation, iterateWorkshopChatStreamFrames, runWorkshopChatTurn, stopWorkshopChatTurn, undoWorkshopSessionMutation, WorkshopChatSessionRegistry, WorkshopSessionStoreError, } from "../workshop/chatTurn.js";
16
+ import { readWorkshopOpenModel, resolveWorkshopModel, } from "../workshop/modelSettings.js";
17
+ import { createWorkshopSession, getWorkshopSession, listWorkshopSessions, resolveWorkshopSessionStoreRoot, updateWorkshopSessionActiveBuildId, } from "../workshop/sessionStore.js";
18
+ import { createWorkshopBuild, focusUnboundWorkshopBuild, getWorkshopBuild, listWorkshopBuilds, listWorkshopPickerRows, resolvePickerCatalogPipelines, resolveWorkshopBuildStoreRoot, updateWorkshopBuild, WorkshopBuildStoreError, } from "../workshop/buildStore.js";
19
+ import { createLiveWorkshopOperatorHost, } from "../operatorAgent/index.js";
20
+ import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
21
+ import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
11
22
  import { listExtensions } from "../config/listExtensions.js";
12
23
  import { listSkills } from "../config/listSkills.js";
13
- import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
24
+ import { artifactMediaType, classifyArtifactContent, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
25
+ import { getRunDiff, listCheckoutChanges, readCheckoutFileBytes, } from "../mcp/checkoutTools.js";
14
26
  import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
27
+ import { BackupError, createBackup, resolveBackupDownloadPath, } from "../runstore/backup.js";
28
+ import { RestoreError, resolveBackupNameForRestore, stageRestoreForBoot, } from "../runstore/restore.js";
29
+ import { iterateExportNdjson } from "../cli/exportAllCommand.js";
30
+ import { buildRunExportPayload, runDetailWithRedactedManifest, } from "../runstore/exportRunPayload.js";
31
+ import { buildDebugBundle } from "../runstore/debugBundle.js";
32
+ import { parseShutdownGraceMs } from "./shutdown.js";
33
+ import { globalStageflowHome } from "../project/globalHome.js";
15
34
  import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
16
35
  import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
17
- import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
36
+ import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
37
+ import { INVALID_SLOT_COUNT_MESSAGE, INVALID_WORKSHOP_MODEL_MESSAGE, parseSlotCount, parseWorkshopModelSetting, readFactorySettings, writeFactorySettings, } from "../runtime/settingsFile.js";
18
38
  import { isTaskFile } from "../runtime/taskInput.js";
39
+ import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
19
40
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
20
41
  import { bootstrapStageflowHost, } from "./bootstrap.js";
42
+ import { assertAllowedHttpAccess, isTrustedLocalHttpRequest, resolveAllowedHosts, } from "./allowedHosts.js";
43
+ import { enforceBearerAuth, loadControlTokens, requiredScopeFor, } from "./controlToken.js";
21
44
  import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
45
+ import { handleApiHealth } from "./healthSurfaces.js";
22
46
  import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "./operatorResults.js";
47
+ import { installShutdownController, makeDrainableHostFromOptional, } from "./shutdown.js";
48
+ import { writeAudit } from "../logging/audit.js";
49
+ import { logger as rootLogger } from "../logging/logger.js";
50
+ import { callerIdFromRequestAuth, getRequestAuth, requestAuthFromBearer, runWithRequestAuth, } from "./requestAuthContext.js";
51
+ const auditLog = rootLogger.child({ component: "audit" });
23
52
  function textPlain(res, status, body) {
24
53
  res.writeHead(status, {
25
54
  "Content-Type": "text/plain; charset=utf-8",
@@ -36,6 +65,28 @@ async function readJsonBody(req) {
36
65
  return {};
37
66
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
38
67
  }
68
+ function isPlainObject(value) {
69
+ return value !== null && typeof value === "object" && !Array.isArray(value);
70
+ }
71
+ function readBuildTieField(body, field) {
72
+ if (!Object.prototype.hasOwnProperty.call(body, field)) {
73
+ return { ok: true, present: false };
74
+ }
75
+ const value = body[field];
76
+ if (value === null)
77
+ return { ok: true, present: true, value: null };
78
+ if (typeof value === "string")
79
+ return { ok: true, present: true, value };
80
+ return { ok: false, error: `${field} must be a string or null` };
81
+ }
82
+ /** Raw bytes, unparsed — for callers (webhook signature verification) that need the exact wire body. */
83
+ async function readRawBody(req) {
84
+ const chunks = [];
85
+ for await (const chunk of req) {
86
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
87
+ }
88
+ return Buffer.concat(chunks);
89
+ }
39
90
  function contentTypeFor(filePath) {
40
91
  const ext = path.extname(filePath).toLowerCase();
41
92
  switch (ext) {
@@ -82,27 +133,6 @@ async function serveStatic(res, uiDistDir, urlPath) {
82
133
  res.end(data);
83
134
  return true;
84
135
  }
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
136
  function isCredentialMutatingApi(method, pathname) {
107
137
  if (method !== "POST")
108
138
  return false;
@@ -111,68 +141,6 @@ function isCredentialMutatingApi(method, pathname) {
111
141
  /^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
112
142
  /^\/api\/providers\/[^/]+\/logout$/.test(pathname));
113
143
  }
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
144
  export function defaultUiDistDir() {
177
145
  const here = path.dirname(fileURLToPath(import.meta.url));
178
146
  if (path.basename(path.dirname(here)) === "src") {
@@ -188,495 +156,2280 @@ export function defaultUiDistDir() {
188
156
  * API; only the former needs the UI files.
189
157
  */
190
158
  export function createOperatorRoutes(deps) {
191
- 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)) {
159
+ const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir, workshopOperatorHost, } = deps;
160
+ const workshopChatRegistry = deps.workshopChatRegistry ??
161
+ new WorkshopChatSessionRegistry(workshopOperatorHost ??
162
+ createLiveWorkshopOperatorHost({
163
+ cwd,
164
+ projectRoot: cwd,
165
+ }));
166
+ const workshopSessionStoreRoot = () => resolveWorkshopSessionStoreRoot();
167
+ const workshopBuildStoreRoot = () => resolveWorkshopBuildStoreRoot();
168
+ const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
169
+ const controlTokens = deps.controlTokens ?? loadControlTokens();
170
+ return async ({ req, res, url, pathname, method, boot }) => {
171
+ if (boot.serveBlocked !== undefined && pathname.startsWith("/api/")) {
172
+ json(res, 503, {
173
+ error: boot.serveBlocked.reason,
174
+ code: boot.serveBlocked.code,
175
+ });
176
+ return true;
177
+ }
178
+ if (pathname.startsWith("/api/")) {
179
+ if (!assertAllowedHttpAccess(allowedHosts, req, res, {
180
+ requireOrigin: isCredentialMutatingApi(method, pathname),
181
+ })) {
195
182
  return true;
196
183
  }
197
- if (isCredentialMutatingApi(method, pathname) &&
198
- !assertCredentialMutatingOrigin(req, res)) {
199
- return true;
184
+ const scope = requiredScopeFor(method, pathname);
185
+ if (scope !== null) {
186
+ const authResult = enforceBearerAuth(controlTokens, req, res, scope);
187
+ if (!authResult.ok)
188
+ return true;
189
+ return runWithRequestAuth(requestAuthFromBearer(authResult.auth, "rest"), () => handleOperatorRequest());
200
190
  }
201
191
  }
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);
192
+ return handleOperatorRequest();
193
+ async function handleOperatorRequest() {
194
+ try {
195
+ if (method === "GET" && pathname === "/api/runs") {
196
+ const filter = {};
197
+ const status = url.searchParams.get("status");
198
+ const since = url.searchParams.get("since");
199
+ const pipeline = url.searchParams.get("pipeline");
200
+ const callerId = url.searchParams.get("caller_id");
201
+ if (status !== null)
202
+ filter.status = status;
203
+ if (since !== null)
204
+ filter.since = since;
205
+ if (pipeline !== null)
206
+ filter.pipeline = pipeline;
207
+ if (callerId !== null)
208
+ filter.caller_id = callerId;
209
+ json(res, 200, {
210
+ runs: await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined),
211
+ });
212
+ return true;
213
+ }
214
+ if (method === "POST" && pathname === "/api/backup") {
215
+ const body = (await readJsonBody(req));
216
+ const callerId = callerIdFromRequestAuth();
217
+ try {
218
+ const result = await createBackup({
219
+ store,
220
+ homeDir: globalStageflowHome(),
221
+ outPath: typeof body.out === "string" ? body.out : undefined,
222
+ dbOnly: body.db_only === true,
223
+ noCredentials: body.no_credentials === true,
224
+ includeA2aArtifacts: body.include_a2a_artifacts === true,
225
+ });
226
+ writeAudit(auditLog, {
227
+ caller_id: callerId,
228
+ surface: getRequestAuth()?.surface ?? "rest",
229
+ action: "backup",
230
+ outcome: "ok",
231
+ });
232
+ json(res, 200, result);
233
+ }
234
+ catch (err) {
235
+ if (err instanceof BackupError) {
236
+ writeAudit(auditLog, {
237
+ caller_id: callerId,
238
+ surface: getRequestAuth()?.surface ?? "rest",
239
+ action: "backup",
240
+ outcome: "error",
241
+ error_code: err.code,
242
+ });
243
+ const status = err.code === "backup_insufficient_disk" ? 507 : 400;
244
+ json(res, status, { error: err.message, code: err.code });
245
+ return true;
246
+ }
247
+ throw err;
248
+ }
249
+ return true;
250
+ }
251
+ const backupGetMatch = pathname.match(/^\/api\/backup\/([^/]+)$/);
252
+ if (method === "GET" && backupGetMatch) {
253
+ const name = decodeURIComponent(backupGetMatch[1] ?? "");
254
+ try {
255
+ const filePath = await resolveBackupDownloadPath(name, globalStageflowHome());
256
+ const st = statSync(filePath);
219
257
  res.writeHead(200, {
220
- "Content-Type": mediaType,
221
- "Content-Length": bytes.length,
258
+ "Content-Type": "application/octet-stream",
259
+ "Content-Length": st.size,
260
+ "Content-Disposition": `attachment; filename="${name}"`,
261
+ });
262
+ createReadStream(filePath).pipe(res);
263
+ }
264
+ catch (err) {
265
+ if (err instanceof BackupError) {
266
+ json(res, 400, { error: err.message, code: err.code });
267
+ return true;
268
+ }
269
+ throw err;
270
+ }
271
+ return true;
272
+ }
273
+ if (method === "POST" && pathname === "/api/restore") {
274
+ const body = (await readJsonBody(req));
275
+ const backupName = typeof body.backup === "string"
276
+ ? body.backup
277
+ : typeof body.path === "string"
278
+ ? body.path
279
+ : undefined;
280
+ if (backupName === undefined) {
281
+ json(res, 400, {
282
+ error: "body.backup (backup name under backups/) is required",
283
+ code: "restore_not_found",
284
+ });
285
+ return true;
286
+ }
287
+ const callerId = callerIdFromRequestAuth();
288
+ try {
289
+ const archivePath = await resolveBackupNameForRestore(path.basename(backupName), globalStageflowHome());
290
+ const graceMs = parseShutdownGraceMs();
291
+ const staged = await stageRestoreForBoot({
292
+ archivePath,
293
+ homeDir: globalStageflowHome(),
294
+ graceMs,
295
+ });
296
+ writeAudit(auditLog, {
297
+ caller_id: callerId,
298
+ surface: getRequestAuth()?.surface ?? "rest",
299
+ action: "restore",
300
+ outcome: "ok",
222
301
  });
223
- res.end(bytes);
302
+ json(res, 202, {
303
+ ok: true,
304
+ staged: staged.stagedPath,
305
+ drain_deadline: staged.marker.drain_deadline,
306
+ });
307
+ const shutdown = deps.getShutdown?.();
308
+ if (shutdown !== undefined) {
309
+ void shutdown.beginDrain();
310
+ }
224
311
  }
225
- else {
226
- const content = await readRunArtifact(store, runId, artifactPath);
227
- textPlain(res, 200, content);
312
+ catch (err) {
313
+ if (err instanceof RestoreError || err instanceof BackupError) {
314
+ writeAudit(auditLog, {
315
+ caller_id: callerId,
316
+ surface: getRequestAuth()?.surface ?? "rest",
317
+ action: "restore",
318
+ outcome: "error",
319
+ error_code: err.code,
320
+ });
321
+ json(res, 400, {
322
+ error: err.message,
323
+ code: err.code,
324
+ });
325
+ return true;
326
+ }
327
+ throw err;
228
328
  }
329
+ return true;
229
330
  }
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 });
331
+ if (method === "GET" && pathname === "/api/export") {
332
+ const filter = {};
333
+ const status = url.searchParams.get("status");
334
+ const since = url.searchParams.get("since");
335
+ const pipeline = url.searchParams.get("pipeline");
336
+ const callerId = url.searchParams.get("caller_id");
337
+ if (status)
338
+ filter.status = status;
339
+ if (since)
340
+ filter.since = since;
341
+ if (pipeline)
342
+ filter.pipeline = pipeline;
343
+ if (callerId)
344
+ filter.caller_id = callerId;
345
+ res.writeHead(200, {
346
+ "Content-Type": "application/x-ndjson; charset=utf-8",
347
+ });
348
+ try {
349
+ for await (const line of iterateExportNdjson({
350
+ store,
351
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
352
+ })) {
353
+ if (req.aborted || res.writableEnded || res.destroyed) {
354
+ break;
355
+ }
356
+ if (!res.write(line)) {
357
+ await new Promise((resolve) => res.once("drain", resolve));
358
+ }
359
+ }
360
+ if (!res.writableEnded && !res.destroyed) {
361
+ res.end();
362
+ }
363
+ }
364
+ catch (err) {
365
+ if (res.headersSent) {
366
+ res.destroy(err instanceof Error ? err : new Error(String(err)));
367
+ }
368
+ else {
369
+ throw err;
370
+ }
371
+ }
372
+ return true;
234
373
  }
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));
374
+ const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
375
+ if (method === "GET" && artifactMatch) {
376
+ const runId = decodeURIComponent(artifactMatch[1] ?? "");
377
+ const artifactPath = url.searchParams.get("path");
378
+ if (artifactPath === null || artifactPath.trim().length === 0) {
379
+ json(res, 400, { error: "path query parameter is required" });
380
+ return true;
381
+ }
382
+ try {
383
+ const mediaType = artifactMediaType(artifactPath);
384
+ if (mediaType !== undefined) {
385
+ const bytes = await readRunArtifactBytes(store, runId, artifactPath);
386
+ res.writeHead(200, {
387
+ "Content-Type": mediaType,
388
+ "Content-Length": bytes.length,
389
+ });
390
+ res.end(bytes);
391
+ }
392
+ else {
393
+ const content = await readRunArtifact(store, runId, artifactPath);
394
+ textPlain(res, 200, content);
395
+ }
396
+ }
397
+ catch (err) {
398
+ const mapped = mapStoreLookupError(err, { policy: "artifact" });
399
+ const status = mapped.kind === "denied" ? 403 : mapped.status;
400
+ json(res, status, { error: mapped.error });
401
+ }
402
+ return true;
243
403
  }
244
- catch (err) {
245
- const mapped = mapStoreLookupError(err, { policy: "run" });
246
- json(res, mapped.status, { error: mapped.error });
404
+ const changesMatch = pathname.match(/^\/api\/runs\/([^/]+)\/changes$/);
405
+ if (method === "GET" && changesMatch) {
406
+ const runId = decodeURIComponent(changesMatch[1] ?? "");
407
+ try {
408
+ const result = await listCheckoutChanges(store, runId);
409
+ if (!result.ok) {
410
+ json(res, result.status, {
411
+ error: result.error,
412
+ code: result.code,
413
+ });
414
+ return true;
415
+ }
416
+ json(res, 200, {
417
+ runId,
418
+ changes: result.changes,
419
+ truncated: result.truncated,
420
+ });
421
+ }
422
+ catch (err) {
423
+ const mapped = mapStoreLookupError(err, { policy: "run" });
424
+ json(res, mapped.status, { error: mapped.error });
425
+ }
426
+ return true;
247
427
  }
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("/")) {
428
+ const diffMatch = pathname.match(/^\/api\/runs\/([^/]+)\/diff$/);
429
+ if (method === "GET" && diffMatch) {
430
+ const runId = decodeURIComponent(diffMatch[1] ?? "");
431
+ const modeParam = url.searchParams.get("mode");
432
+ const mode = modeParam === "patch" || modeParam === "stat" ? modeParam : undefined;
433
+ const filterPath = url.searchParams.get("path") ?? undefined;
434
+ const maxBytesRaw = url.searchParams.get("maxBytes");
435
+ const maxBytes = maxBytesRaw !== null && maxBytesRaw.trim() !== ""
436
+ ? Number(maxBytesRaw)
437
+ : undefined;
253
438
  try {
254
- json(res, 200, await store.readRun(decodeURIComponent(rest)));
439
+ const result = await getRunDiff(store, runId, {
440
+ mode,
441
+ path: filterPath ?? undefined,
442
+ maxBytes: maxBytes !== undefined && Number.isFinite(maxBytes)
443
+ ? maxBytes
444
+ : undefined,
445
+ });
446
+ if (!result.ok) {
447
+ json(res, result.status, {
448
+ error: result.error,
449
+ code: result.code,
450
+ });
451
+ return true;
452
+ }
453
+ json(res, 200, {
454
+ runId,
455
+ mode: result.mode,
456
+ base_sha: result.base_sha,
457
+ base_source: result.base_source,
458
+ content: result.content,
459
+ truncated: result.truncated,
460
+ untracked: result.untracked,
461
+ });
255
462
  }
256
463
  catch (err) {
257
464
  const mapped = mapStoreLookupError(err, { policy: "run" });
258
- json(res, 404, { error: mapped.error });
465
+ json(res, mapped.status, { error: mapped.error });
259
466
  }
260
467
  return true;
261
468
  }
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" });
469
+ const checkoutFileMatch = pathname.match(/^\/api\/runs\/([^/]+)\/file$/);
470
+ if (method === "GET" && checkoutFileMatch) {
471
+ const runId = decodeURIComponent(checkoutFileMatch[1] ?? "");
472
+ const filePath = url.searchParams.get("path");
473
+ if (filePath === null || filePath.trim().length === 0) {
474
+ json(res, 400, { error: "path query parameter is required" });
475
+ return true;
476
+ }
477
+ try {
478
+ const result = await readCheckoutFileBytes(store, runId, filePath);
479
+ if (!result.ok) {
480
+ json(res, result.status, {
481
+ error: result.error,
482
+ code: result.code,
483
+ });
484
+ return true;
485
+ }
486
+ const mediaType = artifactMediaType(filePath);
487
+ if (mediaType !== undefined) {
488
+ res.writeHead(200, {
489
+ "Content-Type": mediaType,
490
+ "Content-Length": result.bytes.length,
491
+ });
492
+ res.end(result.bytes);
493
+ }
494
+ else {
495
+ const classified = classifyArtifactContent(filePath, result.bytes);
496
+ if (classified.kind !== "utf8") {
497
+ json(res, 400, {
498
+ error: "Checkout file is not valid UTF-8 text",
499
+ });
500
+ return true;
501
+ }
502
+ textPlain(res, 200, result.bytes.toString("utf8"));
503
+ }
504
+ }
505
+ catch (err) {
506
+ const mapped = mapStoreLookupError(err, { policy: "artifact" });
507
+ const status = mapped.kind === "denied" ? 403 : mapped.status;
508
+ json(res, status, { error: mapped.error });
509
+ }
269
510
  return true;
270
511
  }
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
- });
512
+ const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
513
+ if (method === "GET" && verificationMatch) {
514
+ const runId = decodeURIComponent(verificationMatch[1] ?? "");
515
+ const stageId = decodeURIComponent(verificationMatch[2] ?? "");
516
+ try {
517
+ json(res, 200, await readStageVerificationHistory(store, runId, stageId));
518
+ }
519
+ catch (err) {
520
+ const mapped = mapStoreLookupError(err, { policy: "run" });
521
+ json(res, mapped.status, { error: mapped.error });
522
+ }
275
523
  return true;
276
524
  }
277
- if (body.checkoutOverride !== undefined &&
278
- typeof body.checkoutOverride !== "string") {
279
- json(res, 400, { error: "checkoutOverride must be a string" });
525
+ const exportMatch = pathname.match(/^\/api\/runs\/([^/]+)\/export$/);
526
+ if (method === "GET" && exportMatch) {
527
+ const runId = decodeURIComponent(exportMatch[1] ?? "");
528
+ try {
529
+ const detail = await store.readRun(runId);
530
+ json(res, 200, buildRunExportPayload(detail));
531
+ }
532
+ catch (err) {
533
+ const mapped = mapStoreLookupError(err, { policy: "run" });
534
+ json(res, mapped.status, { error: mapped.error });
535
+ }
280
536
  return true;
281
537
  }
282
- if (body.skipGates !== undefined && typeof body.skipGates !== "boolean") {
283
- json(res, 400, { error: "skipGates must be a boolean" });
538
+ const debugBundleMatch = pathname.match(/^\/api\/runs\/([^/]+)\/debug-bundle$/);
539
+ if (method === "GET" && debugBundleMatch) {
540
+ const runId = decodeURIComponent(debugBundleMatch[1] ?? "");
541
+ try {
542
+ json(res, 200, await buildDebugBundle(store, runId));
543
+ }
544
+ catch (err) {
545
+ const mapped = mapStoreLookupError(err, { policy: "run" });
546
+ json(res, mapped.status, { error: mapped.error });
547
+ }
284
548
  return true;
285
549
  }
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` });
550
+ if (method === "GET" && pathname.startsWith("/api/runs/")) {
551
+ const rest = pathname.slice("/api/runs/".length);
552
+ if (rest && !rest.includes("/")) {
553
+ try {
554
+ const detail = await store.readRun(decodeURIComponent(rest));
555
+ json(res, 200, runDetailWithRedactedManifest(detail));
556
+ }
557
+ catch (err) {
558
+ const mapped = mapStoreLookupError(err, { policy: "run" });
559
+ json(res, 404, { error: mapped.error });
560
+ }
289
561
  return true;
290
562
  }
291
563
  }
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
- });
564
+ if (method === "POST" && pathname === "/api/projects") {
565
+ if (!isTrustedLocalHttpRequest(req)) {
566
+ json(res, 403, {
567
+ error: "ensure_project is only allowed from trusted local clients (loopback peer and Host)",
568
+ code: "ensure_project_not_allowed",
569
+ });
570
+ return true;
571
+ }
572
+ let body;
573
+ try {
574
+ body = await readJsonBody(req);
575
+ }
576
+ catch {
577
+ json(res, 400, { error: "Invalid JSON body" });
578
+ return true;
579
+ }
580
+ const projectRoot = body !== null &&
581
+ typeof body === "object" &&
582
+ !Array.isArray(body) &&
583
+ typeof body.project_root === "string"
584
+ ? body.project_root.trim()
585
+ : "";
586
+ if (!projectRoot) {
587
+ json(res, 400, { error: "project_root is required" });
588
+ return true;
589
+ }
590
+ if (!path.isAbsolute(projectRoot)) {
591
+ json(res, 400, {
592
+ error: "project_root must be an absolute path",
593
+ });
594
+ return true;
595
+ }
596
+ try {
597
+ const registered = await store.ensureProject(projectRoot);
598
+ json(res, 200, { project_root: registered });
599
+ }
600
+ catch (err) {
601
+ const message = err instanceof Error ? err.message : String(err);
602
+ json(res, 400, {
603
+ error: message,
604
+ code: "invalid_project_root",
605
+ });
606
+ }
607
+ return true;
305
608
  }
306
- catch (err) {
307
- if (err instanceof PipelineValidationError) {
609
+ if (method === "POST" && pathname === "/api/runs") {
610
+ const body = (await readJsonBody(req));
611
+ const tokenField = findTokenShapedField(body);
612
+ if (tokenField !== undefined) {
613
+ json(res, 400, tokenRejectedPayload(tokenField));
614
+ return true;
615
+ }
616
+ const typed = body;
617
+ if (typeof typed.pipeline !== "string" ||
618
+ !typed.pipeline.trim() ||
619
+ typed.task === undefined) {
620
+ json(res, 400, { error: "task and pipeline are required" });
621
+ return true;
622
+ }
623
+ if (typeof typed.task !== "string" && !isTaskFile(typed.task)) {
308
624
  json(res, 400, {
309
- error: "Pipeline validation failed",
310
- validation: err.result,
625
+ error: "task must be a path string or TaskFile object (id and goal required)",
626
+ });
627
+ return true;
628
+ }
629
+ if (typed.checkoutOverride !== undefined &&
630
+ typeof typed.checkoutOverride !== "string") {
631
+ json(res, 400, { error: "checkoutOverride must be a string" });
632
+ return true;
633
+ }
634
+ if (typed.skipGates !== undefined && typeof typed.skipGates !== "boolean") {
635
+ json(res, 400, { error: "skipGates must be a boolean" });
636
+ return true;
637
+ }
638
+ for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
639
+ if (typed[field] !== undefined && typeof typed[field] !== "string") {
640
+ json(res, 400, { error: `${field} must be a string` });
641
+ return true;
642
+ }
643
+ }
644
+ let wireRoot;
645
+ let roots;
646
+ try {
647
+ ({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, typed.project_root));
648
+ }
649
+ catch (err) {
650
+ if (err instanceof CatalogPathError) {
651
+ json(res, 400, catalogPathErrorBody(err));
652
+ return true;
653
+ }
654
+ throw err;
655
+ }
656
+ const wireProjectRoot = wireRoot.project_root;
657
+ let pipelinePath = typed.pipeline.trim();
658
+ let taskInput = typed.task;
659
+ let checkoutOverride;
660
+ try {
661
+ pipelinePath = resolveCatalogRelativePath({
662
+ inputPath: pipelinePath,
663
+ projectRoot: wireProjectRoot,
664
+ roots,
665
+ fieldName: "pipeline",
666
+ }).absolutePath;
667
+ if (typeof typed.task === "string") {
668
+ taskInput = resolveCatalogRelativePath({
669
+ inputPath: typed.task,
670
+ projectRoot: wireProjectRoot,
671
+ roots,
672
+ fieldName: "task",
673
+ }).absolutePath;
674
+ }
675
+ if (typed.checkoutOverride !== undefined) {
676
+ checkoutOverride = resolveCatalogRelativePath({
677
+ inputPath: typed.checkoutOverride,
678
+ projectRoot: wireProjectRoot,
679
+ roots,
680
+ fieldName: "checkout",
681
+ }).absolutePath;
682
+ }
683
+ }
684
+ catch (err) {
685
+ if (err instanceof CatalogPathError) {
686
+ json(res, 400, catalogPathErrorBody(err));
687
+ return true;
688
+ }
689
+ throw err;
690
+ }
691
+ let result;
692
+ const callerId = callerIdFromRequestAuth();
693
+ try {
694
+ result = await manager.startRun({
695
+ task: taskInput,
696
+ pipeline: pipelinePath,
697
+ projectRoot: wireRoot.path,
698
+ ...(checkoutOverride !== undefined
699
+ ? { checkoutOverride }
700
+ : {}),
701
+ ...(typed.skipGates !== undefined ? { skipGates: typed.skipGates } : {}),
702
+ ...(typed.gitSha !== undefined ? { gitSha: typed.gitSha } : {}),
703
+ ...(typed.ciPrUrl !== undefined ? { ciPrUrl: typed.ciPrUrl } : {}),
704
+ ...(typed.ciJobUrl !== undefined ? { ciJobUrl: typed.ciJobUrl } : {}),
705
+ ...(typed.skills !== undefined ? { skills: typed.skills } : {}),
706
+ callerId,
707
+ });
708
+ }
709
+ catch (err) {
710
+ if (err instanceof PipelineValidationError) {
711
+ writeAudit(auditLog, {
712
+ caller_id: callerId,
713
+ surface: getRequestAuth()?.surface ?? "rest",
714
+ action: "start_run",
715
+ outcome: "error",
716
+ error_code: "pipeline_validation",
717
+ });
718
+ json(res, 400, {
719
+ error: "Pipeline validation failed",
720
+ validation: err.result,
721
+ });
722
+ return true;
723
+ }
724
+ if (err instanceof PipelinePreflightError) {
725
+ writeAudit(auditLog, {
726
+ caller_id: callerId,
727
+ surface: getRequestAuth()?.surface ?? "rest",
728
+ action: "start_run",
729
+ outcome: "error",
730
+ error_code: err.code,
731
+ });
732
+ json(res, 400, err.toNetworkBody());
733
+ return true;
734
+ }
735
+ throw err;
736
+ }
737
+ if (!result.ok) {
738
+ writeAudit(auditLog, {
739
+ caller_id: callerId,
740
+ surface: getRequestAuth()?.surface ?? "rest",
741
+ action: "start_run",
742
+ outcome: "error",
743
+ error_code: result.code,
311
744
  });
745
+ json(res, result.status ?? 500, mapStartFailure(result));
312
746
  return true;
313
747
  }
314
- throw err;
748
+ writeAudit(auditLog, {
749
+ caller_id: callerId,
750
+ surface: getRequestAuth()?.surface ?? "rest",
751
+ action: "start_run",
752
+ target_run_id: result.runId,
753
+ outcome: "ok",
754
+ });
755
+ json(res, 202, {
756
+ runId: result.runId,
757
+ ...(result.queued === true
758
+ ? {
759
+ queued: true,
760
+ queuePosition: result.queuePosition,
761
+ ...(result.queuedCode !== undefined
762
+ ? { queuedCode: result.queuedCode }
763
+ : {}),
764
+ }
765
+ : {}),
766
+ });
767
+ return true;
315
768
  }
316
- if (!result.ok) {
317
- json(res, result.status ?? 500, mapStartFailure(result));
769
+ if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
770
+ const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
771
+ const body = (await readJsonBody(req));
772
+ if (body.pinned !== undefined && typeof body.pinned !== "boolean") {
773
+ json(res, 400, { error: "pinned must be a boolean" });
774
+ return true;
775
+ }
776
+ const callerId = callerIdFromRequestAuth();
777
+ const result = await manager.rerun(runId, {
778
+ ...(body.pinned !== undefined ? { pinned: body.pinned } : {}),
779
+ callerId,
780
+ });
781
+ if (!result.ok) {
782
+ writeAudit(auditLog, {
783
+ caller_id: callerId,
784
+ surface: getRequestAuth()?.surface ?? "rest",
785
+ action: "rerun",
786
+ target_run_id: runId,
787
+ outcome: "error",
788
+ error_code: result.code,
789
+ });
790
+ json(res, result.status ?? 500, mapStartFailure(result));
791
+ return true;
792
+ }
793
+ writeAudit(auditLog, {
794
+ caller_id: callerId,
795
+ surface: getRequestAuth()?.surface ?? "rest",
796
+ action: "rerun",
797
+ target_run_id: result.runId,
798
+ outcome: "ok",
799
+ });
800
+ json(res, 202, { runId: result.runId });
318
801
  return true;
319
802
  }
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));
803
+ const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
804
+ if (method === "POST" && answerMatch) {
805
+ const runId = decodeURIComponent(answerMatch[1] ?? "");
806
+ const stageId = decodeURIComponent(answerMatch[2] ?? "");
807
+ let answer;
808
+ try {
809
+ answer = parseAskOperatorAnswer(await readJsonBody(req));
810
+ }
811
+ catch (err) {
812
+ json(res, 400, {
813
+ error: err instanceof Error ? err.message : String(err),
814
+ });
815
+ return true;
816
+ }
817
+ const result = await manager.deliverAnswer(runId, stageId, answer);
818
+ if (!result.ok) {
819
+ json(res, result.status, { error: result.reason });
820
+ return true;
821
+ }
822
+ json(res, 202, {
823
+ ok: true,
824
+ ...(result.verification !== undefined
825
+ ? { verification: result.verification }
826
+ : {}),
827
+ });
328
828
  return true;
329
829
  }
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),
830
+ const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
831
+ if (method === "POST" && feedbackDecisionMatch) {
832
+ const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
833
+ const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
834
+ let body;
835
+ try {
836
+ body = await readJsonBody(req);
837
+ }
838
+ catch {
839
+ json(res, 400, { error: "Invalid JSON body" });
840
+ return true;
841
+ }
842
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
843
+ json(res, 400, { error: "Body must be a JSON object" });
844
+ return true;
845
+ }
846
+ const raw = body;
847
+ if (raw.decision !== "extend" &&
848
+ raw.decision !== "continue" &&
849
+ raw.decision !== "abandon") {
850
+ json(res, 400, {
851
+ error: "decision must be extend, continue, or abandon",
852
+ });
853
+ return true;
854
+ }
855
+ if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
856
+ json(res, 400, { error: "loopId must be a string" });
857
+ return true;
858
+ }
859
+ if (raw.reason !== undefined && typeof raw.reason !== "string") {
860
+ json(res, 400, { error: "reason must be a string" });
861
+ return true;
862
+ }
863
+ const result = await manager.decideFeedbackLoop(runId, stageId, {
864
+ decision: raw.decision,
865
+ ...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
866
+ ...(raw.reason !== undefined ? { reason: raw.reason } : {}),
867
+ });
868
+ if (!result.ok) {
869
+ json(res, result.status ?? 500, { error: result.reason });
870
+ return true;
871
+ }
872
+ json(res, 202, {
873
+ ok: true,
874
+ effect: result.effect,
875
+ loopId: result.loopId,
344
876
  });
345
877
  return true;
346
878
  }
347
- const result = await manager.deliverAnswer(runId, stageId, answer);
348
- if (!result.ok) {
349
- json(res, result.status, { error: result.reason });
879
+ const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
880
+ if (method === "POST" && retryMatch) {
881
+ await readJsonBody(req);
882
+ const runId = decodeURIComponent(retryMatch[1] ?? "");
883
+ const stageId = decodeURIComponent(retryMatch[2] ?? "");
884
+ const result = await manager.retryStage(runId, stageId);
885
+ if (!result.ok) {
886
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
887
+ return true;
888
+ }
889
+ json(res, 202, {
890
+ runId: result.runId,
891
+ stageId: result.stageId,
892
+ attemptIndex: result.attemptIndex,
893
+ });
350
894
  return true;
351
895
  }
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);
896
+ const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
897
+ if (method === "POST" && resumeMatch) {
898
+ await readJsonBody(req);
899
+ const runId = decodeURIComponent(resumeMatch[1] ?? "");
900
+ const stageId = decodeURIComponent(resumeMatch[2] ?? "");
901
+ const result = await manager.resumeTimedOutStage(runId, stageId);
902
+ if (!result.ok) {
903
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
904
+ return true;
905
+ }
906
+ json(res, 202, {
907
+ runId: result.runId,
908
+ stageId: result.stageId,
909
+ attemptIndex: result.attemptIndex,
910
+ });
911
+ return true;
362
912
  }
363
- catch {
364
- json(res, 400, { error: "Invalid JSON body" });
913
+ const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
914
+ if (method === "POST" && recoverMatch) {
915
+ let body;
916
+ try {
917
+ body = await readJsonBody(req);
918
+ }
919
+ catch {
920
+ json(res, 400, { error: "Invalid JSON body" });
921
+ return true;
922
+ }
923
+ const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
924
+ ? body.guidance
925
+ : undefined;
926
+ if (guidance !== undefined && typeof guidance !== "string") {
927
+ json(res, 400, { error: "guidance must be a string" });
928
+ return true;
929
+ }
930
+ const runId = decodeURIComponent(recoverMatch[1] ?? "");
931
+ const stageId = decodeURIComponent(recoverMatch[2] ?? "");
932
+ const result = await manager.recoverManualStage(runId, stageId, guidance);
933
+ if (!result.ok) {
934
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
935
+ return true;
936
+ }
937
+ json(res, 202, {
938
+ runId: result.runId,
939
+ stageId: result.stageId,
940
+ attemptIndex: result.attemptIndex,
941
+ });
365
942
  return true;
366
943
  }
367
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
368
- json(res, 400, { error: "Body must be a JSON object" });
944
+ const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
945
+ if (method === "POST" && stopRecoveryMatch) {
946
+ await readJsonBody(req);
947
+ const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
948
+ const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
949
+ const result = await manager.stopManualRecovery(runId, stageId);
950
+ if (!result.ok) {
951
+ json(res, result.status ?? 500, { error: result.reason });
952
+ return true;
953
+ }
954
+ json(res, 202, result);
369
955
  return true;
370
956
  }
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",
957
+ const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
958
+ if (method === "POST" && abandonMatch) {
959
+ await readJsonBody(req);
960
+ const runId = decodeURIComponent(abandonMatch[1] ?? "");
961
+ const stageId = decodeURIComponent(abandonMatch[2] ?? "");
962
+ const result = await manager.abandonStage(runId, stageId);
963
+ if (!result.ok) {
964
+ json(res, result.status ?? 500, {
965
+ error: result.reason,
966
+ });
967
+ return true;
968
+ }
969
+ json(res, 202, {
970
+ ok: true,
971
+ runId: result.runId,
972
+ stageId: result.stageId,
377
973
  });
378
974
  return true;
379
975
  }
380
- if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
381
- json(res, 400, { error: "loopId must be a string" });
976
+ const cancelMatch = pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/);
977
+ if (method === "POST" && cancelMatch) {
978
+ const body = (await readJsonBody(req));
979
+ const runId = decodeURIComponent(cancelMatch[1] ?? "");
980
+ if (typeof body.reason !== "string" || body.reason.trim().length === 0) {
981
+ json(res, 400, { error: "Cancel reason is required" });
982
+ return true;
983
+ }
984
+ const result = await manager.cancelRun(runId, body.reason);
985
+ if (!result.ok) {
986
+ json(res, result.status ?? 500, {
987
+ error: result.reason,
988
+ });
989
+ return true;
990
+ }
991
+ json(res, 202, {
992
+ ok: true,
993
+ runId: result.runId,
994
+ });
382
995
  return true;
383
996
  }
384
- if (raw.reason !== undefined && typeof raw.reason !== "string") {
385
- json(res, 400, { error: "reason must be a string" });
997
+ if (method === "POST" && pathname === "/api/runs/gc") {
998
+ const body = (await readJsonBody(req));
999
+ const execute = body.execute === true;
1000
+ const result = await manager.gcRuns({ execute, channel: "rest" });
1001
+ if (!result.ok) {
1002
+ json(res, result.status ?? 500, {
1003
+ error: result.reason,
1004
+ });
1005
+ return true;
1006
+ }
1007
+ json(res, 200, {
1008
+ slimmed: result.slimmed,
1009
+ purged: result.purged,
1010
+ bareCachesEvicted: result.bareCachesEvicted,
1011
+ });
386
1012
  return true;
387
1013
  }
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 });
1014
+ const deleteMatch = pathname.match(/^\/api\/runs\/([^/]+)$/);
1015
+ if (method === "DELETE" && deleteMatch) {
1016
+ const runId = decodeURIComponent(deleteMatch[1] ?? "");
1017
+ const forceParam = url.searchParams.get("force");
1018
+ const force = forceParam === "true" || forceParam === "1";
1019
+ const result = await manager.deleteRun(runId, {
1020
+ force,
1021
+ channel: "rest",
1022
+ });
1023
+ if (!result.ok) {
1024
+ json(res, result.status ?? 500, {
1025
+ error: result.reason,
1026
+ });
1027
+ return true;
1028
+ }
1029
+ json(res, 200, {
1030
+ ok: true,
1031
+ runId: result.runId,
1032
+ });
395
1033
  return true;
396
1034
  }
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);
1035
+ if (method === "GET" && pathname === "/api/tasks") {
1036
+ const filter = url.searchParams.get("project_root") ?? undefined;
1037
+ const result = await listTasksMultiProject({
1038
+ store,
1039
+ bootCwd: cwd,
1040
+ projectRootFilter: filter,
1041
+ });
1042
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1043
+ result.items.length === 0) {
1044
+ json(res, 400, {
1045
+ error: result.root_errors[0].message,
1046
+ code: "unknown_project_root",
1047
+ root_errors: result.root_errors,
1048
+ });
1049
+ return true;
1050
+ }
1051
+ json(res, 200, {
1052
+ tasks: result.items,
1053
+ root_errors: result.root_errors,
1054
+ ...(result.tip !== undefined ? { tip: result.tip } : {}),
1055
+ });
1056
+ return true;
443
1057
  }
444
- catch {
445
- json(res, 400, { error: "Invalid JSON body" });
1058
+ if (method === "GET" && pathname === "/api/pipelines") {
1059
+ const filter = url.searchParams.get("project_root") ?? undefined;
1060
+ const result = await listPipelinesMultiProject({
1061
+ store,
1062
+ bootCwd: cwd,
1063
+ projectRootFilter: filter,
1064
+ });
1065
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1066
+ result.items.length === 0) {
1067
+ json(res, 400, {
1068
+ error: result.root_errors[0].message,
1069
+ code: "unknown_project_root",
1070
+ root_errors: result.root_errors,
1071
+ });
1072
+ return true;
1073
+ }
1074
+ json(res, 200, {
1075
+ pipelines: result.items,
1076
+ root_errors: result.root_errors,
1077
+ ...(result.tip !== undefined ? { tip: result.tip } : {}),
1078
+ });
446
1079
  return true;
447
1080
  }
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" });
1081
+ if (method === "GET" && pathname === "/api/stages") {
1082
+ json(res, 404, {
1083
+ error: "Global stage library removed; stages are pipeline-scoped",
1084
+ });
453
1085
  return true;
454
1086
  }
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));
1087
+ if (method === "POST" && pathname === "/api/stages") {
1088
+ let body;
1089
+ try {
1090
+ body = await readJsonBody(req);
1091
+ }
1092
+ catch {
1093
+ json(res, 400, { error: "Invalid JSON body" });
1094
+ return true;
1095
+ }
1096
+ const writeRoot = body !== null &&
1097
+ typeof body === "object" &&
1098
+ !Array.isArray(body) &&
1099
+ typeof body.project_root === "string"
1100
+ ? body.project_root
1101
+ : undefined;
1102
+ let stageWriteRoot;
1103
+ try {
1104
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1105
+ stageWriteRoot = selected.path;
1106
+ }
1107
+ catch (err) {
1108
+ if (err instanceof CatalogPathError) {
1109
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1110
+ return true;
1111
+ }
1112
+ throw err;
1113
+ }
1114
+ const parsed = parseCreateStageBody(body);
1115
+ if ("ok" in parsed) {
1116
+ json(res, parsed.status, { error: parsed.error });
1117
+ return true;
1118
+ }
1119
+ const ctx = await resolveStageflowContext(stageWriteRoot);
1120
+ if (ctx.manifestStatus !== "ok") {
1121
+ json(res, 400, {
1122
+ error: "Project root not found; initialize stageflow.yaml",
1123
+ });
1124
+ return true;
1125
+ }
1126
+ const result = await createStage(ctx.projectRoot, parsed);
1127
+ if (!result.ok) {
1128
+ json(res, result.status, { error: result.error });
1129
+ return true;
1130
+ }
1131
+ json(res, 201, result.stage);
460
1132
  return true;
461
1133
  }
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 });
1134
+ if (method === "POST" && pathname === "/api/pipelines") {
1135
+ let body;
1136
+ try {
1137
+ body = await readJsonBody(req);
1138
+ }
1139
+ catch {
1140
+ json(res, 400, { error: "Invalid JSON body" });
1141
+ return true;
1142
+ }
1143
+ const writeRoot = body !== null &&
1144
+ typeof body === "object" &&
1145
+ !Array.isArray(body) &&
1146
+ typeof body.project_root === "string"
1147
+ ? body.project_root
1148
+ : undefined;
1149
+ let pipelineWriteRoot;
1150
+ try {
1151
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1152
+ pipelineWriteRoot = selected.path;
1153
+ }
1154
+ catch (err) {
1155
+ if (err instanceof CatalogPathError) {
1156
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1157
+ return true;
1158
+ }
1159
+ throw err;
1160
+ }
1161
+ const parsed = parseCreatePipelineBody(body);
1162
+ if ("ok" in parsed) {
1163
+ json(res, parsed.status, { error: parsed.error });
1164
+ return true;
1165
+ }
1166
+ const ctx = await resolveStageflowContext(pipelineWriteRoot);
1167
+ if (ctx.manifestStatus !== "ok") {
1168
+ json(res, 400, {
1169
+ error: "Project root not found; initialize stageflow.yaml",
1170
+ });
1171
+ return true;
1172
+ }
1173
+ const result = await createPipeline(ctx.projectRoot, parsed);
1174
+ if (!result.ok) {
1175
+ json(res, result.status, { error: result.error });
1176
+ return true;
1177
+ }
1178
+ json(res, 201, result.pipeline);
477
1179
  return true;
478
1180
  }
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,
491
- });
492
- return true;
493
- }
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);
1181
+ if (method === "POST" && pathname === "/api/drafts/validate") {
1182
+ let body;
1183
+ try {
1184
+ body = await readJsonBody(req);
1185
+ }
1186
+ catch {
1187
+ json(res, 400, { error: "Invalid JSON body" });
1188
+ return true;
1189
+ }
1190
+ const draftParsed = parseDraftPackageBody(body);
1191
+ if ("ok" in draftParsed) {
1192
+ json(res, draftParsed.status, { error: draftParsed.error });
1193
+ return true;
1194
+ }
1195
+ const writeRoot = body !== null &&
1196
+ typeof body === "object" &&
1197
+ !Array.isArray(body) &&
1198
+ typeof body.project_root === "string"
1199
+ ? body.project_root
1200
+ : undefined;
1201
+ let validateRoot = cwd;
1202
+ if (writeRoot) {
1203
+ try {
1204
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1205
+ validateRoot = selected.path;
1206
+ }
1207
+ catch (err) {
1208
+ if (err instanceof CatalogPathError) {
1209
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1210
+ return true;
1211
+ }
1212
+ throw err;
1213
+ }
1214
+ }
1215
+ const ctx = await resolveStageflowContext(validateRoot);
1216
+ const result = await validateDraftPackage(draftParsed, {
1217
+ cwd: ctx.projectRoot,
1218
+ projectRoot: ctx.projectRoot,
1219
+ strict: true,
1220
+ });
1221
+ json(res, 200, result);
1222
+ return true;
521
1223
  }
522
- catch {
523
- json(res, 400, { error: "Invalid JSON body" });
1224
+ if (method === "POST" && pathname === "/api/drafts/create") {
1225
+ let body;
1226
+ try {
1227
+ body = await readJsonBody(req);
1228
+ }
1229
+ catch {
1230
+ json(res, 400, { error: "Invalid JSON body" });
1231
+ return true;
1232
+ }
1233
+ const writeRoot = body !== null &&
1234
+ typeof body === "object" &&
1235
+ !Array.isArray(body) &&
1236
+ typeof body.project_root === "string"
1237
+ ? body.project_root
1238
+ : undefined;
1239
+ let draftWriteRoot;
1240
+ try {
1241
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1242
+ draftWriteRoot = selected.path;
1243
+ }
1244
+ catch (err) {
1245
+ if (err instanceof CatalogPathError) {
1246
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1247
+ return true;
1248
+ }
1249
+ throw err;
1250
+ }
1251
+ const parsed = parseCreateDraftPackageBody(body);
1252
+ if ("ok" in parsed) {
1253
+ json(res, parsed.status, { error: parsed.error });
1254
+ return true;
1255
+ }
1256
+ const ctx = await resolveStageflowContext(draftWriteRoot);
1257
+ if (!ctx.isGitProject) {
1258
+ json(res, 400, {
1259
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
1260
+ });
1261
+ return true;
1262
+ }
1263
+ const result = (await publishDraftPackage(ctx.projectRoot, parsed, "create")).write;
1264
+ if (!result.ok) {
1265
+ json(res, result.status, {
1266
+ error: result.error,
1267
+ ...(result.findings ? { findings: result.findings } : {}),
1268
+ });
1269
+ return true;
1270
+ }
1271
+ json(res, 201, {
1272
+ pipeline: result.pipeline,
1273
+ pipelinePath: result.pipelinePath,
1274
+ stagePaths: result.stagePaths,
1275
+ ...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
1276
+ });
524
1277
  return true;
525
1278
  }
526
- const parsed = parseCreateStageBody(body);
527
- if ("ok" in parsed) {
528
- json(res, parsed.status, { error: parsed.error });
1279
+ if (method === "POST" && pathname === "/api/drafts/overwrite") {
1280
+ let body;
1281
+ try {
1282
+ body = await readJsonBody(req);
1283
+ }
1284
+ catch {
1285
+ json(res, 400, { error: "Invalid JSON body" });
1286
+ return true;
1287
+ }
1288
+ const writeRoot = body !== null &&
1289
+ typeof body === "object" &&
1290
+ !Array.isArray(body) &&
1291
+ typeof body.project_root === "string"
1292
+ ? body.project_root
1293
+ : undefined;
1294
+ let draftWriteRoot;
1295
+ try {
1296
+ const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1297
+ draftWriteRoot = selected.path;
1298
+ }
1299
+ catch (err) {
1300
+ if (err instanceof CatalogPathError) {
1301
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1302
+ return true;
1303
+ }
1304
+ throw err;
1305
+ }
1306
+ const parsed = parseOverwriteDraftPackageBody(body);
1307
+ if ("ok" in parsed) {
1308
+ json(res, parsed.status, { error: parsed.error });
1309
+ return true;
1310
+ }
1311
+ const ctx = await resolveStageflowContext(draftWriteRoot);
1312
+ if (!ctx.isGitProject) {
1313
+ json(res, 400, {
1314
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
1315
+ });
1316
+ return true;
1317
+ }
1318
+ const result = (await publishDraftPackage(ctx.projectRoot, parsed, "overwrite")).write;
1319
+ if (!result.ok) {
1320
+ json(res, result.status, {
1321
+ error: result.error,
1322
+ ...(result.findings ? { findings: result.findings } : {}),
1323
+ });
1324
+ return true;
1325
+ }
1326
+ json(res, 200, {
1327
+ pipeline: result.pipeline,
1328
+ pipelinePath: result.pipelinePath,
1329
+ stagePaths: result.stagePaths,
1330
+ ...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
1331
+ });
529
1332
  return true;
530
1333
  }
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",
1334
+ if (method === "POST" && pathname === "/api/drafts/open") {
1335
+ let body;
1336
+ try {
1337
+ body = await readJsonBody(req);
1338
+ }
1339
+ catch {
1340
+ json(res, 400, { error: "Invalid JSON body" });
1341
+ return true;
1342
+ }
1343
+ const parsed = parseOpenDraftPackageBody(body);
1344
+ if ("ok" in parsed) {
1345
+ json(res, parsed.status, { error: parsed.error });
1346
+ return true;
1347
+ }
1348
+ const writeRoot = body !== null &&
1349
+ typeof body === "object" &&
1350
+ !Array.isArray(body) &&
1351
+ typeof body.project_root === "string"
1352
+ ? body.project_root
1353
+ : undefined;
1354
+ let wireRoot;
1355
+ let roots;
1356
+ try {
1357
+ ({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
1358
+ }
1359
+ catch (err) {
1360
+ if (err instanceof CatalogPathError) {
1361
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1362
+ return true;
1363
+ }
1364
+ throw err;
1365
+ }
1366
+ let pipelineRel;
1367
+ let taskRel;
1368
+ try {
1369
+ pipelineRel = resolveCatalogRelativePath({
1370
+ inputPath: parsed.path,
1371
+ projectRoot: writeRoot,
1372
+ roots,
1373
+ fieldName: "path",
1374
+ }).relativePath;
1375
+ if (parsed.taskPath) {
1376
+ taskRel = resolveCatalogRelativePath({
1377
+ inputPath: parsed.taskPath,
1378
+ projectRoot: writeRoot,
1379
+ roots,
1380
+ fieldName: "task",
1381
+ }).relativePath;
1382
+ }
1383
+ }
1384
+ catch (err) {
1385
+ if (err instanceof CatalogPathError) {
1386
+ json(res, 400, catalogPathErrorBody(err));
1387
+ return true;
1388
+ }
1389
+ throw err;
1390
+ }
1391
+ const ctx = await resolveStageflowContext(wireRoot.path);
1392
+ const result = await loadDraftPackage(ctx.projectRoot, pipelineRel, {
1393
+ ...(taskRel !== undefined ? { taskPath: taskRel } : {}),
1394
+ });
1395
+ if (!result.ok) {
1396
+ json(res, result.status, { error: result.error });
1397
+ return true;
1398
+ }
1399
+ json(res, 200, {
1400
+ draft: result.draft,
1401
+ destination: result.destination,
1402
+ pipelinePath: result.pipelinePath,
1403
+ ...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
535
1404
  });
536
1405
  return true;
537
1406
  }
538
- const result = await createStage(ctx.projectRoot, parsed);
539
- if (!result.ok) {
540
- json(res, result.status, { error: result.error });
1407
+ if (method === "POST" && pathname === "/api/drafts/attach-task") {
1408
+ let body;
1409
+ try {
1410
+ body = await readJsonBody(req);
1411
+ }
1412
+ catch {
1413
+ json(res, 400, { error: "Invalid JSON body" });
1414
+ return true;
1415
+ }
1416
+ const parsed = parseAttachTaskBody(body);
1417
+ if ("ok" in parsed) {
1418
+ json(res, parsed.status, { error: parsed.error });
1419
+ return true;
1420
+ }
1421
+ const writeRoot = body !== null &&
1422
+ typeof body === "object" &&
1423
+ !Array.isArray(body) &&
1424
+ typeof body.project_root === "string"
1425
+ ? body.project_root
1426
+ : undefined;
1427
+ let wireRoot;
1428
+ let roots;
1429
+ try {
1430
+ ({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
1431
+ }
1432
+ catch (err) {
1433
+ if (err instanceof CatalogPathError) {
1434
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1435
+ return true;
1436
+ }
1437
+ throw err;
1438
+ }
1439
+ let taskRel;
1440
+ try {
1441
+ taskRel = resolveCatalogRelativePath({
1442
+ inputPath: parsed.taskPath,
1443
+ projectRoot: writeRoot,
1444
+ roots,
1445
+ fieldName: "task",
1446
+ }).relativePath;
1447
+ }
1448
+ catch (err) {
1449
+ if (err instanceof CatalogPathError) {
1450
+ json(res, 400, catalogPathErrorBody(err));
1451
+ return true;
1452
+ }
1453
+ throw err;
1454
+ }
1455
+ const ctx = await resolveStageflowContext(wireRoot.path);
1456
+ const result = await loadTaskArtifact(ctx.projectRoot, taskRel);
1457
+ if (!result.ok) {
1458
+ json(res, result.status, { error: result.error });
1459
+ return true;
1460
+ }
1461
+ json(res, 200, {
1462
+ task: result.task,
1463
+ taskPath: result.taskPath,
1464
+ });
541
1465
  return true;
542
1466
  }
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);
1467
+ if (method === "GET" &&
1468
+ pathname === "/api/workshop/autosave") {
1469
+ const keyParam = url.searchParams.get("key");
1470
+ const key = workshopAutosaveSlotKey(keyParam);
1471
+ const writeRoot = url.searchParams.get("project_root") ?? undefined;
1472
+ let storeRoot;
1473
+ try {
1474
+ if (writeRoot) {
1475
+ const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
1476
+ const ctx = await resolveStageflowContext(wireRoot.path);
1477
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1478
+ projectRoot: ctx.projectRoot,
1479
+ isGitProject: ctx.isGitProject,
1480
+ });
1481
+ }
1482
+ else {
1483
+ const ctx = await resolveStageflowContext(cwd);
1484
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1485
+ projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
1486
+ isGitProject: ctx.isGitProject,
1487
+ });
1488
+ }
1489
+ }
1490
+ catch (err) {
1491
+ if (err instanceof CatalogPathError) {
1492
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1493
+ return true;
1494
+ }
1495
+ throw err;
1496
+ }
1497
+ const record = readWorkshopAutosave(storeRoot, key);
1498
+ json(res, 200, { key, autosave: record });
1499
+ return true;
550
1500
  }
551
- catch {
552
- json(res, 400, { error: "Invalid JSON body" });
1501
+ if (method === "PUT" &&
1502
+ pathname === "/api/workshop/autosave") {
1503
+ let body;
1504
+ try {
1505
+ body = await readJsonBody(req);
1506
+ }
1507
+ catch {
1508
+ json(res, 400, { error: "Invalid JSON body" });
1509
+ return true;
1510
+ }
1511
+ if (!isPlainObject(body)) {
1512
+ json(res, 400, { error: "Invalid JSON body" });
1513
+ return true;
1514
+ }
1515
+ const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
1516
+ const writeRoot = typeof body.project_root === "string"
1517
+ ? body.project_root
1518
+ : undefined;
1519
+ let storeRoot;
1520
+ try {
1521
+ if (writeRoot) {
1522
+ const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1523
+ const ctx = await resolveStageflowContext(wireRoot.path);
1524
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1525
+ projectRoot: ctx.projectRoot,
1526
+ isGitProject: ctx.isGitProject,
1527
+ });
1528
+ }
1529
+ else {
1530
+ const ctx = await resolveStageflowContext(cwd);
1531
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1532
+ projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
1533
+ isGitProject: ctx.isGitProject,
1534
+ });
1535
+ }
1536
+ }
1537
+ catch (err) {
1538
+ if (err instanceof CatalogPathError) {
1539
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1540
+ return true;
1541
+ }
1542
+ throw err;
1543
+ }
1544
+ const candidate = {
1545
+ version: 1,
1546
+ key,
1547
+ updatedAt: typeof body.updatedAt === "string" && body.updatedAt
1548
+ ? body.updatedAt
1549
+ : new Date().toISOString(),
1550
+ draft: body.draft,
1551
+ messages: body.messages,
1552
+ autoApply: Boolean(body.autoApply),
1553
+ ...(body.sessionModelOverride === null ||
1554
+ typeof body.sessionModelOverride === "string"
1555
+ ? { sessionModelOverride: body.sessionModelOverride }
1556
+ : {}),
1557
+ ...(body.destination !== undefined
1558
+ ? {
1559
+ destination: body.destination,
1560
+ }
1561
+ : {}),
1562
+ ...(body.savedPath === null || typeof body.savedPath === "string"
1563
+ ? { savedPath: body.savedPath }
1564
+ : {}),
1565
+ ...(body.savedTaskPath === null ||
1566
+ typeof body.savedTaskPath === "string"
1567
+ ? { savedTaskPath: body.savedTaskPath }
1568
+ : {}),
1569
+ ...(isPlainObject(body.diskFingerprints)
1570
+ ? {
1571
+ diskFingerprints: body.diskFingerprints,
1572
+ }
1573
+ : {}),
1574
+ };
1575
+ const parsed = parseWorkshopAutosaveRecord(candidate);
1576
+ if (!parsed) {
1577
+ json(res, 400, { error: "Invalid workshop autosave payload" });
1578
+ return true;
1579
+ }
1580
+ const written = writeWorkshopAutosave(storeRoot, parsed);
1581
+ json(res, 200, { autosave: written });
553
1582
  return true;
554
1583
  }
555
- const parsed = parseCreatePipelineBody(body);
556
- if ("ok" in parsed) {
557
- json(res, parsed.status, { error: parsed.error });
1584
+ if (method === "DELETE" &&
1585
+ pathname === "/api/workshop/autosave") {
1586
+ let body;
1587
+ try {
1588
+ body = await readJsonBody(req);
1589
+ }
1590
+ catch {
1591
+ json(res, 400, { error: "Invalid JSON body" });
1592
+ return true;
1593
+ }
1594
+ if (!isPlainObject(body)) {
1595
+ json(res, 400, { error: "Invalid JSON body" });
1596
+ return true;
1597
+ }
1598
+ const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
1599
+ const writeRoot = typeof body.project_root === "string"
1600
+ ? body.project_root
1601
+ : undefined;
1602
+ let storeRoot;
1603
+ try {
1604
+ if (writeRoot) {
1605
+ const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
1606
+ const ctx = await resolveStageflowContext(wireRoot.path);
1607
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1608
+ projectRoot: ctx.projectRoot,
1609
+ isGitProject: ctx.isGitProject,
1610
+ });
1611
+ }
1612
+ else {
1613
+ const ctx = await resolveStageflowContext(cwd);
1614
+ storeRoot = resolveWorkshopAutosaveStoreRoot({
1615
+ projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
1616
+ isGitProject: ctx.isGitProject,
1617
+ });
1618
+ }
1619
+ }
1620
+ catch (err) {
1621
+ if (err instanceof CatalogPathError) {
1622
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
1623
+ return true;
1624
+ }
1625
+ throw err;
1626
+ }
1627
+ const cleared = clearWorkshopAutosave(storeRoot, key);
1628
+ json(res, 200, { key, cleared });
558
1629
  return true;
559
1630
  }
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",
1631
+ if (method === "GET" && pathname === "/api/workshop/picker") {
1632
+ const filter = url.searchParams.get("project_root") ?? undefined;
1633
+ const result = await listPipelinesMultiProject({
1634
+ store,
1635
+ bootCwd: cwd,
1636
+ projectRootFilter: filter,
1637
+ });
1638
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1639
+ result.items.length === 0) {
1640
+ json(res, 400, {
1641
+ error: result.root_errors[0].message,
1642
+ code: "unknown_project_root",
1643
+ root_errors: result.root_errors,
1644
+ });
1645
+ return true;
1646
+ }
1647
+ json(res, 200, {
1648
+ rows: listWorkshopPickerRows(workshopBuildStoreRoot(), resolvePickerCatalogPipelines(result.items, result.roots), result.roots),
1649
+ root_errors: result.root_errors,
1650
+ ...(result.tip !== undefined ? { tip: result.tip } : {}),
564
1651
  });
565
1652
  return true;
566
1653
  }
567
- const result = await createPipeline(ctx.projectRoot, parsed);
568
- if (!result.ok) {
569
- json(res, result.status, { error: result.error });
1654
+ if (method === "POST" && pathname === "/api/workshop/focus") {
1655
+ let body;
1656
+ try {
1657
+ body = await readJsonBody(req);
1658
+ }
1659
+ catch {
1660
+ json(res, 400, { error: "Invalid JSON body" });
1661
+ return true;
1662
+ }
1663
+ if (!isPlainObject(body)) {
1664
+ json(res, 400, { error: "Invalid JSON body" });
1665
+ return true;
1666
+ }
1667
+ if (typeof body.projectRoot !== "string" ||
1668
+ typeof body.relativePath !== "string") {
1669
+ json(res, 400, { error: "project root and path are required" });
1670
+ return true;
1671
+ }
1672
+ let projectRoot = body.projectRoot;
1673
+ try {
1674
+ const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, body.projectRoot);
1675
+ projectRoot = wireRoot.path;
1676
+ }
1677
+ catch (err) {
1678
+ if (err instanceof CatalogPathError) {
1679
+ json(res, 400, { error: err.message });
1680
+ return true;
1681
+ }
1682
+ throw err;
1683
+ }
1684
+ const focused = await focusUnboundWorkshopBuild(workshopBuildStoreRoot(), {
1685
+ projectRoot,
1686
+ relativePath: body.relativePath,
1687
+ });
1688
+ if (!focused.ok) {
1689
+ json(res, focused.status, { error: focused.error });
1690
+ return true;
1691
+ }
1692
+ json(res, focused.created ? 201 : 200, { build: focused.build });
570
1693
  return true;
571
1694
  }
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);
1695
+ if (method === "GET" &&
1696
+ pathname === "/api/workshop/builds") {
1697
+ json(res, 200, {
1698
+ builds: listWorkshopBuilds(workshopBuildStoreRoot()),
1699
+ });
1700
+ return true;
1701
+ }
1702
+ if (method === "POST" &&
1703
+ pathname === "/api/workshop/builds") {
1704
+ let body = {};
1705
+ try {
1706
+ body = await readJsonBody(req);
1707
+ }
1708
+ catch {
1709
+ json(res, 400, { error: "Invalid JSON body" });
1710
+ return true;
1711
+ }
1712
+ if (!isPlainObject(body)) {
1713
+ json(res, 400, { error: "Invalid JSON body" });
1714
+ return true;
1715
+ }
1716
+ const draftParsed = parseDraftPackageBody(body);
1717
+ if ("ok" in draftParsed) {
1718
+ json(res, draftParsed.status, { error: draftParsed.error });
1719
+ return true;
1720
+ }
1721
+ const projectRoot = readBuildTieField(body, "projectRoot");
1722
+ if (!projectRoot.ok) {
1723
+ json(res, 400, { error: projectRoot.error });
1724
+ return true;
1725
+ }
1726
+ const relativePath = readBuildTieField(body, "relativePath");
1727
+ if (!relativePath.ok) {
1728
+ json(res, 400, { error: relativePath.error });
1729
+ return true;
1730
+ }
1731
+ const build = createWorkshopBuild(workshopBuildStoreRoot(), {
1732
+ draft: draftParsed,
1733
+ ...(typeof body.id === "string" ? { id: body.id } : {}),
1734
+ ...(projectRoot.present ? { projectRoot: projectRoot.value } : {}),
1735
+ ...(relativePath.present
1736
+ ? { relativePath: relativePath.value }
1737
+ : {}),
1738
+ });
1739
+ json(res, 201, { build });
1740
+ return true;
1741
+ }
1742
+ const workshopBuildMatch = pathname.match(/^\/api\/workshop\/builds\/([^/]+)$/);
1743
+ if (method === "GET" && workshopBuildMatch) {
1744
+ const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
1745
+ try {
1746
+ const build = getWorkshopBuild(workshopBuildStoreRoot(), buildId);
1747
+ json(res, 200, { build });
1748
+ }
1749
+ catch (err) {
1750
+ if (err instanceof WorkshopBuildStoreError &&
1751
+ err.code === "workshop_build_not_found") {
1752
+ json(res, 404, {
1753
+ error: err.message,
1754
+ code: err.code,
1755
+ buildId: err.buildId,
1756
+ });
1757
+ return true;
1758
+ }
1759
+ throw err;
1760
+ }
1761
+ return true;
1762
+ }
1763
+ if (method === "PUT" && workshopBuildMatch) {
1764
+ const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
1765
+ let body = {};
1766
+ try {
1767
+ body = await readJsonBody(req);
1768
+ }
1769
+ catch {
1770
+ json(res, 400, { error: "Invalid JSON body" });
1771
+ return true;
1772
+ }
1773
+ if (!isPlainObject(body)) {
1774
+ json(res, 400, { error: "Invalid JSON body" });
1775
+ return true;
1776
+ }
1777
+ let draft;
1778
+ if (body.draft !== undefined) {
1779
+ const draftParsed = parseDraftPackageBody({ draft: body.draft });
1780
+ if ("ok" in draftParsed) {
1781
+ json(res, draftParsed.status, { error: draftParsed.error });
1782
+ return true;
1783
+ }
1784
+ draft = draftParsed;
1785
+ }
1786
+ const projectRoot = readBuildTieField(body, "projectRoot");
1787
+ if (!projectRoot.ok) {
1788
+ json(res, 400, { error: projectRoot.error });
1789
+ return true;
1790
+ }
1791
+ const relativePath = readBuildTieField(body, "relativePath");
1792
+ if (!relativePath.ok) {
1793
+ json(res, 400, { error: relativePath.error });
1794
+ return true;
1795
+ }
1796
+ try {
1797
+ const build = updateWorkshopBuild(workshopBuildStoreRoot(), buildId, {
1798
+ ...(draft ? { draft } : {}),
1799
+ ...(projectRoot.present
1800
+ ? { projectRoot: projectRoot.value }
1801
+ : {}),
1802
+ ...(relativePath.present
1803
+ ? { relativePath: relativePath.value }
1804
+ : {}),
1805
+ });
1806
+ json(res, 200, { build });
1807
+ }
1808
+ catch (err) {
1809
+ if (err instanceof WorkshopBuildStoreError &&
1810
+ err.code === "workshop_build_not_found") {
1811
+ json(res, 404, {
1812
+ error: err.message,
1813
+ code: err.code,
1814
+ buildId: err.buildId,
1815
+ });
1816
+ return true;
1817
+ }
1818
+ throw err;
1819
+ }
1820
+ return true;
619
1821
  }
620
- catch {
621
- json(res, 400, { error: "Invalid JSON body" });
1822
+ if (method === "GET" &&
1823
+ pathname === "/api/workshop/sessions") {
1824
+ const sessions = listWorkshopSessions(workshopSessionStoreRoot());
1825
+ json(res, 200, { sessions });
622
1826
  return true;
623
1827
  }
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",
1828
+ if (method === "POST" &&
1829
+ pathname === "/api/workshop/sessions") {
1830
+ let body = {};
1831
+ try {
1832
+ body = await readJsonBody(req);
1833
+ }
1834
+ catch {
1835
+ json(res, 400, { error: "Invalid JSON body" });
1836
+ return true;
1837
+ }
1838
+ if (!isPlainObject(body)) {
1839
+ json(res, 400, { error: "Invalid JSON body" });
1840
+ return true;
1841
+ }
1842
+ const record = createWorkshopSession(workshopSessionStoreRoot(), {
1843
+ ...(typeof body.id === "string" ? { id: body.id } : {}),
632
1844
  });
1845
+ json(res, 201, { session: record });
633
1846
  return true;
634
1847
  }
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 });
1848
+ const workshopSessionGetMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
1849
+ if (method === "GET" && workshopSessionGetMatch) {
1850
+ const sessionId = decodeURIComponent(workshopSessionGetMatch[1] ?? "");
1851
+ try {
1852
+ const session = getWorkshopSession(workshopSessionStoreRoot(), sessionId);
1853
+ json(res, 200, { session });
1854
+ }
1855
+ catch (err) {
1856
+ if (err instanceof WorkshopSessionStoreError &&
1857
+ err.code === "workshop_session_not_found") {
1858
+ json(res, 404, {
1859
+ error: err.message,
1860
+ code: err.code,
1861
+ sessionId: err.sessionId,
1862
+ });
1863
+ return true;
1864
+ }
1865
+ throw err;
1866
+ }
1867
+ return true;
1868
+ }
1869
+ const workshopSessionPatchMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
1870
+ if (method === "PATCH" && workshopSessionPatchMatch) {
1871
+ const sessionId = decodeURIComponent(workshopSessionPatchMatch[1] ?? "");
1872
+ let body = {};
1873
+ try {
1874
+ body = await readJsonBody(req);
1875
+ }
1876
+ catch {
1877
+ json(res, 400, { error: "Invalid JSON body" });
1878
+ return true;
1879
+ }
1880
+ if (!isPlainObject(body)) {
1881
+ json(res, 400, { error: "Invalid JSON body" });
1882
+ return true;
1883
+ }
1884
+ if (!Object.prototype.hasOwnProperty.call(body, "activeBuildId")) {
1885
+ json(res, 400, { error: "activeBuildId is required" });
1886
+ return true;
1887
+ }
1888
+ const activeBuildId = body.activeBuildId;
1889
+ if (activeBuildId !== null && typeof activeBuildId !== "string") {
1890
+ json(res, 400, { error: "activeBuildId must be a string or null" });
640
1891
  return true;
641
1892
  }
642
- health = manager.setMaxConcurrent(n);
1893
+ try {
1894
+ const session = updateWorkshopSessionActiveBuildId(workshopSessionStoreRoot(), sessionId, activeBuildId);
1895
+ json(res, 200, { session });
1896
+ }
1897
+ catch (err) {
1898
+ if (err instanceof WorkshopSessionStoreError &&
1899
+ err.code === "workshop_session_not_found") {
1900
+ json(res, 404, {
1901
+ error: err.message,
1902
+ code: err.code,
1903
+ sessionId: err.sessionId,
1904
+ });
1905
+ return true;
1906
+ }
1907
+ throw err;
1908
+ }
1909
+ return true;
643
1910
  }
644
- let credential = getCredentialSourceSettings(cwd);
645
- if (hasCredentialSource) {
1911
+ const workshopSessionUndoMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/undo$/);
1912
+ if (method === "POST" && workshopSessionUndoMatch) {
1913
+ const sessionId = decodeURIComponent(workshopSessionUndoMatch[1] ?? "");
1914
+ let body = {};
646
1915
  try {
647
- credential = setCredentialSource(cwd, record.credentialSource);
1916
+ body = await readJsonBody(req);
1917
+ }
1918
+ catch {
1919
+ json(res, 400, { error: "Invalid JSON body" });
1920
+ return true;
1921
+ }
1922
+ if (!isPlainObject(body)) {
1923
+ json(res, 400, { error: "Invalid JSON body" });
1924
+ return true;
1925
+ }
1926
+ const draftParsed = parseDraftPackageBody(body);
1927
+ if ("ok" in draftParsed) {
1928
+ json(res, draftParsed.status, { error: draftParsed.error });
1929
+ return true;
1930
+ }
1931
+ try {
1932
+ const result = await undoWorkshopSessionMutation({
1933
+ sessionId,
1934
+ draft: draftParsed,
1935
+ ...(typeof body.mutationId === "string"
1936
+ ? { mutationId: body.mutationId }
1937
+ : {}),
1938
+ registry: workshopChatRegistry,
1939
+ storeRoot: workshopSessionStoreRoot(),
1940
+ });
1941
+ json(res, result.ok ? 200 : 409, result);
648
1942
  }
649
1943
  catch (err) {
650
- const mapped = mapProviderAuthError(err);
651
- json(res, mapped.status, mapped.body);
1944
+ if (err instanceof WorkshopSessionStoreError &&
1945
+ err.code === "workshop_session_not_found") {
1946
+ json(res, 404, {
1947
+ error: err.message,
1948
+ code: err.code,
1949
+ sessionId: err.sessionId,
1950
+ });
1951
+ return true;
1952
+ }
1953
+ json(res, 400, {
1954
+ error: err instanceof Error ? err.message : String(err),
1955
+ });
1956
+ }
1957
+ return true;
1958
+ }
1959
+ const workshopSessionAcceptMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/accept$/);
1960
+ if (method === "POST" && workshopSessionAcceptMatch) {
1961
+ const sessionId = decodeURIComponent(workshopSessionAcceptMatch[1] ?? "");
1962
+ let body = {};
1963
+ try {
1964
+ body = await readJsonBody(req);
1965
+ }
1966
+ catch {
1967
+ json(res, 400, { error: "Invalid JSON body" });
652
1968
  return true;
653
1969
  }
1970
+ if (!isPlainObject(body)) {
1971
+ json(res, 400, { error: "Invalid JSON body" });
1972
+ return true;
1973
+ }
1974
+ const draftParsed = parseDraftPackageBody(body);
1975
+ if ("ok" in draftParsed) {
1976
+ json(res, draftParsed.status, { error: draftParsed.error });
1977
+ return true;
1978
+ }
1979
+ try {
1980
+ const result = await acceptWorkshopSessionMutation({
1981
+ sessionId,
1982
+ draft: draftParsed,
1983
+ ...(typeof body.mutationId === "string"
1984
+ ? { mutationId: body.mutationId }
1985
+ : {}),
1986
+ registry: workshopChatRegistry,
1987
+ storeRoot: workshopSessionStoreRoot(),
1988
+ });
1989
+ json(res, result.ok ? 200 : 409, result);
1990
+ }
1991
+ catch (err) {
1992
+ if (err instanceof WorkshopSessionStoreError &&
1993
+ err.code === "workshop_session_not_found") {
1994
+ json(res, 404, {
1995
+ error: err.message,
1996
+ code: err.code,
1997
+ sessionId: err.sessionId,
1998
+ });
1999
+ return true;
2000
+ }
2001
+ json(res, 400, {
2002
+ error: err instanceof Error ? err.message : String(err),
2003
+ });
2004
+ }
2005
+ return true;
654
2006
  }
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)
2007
+ if (method === "POST" && pathname === "/api/workshop/chat/stop") {
2008
+ let body;
2009
+ try {
2010
+ body = await readJsonBody(req);
2011
+ }
2012
+ catch {
2013
+ json(res, 400, { error: "Invalid JSON body" });
2014
+ return true;
2015
+ }
2016
+ if (!isPlainObject(body)) {
2017
+ json(res, 400, { error: "Invalid JSON body" });
2018
+ return true;
2019
+ }
2020
+ if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
2021
+ json(res, 400, { error: "sessionId is required" });
2022
+ return true;
2023
+ }
2024
+ const stopped = await stopWorkshopChatTurn(workshopChatRegistry, body.sessionId);
2025
+ json(res, 200, { ok: true, ...stopped });
664
2026
  return true;
665
- json(res, 404, {
666
- error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
667
- });
2027
+ }
2028
+ if (method === "POST" && pathname === "/api/workshop/chat") {
2029
+ let body;
2030
+ try {
2031
+ body = await readJsonBody(req);
2032
+ }
2033
+ catch {
2034
+ json(res, 400, { error: "Invalid JSON body" });
2035
+ return true;
2036
+ }
2037
+ if (!isPlainObject(body)) {
2038
+ json(res, 400, { error: "Invalid JSON body" });
2039
+ return true;
2040
+ }
2041
+ if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
2042
+ json(res, 400, { error: "sessionId is required" });
2043
+ return true;
2044
+ }
2045
+ if (typeof body.message !== "string" || !body.message.trim()) {
2046
+ json(res, 400, { error: "message is required" });
2047
+ return true;
2048
+ }
2049
+ const sessionId = body.sessionId;
2050
+ const draftParsed = parseDraftPackageBody(body);
2051
+ if ("ok" in draftParsed) {
2052
+ json(res, draftParsed.status, { error: draftParsed.error });
2053
+ return true;
2054
+ }
2055
+ const settingsDefault = await readWorkshopOpenModel(cwd);
2056
+ const accept = String(req.headers.accept ?? "");
2057
+ const wantsStream = body.stream === true ||
2058
+ accept.includes("application/x-ndjson") ||
2059
+ accept.includes("text/event-stream");
2060
+ const chatTurnBase = {
2061
+ sessionId: body.sessionId,
2062
+ draft: draftParsed,
2063
+ message: body.message,
2064
+ autoApply: body.autoApply === true,
2065
+ model: typeof body.model === "string" || body.model === null
2066
+ ? body.model
2067
+ : undefined,
2068
+ settingsDefault: settingsDefault ?? null,
2069
+ registry: workshopChatRegistry,
2070
+ storeRoot: workshopSessionStoreRoot(),
2071
+ };
2072
+ // Fail closed on missing sessions before opening an NDJSON body.
2073
+ try {
2074
+ getWorkshopSession(workshopSessionStoreRoot(), body.sessionId);
2075
+ }
2076
+ catch (err) {
2077
+ if (err instanceof WorkshopSessionStoreError &&
2078
+ err.code === "workshop_session_not_found") {
2079
+ json(res, 404, {
2080
+ error: err.message,
2081
+ code: err.code,
2082
+ sessionId: err.sessionId,
2083
+ });
2084
+ return true;
2085
+ }
2086
+ throw err;
2087
+ }
2088
+ if (wantsStream) {
2089
+ res.writeHead(200, {
2090
+ "Content-Type": "application/x-ndjson; charset=utf-8",
2091
+ "Cache-Control": "no-store",
2092
+ });
2093
+ const resolvedModel = resolveWorkshopModel({
2094
+ sessionOverride: typeof body.model === "string" || body.model === null
2095
+ ? body.model
2096
+ : undefined,
2097
+ settingsDefault: settingsDefault ?? null,
2098
+ });
2099
+ const writeFrame = async (frame) => {
2100
+ if (req.aborted || res.writableEnded || res.destroyed)
2101
+ return;
2102
+ const line = `${JSON.stringify(frame)}\n`;
2103
+ if (!res.write(line)) {
2104
+ await new Promise((resolve) => {
2105
+ const onDrain = () => {
2106
+ cleanup();
2107
+ resolve();
2108
+ };
2109
+ const onAbort = () => {
2110
+ cleanup();
2111
+ if (!res.writableEnded && !res.destroyed) {
2112
+ res.destroy();
2113
+ }
2114
+ resolve();
2115
+ };
2116
+ const cleanup = () => {
2117
+ res.off("drain", onDrain);
2118
+ req.off("aborted", onAbort);
2119
+ req.off("close", onAbort);
2120
+ res.off("close", onAbort);
2121
+ res.off("error", onAbort);
2122
+ };
2123
+ res.once("drain", onDrain);
2124
+ req.once("aborted", onAbort);
2125
+ req.once("close", onAbort);
2126
+ res.once("close", onAbort);
2127
+ res.once("error", onAbort);
2128
+ if (req.aborted || res.writableEnded || res.destroyed) {
2129
+ onAbort();
2130
+ }
2131
+ });
2132
+ }
2133
+ };
2134
+ let streamedDelta = false;
2135
+ let writeChain = Promise.resolve();
2136
+ let turnFinished = false;
2137
+ const enqueueFrame = (frame) => {
2138
+ writeChain = writeChain.then(() => writeFrame(frame));
2139
+ };
2140
+ const stopIfClientLeft = () => {
2141
+ if (turnFinished || res.writableEnded)
2142
+ return;
2143
+ void workshopChatRegistry.abortTurn(sessionId);
2144
+ };
2145
+ res.once("close", stopIfClientLeft);
2146
+ try {
2147
+ const turn = await runWorkshopChatTurn({
2148
+ ...chatTurnBase,
2149
+ onDelta: (text) => {
2150
+ streamedDelta = true;
2151
+ enqueueFrame({ type: "delta", text });
2152
+ },
2153
+ onActivity: (update) => {
2154
+ enqueueFrame({ type: "activity", ...update });
2155
+ },
2156
+ onPointerChange: (frame) => {
2157
+ enqueueFrame({
2158
+ type: "pointer-change",
2159
+ buildId: frame.buildId,
2160
+ draft: frame.draft,
2161
+ });
2162
+ },
2163
+ });
2164
+ turnFinished = true;
2165
+ await writeChain;
2166
+ for (const frame of iterateWorkshopChatStreamFrames(turn, {
2167
+ chunkAssistantText: !streamedDelta,
2168
+ })) {
2169
+ await writeFrame(frame);
2170
+ }
2171
+ if (!res.writableEnded && !res.destroyed) {
2172
+ res.end();
2173
+ }
2174
+ }
2175
+ catch (err) {
2176
+ turnFinished = true;
2177
+ await writeChain.catch(() => undefined);
2178
+ const message = err instanceof Error ? err.message : String(err);
2179
+ const errorEvent = { type: "error", message };
2180
+ await writeFrame({
2181
+ type: "event",
2182
+ event: errorEvent,
2183
+ });
2184
+ await writeFrame({
2185
+ type: "done",
2186
+ sessionId: body.sessionId,
2187
+ events: [errorEvent],
2188
+ draft: draftParsed,
2189
+ pending: null,
2190
+ autoApply: false,
2191
+ model: resolvedModel,
2192
+ buildId: workshopChatRegistry.getPinnedBuildId(body.sessionId),
2193
+ });
2194
+ if (!res.writableEnded && !res.destroyed) {
2195
+ res.end();
2196
+ }
2197
+ }
2198
+ return true;
2199
+ }
2200
+ let turn;
2201
+ try {
2202
+ turn = await runWorkshopChatTurn(chatTurnBase);
2203
+ }
2204
+ catch (err) {
2205
+ if (err instanceof WorkshopSessionStoreError &&
2206
+ err.code === "workshop_session_not_found") {
2207
+ json(res, 404, {
2208
+ error: err.message,
2209
+ code: err.code,
2210
+ sessionId: err.sessionId,
2211
+ });
2212
+ return true;
2213
+ }
2214
+ json(res, 400, {
2215
+ error: err instanceof Error ? err.message : String(err),
2216
+ });
2217
+ return true;
2218
+ }
2219
+ json(res, 200, turn);
2220
+ return true;
2221
+ }
2222
+ if (method === "POST" &&
2223
+ pathname === "/api/workshop/disk-change") {
2224
+ let body;
2225
+ try {
2226
+ body = await readJsonBody(req);
2227
+ }
2228
+ catch {
2229
+ json(res, 400, { error: "Invalid JSON body" });
2230
+ return true;
2231
+ }
2232
+ if (!isPlainObject(body)) {
2233
+ json(res, 400, { error: "Invalid JSON body" });
2234
+ return true;
2235
+ }
2236
+ if (typeof body.pipelinePath !== "string" || !body.pipelinePath.trim()) {
2237
+ json(res, 400, { error: "pipelinePath is required" });
2238
+ return true;
2239
+ }
2240
+ const draftParsed = parseDraftPackageBody(body);
2241
+ if ("ok" in draftParsed) {
2242
+ json(res, draftParsed.status, { error: draftParsed.error });
2243
+ return true;
2244
+ }
2245
+ const writeRoot = typeof body.project_root === "string"
2246
+ ? body.project_root
2247
+ : undefined;
2248
+ let projectRoot;
2249
+ try {
2250
+ const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
2251
+ const ctx = await resolveStageflowContext(wireRoot.path);
2252
+ projectRoot = ctx.projectRoot;
2253
+ }
2254
+ catch (err) {
2255
+ if (err instanceof CatalogPathError) {
2256
+ json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
2257
+ return true;
2258
+ }
2259
+ throw err;
2260
+ }
2261
+ const relativePaths = draftPackageDiskRelativePaths({
2262
+ pipelinePath: body.pipelinePath,
2263
+ draft: draftParsed,
2264
+ taskPath: typeof body.taskPath === "string" ? body.taskPath : null,
2265
+ });
2266
+ const current = fingerprintPackageFiles(projectRoot, relativePaths);
2267
+ const baseline = isPlainObject(body.baseline) &&
2268
+ Object.values(body.baseline).every((v) => typeof v === "string")
2269
+ ? body.baseline
2270
+ : null;
2271
+ const detection = detectDiskChange({ baseline, current });
2272
+ json(res, 200, {
2273
+ fingerprints: current,
2274
+ changed: detection.changed,
2275
+ changedPaths: detection.changedPaths,
2276
+ });
2277
+ return true;
2278
+ }
2279
+ if (method === "GET" && pathname === "/api/models") {
2280
+ const filter = url.searchParams.get("project_root") ?? undefined;
2281
+ const result = await listModelsMultiProject({
2282
+ store,
2283
+ bootCwd: cwd,
2284
+ projectRootFilter: filter,
2285
+ });
2286
+ if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
2287
+ result.items.length === 0) {
2288
+ json(res, 400, {
2289
+ error: result.root_errors[0].message,
2290
+ code: "unknown_project_root",
2291
+ root_errors: result.root_errors,
2292
+ });
2293
+ return true;
2294
+ }
2295
+ json(res, 200, {
2296
+ models: result.models,
2297
+ entries: result.items,
2298
+ root_errors: result.root_errors,
2299
+ });
2300
+ return true;
2301
+ }
2302
+ if (await handleProviderRoutes(req, res, {
2303
+ cwd,
2304
+ readJsonBody,
2305
+ json,
2306
+ providerAuthContext,
2307
+ })) {
2308
+ return true;
2309
+ }
2310
+ if (await handleProjectMcpRoutes(req, res, {
2311
+ projectRoot: rootDir,
2312
+ json,
2313
+ })) {
2314
+ return true;
2315
+ }
2316
+ if (await handleTriggerRoutes(req, res, {
2317
+ cwd,
2318
+ manager,
2319
+ store,
2320
+ json,
2321
+ readJsonBody,
2322
+ readRawBody,
2323
+ auditLog,
2324
+ })) {
2325
+ return true;
2326
+ }
2327
+ if (method === "GET" && pathname === "/api/skills") {
2328
+ json(res, 200, await listSkills({ cwd, agentDir }));
2329
+ return true;
2330
+ }
2331
+ if (method === "GET" && pathname === "/api/extensions") {
2332
+ json(res, 200, await listExtensions({ cwd, agentDir }));
2333
+ return true;
2334
+ }
2335
+ if (method === "GET" && pathname === "/api/health") {
2336
+ await handleApiHealth(req, res, boot);
2337
+ return true;
2338
+ }
2339
+ if (method === "GET" && pathname === "/api/settings") {
2340
+ const health = manager.getHealth();
2341
+ const credential = getCredentialSourceSettings(cwd);
2342
+ const factory = readFactorySettings(cwd);
2343
+ const defaultModel = await readWorkshopOpenModel(cwd);
2344
+ json(res, 200, {
2345
+ maxConcurrent: health.maxConcurrent,
2346
+ ...credential,
2347
+ ...(factory.workshopModel !== undefined
2348
+ ? { workshopModel: factory.workshopModel }
2349
+ : {}),
2350
+ defaultModel,
2351
+ });
2352
+ return true;
2353
+ }
2354
+ if (method === "POST" && pathname === "/api/settings") {
2355
+ let body;
2356
+ try {
2357
+ body = await readJsonBody(req);
2358
+ }
2359
+ catch {
2360
+ json(res, 400, { error: "Invalid JSON body" });
2361
+ return true;
2362
+ }
2363
+ const record = body !== null && typeof body === "object" && !Array.isArray(body)
2364
+ ? body
2365
+ : {};
2366
+ const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
2367
+ const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
2368
+ const hasWorkshopModel = Object.prototype.hasOwnProperty.call(record, "workshopModel");
2369
+ if (!hasMax && !hasCredentialSource && !hasWorkshopModel) {
2370
+ json(res, 400, {
2371
+ error: "maxConcurrent, credentialSource, or workshopModel is required",
2372
+ });
2373
+ return true;
2374
+ }
2375
+ let health = manager.getHealth();
2376
+ if (hasMax) {
2377
+ const n = parseSlotCount(record.maxConcurrent);
2378
+ if (n === undefined) {
2379
+ json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
2380
+ return true;
2381
+ }
2382
+ health = manager.setMaxConcurrent(n);
2383
+ }
2384
+ let credential = getCredentialSourceSettings(cwd);
2385
+ if (hasCredentialSource) {
2386
+ try {
2387
+ credential = setCredentialSource(cwd, record.credentialSource);
2388
+ }
2389
+ catch (err) {
2390
+ const mapped = mapProviderAuthError(err);
2391
+ json(res, mapped.status, mapped.body);
2392
+ return true;
2393
+ }
2394
+ }
2395
+ let workshopModel = readFactorySettings(cwd).workshopModel;
2396
+ if (hasWorkshopModel) {
2397
+ const parsed = parseWorkshopModelSetting(record.workshopModel);
2398
+ if (parsed === undefined) {
2399
+ json(res, 400, { error: INVALID_WORKSHOP_MODEL_MESSAGE });
2400
+ return true;
2401
+ }
2402
+ writeFactorySettings(cwd, { workshopModel: parsed });
2403
+ workshopModel = parsed;
2404
+ }
2405
+ json(res, 200, {
2406
+ ...health,
2407
+ ...credential,
2408
+ ...(workshopModel !== undefined ? { workshopModel } : {}),
2409
+ defaultModel: await readWorkshopOpenModel(cwd),
2410
+ });
2411
+ return true;
2412
+ }
2413
+ if (method === "GET" && uiDistDir !== undefined) {
2414
+ const served = await serveStatic(res, uiDistDir, pathname);
2415
+ if (served)
2416
+ return true;
2417
+ json(res, 404, {
2418
+ error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
2419
+ });
2420
+ return true;
2421
+ }
2422
+ json(res, 404, { error: "Not found" });
668
2423
  return true;
669
2424
  }
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
- });
2425
+ catch (err) {
2426
+ if (!res.headersSent) {
2427
+ json(res, 500, {
2428
+ error: err instanceof Error ? err.message : String(err),
2429
+ });
2430
+ }
2431
+ return true;
678
2432
  }
679
- return true;
680
2433
  }
681
2434
  };
682
2435
  }
@@ -685,21 +2438,45 @@ export async function startUiServer(options) {
685
2438
  const port = options.port ?? DEFAULT_PORT;
686
2439
  const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
687
2440
  const boot = await bootstrapStageflowHost(options);
688
- const { manager, store, cwd, agentDir, rootDir } = boot;
2441
+ const { cwd, agentDir, rootDir } = boot;
689
2442
  const providerAuthContext = boot.providerAuthContext;
690
- return createHttpHost({
691
- boot,
692
- host,
693
- port,
694
- routes: createOperatorRoutes({
695
- manager,
696
- store,
2443
+ const allowedHosts = options.allowedHosts ?? resolveAllowedHosts();
2444
+ const controlTokens = options.controlTokens ?? loadControlTokens();
2445
+ let shutdown;
2446
+ const routes = boot.serveBlocked !== undefined ||
2447
+ boot.manager === undefined ||
2448
+ boot.store === undefined
2449
+ ? async () => false
2450
+ : createOperatorRoutes({
2451
+ manager: boot.manager,
2452
+ store: boot.store,
697
2453
  cwd,
698
2454
  agentDir,
699
2455
  rootDir,
700
2456
  providerAuthContext,
701
2457
  uiDistDir,
702
- }),
2458
+ allowedHosts,
2459
+ controlTokens,
2460
+ getShutdown: () => shutdown,
2461
+ ...(options.workshopOperatorHost
2462
+ ? { workshopOperatorHost: options.workshopOperatorHost }
2463
+ : {}),
2464
+ });
2465
+ const envelope = await createHttpHost({
2466
+ boot,
2467
+ host,
2468
+ port,
2469
+ allowedHosts,
2470
+ controlTokens,
2471
+ routes,
2472
+ });
2473
+ shutdown = installShutdownController({
2474
+ server: envelope.server,
2475
+ host: makeDrainableHostFromOptional(envelope.manager, envelope.store),
2476
+ });
2477
+ envelope.server.on("close", () => {
2478
+ shutdown?.uninstall();
703
2479
  });
2480
+ return { ...envelope, shutdown };
704
2481
  }
705
2482
  export { DEFAULT_PORT };