stageflow 0.3.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +44 -14
  2. package/dist/agent/activity.d.ts +2 -0
  3. package/dist/agent/cursorProvider.d.ts +1 -10
  4. package/dist/agent/cursorProvider.js +18 -10
  5. package/dist/agent/fakeAgent.js +11 -7
  6. package/dist/agent/piAdapter.d.ts +2 -1
  7. package/dist/agent/piAdapter.js +28 -25
  8. package/dist/agent/port.d.ts +6 -1
  9. package/dist/agent/port.js +3 -0
  10. package/dist/cli/artifactCommand.d.ts +10 -0
  11. package/dist/cli/artifactCommand.js +120 -0
  12. package/dist/cli/envelopeCommand.d.ts +32 -0
  13. package/dist/cli/envelopeCommand.js +285 -0
  14. package/dist/cli/exportRunCommand.d.ts +10 -0
  15. package/dist/cli/exportRunCommand.js +150 -0
  16. package/dist/cli/handoffFormat.d.ts +22 -0
  17. package/dist/cli/handoffFormat.js +61 -0
  18. package/dist/cli/runCommand.d.ts +5 -1
  19. package/dist/cli/runCommand.js +36 -5
  20. package/dist/cli/runOutput.d.ts +4 -1
  21. package/dist/cli/runOutput.js +36 -8
  22. package/dist/cli/runsCommand.d.ts +22 -0
  23. package/dist/cli/runsCommand.js +591 -0
  24. package/dist/cli/skillsCommand.d.ts +16 -0
  25. package/dist/cli/skillsCommand.js +459 -0
  26. package/dist/cli/validateOutput.d.ts +2 -2
  27. package/dist/cli/validateOutput.js +2 -2
  28. package/dist/cli.js +110 -5
  29. package/dist/config/loadPipeline.js +5 -1
  30. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  31. package/dist/config/normalizePipelineStageEntry.js +25 -1
  32. package/dist/config/pipelineStageKeys.js +1 -1
  33. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  34. package/dist/config/resolveForkEmitContext.js +23 -1
  35. package/dist/config/resolvePipelineDag.js +36 -4
  36. package/dist/envelope/check.d.ts +2 -0
  37. package/dist/envelope/check.js +51 -0
  38. package/dist/envelope/cloneForks.d.ts +2 -0
  39. package/dist/envelope/cloneForks.js +62 -0
  40. package/dist/envelope/forkChoice.js +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +3 -1
  43. package/dist/mcp/catalogTools.d.ts +3 -0
  44. package/dist/mcp/catalogTools.js +187 -0
  45. package/dist/mcp/controlTools.d.ts +3 -0
  46. package/dist/mcp/controlTools.js +147 -0
  47. package/dist/mcp/deps.d.ts +11 -0
  48. package/dist/mcp/deps.js +1 -0
  49. package/dist/mcp/projectRun.d.ts +1 -40
  50. package/dist/mcp/projectRun.js +1 -64
  51. package/dist/mcp/resources.d.ts +5 -0
  52. package/dist/mcp/resources.js +45 -0
  53. package/dist/mcp/server.d.ts +16 -3
  54. package/dist/mcp/server.js +159 -3
  55. package/dist/mcp/toolResults.d.ts +7 -0
  56. package/dist/mcp/toolResults.js +6 -0
  57. package/dist/mcp/tools.d.ts +2 -7
  58. package/dist/mcp/tools.js +52 -103
  59. package/dist/mcp/waitRun.d.ts +46 -0
  60. package/dist/mcp/waitRun.js +146 -0
  61. package/dist/mcp/waitingGates.d.ts +12 -0
  62. package/dist/mcp/waitingGates.js +83 -0
  63. package/dist/package-meta.d.ts +1 -0
  64. package/dist/package-meta.js +1 -0
  65. package/dist/projection/projectRun.d.ts +44 -0
  66. package/dist/projection/projectRun.js +71 -0
  67. package/dist/prompt/priorEnvelope.d.ts +1 -1
  68. package/dist/prompt/priorEnvelope.js +4 -1
  69. package/dist/runstore/paths.js +2 -0
  70. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  71. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  72. package/dist/runstore/port.d.ts +13 -2
  73. package/dist/runstore/port.js +4 -1
  74. package/dist/runstore/runProjection.d.ts +1 -1
  75. package/dist/runstore/runProjection.js +14 -3
  76. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  77. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  78. package/dist/runstore/stageInstanceId.d.ts +4 -0
  79. package/dist/runstore/stageInstanceId.js +23 -0
  80. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  81. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  82. package/dist/runstore/trackProjection.js +28 -12
  83. package/dist/runstore/workspaceLayout.d.ts +1 -0
  84. package/dist/runstore/workspaceLayout.js +1 -1
  85. package/dist/runtime/cloneSchedule.d.ts +18 -0
  86. package/dist/runtime/cloneSchedule.js +233 -0
  87. package/dist/runtime/envelopeRouting.d.ts +2 -1
  88. package/dist/runtime/envelopeRouting.js +80 -13
  89. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  90. package/dist/runtime/pipelineScheduler.js +166 -30
  91. package/dist/runtime/resumeReconstruct.js +8 -3
  92. package/dist/runtime/runChangeBus.d.ts +18 -0
  93. package/dist/runtime/runChangeBus.js +83 -0
  94. package/dist/runtime/runManager.d.ts +7 -0
  95. package/dist/runtime/runManager.js +31 -2
  96. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  97. package/dist/runtime/runRetryCoordinator.js +4 -1
  98. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  99. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  100. package/dist/runtime/stageRunner.d.ts +1 -0
  101. package/dist/runtime/stageRunner.js +17 -15
  102. package/dist/runtime/stageWorker.js +11 -4
  103. package/dist/server/bootstrap.d.ts +32 -0
  104. package/dist/server/bootstrap.js +56 -0
  105. package/dist/server/createHttpHost.d.ts +34 -0
  106. package/dist/server/createHttpHost.js +70 -0
  107. package/dist/server/http.d.ts +6 -13
  108. package/dist/server/http.js +295 -374
  109. package/dist/server/mcpHost.d.ts +22 -0
  110. package/dist/server/mcpHost.js +21 -0
  111. package/dist/server/operatorResults.d.ts +13 -0
  112. package/dist/server/operatorResults.js +22 -0
  113. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  114. package/dist/tools/emitStageEnvelope.js +22 -6
  115. package/dist/types/envelope.d.ts +2 -0
  116. package/dist/types/forkChoice.d.ts +22 -0
  117. package/dist/types/pipeline.d.ts +8 -0
  118. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  119. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  120. package/dist/ui/index.html +2 -2
  121. package/package.json +2 -1
  122. package/skills/install-suite.sh +107 -0
  123. package/skills/stageflow/SKILL.md +26 -0
  124. package/skills/stageflow/references/control-surface.md +28 -0
  125. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  126. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  127. package/skills/stageflow-author/SKILL.md +58 -0
  128. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  130. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  131. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  132. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  133. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  134. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  135. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  136. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  137. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  138. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  139. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  140. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  141. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  142. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  143. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  144. package/skills/stageflow-delegate/SKILL.md +18 -0
  145. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  146. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  147. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  148. package/skills/stageflow-run/SKILL.md +182 -0
  149. package/skills/stageflow-run/references/mcp-call.md +17 -0
  150. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  151. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  152. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  153. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  154. package/skills/stageflow-session-capture/SKILL.md +94 -0
  155. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  156. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  157. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  158. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  159. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  160. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  161. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  162. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  163. package/skills/stageflow-setup/SKILL.md +110 -0
  164. package/dist/ui/assets/index-CFSzDZje.js +0 -118
