stageflow 0.27.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 (261) hide show
  1. package/README.md +4 -0
  2. package/builtin-skills/browser/SKILL.md +42 -0
  3. package/dist/agent/claudeAdapter.js +2 -2
  4. package/dist/agent/cursorProvider.d.ts +2 -0
  5. package/dist/agent/cursorProvider.js +30 -5
  6. package/dist/agent/fakeAgent.d.ts +1 -0
  7. package/dist/agent/fakeAgent.js +13 -0
  8. package/dist/agent/piAdapter.d.ts +3 -24
  9. package/dist/agent/piAdapter.js +49 -47
  10. package/dist/agent/piSessionFactory.d.ts +38 -0
  11. package/dist/agent/piSessionFactory.js +60 -0
  12. package/dist/agent/port.d.ts +2 -0
  13. package/dist/agent/streamLogRedact.d.ts +6 -0
  14. package/dist/agent/streamLogRedact.js +23 -1
  15. package/dist/browser/anchor.d.ts +27 -0
  16. package/dist/browser/anchor.js +127 -0
  17. package/dist/browser/auditSink.d.ts +38 -0
  18. package/dist/browser/auditSink.js +34 -0
  19. package/dist/browser/browserHost.d.ts +86 -0
  20. package/dist/browser/browserHost.js +7 -0
  21. package/dist/browser/browserSweep.d.ts +14 -0
  22. package/dist/browser/browserSweep.js +54 -0
  23. package/dist/browser/browserTeardown.d.ts +56 -0
  24. package/dist/browser/browserTeardown.js +235 -0
  25. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  26. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  27. package/dist/browser/gateHandoff.d.ts +25 -0
  28. package/dist/browser/gateHandoff.js +83 -0
  29. package/dist/browser/humanLogin.d.ts +18 -0
  30. package/dist/browser/humanLogin.js +34 -0
  31. package/dist/browser/keyProvider.d.ts +10 -0
  32. package/dist/browser/keyProvider.js +39 -0
  33. package/dist/browser/localBrowserHost.d.ts +10 -0
  34. package/dist/browser/localBrowserHost.js +152 -0
  35. package/dist/browser/localProfileLock.d.ts +2 -0
  36. package/dist/browser/localProfileLock.js +218 -0
  37. package/dist/browser/localProfileStore.d.ts +5 -0
  38. package/dist/browser/localProfileStore.js +91 -0
  39. package/dist/browser/loginCheck.d.ts +53 -0
  40. package/dist/browser/loginCheck.js +147 -0
  41. package/dist/browser/memoryProfileLock.d.ts +2 -0
  42. package/dist/browser/memoryProfileLock.js +53 -0
  43. package/dist/browser/memoryProfileStore.d.ts +2 -0
  44. package/dist/browser/memoryProfileStore.js +31 -0
  45. package/dist/browser/navigationAudit.d.ts +16 -0
  46. package/dist/browser/navigationAudit.js +53 -0
  47. package/dist/browser/persistedEnv.d.ts +3 -0
  48. package/dist/browser/persistedEnv.js +15 -0
  49. package/dist/browser/profileLock.d.ts +40 -0
  50. package/dist/browser/profileLock.js +10 -0
  51. package/dist/browser/profileStore.d.ts +23 -0
  52. package/dist/browser/profileStore.js +39 -0
  53. package/dist/browser/runLiveness.d.ts +13 -0
  54. package/dist/browser/runLiveness.js +39 -0
  55. package/dist/browser/sitePolicy.d.ts +10 -0
  56. package/dist/browser/sitePolicy.js +61 -0
  57. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  58. package/dist/browser/stageBrowserEnv.js +147 -0
  59. package/dist/browser/stageProfileLock.d.ts +17 -0
  60. package/dist/browser/stageProfileLock.js +39 -0
  61. package/dist/cli/browserCommand.d.ts +44 -0
  62. package/dist/cli/browserCommand.js +472 -0
  63. package/dist/cli/hostClient.d.ts +20 -0
  64. package/dist/cli/hostClient.js +36 -2
  65. package/dist/cli/initCommand.d.ts +1 -0
  66. package/dist/cli/initCommand.js +23 -7
  67. package/dist/cli/initTemplates.d.ts +5 -2
  68. package/dist/cli/initTemplates.js +25 -2
  69. package/dist/cli/runsCommand.js +6 -2
  70. package/dist/cli/triggerCommand.d.ts +12 -0
  71. package/dist/cli/triggerCommand.js +183 -0
  72. package/dist/cli/validateCommand.d.ts +1 -1
  73. package/dist/cli/validateCommand.js +23 -6
  74. package/dist/cli.js +31 -2
  75. package/dist/config/browseCatalog.d.ts +1 -0
  76. package/dist/config/browseCatalog.js +5 -4
  77. package/dist/config/builtinSkills.d.ts +4 -0
  78. package/dist/config/builtinSkills.js +23 -0
  79. package/dist/config/catalogRelativePath.d.ts +1 -0
  80. package/dist/config/catalogRelativePath.js +10 -2
  81. package/dist/config/createTrigger.d.ts +27 -0
  82. package/dist/config/createTrigger.js +232 -0
  83. package/dist/config/draftPackage.d.ts +117 -0
  84. package/dist/config/draftPackage.js +550 -0
  85. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  86. package/dist/config/ensureCatalogScanRoot.js +73 -0
  87. package/dist/config/hostConfig.d.ts +2 -0
  88. package/dist/config/hostConfig.js +27 -0
  89. package/dist/config/listSkills.d.ts +2 -2
  90. package/dist/config/listSkills.js +17 -3
  91. package/dist/config/loadOutcome.d.ts +6 -0
  92. package/dist/config/loadPipeline.js +16 -0
  93. package/dist/config/loadStage.js +7 -0
  94. package/dist/config/loadStageflowManifest.js +25 -0
  95. package/dist/config/loadTrigger.d.ts +9 -0
  96. package/dist/config/loadTrigger.js +120 -0
  97. package/dist/config/multiProjectCatalog.d.ts +2 -0
  98. package/dist/config/multiProjectCatalog.js +26 -18
  99. package/dist/config/normalizePipelineStageEntry.js +8 -1
  100. package/dist/config/parseCompletionContract.js +6 -0
  101. package/dist/config/pipelineStageKeys.js +1 -0
  102. package/dist/config/publishDraftPackage.d.ts +7 -0
  103. package/dist/config/publishDraftPackage.js +25 -0
  104. package/dist/config/resolveCatalogContext.js +1 -1
  105. package/dist/config/resolvePipelineDag.js +1 -1
  106. package/dist/config/scanCatalogPaths.d.ts +1 -1
  107. package/dist/config/scanCatalogPaths.js +6 -2
  108. package/dist/config/stageBrowser.d.ts +12 -0
  109. package/dist/config/stageBrowser.js +118 -0
  110. package/dist/config/triggerCatalog.d.ts +10 -0
  111. package/dist/config/triggerCatalog.js +37 -0
  112. package/dist/config/validateCatalog.d.ts +13 -3
  113. package/dist/config/validateCatalog.js +83 -2
  114. package/dist/config/validateCompletionContract.js +8 -0
  115. package/dist/envelope/preEmitChecks.d.ts +1 -1
  116. package/dist/envelope/preEmitChecks.js +8 -0
  117. package/dist/logging/redact.d.ts +4 -0
  118. package/dist/logging/redact.js +17 -0
  119. package/dist/mcp/catalogTools.js +43 -8
  120. package/dist/mcp/controlTools.js +3 -1
  121. package/dist/mcp/deps.d.ts +2 -0
  122. package/dist/mcp/server.js +1 -0
  123. package/dist/mcp/tools.js +2 -0
  124. package/dist/mcp/triggerTools.d.ts +3 -0
  125. package/dist/mcp/triggerTools.js +176 -0
  126. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  127. package/dist/operatorAgent/docsRetrieval.js +269 -0
  128. package/dist/operatorAgent/draftContext.d.ts +16 -0
  129. package/dist/operatorAgent/draftContext.js +68 -0
  130. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  131. package/dist/operatorAgent/fakeHost.js +92 -0
  132. package/dist/operatorAgent/host.d.ts +40 -0
  133. package/dist/operatorAgent/host.js +179 -0
  134. package/dist/operatorAgent/index.d.ts +9 -0
  135. package/dist/operatorAgent/index.js +8 -0
  136. package/dist/operatorAgent/piModel.d.ts +80 -0
  137. package/dist/operatorAgent/piModel.js +528 -0
  138. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  139. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  140. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  141. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  142. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  143. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  144. package/dist/operatorAgent/proposals.d.ts +32 -0
  145. package/dist/operatorAgent/proposals.js +144 -0
  146. package/dist/operatorAgent/types.d.ts +128 -0
  147. package/dist/operatorAgent/types.js +1 -0
  148. package/dist/package-meta.d.ts +1 -1
  149. package/dist/package-meta.js +1 -1
  150. package/dist/project/findManifestRoot.d.ts +3 -0
  151. package/dist/project/findManifestRoot.js +35 -0
  152. package/dist/project/resolveStageflowContext.d.ts +7 -0
  153. package/dist/project/resolveStageflowContext.js +44 -28
  154. package/dist/runstore/assertTmpdir.js +6 -2
  155. package/dist/runstore/port.d.ts +28 -0
  156. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  157. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  158. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  159. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  160. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  161. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  162. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  163. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  164. package/dist/runstore/sqlite/migrations/index.js +6 -0
  165. package/dist/runtime/answerResume.d.ts +1 -0
  166. package/dist/runtime/answerResume.js +3 -1
  167. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  168. package/dist/runtime/completionCheckRunner.js +22 -0
  169. package/dist/runtime/credentialBinding.d.ts +2 -0
  170. package/dist/runtime/credentialBinding.js +3 -0
  171. package/dist/runtime/emailSource.d.ts +72 -0
  172. package/dist/runtime/emailSource.js +304 -0
  173. package/dist/runtime/githubPollSource.d.ts +86 -0
  174. package/dist/runtime/githubPollSource.js +284 -0
  175. package/dist/runtime/pipelineRunner.d.ts +4 -0
  176. package/dist/runtime/pipelineRunner.js +3 -0
  177. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  178. package/dist/runtime/pipelineScheduler.js +85 -1
  179. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  180. package/dist/runtime/runManager.d.ts +11 -0
  181. package/dist/runtime/runManager.js +149 -1
  182. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  183. package/dist/runtime/runRetryCoordinator.js +1 -0
  184. package/dist/runtime/scheduleSource.d.ts +38 -0
  185. package/dist/runtime/scheduleSource.js +119 -0
  186. package/dist/runtime/settingsFile.d.ts +5 -0
  187. package/dist/runtime/settingsFile.js +61 -54
  188. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  189. package/dist/runtime/stageEnvironment.js +1 -0
  190. package/dist/runtime/stageHitl.d.ts +1 -0
  191. package/dist/runtime/stageHitl.js +5 -1
  192. package/dist/runtime/stageProcessLauncher.d.ts +2 -0
  193. package/dist/runtime/stageProcessLauncher.js +44 -5
  194. package/dist/runtime/stageRunner.d.ts +5 -0
  195. package/dist/runtime/stageRunner.js +30 -6
  196. package/dist/runtime/stageStreamLog.d.ts +1 -0
  197. package/dist/runtime/stageStreamLog.js +2 -1
  198. package/dist/runtime/triggerCatalog.d.ts +6 -0
  199. package/dist/runtime/triggerCatalog.js +24 -0
  200. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  201. package/dist/runtime/triggerEventMatch.js +6 -0
  202. package/dist/runtime/triggerPort.d.ts +14 -0
  203. package/dist/runtime/triggerPort.js +1 -0
  204. package/dist/runtime/triggerRunner.d.ts +17 -0
  205. package/dist/runtime/triggerRunner.js +89 -0
  206. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  207. package/dist/runtime/verifiedStageExecution.js +14 -0
  208. package/dist/runtime/webhookSignature.d.ts +1 -0
  209. package/dist/runtime/webhookSignature.js +12 -0
  210. package/dist/server/bootstrap.d.ts +6 -0
  211. package/dist/server/bootstrap.js +64 -0
  212. package/dist/server/controlToken.js +13 -0
  213. package/dist/server/createHttpHost.js +3 -0
  214. package/dist/server/healthSurfaces.js +31 -6
  215. package/dist/server/http.d.ts +15 -0
  216. package/dist/server/http.js +1251 -74
  217. package/dist/server/requestAuthContext.d.ts +1 -1
  218. package/dist/server/triggerRoutes.d.ts +15 -0
  219. package/dist/server/triggerRoutes.js +268 -0
  220. package/dist/tools/askOperator.d.ts +3 -2
  221. package/dist/types/completion.d.ts +3 -0
  222. package/dist/types/pipeline.d.ts +3 -1
  223. package/dist/types/preEmitCheck.d.ts +7 -0
  224. package/dist/types/stage.d.ts +14 -0
  225. package/dist/types/stageflowManifest.d.ts +3 -0
  226. package/dist/types/trigger.d.ts +18 -0
  227. package/dist/types/trigger.js +1 -0
  228. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  229. package/dist/ui/assets/index-CntB4bZR.js +133 -0
  230. package/dist/ui/index.html +2 -2
  231. package/dist/workshop/autosave.d.ts +58 -0
  232. package/dist/workshop/autosave.js +222 -0
  233. package/dist/workshop/buildStore.d.ts +79 -0
  234. package/dist/workshop/buildStore.js +326 -0
  235. package/dist/workshop/chatTurn.d.ts +232 -0
  236. package/dist/workshop/chatTurn.js +475 -0
  237. package/dist/workshop/modelSettings.d.ts +18 -0
  238. package/dist/workshop/modelSettings.js +44 -0
  239. package/dist/workshop/sessionStore.d.ts +61 -0
  240. package/dist/workshop/sessionStore.js +254 -0
  241. package/dist/workshop/workshopPin.d.ts +72 -0
  242. package/dist/workshop/workshopPin.js +193 -0
  243. package/examples/README.md +2 -1
  244. package/examples/browser-session/README.md +77 -0
  245. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  246. package/examples/browser-session/browser-session.task.yaml +6 -0
  247. package/examples/browser-session/check-login.yaml +22 -0
  248. package/examples/browser-session/fixture-server.mjs +64 -0
  249. package/examples/browser-session/human-login.yaml +31 -0
  250. package/examples/browser-session/merge.yaml +36 -0
  251. package/examples/browser-session/work-a.yaml +41 -0
  252. package/examples/browser-session/work-b.yaml +41 -0
  253. package/examples/stage-mcp/README.md +3 -2
  254. package/examples/stageflow.yaml +51 -0
  255. package/package.json +5 -1
  256. package/skills/stageflow/references/control-surface.md +3 -1
  257. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  258. package/skills/stageflow-run/SKILL.md +2 -0
  259. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  260. package/skills/stageflow-setup/SKILL.md +11 -0
  261. package/dist/ui/assets/index-B6hlSXXn.js +0 -120
