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.
Files changed (153) hide show
  1. package/README.md +22 -1
  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/skillsCommand.d.ts +16 -0
  23. package/dist/cli/skillsCommand.js +459 -0
  24. package/dist/cli.js +78 -5
  25. package/dist/config/loadPipeline.js +5 -1
  26. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  27. package/dist/config/normalizePipelineStageEntry.js +25 -1
  28. package/dist/config/pipelineStageKeys.js +1 -1
  29. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  30. package/dist/config/resolveForkEmitContext.js +23 -1
  31. package/dist/config/resolvePipelineDag.js +36 -4
  32. package/dist/envelope/check.d.ts +2 -0
  33. package/dist/envelope/check.js +51 -0
  34. package/dist/envelope/cloneForks.d.ts +2 -0
  35. package/dist/envelope/cloneForks.js +62 -0
  36. package/dist/envelope/forkChoice.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +2 -0
  39. package/dist/mcp/catalogTools.d.ts +3 -0
  40. package/dist/mcp/catalogTools.js +187 -0
  41. package/dist/mcp/controlTools.d.ts +3 -0
  42. package/dist/mcp/controlTools.js +147 -0
  43. package/dist/mcp/deps.d.ts +11 -0
  44. package/dist/mcp/deps.js +1 -0
  45. package/dist/mcp/projectRun.d.ts +1 -40
  46. package/dist/mcp/projectRun.js +1 -64
  47. package/dist/mcp/resources.d.ts +5 -0
  48. package/dist/mcp/resources.js +45 -0
  49. package/dist/mcp/server.d.ts +16 -3
  50. package/dist/mcp/server.js +159 -3
  51. package/dist/mcp/toolResults.d.ts +7 -0
  52. package/dist/mcp/toolResults.js +6 -0
  53. package/dist/mcp/tools.d.ts +2 -7
  54. package/dist/mcp/tools.js +52 -103
  55. package/dist/mcp/waitRun.d.ts +46 -0
  56. package/dist/mcp/waitRun.js +146 -0
  57. package/dist/mcp/waitingGates.d.ts +12 -0
  58. package/dist/mcp/waitingGates.js +83 -0
  59. package/dist/projection/projectRun.d.ts +44 -0
  60. package/dist/projection/projectRun.js +71 -0
  61. package/dist/prompt/priorEnvelope.d.ts +1 -1
  62. package/dist/prompt/priorEnvelope.js +4 -1
  63. package/dist/runstore/paths.js +2 -0
  64. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  65. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  66. package/dist/runstore/port.d.ts +13 -2
  67. package/dist/runstore/port.js +4 -1
  68. package/dist/runstore/runProjection.d.ts +1 -1
  69. package/dist/runstore/runProjection.js +14 -3
  70. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  71. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  72. package/dist/runstore/stageInstanceId.d.ts +4 -0
  73. package/dist/runstore/stageInstanceId.js +23 -0
  74. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  75. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  76. package/dist/runstore/trackProjection.js +28 -12
  77. package/dist/runstore/workspaceLayout.d.ts +1 -0
  78. package/dist/runstore/workspaceLayout.js +1 -1
  79. package/dist/runtime/cloneSchedule.d.ts +18 -0
  80. package/dist/runtime/cloneSchedule.js +233 -0
  81. package/dist/runtime/envelopeRouting.d.ts +2 -1
  82. package/dist/runtime/envelopeRouting.js +80 -13
  83. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  84. package/dist/runtime/pipelineScheduler.js +166 -30
  85. package/dist/runtime/resumeReconstruct.js +8 -3
  86. package/dist/runtime/runChangeBus.d.ts +18 -0
  87. package/dist/runtime/runChangeBus.js +83 -0
  88. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  89. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  90. package/dist/runtime/stageRunner.d.ts +1 -0
  91. package/dist/runtime/stageRunner.js +17 -15
  92. package/dist/runtime/stageWorker.js +11 -4
  93. package/dist/server/bootstrap.d.ts +32 -0
  94. package/dist/server/bootstrap.js +56 -0
  95. package/dist/server/createHttpHost.d.ts +34 -0
  96. package/dist/server/createHttpHost.js +70 -0
  97. package/dist/server/http.d.ts +6 -13
  98. package/dist/server/http.js +295 -374
  99. package/dist/server/mcpHost.d.ts +22 -0
  100. package/dist/server/mcpHost.js +21 -0
  101. package/dist/server/operatorResults.d.ts +13 -0
  102. package/dist/server/operatorResults.js +22 -0
  103. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  104. package/dist/tools/emitStageEnvelope.js +22 -6
  105. package/dist/types/envelope.d.ts +2 -0
  106. package/dist/types/forkChoice.d.ts +22 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  109. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +2 -1
  112. package/skills/install-suite.sh +107 -0
  113. package/skills/stageflow/SKILL.md +26 -0
  114. package/skills/stageflow/references/control-surface.md +26 -0
  115. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  116. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  117. package/skills/stageflow-author/SKILL.md +58 -0
  118. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  119. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  120. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  121. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  122. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  123. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  124. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  125. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  126. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  127. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  128. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  130. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  131. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  132. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  133. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  134. package/skills/stageflow-delegate/SKILL.md +18 -0
  135. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  136. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  137. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  138. package/skills/stageflow-run/SKILL.md +151 -0
  139. package/skills/stageflow-run/references/mcp-call.md +17 -0
  140. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  141. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  142. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  143. package/skills/stageflow-session-capture/SKILL.md +94 -0
  144. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  145. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  146. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  147. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  148. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  149. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  150. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  151. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  152. package/skills/stageflow-setup/SKILL.md +110 -0
  153. 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 ui [--port ${DEFAULT_PORT}]
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
- ${PROVIDERS_USAGE}`;
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
- stages.push(stageOutcome.value);
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 = {
@@ -13,4 +13,6 @@ export declare function toWiringRefs(entries: NormalizedPipelineStageEntry[]): A
13
13
  select: "one" | "subset";
14
14
  allow_none?: boolean;
15
15
  };
16
+ clonable?: boolean;
17
+ clone_cap?: number;
16
18
  }>;
@@ -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,4 +1,4 @@
1
- export const WIRING_KEYS = new Set(["id", "needs", "fork", "uses"]);
1
+ export const WIRING_KEYS = new Set(["id", "needs", "fork", "uses", "clonable", "clone_cap"]);
2
2
  export const BODY_KEYS = new Set([
3
3
  "system_prompt",
4
4
  "model",
@@ -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: dag.childrenOf[stageId] ?? [],
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(forkValue !== undefined ? { id: entry.id, fork: forkValue } : { id: entry.id });
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(forkValue !== undefined
66
- ? { id: entry.id, needs: entry.needs, fork: forkValue }
67
- : { id: entry.id, needs: entry.needs });
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) {
@@ -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;
@@ -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,2 @@
1
+ import type { CloneEmitContext } from "../types/forkChoice.js";
2
+ export declare function assertCloneForks(params: unknown, cloneEmitContext: CloneEmitContext): void;
@@ -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";
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export declare function registerCatalogTools(server: McpServer, deps: McpToolDeps): void;