stageflow 0.2.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 (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,12 +1,18 @@
1
- import { readdir } from "node:fs/promises";
2
1
  import path from "node:path";
3
- import { loadPipelineOutcome, loadPipelineValidated } from "./loadPipeline.js";
2
+ import { loadPipelineOutcome, resolvePipelinePath } from "./loadPipeline.js";
4
3
  import { loadStageOutcome } from "./loadStage.js";
4
+ import { loadTaskOutcome } from "./loadTask.js";
5
5
  import { readYamlObject } from "./readYamlObject.js";
6
- import { extractPipelineStageIds } from "./resolvePipelineDag.js";
6
+ import { resolveCatalogContext } from "./resolveCatalogContext.js";
7
+ import { getCatalogScanPaths } from "./browseCatalog.js";
8
+ import { manifestPathForProject } from "./loadStageflowManifest.js";
7
9
  export function relPath(cwd, absPath) {
8
- return path.relative(cwd, absPath);
10
+ return path.relative(cwd, absPath).replace(/\\/g, "/");
9
11
  }
12
+ const STRICT_CATALOG_WARNINGS = new Set([
13
+ "catalog.manifest_missing",
14
+ "catalog.empty_catalog",
15
+ ]);
10
16
  const SEVERITY_RANK = {
11
17
  error: 0,
12
18
  warning: 1,
@@ -28,7 +34,7 @@ function sortFindings(findings) {
28
34
  export function effectiveSeverity(finding, strict) {
29
35
  if (strict &&
30
36
  finding.severity === "warning" &&
31
- finding.code === "catalog.orphan_stage") {
37
+ STRICT_CATALOG_WARNINGS.has(finding.code)) {
32
38
  return "error";
33
39
  }
34
40
  return finding.severity;
@@ -82,6 +88,24 @@ function findingStageError(cwd, absPath, message, code, stageId) {
82
88
  stageId,
83
89
  }, "error");
84
90
  }
91
+ function findingTaskError(cwd, absPath, message, code) {
92
+ return baseFinding({
93
+ cwd,
94
+ absPath,
95
+ message,
96
+ code,
97
+ category: "task",
98
+ }, "error");
99
+ }
100
+ function findingCatalog(cwd, absPath, message, code, severity) {
101
+ return baseFinding({
102
+ cwd,
103
+ absPath,
104
+ message,
105
+ code,
106
+ category: "catalog",
107
+ }, severity);
108
+ }
85
109
  export function findingsFromLoadIssues(cwd, absPath, issues) {
86
110
  return issues.flatMap((issue) => {
87
111
  if (issue.category === "pipeline") {
@@ -94,6 +118,18 @@ export function findingsFromLoadIssues(cwd, absPath, issues) {
94
118
  findingStageError(cwd, absPath, issue.message, issue.code, issue.stageId),
95
119
  ];
96
120
  }
121
+ if (issue.category === "task") {
122
+ return [
123
+ findingTaskError(cwd, absPath, issue.message, issue.code),
124
+ ];
125
+ }
126
+ if (issue.category === "catalog") {
127
+ const code = issue.code;
128
+ const severity = code === "catalog.manifest_missing" || code === "catalog.empty_catalog"
129
+ ? "warning"
130
+ : "error";
131
+ return [findingCatalog(cwd, absPath, issue.message, code, severity)];
132
+ }
97
133
  return [];
98
134
  });
99
135
  }
@@ -107,40 +143,6 @@ export function findingStageIdFilenameMismatch(cwd, absPath, fileStem, declaredI
107
143
  stageId: declaredId,
108
144
  }, "error");
109
145
  }