@@ -6,8 +6,17 @@ import { getCredentialSourceSettings, setCredentialSource, } from "../agent/prov
6
6
  import { mapProviderAuthError } from "../agent/providerInspect.js";
7
7
  import { handleProviderRoutes } from "./providerRoutes.js";
8
8
  import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
9
+ import { handleTriggerRoutes } from "./triggerRoutes.js";
9
10
  import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
10
11
  import { createStage, parseCreateStageBody } from "../config/createStage.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";
11
20
  import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
12
21
  import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
13
22
  import { listExtensions } from "../config/listExtensions.js";
@@ -25,7 +34,7 @@ import { globalStageflowHome } from "../project/globalHome.js";
25
34
  import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
26
35
  import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
27
36
  import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
28
- import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
37
+ import { INVALID_SLOT_COUNT_MESSAGE, INVALID_WORKSHOP_MODEL_MESSAGE, parseSlotCount, parseWorkshopModelSetting, readFactorySettings, writeFactorySettings, } from "../runtime/settingsFile.js";
29
38
  import { isTaskFile } from "../runtime/taskInput.js";
30
39
  import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
31
40
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
@@ -56,6 +65,28 @@ async function readJsonBody(req) {
56
65
  return {};
57
66
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
58
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
+ }
59
90
  function contentTypeFor(filePath) {
60
91
  const ext = path.extname(filePath).toLowerCase();
61
92
  switch (ext) {
@@ -125,7 +156,15 @@ export function defaultUiDistDir() {
125
156
  * API; only the former needs the UI files.
126
157
  */
127
158
  export function createOperatorRoutes(deps) {
128
- const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
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();
129
168
  const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
130
169
  const controlTokens = deps.controlTokens ?? loadControlTokens();
131
170
  return async ({ req, res, url, pathname, method, boot }) => {
@@ -780,7 +819,12 @@ export function createOperatorRoutes(deps) {
780
819
  json(res, result.status, { error: result.reason });
781
820
  return true;
782
821
  }
783
- json(res, 202, { ok: true });
822
+ json(res, 202, {
823
+ ok: true,
824
+ ...(result.verification !== undefined
825
+ ? { verification: result.verification }
826
+ : {}),
827
+ });
784
828
  return true;
785
829
  }
786
830
  const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
@@ -1007,6 +1051,7 @@ export function createOperatorRoutes(deps) {
1007
1051
  json(res, 200, {
1008
1052
  tasks: result.items,
1009
1053
  root_errors: result.root_errors,
1054
+ ...(result.tip !== undefined ? { tip: result.tip } : {}),
1010
1055
  });
1011
1056
  return true;
1012
1057
  }
@@ -1029,6 +1074,7 @@ export function createOperatorRoutes(deps) {
1029
1074
  json(res, 200, {
1030
1075
  pipelines: result.items,
1031
1076
  root_errors: result.root_errors,
1077
+ ...(result.tip !== undefined ? { tip: result.tip } : {}),
1032
1078
  });
1033
1079
  return true;
1034
1080
  }
@@ -1071,9 +1117,9 @@ export function createOperatorRoutes(deps) {
1071
1117
  return true;
1072
1118
  }
1073
1119
  const ctx = await resolveStageflowContext(stageWriteRoot);
1074
- if (!ctx.isGitProject) {
1120
+ if (ctx.manifestStatus !== "ok") {
1075
1121
  json(res, 400, {
1076
- error: "Project root not found; initialize stageflow.yaml in a git repo",
1122
+ error: "Project root not found; initialize stageflow.yaml",
1077
1123
  });
1078
1124
  return true;
1079
1125
  }
@@ -1118,9 +1164,9 @@ export function createOperatorRoutes(deps) {
1118
1164
  return true;
1119
1165
  }
1120
1166
  const ctx = await resolveStageflowContext(pipelineWriteRoot);
1121
- if (!ctx.isGitProject) {
1167
+ if (ctx.manifestStatus !== "ok") {
1122
1168
  json(res, 400, {
1123
- error: "Project root not found; initialize stageflow.yaml in a git repo",
1169
+ error: "Project root not found; initialize stageflow.yaml",
1124
1170
  });
1125
1171
  return true;
1126
1172
  }
@@ -1132,65 +1178,233 @@ export function createOperatorRoutes(deps) {
1132
1178
  json(res, 201, result.pipeline);
1133
1179
  return true;
1134
1180
  }
1135
- if (method === "GET" && pathname === "/api/models") {
1136
- const filter = url.searchParams.get("project_root") ?? undefined;
1137
- const result = await listModelsMultiProject({
1138
- store,
1139
- bootCwd: cwd,
1140
- projectRootFilter: filter,
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,
1141
1220
  });
1142
- if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
1143
- result.items.length === 0) {
1221
+ json(res, 200, result);
1222
+ return true;
1223
+ }
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) {
1144
1258
  json(res, 400, {
1145
- error: result.root_errors[0].message,
1146
- code: "unknown_project_root",
1147
- root_errors: result.root_errors,
1259
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
1148
1260
  });
1149
1261
  return true;
1150
1262
  }
1151
- json(res, 200, {
1152
- models: result.models,
1153
- entries: result.items,
1154
- root_errors: result.root_errors,
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 } : {}),
1155
1276
  });
1156
1277
  return true;
1157
1278
  }
1158
- if (await handleProviderRoutes(req, res, {
1159
- cwd,
1160
- readJsonBody,
1161
- json,
1162
- providerAuthContext,
1163
- })) {
1164
- return true;
1165
- }
1166
- if (await handleProjectMcpRoutes(req, res, {
1167
- projectRoot: rootDir,
1168
- json,
1169
- })) {
1170
- return true;
1171
- }
1172
- if (method === "GET" && pathname === "/api/skills") {
1173
- json(res, 200, await listSkills({ cwd, agentDir }));
1174
- return true;
1175
- }
1176
- if (method === "GET" && pathname === "/api/extensions") {
1177
- json(res, 200, await listExtensions({ cwd, agentDir }));
1178
- return true;
1179
- }
1180
- if (method === "GET" && pathname === "/api/health") {
1181
- await handleApiHealth(req, res, boot);
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
+ });
1182
1332
  return true;
1183
1333
  }
