stageflow 0.2.0 → 0.8.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 (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,54 +1,21 @@
1
- import { createServer } from "node:http";
2
1
  import { readFile, access } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
- import { localhostHostValidation, localhostOriginValidation, } from "@modelcontextprotocol/node";
7
4
  import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
8
5
  import { handleProviderRoutes, providerAuthErrorBody, } from "./providerRoutes.js";
9
6
  import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
10
7
  import { createStage, parseCreateStageBody } from "../config/createStage.js";
11
- import { listModels, listPipelines, listStages, listTasks, } from "../config/listConfig.js";
8
+ import { browseCatalog } from "../config/browseCatalog.js";
12
9
  import { listExtensions } from "../config/listExtensions.js";
13
10
  import { listSkills } from "../config/listSkills.js";
14
11
  import { readRunArtifact } from "../mcp/readArtifact.js";
15
- import { handleMcpHttpRequest } from "../mcp/server.js";
16
- import { createRunStore } from "../runstore/createStore.js";
17
- import { RunManager, } from "../runtime/runManager.js";
12
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
18
13
  import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
19
14
  import { isTaskFile } from "../runtime/taskInput.js";
20
15
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
21
- const DEFAULT_PORT = 3847;
22
- function json(res, status, body) {
23
- const payload = JSON.stringify(body);
24
- res.writeHead(status, {
25
- "Content-Type": "application/json; charset=utf-8",
26
- "Content-Length": Buffer.byteLength(payload),
27
- });
28
- res.end(payload);
29
- }
30
- function startFailureBody(result) {
31
- const { ok: _ok, status: _status, reason, ...rest } = result;
32
- return { error: reason, ...rest };
33
- }
34
- function inferRetryStageErrorCode(reason) {
35
- if (/retry already in progress/i.test(reason))
36
- return "retry_in_progress";
37
- if (/waiting for input/i.test(reason))
38
- return "hitl_not_retriable";
39
- if (/already has active orchestration/i.test(reason))
40
- return "run_not_retryable";
41
- if (/run is not failed/i.test(reason))
42
- return "run_not_retryable";
43
- if (/stage is not failed/i.test(reason))
44
- return "stage_not_failed";
45
- return undefined;
46
- }
47
- function retryStageFailureBody(result) {
48
- const { ok: _ok, status: _status, reason, ...rest } = result;
49
- const code = inferRetryStageErrorCode(reason);
50
- return { error: reason, ...(code ? { code } : {}), ...rest };
51
- }
16
+ import { bootstrapStageflowHost, } from "./bootstrap.js";
17
+ import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
18
+ import { mapRetryStageFailure, mapStartFailure } from "./operatorResults.js";
52
19
  function textPlain(res, status, body) {
53
20
  res.writeHead(status, {
54
21
  "Content-Type": "text/plain; charset=utf-8",
@@ -202,360 +169,338 @@ export function defaultUiDistDir() {
202
169
  return path.resolve(here, "../ui");
203
170
  }
204
171
  export async function startUiServer(options) {
205
- const cwd = options.cwd ?? process.cwd();
206
- const agentDir = options.agentDir ?? getAgentDir();
207
- const rootDir = options.rootDir ?? cwd;
208
172
  const host = options.host ?? "127.0.0.1";
209
173
  const port = options.port ?? DEFAULT_PORT;
210
- const providerAuthContext = options.providerAuthContext;
211
174
  const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
212
- const store = options.store ??
213
- createRunStore({ rootDir, kind: options.storeKind });
214
- const manager = new RunManager({
215
- agent: options.agent,
216
- cwd,
217
- store,
218
- maxConcurrent: options.maxConcurrent,
219
- operatorCatalog: { cwd, agentDir },
220
- });
221
- await manager.attachWaitingStages();
222
- await manager.reconcileOrphanedStages();
223
- const validateMcpHost = localhostHostValidation();
224
- const validateMcpOrigin = localhostOriginValidation();
225
- const server = createServer(async (req, res) => {
226
- const method = req.method ?? "GET";
227
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
228
- const pathname = url.pathname;
229
- if (pathname === "/mcp") {
230
- if (!validateMcpHost(req, res) || !validateMcpOrigin(req, res)) {
231
- return;
175
+ const boot = await bootstrapStageflowHost(options);
176
+ const { manager, store, cwd, agentDir } = boot;
177
+ const providerAuthContext = boot.providerAuthContext;
178
+ return createHttpHost({
179
+ boot,
180
+ host,
181
+ port,
182
+ routes: async ({ req, res, url, pathname, method }) => {
183
+ if (isMutatingApi(method, pathname)) {
184
+ if (!assertLoopbackHttpAccess(req, res)) {
185
+ return true;
186
+ }
187
+ if (isCredentialMutatingApi(method, pathname) &&
188
+ !assertCredentialMutatingOrigin(req, res)) {
189
+ return true;
190
+ }
232
191
  }
233
192
  try {
234
- await handleMcpHttpRequest(req, res, { manager, store, cwd });
235
- }
236
- catch (err) {
237
- if (!res.headersSent) {
238
- res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
239
- res.end(err instanceof Error ? err.message : String(err));
193
+ if (method === "GET" && pathname === "/api/runs") {
194
+ json(res, 200, { runs: await store.listRuns() });
195
+ return true;
240
196
  }
241
- }
242
- return;
243
- }
244
- if (isMutatingApi(method, pathname)) {
245
- if (!assertLoopbackHttpAccess(req, res)) {
246
- return;
247
- }
248
- if (isCredentialMutatingApi(method, pathname) &&
249
- !assertCredentialMutatingOrigin(req, res)) {
250
- return;
251
- }
252
- }
253
- try {
254
- if (method === "GET" && pathname === "/api/runs") {
255
- json(res, 200, { runs: await store.listRuns() });
256
- return;
257
- }
258
- const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
259
- if (method === "GET" && artifactMatch) {
260
- const runId = decodeURIComponent(artifactMatch[1] ?? "");
261
- const artifactPath = url.searchParams.get("path");
262
- if (artifactPath === null || artifactPath.trim().length === 0) {
263
- json(res, 400, { error: "path query parameter is required" });
264
- return;
197
+ const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
198
+ if (method === "GET" && artifactMatch) {
199
+ const runId = decodeURIComponent(artifactMatch[1] ?? "");
200
+ const artifactPath = url.searchParams.get("path");
201
+ if (artifactPath === null || artifactPath.trim().length === 0) {
202
+ json(res, 400, { error: "path query parameter is required" });
203
+ return true;
204
+ }
205
+ try {
206
+ const content = await readRunArtifact(store, runId, artifactPath);
207
+ textPlain(res, 200, content);
208
+ }
209
+ catch (err) {
210
+ const message = err instanceof Error ? err.message : String(err);
211
+ if (message === "Artifact path denied") {
212
+ json(res, 403, { error: message });
213
+ return true;
214
+ }
215
+ const notFound = message.startsWith("Run not found") ||
216
+ message.startsWith("Artifact not found") ||
217
+ /no such|not found/i.test(message);
218
+ json(res, notFound ? 404 : 400, { error: message });
219
+ }
220
+ return true;
221
+ }
222
+ if (method === "GET" && pathname.startsWith("/api/runs/")) {
223
+ const rest = pathname.slice("/api/runs/".length);
224
+ if (rest && !rest.includes("/")) {
225
+ try {
226
+ json(res, 200, await store.readRun(decodeURIComponent(rest)));
227
+ }
228
+ catch (err) {
229
+ json(res, 404, {
230
+ error: err instanceof Error ? err.message : String(err),
231
+ });
232
+ }
233
+ return true;
234
+ }
265
235
  }
266
- try {
267
- const content = await readRunArtifact(store, runId, artifactPath);
268
- textPlain(res, 200, content);
236
+ if (method === "POST" && pathname === "/api/runs") {
237
+ const body = (await readJsonBody(req));
238
+ if (typeof body.pipeline !== "string" ||
239
+ !body.pipeline.trim() ||
240
+ body.task === undefined) {
241
+ json(res, 400, { error: "task and pipeline are required" });
242
+ return true;
243
+ }
244
+ if (typeof body.task !== "string" && !isTaskFile(body.task)) {
245
+ json(res, 400, {
246
+ error: "task must be a path string or TaskFile object (id and goal required)",
247
+ });
248
+ return true;
249
+ }
250
+ const result = await manager.startRun({
251
+ task: body.task,
252
+ pipeline: body.pipeline.trim(),
253
+ });
254
+ if (!result.ok) {
255
+ json(res, result.status ?? 500, mapStartFailure(result));
256
+ return true;
257
+ }
258
+ json(res, 202, { runId: result.runId });
259
+ return true;
269
260
  }
270
- catch (err) {
271
- const message = err instanceof Error ? err.message : String(err);
272
- if (message === "Artifact path denied") {
273
- json(res, 403, { error: message });
274
- return;
261
+ if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
262
+ const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
263
+ const result = await manager.rerun(runId);
264
+ if (!result.ok) {
265
+ json(res, result.status ?? 500, mapStartFailure(result));
266
+ return true;
275
267
  }
276
- const notFound = message.startsWith("Run not found") ||
277
- message.startsWith("Artifact not found") ||
278
- /no such|not found/i.test(message);
279
- json(res, notFound ? 404 : 400, { error: message });
268
+ json(res, 202, { runId: result.runId });
269
+ return true;
280
270
  }
281
- return;
282
- }
283
- if (method === "GET" && pathname.startsWith("/api/runs/")) {
284
- const rest = pathname.slice("/api/runs/".length);
285
- if (rest && !rest.includes("/")) {
271
+ const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
272
+ if (method === "POST" && answerMatch) {
273
+ const runId = decodeURIComponent(answerMatch[1] ?? "");
274
+ const stageId = decodeURIComponent(answerMatch[2] ?? "");
275
+ let answer;
286
276
  try {
287
- json(res, 200, await store.readRun(decodeURIComponent(rest)));
277
+ answer = parseAskOperatorAnswer(await readJsonBody(req));
288
278
  }
289
279
  catch (err) {
290
- json(res, 404, {
280
+ json(res, 400, {
291
281
  error: err instanceof Error ? err.message : String(err),
292
282
  });
283
+ return true;
293
284
  }
294
- return;
295
- }
296
- }
297
- if (method === "POST" && pathname === "/api/runs") {
298
- const body = (await readJsonBody(req));
299
- if (typeof body.pipeline !== "string" ||
300
- !body.pipeline.trim() ||
301
- body.task === undefined) {
302
- json(res, 400, { error: "task and pipeline are required" });
303
- return;
285
+ const result = await manager.deliverAnswer(runId, stageId, answer);
286
+ if (!result.ok) {
287
+ json(res, result.status, { error: result.reason });
288
+ return true;
289
+ }
290
+ json(res, 202, { ok: true });
291
+ return true;
304
292
  }
305
- if (typeof body.task !== "string" && !isTaskFile(body.task)) {
306
- json(res, 400, {
307
- error: "task must be a path string or TaskFile object (id and goal required)",
293
+ const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
294
+ if (method === "POST" && retryMatch) {
295
+ await readJsonBody(req);
296
+ const runId = decodeURIComponent(retryMatch[1] ?? "");
297
+ const stageId = decodeURIComponent(retryMatch[2] ?? "");
298
+ const result = await manager.retryStage(runId, stageId);
299
+ if (!result.ok) {
300
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
301
+ return true;
302
+ }
303
+ json(res, 202, {
304
+ runId: result.runId,
305
+ stageId: result.stageId,
306
+ attemptIndex: result.attemptIndex,
308
307
  });
309
- return;
310
- }
311
- const result = await manager.startRun({
312
- task: body.task,
313
- pipeline: body.pipeline.trim(),
314
- });
315
- if (!result.ok) {
316
- json(res, result.status ?? 500, startFailureBody(result));
317
- return;
318
- }
319
- json(res, 202, { runId: result.runId });
320
- return;
321
- }
322
- if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
323
- const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
324
- const result = await manager.rerun(runId);
325
- if (!result.ok) {
326
- json(res, result.status ?? 500, startFailureBody(result));
327
- return;
328
- }
329
- json(res, 202, { runId: result.runId });
330
- return;
331
- }
332
- const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
333
- if (method === "POST" && answerMatch) {
334
- const runId = decodeURIComponent(answerMatch[1] ?? "");
335
- const stageId = decodeURIComponent(answerMatch[2] ?? "");
336
- let answer;
337
- try {
338
- answer = parseAskOperatorAnswer(await readJsonBody(req));
308
+ return true;
339
309
  }
340
- catch (err) {
341
- json(res, 400, {
342
- error: err instanceof Error ? err.message : String(err),
310
+ const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
311
+ if (method === "POST" && abandonMatch) {
312
+ await readJsonBody(req);
313
+ const runId = decodeURIComponent(abandonMatch[1] ?? "");
314
+ const stageId = decodeURIComponent(abandonMatch[2] ?? "");
315
+ const result = await manager.abandonStage(runId, stageId);
316
+ if (!result.ok) {
317
+ json(res, result.status ?? 500, {
318
+ error: result.reason,
319
+ });
320
+ return true;
321
+ }
322
+ json(res, 202, {
323
+ ok: true,
324
+ runId: result.runId,
325
+ stageId: result.stageId,
343
326
  });
344
- return;
327
+ return true;
345
328
  }
346
- const result = await manager.deliverAnswer(runId, stageId, answer);
347
- if (!result.ok) {
348
- json(res, result.status, { error: result.reason });
349
- return;
329
+ if (method === "GET" && pathname === "/api/tasks") {
330
+ const catalog = await browseCatalog(cwd);
331
+ json(res, 200, { tasks: catalog.tasks });
332
+ return true;
350
333
  }
351
- json(res, 202, { ok: true });
352
- return;
353
- }
354
- const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
355
- if (method === "POST" && retryMatch) {
356
- await readJsonBody(req);
357
- const runId = decodeURIComponent(retryMatch[1] ?? "");
358
- const stageId = decodeURIComponent(retryMatch[2] ?? "");
359
- const result = await manager.retryStage(runId, stageId);
360
- if (!result.ok) {
361
- json(res, result.status ?? 500, retryStageFailureBody(result));
362
- return;
334
+ if (method === "GET" && pathname === "/api/pipelines") {
335
+ const catalog = await browseCatalog(cwd);
336
+ json(res, 200, { pipelines: catalog.pipelines });
337
+ return true;
363
338
  }
364
- json(res, 202, {
365
- runId: result.runId,
366
- stageId: result.stageId,
367
- attemptIndex: result.attemptIndex,
368
- });
369
- return;
370
- }
371
- const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
372
- if (method === "POST" && abandonMatch) {
373
- await readJsonBody(req);
374
- const runId = decodeURIComponent(abandonMatch[1] ?? "");
375
- const stageId = decodeURIComponent(abandonMatch[2] ?? "");
376
- const result = await manager.abandonStage(runId, stageId);
377
- if (!result.ok) {
378
- json(res, result.status ?? 500, {
379
- error: result.reason,
339
+ if (method === "GET" && pathname === "/api/stages") {
340
+ json(res, 404, {
341
+ error: "Global stage library removed; stages are pipeline-scoped",
380
342
  });
381
- return;
382
- }
383
- json(res, 202, {
384
- ok: true,
385
- runId: result.runId,
386
- stageId: result.stageId,
387
- });
388
- return;
389
- }
390
- if (method === "GET" && pathname === "/api/tasks") {
391
- json(res, 200, { tasks: await listTasks(cwd) });
392
- return;
393
- }
394
- if (method === "GET" && pathname === "/api/pipelines") {
395
- json(res, 200, { pipelines: await listPipelines(cwd) });
396
- return;
397
- }
398
- if (method === "GET" && pathname === "/api/stages") {
399
- json(res, 200, { stages: await listStages(cwd) });
400
- return;
401
- }
402
- if (method === "POST" && pathname === "/api/stages") {
403
- let body;
404
- try {
405
- body = await readJsonBody(req);
406
- }
407
- catch {
408
- json(res, 400, { error: "Invalid JSON body" });
409
- return;
410
- }
411
- const parsed = parseCreateStageBody(body);
412
- if ("ok" in parsed) {
413
- json(res, parsed.status, { error: parsed.error });
414
- return;
343
+ return true;
415
344
  }
416
- const result = await createStage(cwd, parsed);
417
- if (!result.ok) {
418
- json(res, result.status, { error: result.error });
419
- return;
345
+ if (method === "POST" && pathname === "/api/stages") {
346
+ let body;
347
+ try {
348
+ body = await readJsonBody(req);
349
+ }
350
+ catch {
351
+ json(res, 400, { error: "Invalid JSON body" });
352
+ return true;
353
+ }
354
+ const parsed = parseCreateStageBody(body);
355
+ if ("ok" in parsed) {
356
+ json(res, parsed.status, { error: parsed.error });
357
+ return true;
358
+ }
359
+ const ctx = await resolveStageflowContext(cwd);
360
+ if (!ctx.isGitProject) {
361
+ json(res, 400, {
362
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
363
+ });
364
+ return true;
365
+ }
366
+ const result = await createStage(ctx.projectRoot, parsed);
367
+ if (!result.ok) {
368
+ json(res, result.status, { error: result.error });
369
+ return true;
370
+ }
371
+ json(res, 201, result.stage);
372
+ return true;
420
373
  }
421
- json(res, 201, result.stage);
422
- return;
423
- }
424
- if (method === "POST" && pathname === "/api/pipelines") {
425
- let body;
426
- try {
427
- body = await readJsonBody(req);
374
+ if (method === "POST" && pathname === "/api/pipelines") {
375
+ let body;
376
+ try {
377
+ body = await readJsonBody(req);
378
+ }
379
+ catch {
380
+ json(res, 400, { error: "Invalid JSON body" });
381
+ return true;
382
+ }
383
+ const parsed = parseCreatePipelineBody(body);
384
+ if ("ok" in parsed) {
385
+ json(res, parsed.status, { error: parsed.error });
386
+ return true;
387
+ }
388
+ const ctx = await resolveStageflowContext(cwd);
389
+ if (!ctx.isGitProject) {
390
+ json(res, 400, {
391
+ error: "Project root not found; initialize stageflow.yaml in a git repo",
392
+ });
393
+ return true;
394
+ }
395
+ const result = await createPipeline(ctx.projectRoot, parsed);
396
+ if (!result.ok) {
397
+ json(res, result.status, { error: result.error });
398
+ return true;
399
+ }
400
+ json(res, 201, result.pipeline);
401
+ return true;
428
402
  }
429
- catch {
430
- json(res, 400, { error: "Invalid JSON body" });
431
- return;
403
+ if (method === "GET" && pathname === "/api/models") {
404
+ const catalog = await browseCatalog(cwd);
405
+ json(res, 200, { models: catalog.models });
406
+ return true;
432
407
  }
433
- const parsed = parseCreatePipelineBody(body);
434
- if ("ok" in parsed) {
435
- json(res, parsed.status, { error: parsed.error });
436
- return;
408
+ if (await handleProviderRoutes(req, res, {
409
+ cwd,
410
+ readJsonBody,
411
+ json,
412
+ providerAuthContext,
413
+ })) {
414
+ return true;
437
415
  }
438
- const result = await createPipeline(cwd, parsed);
439
- if (!result.ok) {
440
- json(res, result.status, { error: result.error });
441
- return;
416
+ if (method === "GET" && pathname === "/api/skills") {
417
+ json(res, 200, await listSkills({ cwd, agentDir }));
418
+ return true;
442
419
  }
443
- json(res, 201, result.pipeline);
444
- return;
445
- }
446
- if (method === "GET" && pathname === "/api/models") {
447
- json(res, 200, { models: await listModels(cwd) });
448
- return;
449
- }
450
- if (await handleProviderRoutes(req, res, {
451
- cwd,
452
- readJsonBody,
453
- json,
454
- providerAuthContext,
455
- })) {
456
- return;
457
- }
458
- if (method === "GET" && pathname === "/api/skills") {
459
- json(res, 200, await listSkills({ cwd, agentDir }));
460
- return;
461
- }
462
- if (method === "GET" && pathname === "/api/extensions") {
463
- json(res, 200, await listExtensions({ cwd, agentDir }));
464
- return;
465
- }
466
- if (method === "GET" && pathname === "/api/health") {
467
- json(res, 200, manager.getHealth());
468
- return;
469
- }
470
- if (method === "GET" && pathname === "/api/settings") {
471
- const health = manager.getHealth();
472
- const credential = getCredentialSourceSettings(cwd);
473
- json(res, 200, {
474
- maxConcurrent: health.maxConcurrent,
475
- ...credential,
476
- });
477
- return;
478
- }
479
- if (method === "POST" && pathname === "/api/settings") {
480
- let body;
481
- try {
482
- body = await readJsonBody(req);
420
+ if (method === "GET" && pathname === "/api/extensions") {
421
+ json(res, 200, await listExtensions({ cwd, agentDir }));
422
+ return true;
483
423
  }
484
- catch {
485
- json(res, 400, { error: "Invalid JSON body" });
486
- return;
424
+ if (method === "GET" && pathname === "/api/health") {
425
+ json(res, 200, manager.getHealth());
426
+ return true;
487
427
  }
488
- const record = body !== null && typeof body === "object" && !Array.isArray(body)
489
- ? body
490
- : {};
491
- const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
492
- const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
493
- if (!hasMax && !hasCredentialSource) {
494
- json(res, 400, {
495
- error: "maxConcurrent or credentialSource is required",
428
+ if (method === "GET" && pathname === "/api/settings") {
429
+ const health = manager.getHealth();
430
+ const credential = getCredentialSourceSettings(cwd);
431
+ json(res, 200, {
432
+ maxConcurrent: health.maxConcurrent,
433
+ ...credential,
496
434
  });
497
- return;
498
- }
499
- let health = manager.getHealth();
500
- if (hasMax) {
501
- const n = parseSlotCount(record.maxConcurrent);
502
- if (n === undefined) {
503
- json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
504
- return;
505
- }
506
- health = manager.setMaxConcurrent(n);
435
+ return true;
507
436
  }
508
- let credential = getCredentialSourceSettings(cwd);
509
- if (hasCredentialSource) {
437
+ if (method === "POST" && pathname === "/api/settings") {
438
+ let body;
510
439
  try {
511
- credential = setCredentialSource(cwd, record.credentialSource);
440
+ body = await readJsonBody(req);
512
441
  }
513
- catch (err) {
514
- const mapped = providerAuthErrorBody(err);
515
- json(res, mapped.status, mapped.body);
516
- return;
442
+ catch {
443
+ json(res, 400, { error: "Invalid JSON body" });
444
+ return true;
445
+ }
446
+ const record = body !== null && typeof body === "object" && !Array.isArray(body)
447
+ ? body
448
+ : {};
449
+ const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
450
+ const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
451
+ if (!hasMax && !hasCredentialSource) {
452
+ json(res, 400, {
453
+ error: "maxConcurrent or credentialSource is required",
454
+ });
455
+ return true;
517
456
  }
457
+ let health = manager.getHealth();
458
+ if (hasMax) {
459
+ const n = parseSlotCount(record.maxConcurrent);
460
+ if (n === undefined) {
461
+ json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
462
+ return true;
463
+ }
464
+ health = manager.setMaxConcurrent(n);
465
+ }
466
+ let credential = getCredentialSourceSettings(cwd);
467
+ if (hasCredentialSource) {
468
+ try {
469
+ credential = setCredentialSource(cwd, record.credentialSource);
470
+ }
471
+ catch (err) {
472
+ const mapped = providerAuthErrorBody(err);
473
+ json(res, mapped.status, mapped.body);
474
+ return true;
475
+ }
476
+ }
477
+ json(res, 200, {
478
+ ...health,
479
+ ...credential,
480
+ });
481
+ return true;
518
482
  }
519
- json(res, 200, {
520
- ...health,
521
- ...credential,
522
- });
523
- return;
524
- }
525
- if (method === "GET") {
526
- const served = await serveStatic(res, uiDistDir, pathname);
527
- if (served)
528
- return;
529
- json(res, 404, {
530
- error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
531
- });
532
- return;
483
+ if (method === "GET") {
484
+ const served = await serveStatic(res, uiDistDir, pathname);
485
+ if (served)
486
+ return true;
487
+ json(res, 404, {
488
+ error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
489
+ });
490
+ return true;
491
+ }
492
+ json(res, 404, { error: "Not found" });
493
+ return true;
533
494
  }
534
- json(res, 404, { error: "Not found" });
535
- }
536
- catch (err) {
537
- if (!res.headersSent) {
538
- json(res, 500, {
539
- error: err instanceof Error ? err.message : String(err),
540
- });
495
+ catch (err) {
496
+ if (!res.headersSent) {
497
+ json(res, 500, {
498
+ error: err instanceof Error ? err.message : String(err),
499
+ });
500
+ }
501
+ return true;
541
502
  }
542
- }
543
- });
544
- await new Promise((resolve, reject) => {
545
- server.listen(port, host, () => resolve());
546
- server.on("error", reject);
503
+ },
547
504
  });
548
- const address = server.address();
549
- const boundPort = address && typeof address !== "string" ? address.port : port;
550
- const url = `http://${host}:${boundPort}`;
551
- return {
552
- server,
553
- port: boundPort,
554
- host,
555
- url,
556
- mcpUrl: `${url}/mcp`,
557
- manager,
558
- store,
559
- };
560
505
  }
561
506
  export { DEFAULT_PORT };