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/cli.js
CHANGED
|
@@ -4,27 +4,40 @@ import { realpathSync } from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { PiAgentAdapter } from "./agent/piAdapter.js";
|
|
7
|
+
import { ARTIFACT_USAGE, runArtifactCommand } from "./cli/artifactCommand.js";
|
|
8
|
+
import { ENVELOPE_USAGE, runEnvelopeCommand, } from "./cli/envelopeCommand.js";
|
|
9
|
+
import { EXPORT_RUN_USAGE, runExportRunCommand, } from "./cli/exportRunCommand.js";
|
|
7
10
|
import { INIT_USAGE, runInitCommand } from "./cli/initCommand.js";
|
|
8
11
|
import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
|
|
9
12
|
import { RUN_USAGE, runRunCommand } from "./cli/runCommand.js";
|
|
10
13
|
import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
|
|
14
|
+
import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
|
|
11
15
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
12
16
|
import { createRunStore } from "./runstore/createStore.js";
|
|
13
17
|
import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
|
|
14
18
|
import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
|
|
15
19
|
import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
|
|
16
20
|
import { DEFAULT_PORT, startUiServer } from "./server/http.js";
|
|
21
|
+
import { startMcpServer } from "./server/mcpHost.js";
|
|
22
|
+
import { resolveMcpStateless } from "./mcp/server.js";
|
|
17
23
|
const USAGE = `Usage:
|
|
18
24
|
sf init
|
|
19
|
-
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
|
|
25
|
+
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
|
|
20
26
|
sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
|
|
21
|
-
sf
|
|
27
|
+
sf artifact read --run <runId> --path <relPath> [--out <file>]
|
|
28
|
+
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
|
|
29
|
+
sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]
|
|
30
|
+
sf ui [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
31
|
+
sf mcp [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
22
32
|
sf providers list
|
|
23
33
|
sf providers status [--provider <id>]
|
|
24
34
|
sf providers detect
|
|
25
35
|
sf providers source [get | set <pi_home|sf_owned>]
|
|
26
36
|
sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]
|
|
27
37
|
sf providers logout <providerId>
|
|
38
|
+
sf skills list
|
|
39
|
+
sf skills install --from-path <dir> [--skill-name <name>]
|
|
40
|
+
sf skills install --from-zip <url-or-path> [--skill-name <name>] [--checksum sha256:<hex>]
|
|
28
41
|
sf --help
|
|
29
42
|
|
|
30
43
|
Stageflow (sf) runs YAML-defined automatic stage pipelines.
|
|
@@ -37,7 +50,15 @@ ${RUN_USAGE}
|
|
|
37
50
|
|
|
38
51
|
${VALIDATE_USAGE}
|
|
39
52
|
|
|
40
|
-
${
|
|
53
|
+
${ARTIFACT_USAGE}
|
|
54
|
+
|
|
55
|
+
${ENVELOPE_USAGE}
|
|
56
|
+
|
|
57
|
+
${EXPORT_RUN_USAGE}
|
|
58
|
+
|
|
59
|
+
${PROVIDERS_USAGE}
|
|
60
|
+
|
|
61
|
+
${SKILLS_USAGE}`;
|
|
41
62
|
function parseArgs(argv) {
|
|
42
63
|
const args = argv.slice(2);
|
|
43
64
|
if (args.length === 0) {
|
|
@@ -51,7 +72,11 @@ function parseArgs(argv) {
|
|
|
51
72
|
if (command === "providers" ||
|
|
52
73
|
command === "validate" ||
|
|
53
74
|
command === "run" ||
|
|
54
|
-
command === "init"
|
|
75
|
+
command === "init" ||
|
|
76
|
+
command === "artifact" ||
|
|
77
|
+
command === "envelope" ||
|
|
78
|
+
command === "export-run" ||
|
|
79
|
+
command === "skills") {
|
|
55
80
|
return { help: false, command };
|
|
56
81
|
}
|
|
57
82
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -61,6 +86,7 @@ function parseArgs(argv) {
|
|
|
61
86
|
let pipeline;
|
|
62
87
|
let port;
|
|
63
88
|
let checkout;
|
|
89
|
+
let mcpStateless = false;
|
|
64
90
|
for (let i = 1; i < args.length; i++) {
|
|
65
91
|
if (args[i] === "--task") {
|
|
66
92
|
task = args[++i];
|
|
@@ -82,8 +108,11 @@ function parseArgs(argv) {
|
|
|
82
108
|
throw new Error(`Invalid --port: ${raw}`);
|
|
83
109
|
}
|
|
84
110
|
}
|
|
111
|
+
else if (args[i] === "--mcp-stateless") {
|
|
112
|
+
mcpStateless = true;
|
|
113
|
+
}
|
|
85
114
|
}
|
|
86
|
-
return { help: false, command, task, pipeline, port, checkout };
|
|
115
|
+
return { help: false, command, task, pipeline, port, checkout, mcpStateless };
|
|
87
116
|
}
|
|
88
117
|
export function parseRunStageArgs(argv) {
|
|
89
118
|
let runId;
|
|
@@ -225,17 +254,45 @@ async function main(argv) {
|
|
|
225
254
|
isGitProject: ctx.isGitProject,
|
|
226
255
|
});
|
|
227
256
|
}
|
|
257
|
+
if (parsed.command === "artifact") {
|
|
258
|
+
return runArtifactCommand(argv.slice(3), {
|
|
259
|
+
cwd: ctx.invocationCwd,
|
|
260
|
+
projectRoot: ctx.projectRoot,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (parsed.command === "envelope") {
|
|
264
|
+
return runEnvelopeCommand(argv.slice(3), {
|
|
265
|
+
cwd: ctx.invocationCwd,
|
|
266
|
+
projectRoot: ctx.projectRoot,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (parsed.command === "export-run") {
|
|
270
|
+
return runExportRunCommand(argv.slice(3), {
|
|
271
|
+
cwd: ctx.invocationCwd,
|
|
272
|
+
projectRoot: ctx.projectRoot,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
228
275
|
if (parsed.command === "providers") {
|
|
229
276
|
return runProvidersCommand(argv.slice(3), ctx.invocationCwd);
|
|
230
277
|
}
|
|
278
|
+
if (parsed.command === "skills") {
|
|
279
|
+
return runSkillsCommand(argv.slice(3), {
|
|
280
|
+
cwd: ctx.invocationCwd,
|
|
281
|
+
projectRoot: ctx.projectRoot,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
231
284
|
const store = createRunStore({ rootDir: ctx.projectRoot });
|
|
232
285
|
if (parsed.command === "ui") {
|
|
286
|
+
const mcpStateless = resolveMcpStateless({
|
|
287
|
+
mcpStateless: parsed.mcpStateless,
|
|
288
|
+
});
|
|
233
289
|
const { url, mcpUrl } = await startUiServer({
|
|
234
290
|
agent: new PiAgentAdapter(),
|
|
235
291
|
store,
|
|
236
292
|
cwd: ctx.invocationCwd,
|
|
237
293
|
rootDir: ctx.projectRoot,
|
|
238
294
|
port: parsed.port,
|
|
295
|
+
mcpStateless,
|
|
239
296
|
});
|
|
240
297
|
console.log(`Operator console: ${url}`);
|
|
241
298
|
console.log(`MCP endpoint: ${mcpUrl}`);
|
|
@@ -243,6 +300,22 @@ async function main(argv) {
|
|
|
243
300
|
await new Promise(() => undefined);
|
|
244
301
|
return 0;
|
|
245
302
|
}
|
|
303
|
+
if (parsed.command === "mcp") {
|
|
304
|
+
const mcpStateless = resolveMcpStateless({
|
|
305
|
+
mcpStateless: parsed.mcpStateless,
|
|
306
|
+
});
|
|
307
|
+
const { mcpUrl } = await startMcpServer({
|
|
308
|
+
agent: new PiAgentAdapter(),
|
|
309
|
+
store,
|
|
310
|
+
cwd: ctx.invocationCwd,
|
|
311
|
+
rootDir: ctx.projectRoot,
|
|
312
|
+
port: parsed.port,
|
|
313
|
+
mcpStateless,
|
|
314
|
+
});
|
|
315
|
+
console.log(`MCP endpoint: ${mcpUrl}`);
|
|
316
|
+
await new Promise(() => undefined);
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
246
319
|
if (parsed.command === "internal") {
|
|
247
320
|
const subArgs = argv.slice(3);
|
|
248
321
|
const subcommand = subArgs[0];
|
|
@@ -96,7 +96,11 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
96
96
|
},
|
|
97
97
|
]);
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
const stage = {
|
|
100
|
+
...stageOutcome.value,
|
|
101
|
+
...(entry.skill !== undefined ? { skill: entry.skill } : {}),
|
|
102
|
+
};
|
|
103
|
+
stages.push(stage);
|
|
100
104
|
stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
|
|
101
105
|
}
|
|
102
106
|
const pipeline = {
|
|
@@ -28,7 +28,21 @@ function extractBodyRaw(raw) {
|
|
|
28
28
|
return body;
|
|
29
29
|
}
|
|
30
30
|
function hasBodyKey(raw) {
|
|
31
|
-
return Object.keys(raw).some((key) => BODY_KEYS.has(key));
|
|
31
|
+
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill");
|
|
32
|
+
}
|
|
33
|
+
function readSkill(raw) {
|
|
34
|
+
if (raw.skill === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof raw.skill !== "string" || raw.skill.trim() === "") {
|
|
37
|
+
return loadFailure([
|
|
38
|
+
{
|
|
39
|
+
code: "stage.invalid_skill",
|
|
40
|
+
message: "skill must be a non-empty string",
|
|
41
|
+
category: "stage",
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
return loadSuccess(raw.skill.trim());
|
|
32
46
|
}
|
|
33
47
|
function stringStageRefMessage(index, declaringPath, entry) {
|
|
34
48
|
const hint = entry
|
|
@@ -74,6 +88,11 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
74
88
|
}
|
|
75
89
|
const uses = typeof raw.uses === "string" ? raw.uses : undefined;
|
|
76
90
|
const hasBody = hasBodyKey(raw);
|
|
91
|
+
const skillOutcome = readSkill(raw);
|
|
92
|
+
if (skillOutcome !== undefined && !skillOutcome.ok) {
|
|
93
|
+
return skillOutcome;
|
|
94
|
+
}
|
|
95
|
+
const skill = skillOutcome?.ok ? skillOutcome.value : undefined;
|
|
77
96
|
if (uses && hasBody) {
|
|
78
97
|
return loadFailure([
|
|
79
98
|
{
|
|
@@ -161,6 +180,9 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
161
180
|
body,
|
|
162
181
|
...(typeof raw.needs === "string" ? { needs: raw.needs } : {}),
|
|
163
182
|
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
183
|
+
...(raw.clonable !== undefined ? { clonable: raw.clonable } : {}),
|
|
184
|
+
...(raw.clone_cap !== undefined ? { clone_cap: raw.clone_cap } : {}),
|
|
185
|
+
...(skill !== undefined ? { skill } : {}),
|
|
164
186
|
};
|
|
165
187
|
const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
|
|
166
188
|
if (priorPath) {
|
|
@@ -182,5 +204,7 @@ export function toWiringRefs(entries) {
|
|
|
182
204
|
id: entry.id,
|
|
183
205
|
...(entry.needs !== undefined ? { needs: entry.needs } : {}),
|
|
184
206
|
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
207
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
208
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
185
209
|
}));
|
|
186
210
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ForkEmitContext } from "../types/forkChoice.js";
|
|
1
|
+
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
2
2
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
3
|
+
export declare function resolveCloneEmitContext(dag: ResolvedPipelineDag, stageId: string): CloneEmitContext | undefined;
|
|
3
4
|
export declare function resolveForkEmitContext(dag: ResolvedPipelineDag, stageId: string): ForkEmitContext | undefined;
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
export function resolveCloneEmitContext(dag, stageId) {
|
|
2
|
+
const childIds = dag.childrenOf[stageId] ?? [];
|
|
3
|
+
const clonableSuccessors = [];
|
|
4
|
+
for (const childId of childIds) {
|
|
5
|
+
const node = dag.nodes.find((n) => n.id === childId);
|
|
6
|
+
if (node?.clonable === true) {
|
|
7
|
+
clonableSuccessors.push({
|
|
8
|
+
successorId: childId,
|
|
9
|
+
cloneCap: node.clone_cap,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (clonableSuccessors.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { clonableSuccessors };
|
|
16
|
+
}
|
|
1
17
|
export function resolveForkEmitContext(dag, stageId) {
|
|
2
18
|
const dagNode = dag.nodes.find((n) => n.id === stageId);
|
|
3
19
|
if (!dagNode?.fork)
|
|
4
20
|
return undefined;
|
|
21
|
+
const immediateSuccessorIds = (dag.childrenOf[stageId] ?? []).filter((childId) => {
|
|
22
|
+
const child = dag.nodes.find((n) => n.id === childId);
|
|
23
|
+
return child?.clonable !== true;
|
|
24
|
+
});
|
|
25
|
+
if (immediateSuccessorIds.length === 0)
|
|
26
|
+
return undefined;
|
|
5
27
|
return {
|
|
6
|
-
immediateSuccessorIds
|
|
28
|
+
immediateSuccessorIds,
|
|
7
29
|
forkShape: {
|
|
8
30
|
cardinality: dagNode.fork.select,
|
|
9
31
|
allowNone: dagNode.fork.allow_none,
|
|
@@ -49,8 +49,16 @@ export function parsePipelineStageEntries(raw, ctx) {
|
|
|
49
49
|
}
|
|
50
50
|
forkValue = entry.fork;
|
|
51
51
|
}
|
|
52
|
+
const clonableFields = {
|
|
53
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
54
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
55
|
+
};
|
|
52
56
|
if (entry.needs === undefined) {
|
|
53
|
-
entries.push(
|
|
57
|
+
entries.push({
|
|
58
|
+
id: entry.id,
|
|
59
|
+
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
60
|
+
...clonableFields,
|
|
61
|
+
});
|
|
54
62
|
continue;
|
|
55
63
|
}
|
|
56
64
|
if (Array.isArray(entry.needs)) {
|
|
@@ -62,9 +70,12 @@ export function parsePipelineStageEntries(raw, ctx) {
|
|
|
62
70
|
if (typeof entry.needs !== "string" || !entry.needs) {
|
|
63
71
|
throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a non-empty string`));
|
|
64
72
|
}
|
|
65
|
-
entries.push(
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
entries.push({
|
|
74
|
+
id: entry.id,
|
|
75
|
+
needs: entry.needs,
|
|
76
|
+
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
77
|
+
...clonableFields,
|
|
78
|
+
});
|
|
68
79
|
}
|
|
69
80
|
return entries;
|
|
70
81
|
}
|
|
@@ -74,6 +85,8 @@ function normalizeToEdges(entries) {
|
|
|
74
85
|
needs: entry.needs ?? null,
|
|
75
86
|
stageIndex: index,
|
|
76
87
|
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
88
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
89
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
77
90
|
}));
|
|
78
91
|
}
|
|
79
92
|
function detectDuplicateIds(edges, ctx) {
|
|
@@ -213,6 +226,9 @@ function buildResolvedPipelineDag(edges) {
|
|
|
213
226
|
...(edge.fork !== undefined
|
|
214
227
|
? { fork: { select: edge.fork.select, allow_none: edge.fork.allow_none ?? false } }
|
|
215
228
|
: {}),
|
|
229
|
+
...(edge.clonable === true
|
|
230
|
+
? { clonable: true, clone_cap: edge.clone_cap ?? 5 }
|
|
231
|
+
: {}),
|
|
216
232
|
}));
|
|
217
233
|
return { nodes, roots, childrenOf };
|
|
218
234
|
}
|
|
@@ -228,6 +244,21 @@ function validateForkFields(edges, dag, ctx) {
|
|
|
228
244
|
}
|
|
229
245
|
}
|
|
230
246
|
}
|
|
247
|
+
function validateClonableFields(edges, dag, ctx) {
|
|
248
|
+
for (const edge of edges) {
|
|
249
|
+
if (edge.clone_cap !== undefined && edge.clonable !== true) {
|
|
250
|
+
throw new Error(formatError(ctx, `stage "${edge.id}": clone_cap requires clonable: true`));
|
|
251
|
+
}
|
|
252
|
+
if (edge.clonable === true && edge.clone_cap !== undefined) {
|
|
253
|
+
if (!Number.isInteger(edge.clone_cap) || edge.clone_cap < 2) {
|
|
254
|
+
throw new Error(formatError(ctx, `stage "${edge.id}": clone_cap must be an integer greater than or equal to 2`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (edge.clonable === true && (dag.childrenOf[edge.id] ?? []).length === 0) {
|
|
258
|
+
throw new Error(formatError(ctx, `clonable on stage "${edge.id}": no children in the DAG`));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
231
262
|
export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
232
263
|
const edges = normalizeToEdges(refs);
|
|
233
264
|
detectDuplicateIds(edges, ctx);
|
|
@@ -239,6 +270,7 @@ export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
|
239
270
|
.map((edge) => edge.id);
|
|
240
271
|
const dag = buildResolvedPipelineDag(edges);
|
|
241
272
|
validateForkFields(edges, dag, ctx);
|
|
273
|
+
validateClonableFields(edges, dag, ctx);
|
|
242
274
|
return { stages, dag };
|
|
243
275
|
}
|
|
244
276
|
export function resolvePipelineDag(rawStages, ctx) {
|
package/dist/envelope/check.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { type StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { CloneForkItem } from "../types/forkChoice.js";
|
|
3
|
+
export declare function parseCloneForks(raw: unknown): CloneForkItem[];
|
|
2
4
|
export declare function assertRequiredEnvelope(value: unknown): StageEnvelope;
|
|
3
5
|
export declare function isAdvancingEnvelope(envelope: StageEnvelope): boolean;
|
package/dist/envelope/check.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { EnvelopeError, } from "../types/envelope.js";
|
|
2
|
+
function parseCloneForkItem(raw, index) {
|
|
3
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4
|
+
throw new EnvelopeError(`clone_forks[${index}] must be an object`);
|
|
5
|
+
}
|
|
6
|
+
const record = raw;
|
|
7
|
+
if (typeof record.successor_id !== "string") {
|
|
8
|
+
throw new EnvelopeError(`clone_forks[${index}].successor_id must be a string`);
|
|
9
|
+
}
|
|
10
|
+
const action = record.action;
|
|
11
|
+
if (action !== "skip" && action !== "once" && action !== "fanout") {
|
|
12
|
+
throw new EnvelopeError(`clone_forks[${index}].action must be "skip", "once", or "fanout"`);
|
|
13
|
+
}
|
|
14
|
+
const item = {
|
|
15
|
+
successor_id: record.successor_id,
|
|
16
|
+
action,
|
|
17
|
+
};
|
|
18
|
+
if (record.envelope !== undefined) {
|
|
19
|
+
item.envelope = assertRequiredEnvelope(record.envelope);
|
|
20
|
+
}
|
|
21
|
+
if (record.mode !== undefined) {
|
|
22
|
+
if (record.mode !== "parallel" && record.mode !== "sequential") {
|
|
23
|
+
throw new EnvelopeError(`clone_forks[${index}].mode must be "parallel" or "sequential"`);
|
|
24
|
+
}
|
|
25
|
+
item.mode = record.mode;
|
|
26
|
+
}
|
|
27
|
+
if (record.clones !== undefined) {
|
|
28
|
+
if (!Array.isArray(record.clones)) {
|
|
29
|
+
throw new EnvelopeError(`clone_forks[${index}].clones must be an array`);
|
|
30
|
+
}
|
|
31
|
+
item.clones = record.clones.map((clone, cloneIndex) => {
|
|
32
|
+
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
33
|
+
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}] must be an object`);
|
|
34
|
+
}
|
|
35
|
+
const cloneRecord = clone;
|
|
36
|
+
if (cloneRecord.envelope === undefined) {
|
|
37
|
+
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}].envelope is required`);
|
|
38
|
+
}
|
|
39
|
+
return { envelope: assertRequiredEnvelope(cloneRecord.envelope) };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
44
|
+
export function parseCloneForks(raw) {
|
|
45
|
+
if (!Array.isArray(raw)) {
|
|
46
|
+
throw new EnvelopeError("clone_forks must be an array");
|
|
47
|
+
}
|
|
48
|
+
return raw.map((item, index) => parseCloneForkItem(item, index));
|
|
49
|
+
}
|
|
2
50
|
export function assertRequiredEnvelope(value) {
|
|
3
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4
52
|
throw new EnvelopeError("envelope must be an object");
|
|
@@ -32,6 +80,9 @@ export function assertRequiredEnvelope(value) {
|
|
|
32
80
|
}
|
|
33
81
|
envelope.fork_choice = record.fork_choice;
|
|
34
82
|
}
|
|
83
|
+
if (record.clone_forks !== undefined) {
|
|
84
|
+
envelope.clone_forks = parseCloneForks(record.clone_forks);
|
|
85
|
+
}
|
|
35
86
|
if (record.payload !== undefined) {
|
|
36
87
|
if (record.payload === null ||
|
|
37
88
|
typeof record.payload !== "object" ||
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseCloneForks } from "./check.js";
|
|
2
|
+
import { EnvelopeError } from "../types/envelope.js";
|
|
3
|
+
function fieldPresent(item, key) {
|
|
4
|
+
return item[key] !== undefined;
|
|
5
|
+
}
|
|
6
|
+
export function assertCloneForks(params, cloneEmitContext) {
|
|
7
|
+
const record = params;
|
|
8
|
+
if (record.status === "failure")
|
|
9
|
+
return;
|
|
10
|
+
if (record.clone_forks === undefined || !Array.isArray(record.clone_forks)) {
|
|
11
|
+
throw new EnvelopeError("clonable successor requires clone_forks on success");
|
|
12
|
+
}
|
|
13
|
+
const items = parseCloneForks(record.clone_forks);
|
|
14
|
+
if (items.length === 0 && cloneEmitContext.clonableSuccessors.length > 0) {
|
|
15
|
+
throw new EnvelopeError("clone_forks must not be empty when clonable successors exist");
|
|
16
|
+
}
|
|
17
|
+
const expectedIds = cloneEmitContext.clonableSuccessors.map((s) => s.successorId);
|
|
18
|
+
const expectedSet = new Set(expectedIds);
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
if (seen.has(item.successor_id)) {
|
|
22
|
+
throw new EnvelopeError(`clone_forks contains duplicate successor_id: ${item.successor_id}`);
|
|
23
|
+
}
|
|
24
|
+
if (!expectedSet.has(item.successor_id)) {
|
|
25
|
+
throw new EnvelopeError(`clone_forks contains extra successor_id: ${item.successor_id}`);
|
|
26
|
+
}
|
|
27
|
+
seen.add(item.successor_id);
|
|
28
|
+
const capEntry = cloneEmitContext.clonableSuccessors.find((s) => s.successorId === item.successor_id);
|
|
29
|
+
const cloneCap = capEntry?.cloneCap;
|
|
30
|
+
if (item.action === "skip") {
|
|
31
|
+
if (fieldPresent(item, "envelope") ||
|
|
32
|
+
fieldPresent(item, "mode") ||
|
|
33
|
+
fieldPresent(item, "clones")) {
|
|
34
|
+
throw new EnvelopeError(`clone_forks skip for ${item.successor_id} must not include envelope, mode, or clones`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (item.action === "once") {
|
|
38
|
+
if (!fieldPresent(item, "envelope")) {
|
|
39
|
+
throw new EnvelopeError(`clone_forks once for ${item.successor_id} requires envelope`);
|
|
40
|
+
}
|
|
41
|
+
if (fieldPresent(item, "mode") || fieldPresent(item, "clones")) {
|
|
42
|
+
throw new EnvelopeError(`clone_forks once for ${item.successor_id} must not include mode or clones`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (item.action === "fanout") {
|
|
46
|
+
if (fieldPresent(item, "envelope")) {
|
|
47
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} must not include a top-level envelope`);
|
|
48
|
+
}
|
|
49
|
+
if (!fieldPresent(item, "mode") || !fieldPresent(item, "clones") || item.clones === undefined) {
|
|
50
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} requires mode and clones`);
|
|
51
|
+
}
|
|
52
|
+
if (cloneCap === undefined || item.clones.length < 2 || item.clones.length > cloneCap) {
|
|
53
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} must have between 2 and ${cloneCap} clones`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const id of expectedIds) {
|
|
58
|
+
if (!seen.has(id)) {
|
|
59
|
+
throw new EnvelopeError(`clone_forks missing successor_id: ${id}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -7,7 +7,7 @@ function assertForkChoiceLegality(choices, forkEmitContext) {
|
|
|
7
7
|
const { immediateSuccessorIds, forkShape } = forkEmitContext;
|
|
8
8
|
for (const id of choices) {
|
|
9
9
|
if (!immediateSuccessorIds.includes(id)) {
|
|
10
|
-
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}`);
|
|
10
|
+
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}; clonable successors belong in clone_forks`);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
if (choices.length === 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -15,3 +15,5 @@ export type { RunStore, RunSummary, RunDetail, RunMeta, RunStatus, StageSnapshot
|
|
|
15
15
|
export { deriveStatusFromStages } from "./runstore/port.js";
|
|
16
16
|
export { RunManager } from "./runtime/runManager.js";
|
|
17
17
|
export { startUiServer } from "./server/http.js";
|
|
18
|
+
export { startMcpServer } from "./server/mcpHost.js";
|
|
19
|
+
export { DEFAULT_PORT } from "./server/mcpHost.js";
|
package/dist/index.js
CHANGED
|
@@ -9,3 +9,5 @@ export { createRunStore } from "./runstore/createStore.js";
|
|
|
9
9
|
export { deriveStatusFromStages } from "./runstore/port.js";
|
|
10
10
|
export { RunManager } from "./runtime/runManager.js";
|
|
11
11
|
export { startUiServer } from "./server/http.js";
|
|
12
|
+
export { startMcpServer } from "./server/mcpHost.js";
|
|
13
|
+
export { DEFAULT_PORT } from "./server/mcpHost.js";
|