1184
- if (method === "GET" && pathname === "/api/settings") {
1185
- const health = manager.getHealth();
1186
- const credential = getCredentialSourceSettings(cwd);
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
+ }
1187
1399
  json(res, 200, {
1188
- maxConcurrent: health.maxConcurrent,
1189
- ...credential,
1400
+ draft: result.draft,
1401
+ destination: result.destination,
1402
+ pipelinePath: result.pipelinePath,
1403
+ ...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
1190
1404
  });
1191
1405
  return true;
1192
1406
  }
1193
- if (method === "POST" && pathname === "/api/settings") {
1407
+ if (method === "POST" && pathname === "/api/drafts/attach-task") {
1194
1408
  let body;
1195
1409
  try {
1196
1410
  body = await readJsonBody(req);
@@ -1199,40 +1413,1000 @@ export function createOperatorRoutes(deps) {
1199
1413
  json(res, 400, { error: "Invalid JSON body" });
1200
1414
  return true;
1201
1415
  }
1202
- const record = body !== null && typeof body === "object" && !Array.isArray(body)
1203
- ? body
1204
- : {};
1205
- const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
1206
- const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
1207
- if (!hasMax && !hasCredentialSource) {
1208
- json(res, 400, {
1209
- error: "maxConcurrent or credentialSource is required",
1210
- });
1416
+ const parsed = parseAttachTaskBody(body);
1417
+ if ("ok" in parsed) {
1418
+ json(res, parsed.status, { error: parsed.error });
1211
1419
  return true;
1212
1420
  }
1213
- let health = manager.getHealth();
1214
- if (hasMax) {
1215
- const n = parseSlotCount(record.maxConcurrent);
1216
- if (n === undefined) {
1217
- json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
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));
1218
1435
  return true;
1219
1436
  }
1220
- health = manager.setMaxConcurrent(n);
1437
+ throw err;
1221
1438
  }
1222
- let credential = getCredentialSourceSettings(cwd);
1223
- if (hasCredentialSource) {
1224
- try {
1225
- credential = setCredentialSource(cwd, record.credentialSource);
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;
1226
1452
  }
1227
- catch (err) {
1228
- const mapped = mapProviderAuthError(err);
1229
- json(res, mapped.status, mapped.body);
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
+ });
1465
+ return true;
1466
+ }
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;
1500
+ }
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 });
1582
+ return true;
1583
+ }
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 });
1629
+ return true;
1630
+ }
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 } : {}),
1651
+ });
1652
+ return true;
1653
+ }
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 });
1693
+ return true;
1694
+ }
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;
1821
+ }
1822
+ if (method === "GET" &&
1823
+ pathname === "/api/workshop/sessions") {
1824
+ const sessions = listWorkshopSessions(workshopSessionStoreRoot());
1825
+ json(res, 200, { sessions });
1826
+ return true;
1827
+ }
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 } : {}),
1844
+ });
1845
+ json(res, 201, { session: record });
1846
+ return true;
1847
+ }
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" });
1891
+ return true;
1892
+ }
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;
1910
+ }
1911
+ const workshopSessionUndoMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/undo$/);
1912
+ if (method === "POST" && workshopSessionUndoMatch) {
1913
+ const sessionId = decodeURIComponent(workshopSessionUndoMatch[1] ?? "");
1914
+ let body = {};
1915
+ try {
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);
1942
+ }
1943
+ catch (err) {
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" });
1968
+ return true;
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;
2006
+ }
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 });
2026
+ return true;
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 });
1230
2400
  return true;
1231
2401
  }
2402
+ writeFactorySettings(cwd, { workshopModel: parsed });
2403
+ workshopModel = parsed;
1232
2404
  }
1233
2405
  json(res, 200, {
1234
2406
  ...health,
1235
2407
  ...credential,
2408
+ ...(workshopModel !== undefined ? { workshopModel } : {}),
2409
+ defaultModel: await readWorkshopOpenModel(cwd),
1236
2410
  });
1237
2411
  return true;
1238
2412
  }
@@ -1284,6 +2458,9 @@ export async function startUiServer(options) {
1284
2458
  allowedHosts,
1285
2459
  controlTokens,
1286
2460
  getShutdown: () => shutdown,
2461
+ ...(options.workshopOperatorHost
2462
+ ? { workshopOperatorHost: options.workshopOperatorHost }
2463
+ : {}),
1287
2464
  });
1288
2465
  const envelope = await createHttpHost({
1289
2466
  boot,