110
- function findingCatalogDuplicatePipelineId(cwd, absPath, pipelineId, conflictingPaths) {
111
- const relConflicts = conflictingPaths.map((p) => relPath(cwd, p));
112
- return baseFinding({
113
- cwd,
114
- absPath,
115
- message: `Duplicate pipeline id "${pipelineId}" declared in: ${relConflicts.join(", ")}`,
116
- code: "catalog.duplicate_pipeline_id",
117
- category: "catalog",
118
- pipelineId,
119
- }, "error");
120
- }
121
- function findingCatalogOrphanStage(cwd, absPath, stageId) {
122
- return baseFinding({
123
- cwd,
124
- absPath,
125
- message: `Stage "${stageId}" is not referenced by any pipeline`,
126
- code: "catalog.orphan_stage",
127
- category: "catalog",
128
- stageId,
129
- }, "warning");
130
- }
131
- async function listYamlFiles(dir) {
132
- let entries;
133
- try {
134
- entries = await readdir(dir, { withFileTypes: true });
135
- }
136
- catch {
137
- return [];
138
- }
139
- return entries
140
- .filter((entry) => entry.isFile() && entry.name.endsWith(".yaml"))
141
- .map((entry) => path.join(dir, entry.name))
142
- .sort((a, b) => a.localeCompare(b));
143
- }
144
146
  async function checkStageIdFilename(cwd, stagePath) {
145
147
  const fileStem = path.basename(stagePath, ".yaml");
146
148
  try {
@@ -169,52 +171,79 @@ async function validateStageFile(cwd, stagePath) {
169
171
  }
170
172
  return findings;
171
173
  }
172
- async function validatePipelineFile(pipelinePath, options) {
173
- const { cwd, stagesDir, validateStages } = options;
174
- const findings = [];
175
- const referencedStageIds = new Set();
176
- let pipelineId;
174
+ async function runPipelineValidation(nameOrPath, options) {
175
+ const { cwd, validateStages } = options;
176
+ let pipelinePath;
177
177
  try {
178
- const raw = await readYamlObject(pipelinePath);
179
- if (typeof raw?.id === "string") {
180
- pipelineId = raw.id;
181
- if (Array.isArray(raw.stages)) {
182
- const stageIds = extractPipelineStageIds(raw.stages);
183
- if (stageIds) {
184
- for (const stageId of stageIds) {
185
- referencedStageIds.add(stageId);
186
- }
187
- }
188
- }
189
- }
178
+ pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
190
179
  }
191
- catch {
192
- // loadPipelineOutcome will report read errors
180
+ catch (err) {
181
+ const message = err instanceof Error ? err.message : String(err);
182
+ return {
183
+ ok: false,
184
+ findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
185
+ {
186
+ code: "pipeline.load_error",
187
+ message,
188
+ category: "pipeline",
189
+ },
190
+ ]),
191
+ };
193
192
  }
194
- const outcome = await loadPipelineOutcome(pipelinePath, { cwd, stagesDir });
193
+ const outcome = await loadPipelineOutcome(pipelinePath, { cwd });
194
+ const findings = [];
195
195
  if (!outcome.ok) {
196
196
  findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
197
- if (validateStages) {
198
- for (const stageId of referencedStageIds) {
199
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
200
- findings.push(...(await validateStageFile(cwd, stagePath)));
197
+ return { ok: false, findings };
198
+ }
199
+ if (validateStages && outcome.value.stageSources) {
200
+ for (const source of Object.values(outcome.value.stageSources)) {
201
+ if (source.kind === "file") {
202
+ findings.push(...(await validateStageFile(cwd, source.path)));
201
203
  }
202
204
  }
203
- return { findings, pipelineId, referencedStageIds };
204
205
  }
205
- for (const stageId of outcome.value.pipeline.stages) {
206
- referencedStageIds.add(stageId);
207
- if (validateStages) {
208
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
209
- findings.push(...(await validateStageFile(cwd, stagePath)));
210
- }
206
+ if (findings.some((finding) => finding.severity === "error")) {
207
+ return { ok: false, findings };
211
208
  }
212
- return { findings, pipelineId: outcome.value.pipeline.id, referencedStageIds };
209
+ return { ok: true, loaded: outcome.value, findings };
210
+ }
211
+ export async function validatePipeline(nameOrPath, options = {}) {
212
+ const cwd = options.cwd ?? process.cwd();
213
+ const validateStages = options.validateStages ?? true;
214
+ const strict = options.strict ?? false;
215
+ const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
216
+ return buildValidationResult("pipeline", core.findings, strict);
213
217
  }
214
- async function collectPipelineIds(pipelinesDir) {
215
- const files = await listYamlFiles(pipelinesDir);
218
+ export async function loadPipelineValidated(nameOrPath, options = {}) {
219
+ const cwd = options.cwd ?? process.cwd();
220
+ const validateStages = options.validateStages ?? true;
221
+ const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
222
+ if (core.ok) {
223
+ return { ok: true, loaded: core.loaded };
224
+ }
225
+ return { ok: false, findings: core.findings };
226
+ }
227
+ function findingCatalogDuplicatePipelineId(cwd, absPath, pipelineId, conflictingPaths) {
228
+ const relConflicts = conflictingPaths.map((p) => relPath(cwd, p));
229
+ return baseFinding({
230
+ cwd,
231
+ absPath,
232
+ message: `Duplicate pipeline id "${pipelineId}" declared in: ${relConflicts.join(", ")}`,
233
+ code: "catalog.duplicate_pipeline_id",
234
+ category: "catalog",
235
+ pipelineId,
236
+ }, "error");
237
+ }
238
+ function findingManifestMissing(cwd, projectRoot) {
239
+ return findingCatalog(cwd, manifestPathForProject(projectRoot), "No stageflow.yaml manifest found at project root", "catalog.manifest_missing", "warning");
240
+ }
241
+ function findingEmptyCatalog(cwd, manifestPath, kind) {
242
+ return findingCatalog(cwd, manifestPath, `Manifest catalog.${kind} is empty`, "catalog.empty_catalog", "warning");
243
+ }
244
+ async function collectPipelineIdsFromPaths(pipelinePaths) {
216
245
  const idToPaths = new Map();
217
- for (const filePath of files) {
246
+ for (const filePath of pipelinePaths) {
218
247
  try {
219
248
  const raw = await readYamlObject(filePath);
220
249
  if (typeof raw?.id !== "string")
@@ -240,83 +269,91 @@ function findingsForDuplicatePipelineIds(cwd, idToPaths) {
240
269
  }
241
270
  return findings;
242
271
  }
243
- async function collectReferencedStageIds(pipelinesDir) {
244
- const files = await listYamlFiles(pipelinesDir);
245
- const referenced = new Set();
246
- for (const filePath of files) {
247
- try {
248
- const raw = await readYamlObject(filePath);
249
- if (typeof raw?.id !== "string" || !Array.isArray(raw.stages))
250
- continue;
251
- const stageIds = extractPipelineStageIds(raw.stages);
252
- if (!stageIds)
253
- continue;
254
- for (const stageId of stageIds) {
255
- referenced.add(stageId);
256
- }
272
+ async function validateManifestAll(invocationCwd) {
273
+ const ctx = await resolveCatalogContext(invocationCwd);
274
+ const findings = [];
275
+ if (ctx.manifestStatus === "not_git" || ctx.manifestStatus === "missing") {
276
+ if (ctx.projectRoot) {
277
+ findings.push(findingManifestMissing(invocationCwd, ctx.projectRoot));
257
278
  }
258
- catch {
259
- // skip unreadable
279
+ else {
280
+ findings.push(findingCatalog(invocationCwd, path.resolve(invocationCwd), "Not inside a git repository; no catalog manifest", "catalog.manifest_missing", "warning"));
260
281
  }
282
+ return findings;
261
283
  }
262
- return referenced;
263
- }
264
- async function findingsForOrphanStages(cwd, stagesDir, referencedStageIds) {
265
- const findings = [];
266
- const stageFiles = await listYamlFiles(stagesDir);
267
- for (const stagePath of stageFiles) {
268
- const stageId = path.basename(stagePath, ".yaml");
269
- if (referencedStageIds.has(stageId))
270
- continue;
271
- findings.push(findingCatalogOrphanStage(cwd, stagePath, stageId));
272
- findings.push(...(await validateStageFile(cwd, stagePath)));
284
+ if (ctx.manifestStatus === "invalid") {
285
+ const manifestPath = ctx.projectRoot
286
+ ? manifestPathForProject(ctx.projectRoot)
287
+ : path.resolve(invocationCwd, "stageflow.yaml");
288
+ findings.push(...findingsFromLoadIssues(invocationCwd, manifestPath, ctx.issues));
289
+ return findings;
290
+ }
291
+ const { projectRoot, manifest } = ctx;
292
+ if (!projectRoot || !manifest) {
293
+ return findings;
294
+ }
295
+ if (manifest.manifest.catalog.pipelines.length === 0) {
296
+ findings.push(findingEmptyCatalog(invocationCwd, manifest.path, "pipelines"));
297
+ }
298
+ if (manifest.manifest.catalog.tasks.length === 0) {
299
+ findings.push(findingEmptyCatalog(invocationCwd, manifest.path, "tasks"));
300
+ }
301
+ const scanPaths = await getCatalogScanPaths(ctx);
302
+ if (!scanPaths) {
303
+ return findings;
304
+ }
305
+ const { pipelinePaths, taskPaths } = scanPaths;
306
+ for (const pipelinePath of pipelinePaths) {
307
+ const core = await runPipelineValidation(pipelinePath, {
308
+ cwd: projectRoot,
309
+ validateStages: true,
310
+ });
311
+ if (!core.ok) {
312
+ findings.push(...core.findings);
313
+ }
273
314
  }
315
+ for (const taskPath of taskPaths) {
316
+ const outcome = await loadTaskOutcome(taskPath);
317
+ if (!outcome.ok) {
318
+ findings.push(...findingsFromLoadIssues(projectRoot, taskPath, outcome.issues));
319
+ }
320
+ }
321
+ const idToPaths = await collectPipelineIdsFromPaths(pipelinePaths);
322
+ findings.push(...findingsForDuplicatePipelineIds(projectRoot, idToPaths));
274
323
  return findings;
275
324
  }
325
+ async function validateSingleTask(invocationCwd, taskArg) {
326
+ const ctx = await resolveCatalogContext(invocationCwd);
327
+ const relCwd = ctx.projectRoot ?? invocationCwd;
328
+ const taskPath = path.resolve(invocationCwd, taskArg);
329
+ const outcome = await loadTaskOutcome(taskPath);
330
+ if (!outcome.ok) {
331
+ return findingsFromLoadIssues(relCwd, taskPath, outcome.issues);
332
+ }
333
+ return [];
334
+ }
276
335
  export async function validateCatalog(options) {
277
336
  const cwd = options.cwd ?? process.cwd();
278
- const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
279
337
  const strict = options.strict ?? false;
280
- const pipelinesDir = path.join(cwd, "pipelines");
281
338
  if (options.scope === "pipeline" && !options.pipeline) {
282
339
  throw new Error("validateCatalog: pipeline is required when scope is \"pipeline\"");
283
340
  }
341
+ if (options.scope === "task" && !options.task) {
342
+ throw new Error("validateCatalog: task is required when scope is \"task\"");
343
+ }
284
344
  const allFindings = [];
285
345
  if (options.scope === "pipeline") {
286
- const result = await loadPipelineValidated(options.pipeline, {
346
+ return validatePipeline(options.pipeline, {
287
347
  cwd,
288
- stagesDir,
289
348
  validateStages: true,
349
+ strict,
290
350
  });
291
- if (!result.ok) {
292
- allFindings.push(...result.findings);
293
- }
351
+ }
352
+ else if (options.scope === "task") {
353
+ allFindings.push(...(await validateSingleTask(cwd, options.task)));
294
354
  }
295
355
  else {
296
- const referencedStageIds = await collectReferencedStageIds(pipelinesDir);
297
- const pipelineFiles = await listYamlFiles(pipelinesDir);
298
- for (const pipelinePath of pipelineFiles) {
299
- const result = await validatePipelineFile(pipelinePath, {
300
- cwd,
301
- stagesDir,
302
- validateStages: false,
303
- });
304
- allFindings.push(...result.findings);
305
- for (const stageId of result.referencedStageIds) {
306
- referencedStageIds.add(stageId);
307
- }
308
- }
309
- const validatedStages = new Set();
310
- for (const stageId of referencedStageIds) {
311
- if (validatedStages.has(stageId))
312
- continue;
313
- validatedStages.add(stageId);
314
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
315
- allFindings.push(...(await validateStageFile(cwd, stagePath)));
316
- }
317
- const idToPaths = await collectPipelineIds(pipelinesDir);
318
- allFindings.push(...findingsForDuplicatePipelineIds(cwd, idToPaths));
319
- allFindings.push(...(await findingsForOrphanStages(cwd, stagesDir, referencedStageIds)));
356
+ allFindings.push(...(await validateManifestAll(cwd)));
320
357
  }
321
358
  return buildValidationResult(options.scope, allFindings, strict);
322
359
  }
@@ -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");
@@ -25,6 +73,16 @@ export function assertRequiredEnvelope(value) {
25
73
  summary: record.summary,
26
74
  artifacts: record.artifacts,
27
75
  };
76
+ if (record.fork_choice !== undefined) {
77
+ if (!Array.isArray(record.fork_choice) ||
78
+ !record.fork_choice.every((item) => typeof item === "string")) {
79
+ throw new EnvelopeError("fork_choice must be an array of strings");
80
+ }
81
+ envelope.fork_choice = record.fork_choice;
82
+ }
83
+ if (record.clone_forks !== undefined) {
84
+ envelope.clone_forks = parseCloneForks(record.clone_forks);
85
+ }
28
86
  if (record.payload !== undefined) {
29
87
  if (record.payload === null ||
30
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { ForkEmitContext } from "../types/forkChoice.js";
2
+ export declare function assertForkEnvelope(params: unknown, forkEmitContext: ForkEmitContext): void;
3
+ export declare function normalizeForkChoice(raw: string[] | undefined, mode: "emit" | "stored", forkEmitContext?: ForkEmitContext): Set<string>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Emit mode rejects missing fork_choice on fork success; stored mode treats
3
+ * undefined as an empty choice set (scheduler defensive path).
4
+ */
5
+ import { EnvelopeError } from "../types/envelope.js";
6
+ function assertForkChoiceLegality(choices, forkEmitContext) {
7
+ const { immediateSuccessorIds, forkShape } = forkEmitContext;
8
+ for (const id of choices) {
9
+ if (!immediateSuccessorIds.includes(id)) {
10
+ throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}; clonable successors belong in clone_forks`);
11
+ }
12
+ }
13
+ if (choices.length === 0) {
14
+ if (forkShape === null || !forkShape.allowNone) {
15
+ throw new EnvelopeError("fork_choice must not be empty");
16
+ }
17
+ }
18
+ if (forkShape !== null && forkShape.cardinality === "one" && choices.length !== 1) {
19
+ throw new EnvelopeError("fork shape requires exactly one choice");
20
+ }
21
+ }
22
+ export function assertForkEnvelope(params, forkEmitContext) {
23
+ const record = params;
24
+ if (record.status === "failure")
25
+ return;
26
+ if (record.fork_choice === undefined) {
27
+ throw new EnvelopeError("fork stage must include fork_choice on success");
28
+ }
29
+ assertForkChoiceLegality(record.fork_choice, forkEmitContext);
30
+ }
31
+ export function normalizeForkChoice(raw, mode, forkEmitContext) {
32
+ if (mode === "stored") {
33
+ if (raw === undefined)
34
+ return new Set();
35
+ return new Set(raw);
36
+ }
37
+ if (forkEmitContext === undefined) {
38
+ if (raw === undefined)
39
+ return new Set();
40
+ return new Set(raw);
41
+ }
42
+ if (raw === undefined) {
43
+ throw new EnvelopeError("fork stage must include fork_choice on success");
44
+ }
45
+ assertForkChoiceLegality(raw, forkEmitContext);
46
+ return new Set(raw);
47
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/findProjectRoot.js";
2
+ export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
3
+ export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
4
+ export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
1
5
  export { PACKAGE_NAME } from "./package-meta.js";
2
6
  export type { StageEnvelope } from "./types/envelope.js";
3
7
  export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
@@ -11,3 +15,5 @@ export type { RunStore, RunSummary, RunDetail, RunMeta, RunStatus, StageSnapshot
11
15
  export { deriveStatusFromStages } from "./runstore/port.js";
12
16
  export { RunManager } from "./runtime/runManager.js";
13
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
@@ -1,3 +1,7 @@
1
+ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/findProjectRoot.js";
2
+ export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
3
+ export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
4
+ export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
1
5
  export { PACKAGE_NAME } from "./package-meta.js";
2
6
  export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
3
7
  export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
@@ -5,3 +9,5 @@ export { createRunStore } from "./runstore/createStore.js";
5
9
  export { deriveStatusFromStages } from "./runstore/port.js";
6
10
  export { RunManager } from "./runtime/runManager.js";
7
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;