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