stageflow 0.3.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.
- package/README.md +22 -1
- package/dist/agent/activity.d.ts +2 -0
- package/dist/agent/cursorProvider.d.ts +1 -10
- package/dist/agent/cursorProvider.js +18 -10
- package/dist/agent/fakeAgent.js +11 -7
- package/dist/agent/piAdapter.d.ts +2 -1
- package/dist/agent/piAdapter.js +28 -25
- package/dist/agent/port.d.ts +6 -1
- package/dist/agent/port.js +3 -0
- package/dist/cli/artifactCommand.d.ts +10 -0
- package/dist/cli/artifactCommand.js +120 -0
- package/dist/cli/envelopeCommand.d.ts +32 -0
- package/dist/cli/envelopeCommand.js +285 -0
- package/dist/cli/exportRunCommand.d.ts +10 -0
- package/dist/cli/exportRunCommand.js +150 -0
- package/dist/cli/handoffFormat.d.ts +22 -0
- package/dist/cli/handoffFormat.js +61 -0
- package/dist/cli/runCommand.d.ts +5 -1
- package/dist/cli/runCommand.js +36 -5
- package/dist/cli/runOutput.d.ts +4 -1
- package/dist/cli/runOutput.js +36 -8
- package/dist/cli/skillsCommand.d.ts +16 -0
- package/dist/cli/skillsCommand.js +459 -0
- package/dist/cli.js +78 -5
- package/dist/config/loadPipeline.js +5 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
- package/dist/config/normalizePipelineStageEntry.js +25 -1
- package/dist/config/pipelineStageKeys.js +1 -1
- package/dist/config/resolveForkEmitContext.d.ts +2 -1
- package/dist/config/resolveForkEmitContext.js +23 -1
- package/dist/config/resolvePipelineDag.js +36 -4
- package/dist/envelope/check.d.ts +2 -0
- package/dist/envelope/check.js +51 -0
- package/dist/envelope/cloneForks.d.ts +2 -0
- package/dist/envelope/cloneForks.js +62 -0
- package/dist/envelope/forkChoice.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/catalogTools.d.ts +3 -0
- package/dist/mcp/catalogTools.js +187 -0
- package/dist/mcp/controlTools.d.ts +3 -0
- package/dist/mcp/controlTools.js +147 -0
- package/dist/mcp/deps.d.ts +11 -0
- package/dist/mcp/deps.js +1 -0
- package/dist/mcp/projectRun.d.ts +1 -40
- package/dist/mcp/projectRun.js +1 -64
- package/dist/mcp/resources.d.ts +5 -0
- package/dist/mcp/resources.js +45 -0
- package/dist/mcp/server.d.ts +16 -3
- package/dist/mcp/server.js +159 -3
- package/dist/mcp/toolResults.d.ts +7 -0
- package/dist/mcp/toolResults.js +6 -0
- package/dist/mcp/tools.d.ts +2 -7
- package/dist/mcp/tools.js +52 -103
- package/dist/mcp/waitRun.d.ts +46 -0
- package/dist/mcp/waitRun.js +146 -0
- package/dist/mcp/waitingGates.d.ts +12 -0
- package/dist/mcp/waitingGates.js +83 -0
- package/dist/projection/projectRun.d.ts +44 -0
- package/dist/projection/projectRun.js +71 -0
- package/dist/prompt/priorEnvelope.d.ts +1 -1
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/paths.js +2 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
- package/dist/runstore/pipelineDagSnapshot.js +90 -1
- package/dist/runstore/port.d.ts +13 -2
- package/dist/runstore/port.js +4 -1
- package/dist/runstore/runProjection.d.ts +1 -1
- package/dist/runstore/runProjection.js +14 -3
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
- package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
- package/dist/runstore/stageInstanceId.d.ts +4 -0
- package/dist/runstore/stageInstanceId.js +23 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
- package/dist/runstore/syntheticStageSnapshot.js +2 -1
- package/dist/runstore/trackProjection.js +28 -12
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +1 -1
- package/dist/runtime/cloneSchedule.d.ts +18 -0
- package/dist/runtime/cloneSchedule.js +233 -0
- package/dist/runtime/envelopeRouting.d.ts +2 -1
- package/dist/runtime/envelopeRouting.js +80 -13
- package/dist/runtime/pipelineScheduler.d.ts +6 -2
- package/dist/runtime/pipelineScheduler.js +166 -30
- package/dist/runtime/resumeReconstruct.js +8 -3
- package/dist/runtime/runChangeBus.d.ts +18 -0
- package/dist/runtime/runChangeBus.js +83 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
- package/dist/runtime/stageAttemptBootstrap.js +17 -9
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +17 -15
- package/dist/runtime/stageWorker.js +11 -4
- package/dist/server/bootstrap.d.ts +32 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/createHttpHost.d.ts +34 -0
- package/dist/server/createHttpHost.js +70 -0
- package/dist/server/http.d.ts +6 -13
- package/dist/server/http.js +295 -374
- package/dist/server/mcpHost.d.ts +22 -0
- package/dist/server/mcpHost.js +21 -0
- package/dist/server/operatorResults.d.ts +13 -0
- package/dist/server/operatorResults.js +22 -0
- package/dist/tools/emitStageEnvelope.d.ts +11 -2
- package/dist/tools/emitStageEnvelope.js +22 -6
- package/dist/types/envelope.d.ts +2 -0
- package/dist/types/forkChoice.d.ts +22 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +118 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/install-suite.sh +107 -0
- package/skills/stageflow/SKILL.md +26 -0
- package/skills/stageflow/references/control-surface.md +26 -0
- package/skills/stageflow/scripts/detect-host.mjs +54 -0
- package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
- package/skills/stageflow-author/SKILL.md +58 -0
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
- package/skills/stageflow-author/references/catalog-mapping.md +131 -0
- package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
- package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
- package/skills/stageflow-author/references/validate-and-report.md +38 -0
- package/skills/stageflow-delegate/SKILL.md +18 -0
- package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
- package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
- package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
- package/skills/stageflow-run/SKILL.md +151 -0
- package/skills/stageflow-run/references/mcp-call.md +17 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
- package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
- package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
- package/skills/stageflow-session-capture/SKILL.md +94 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
- package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
- package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
- package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
- package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
- package/skills/stageflow-setup/SKILL.md +110 -0
- package/dist/ui/assets/index-CFSzDZje.js +0 -118
package/dist/server/http.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
277
|
+
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
297
278
|
}
|
|
298
279
|
catch (err) {
|
|
299
|
-
json(res,
|
|
280
|
+
json(res, 400, {
|
|
300
281
|
error: err instanceof Error ? err.message : String(err),
|
|
301
282
|
});
|
|
283
|
+
return true;
|
|
302
284
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
json(res,
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
408
|
+
if (await handleProviderRoutes(req, res, {
|
|
409
|
+
cwd,
|
|
410
|
+
readJsonBody,
|
|
411
|
+
json,
|
|
412
|
+
providerAuthContext,
|
|
413
|
+
})) {
|
|
414
|
+
return true;
|
|
450
415
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return;
|
|
416
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
417
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
418
|
+
return true;
|
|
455
419
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return;
|
|
420
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
421
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
422
|
+
return true;
|
|
460
423
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return;
|
|
424
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
425
|
+
json(res, 200, manager.getHealth());
|
|
426
|
+
return true;
|
|
465
427
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
533
|
-
|
|
437
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
438
|
+
let body;
|
|
534
439
|
try {
|
|
535
|
-
|
|
440
|
+
body = await readJsonBody(req);
|
|
536
441
|
}
|
|
537
|
-
catch
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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 };
|