@@ -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
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
2
2
  export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
3
3
  export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
4
4
  export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
5
- export { PACKAGE_NAME } from "./package-meta.js";
5
+ export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
6
6
  export type { StageEnvelope } from "./types/envelope.js";
7
7
  export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
8
8
  export type { AgentPort } from "./agent/port.js";
@@ -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
@@ -2,10 +2,12 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
2
2
  export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
3
3
  export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
4
4
  export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
5
- export { PACKAGE_NAME } from "./package-meta.js";
5
+ export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
6
6
  export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
7
7
  export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
8
8
  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;
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+ import { browseCatalog } from "../config/browseCatalog.js";
3
+ import { loadPipeline } from "../config/loadPipeline.js";
4
+ import { validateCatalog } from "../config/validateCatalog.js";
5
+ import { projectRunForMcp } from "./projectRun.js";
6
+ import { readRunArtifact } from "./readArtifact.js";
7
+ import { textResult } from "./toolResults.js";
8
+ const taskFileSchema = z.object({
9
+ id: z.string(),
10
+ goal: z.string(),
11
+ context: z.string().optional(),
12
+ constraints: z.string().optional(),
13
+ checkout: z.string().optional(),
14
+ });
15
+ const startRunSchema = z
16
+ .object({
17
+ pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
18
+ task_path: z.string().optional(),
19
+ task: taskFileSchema.optional(),
20
+ })
21
+ .refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
22
+ message: "Exactly one of task_path or task is required",
23
+ });
24
+ const runStatusSchema = z.enum(["created", "running", "succeeded", "failed"]);
25
+ export function registerCatalogTools(server, deps) {
26
+ const { manager, store, cwd } = deps;
27
+ server.registerTool("list_pipelines", {
28
+ description: "List manifest-declared pipeline paths from the project catalog",
29
+ inputSchema: z.object({}),
30
+ }, async () => {
31
+ const catalog = await browseCatalog(cwd);
32
+ return textResult({ pipelines: catalog.pipelines });
33
+ });
34
+ server.registerTool("list_tasks", {
35
+ description: "List manifest-declared task paths from the project catalog",
36
+ inputSchema: z.object({}),
37
+ }, async () => {
38
+ const catalog = await browseCatalog(cwd);
39
+ return textResult({ tasks: catalog.tasks });
40
+ });
41
+ server.registerTool("list_runs", {
42
+ description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path).",
43
+ inputSchema: z.object({
44
+ status: runStatusSchema.optional(),
45
+ since: z.string().optional(),
46
+ pipeline: z.string().optional(),
47
+ }),
48
+ }, async ({ status, since, pipeline }) => {
49
+ const filter = {};
50
+ if (status !== undefined)
51
+ filter.status = status;
52
+ if (since !== undefined) {
53
+ if (!Number.isFinite(Date.parse(since))) {
54
+ return textResult({ error: "since must be a valid date", status: 400 }, true);
55
+ }
56
+ filter.since = since;
57
+ }
58
+ if (pipeline !== undefined)
59
+ filter.pipeline = pipeline;
60
+ const runs = await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined);
61
+ return textResult({ runs });
62
+ });
63
+ server.registerTool("get_health", {
64
+ description: "Server health and soft-max run capacity: activeRunIds, activeCount, maxConcurrent, slotsAvailable. Start until slotsAvailable is 0; then wait for a run to finish or raise STAGEFLOW_MAX_CONCURRENT_RUNS.",
65
+ inputSchema: z.object({}),
66
+ }, async () => textResult(manager.getHealth()));
67
+ server.registerTool("start_run", {
68
+ description: "Start a pipeline run using a filesystem pipeline path and either task_path (catalog task file) or an inline task object. Returns { runId }. On conflict returns isError with code busy_capacity (soft max full) or busy_checkout (same checkout leased), plus activeCount/maxConcurrent/activeRunIds and optional conflictingRunId/conflictingCheckout.",
69
+ inputSchema: startRunSchema,
70
+ }, async ({ pipeline, task_path, task }) => {
71
+ if (!pipeline.trim()) {
72
+ return textResult({ error: "pipeline is required" }, true);
73
+ }
74
+ const taskInput = task_path ?? task;
75
+ if (taskInput === undefined) {
76
+ return textResult({ error: "Exactly one of task_path or task is required" }, true);
77
+ }
78
+ const result = await manager.startRun({ pipeline, task: taskInput });
79
+ if (!result.ok) {
80
+ const { ok: _ok, reason, ...rest } = result;
81
+ return textResult({ error: reason, ...rest }, true);
82
+ }
83
+ return textResult({ runId: result.runId });
84
+ });
85
+ server.registerTool("get_run", {
86
+ description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events / get_envelope for timelines and full envelopes.",
87
+ inputSchema: z.object({
88
+ runId: z.string(),
89
+ }),
90
+ }, async ({ runId }) => {
91
+ try {
92
+ const detail = await store.readRun(runId);
93
+ return textResult(projectRunForMcp(detail));
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ const notFound = /not found|no such|unknown run/i.test(message);
98
+ return textResult({
99
+ error: message,
100
+ status: notFound ? 404 : 500,
101
+ }, true);
102
+ }
103
+ });
104
+ server.registerTool("read_artifact", {
105
+ description: "Read a text artifact from a run workspace by relative path (contained under the run workspace).",
106
+ inputSchema: z.object({
107
+ runId: z.string(),
108
+ path: z.string(),
109
+ }),
110
+ }, async ({ runId, path: artifactPath }) => {
111
+ try {
112
+ const content = await readRunArtifact(store, runId, artifactPath);
113
+ return textResult({ runId, path: artifactPath, content });
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ const notFound = message.startsWith("Run not found") ||
118
+ message.startsWith("Artifact not found") ||
119
+ /no such|not found/i.test(message);
120
+ return textResult({
121
+ error: message,
122
+ status: notFound ? 404 : 400,
123
+ }, true);
124
+ }
125
+ });
126
+ server.registerTool("validate", {
127
+ description: "Validate the project catalog (full), a pipeline path, or a task path. Returns ValidationResult JSON (ok, summary, findings).",
128
+ inputSchema: z.object({
129
+ pipeline: z.string().optional(),
130
+ task: z.string().optional(),
131
+ strict: z.boolean().optional(),
132
+ }),
133
+ }, async ({ pipeline, task, strict }) => {
134
+ try {
135
+ let scope = "full";
136
+ if (pipeline !== undefined && pipeline.trim())
137
+ scope = "pipeline";
138
+ else if (task !== undefined && task.trim())
139
+ scope = "task";
140
+ const result = await validateCatalog({
141
+ cwd,
142
+ scope,
143
+ ...(scope === "pipeline" ? { pipeline: pipeline.trim() } : {}),
144
+ ...(scope === "task" ? { task: task.trim() } : {}),
145
+ strict: strict ?? false,
146
+ });
147
+ return textResult(result);
148
+ }
149
+ catch (err) {
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ return textResult({ error: message, status: 400 }, true);
152
+ }
153
+ });
154
+ server.registerTool("describe_pipeline", {
155
+ description: "Describe a pipeline DAG: stages with needs, fork, clonable, clone_cap, and gate_kinds. Input is a filesystem pipeline path (same as start_run).",
156
+ inputSchema: z.object({
157
+ pipeline: z.string(),
158
+ }),
159
+ }, async ({ pipeline }) => {
160
+ if (!pipeline.trim()) {
161
+ return textResult({ error: "pipeline is required", status: 400 }, true);
162
+ }
163
+ try {
164
+ const loaded = await loadPipeline(pipeline.trim(), { cwd });
165
+ const gateById = new Map(loaded.stages.map((s) => [s.id, s.gate_kinds]));
166
+ const stages = loaded.dag.nodes.map((node) => ({
167
+ id: node.id,
168
+ needs: node.needs,
169
+ ...(node.fork !== undefined ? { fork: node.fork } : {}),
170
+ ...(node.clonable !== undefined ? { clonable: node.clonable } : {}),
171
+ ...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
172
+ ...(gateById.get(node.id) !== undefined
173
+ ? { gate_kinds: gateById.get(node.id) }
174
+ : {}),
175
+ }));
176
+ return textResult({
177
+ id: loaded.pipeline.id,
178
+ path: loaded.pipelinePath,
179
+ stages,
180
+ });
181
+ }
182
+ catch (err) {
183
+ const message = err instanceof Error ? err.message : String(err);
184
+ return textResult({ error: message, status: 404 }, true);
185
+ }
186
+ });
187
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export declare function registerControlTools(server: McpServer, deps: McpToolDeps): void;