stageflow 0.1.0 → 0.3.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 (132) hide show
  1. package/README.md +190 -13
  2. package/dist/agent/activity.d.ts +10 -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/fakeAgent.js +4 -0
  7. package/dist/agent/piAdapter.d.ts +16 -0
  8. package/dist/agent/piAdapter.js +88 -21
  9. package/dist/agent/port.d.ts +2 -0
  10. package/dist/agent/providerAuth.js +12 -7
  11. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  12. package/dist/catalog/displayCatalogPath.js +28 -0
  13. package/dist/cli/ciIdentity.d.ts +14 -0
  14. package/dist/cli/ciIdentity.js +52 -0
  15. package/dist/cli/initCommand.d.ts +9 -0
  16. package/dist/cli/initCommand.js +71 -0
  17. package/dist/cli/initTemplates.d.ts +3 -0
  18. package/dist/cli/initTemplates.js +19 -0
  19. package/dist/cli/operatorCatalog.d.ts +10 -0
  20. package/dist/cli/operatorCatalog.js +16 -0
  21. package/dist/cli/runCommand.d.ts +26 -0
  22. package/dist/cli/runCommand.js +209 -0
  23. package/dist/cli/runOutput.d.ts +19 -0
  24. package/dist/cli/runOutput.js +125 -0
  25. package/dist/cli/validateCommand.d.ts +1 -1
  26. package/dist/cli/validateCommand.js +20 -3
  27. package/dist/cli.d.ts +12 -1
  28. package/dist/cli.js +82 -66
  29. package/dist/config/browseCatalog.d.ts +45 -0
  30. package/dist/config/browseCatalog.js +170 -0
  31. package/dist/config/createPipeline.d.ts +12 -3
  32. package/dist/config/createPipeline.js +292 -100
  33. package/dist/config/createStage.d.ts +10 -4
  34. package/dist/config/createStage.js +44 -14
  35. package/dist/config/listConfig.d.ts +14 -19
  36. package/dist/config/listConfig.js +26 -191
  37. package/dist/config/listModelsFromManifest.d.ts +2 -0
  38. package/dist/config/listModelsFromManifest.js +4 -0
  39. package/dist/config/loadOutcome.d.ts +4 -0
  40. package/dist/config/loadPipeline.d.ts +1 -15
  41. package/dist/config/loadPipeline.js +84 -142
  42. package/dist/config/loadStage.d.ts +4 -0
  43. package/dist/config/loadStage.js +60 -32
  44. package/dist/config/loadStageflowManifest.d.ts +5 -0
  45. package/dist/config/loadStageflowManifest.js +157 -0
  46. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  47. package/dist/config/mergePipelineIncludes.js +141 -0
  48. package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
  49. package/dist/config/normalizePipelineStageEntry.js +186 -0
  50. package/dist/config/pipelineStageKeys.d.ts +4 -0
  51. package/dist/config/pipelineStageKeys.js +14 -0
  52. package/dist/config/resolveCatalogContext.d.ts +13 -0
  53. package/dist/config/resolveCatalogContext.js +13 -0
  54. package/dist/config/resolveForkEmitContext.d.ts +3 -0
  55. package/dist/config/resolveForkEmitContext.js +12 -0
  56. package/dist/config/resolvePipelineDag.d.ts +5 -1
  57. package/dist/config/resolvePipelineDag.js +54 -36
  58. package/dist/config/scanCatalogPaths.d.ts +4 -0
  59. package/dist/config/scanCatalogPaths.js +83 -0
  60. package/dist/config/validateCatalog.d.ts +22 -4
  61. package/dist/config/validateCatalog.js +169 -132
  62. package/dist/envelope/check.js +7 -0
  63. package/dist/envelope/forkChoice.d.ts +3 -0
  64. package/dist/envelope/forkChoice.js +47 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +4 -0
  67. package/dist/mcp/projectRun.d.ts +3 -0
  68. package/dist/mcp/projectRun.js +7 -0
  69. package/dist/mcp/server.js +1 -1
  70. package/dist/mcp/tools.js +28 -11
  71. package/dist/project/findProjectRoot.d.ts +2 -0
  72. package/dist/project/findProjectRoot.js +51 -0
  73. package/dist/project/globalHome.d.ts +2 -0
  74. package/dist/project/globalHome.js +19 -0
  75. package/dist/project/resolveProjectContext.d.ts +9 -0
  76. package/dist/project/resolveProjectContext.js +38 -0
  77. package/dist/project/resolveStageflowContext.d.ts +11 -0
  78. package/dist/project/resolveStageflowContext.js +66 -0
  79. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  80. package/dist/runstore/normalizeCatalogPath.js +4 -0
  81. package/dist/runstore/port.d.ts +15 -0
  82. package/dist/runstore/runProjection.js +11 -1
  83. package/dist/runstore/sqlite/SqliteRunStore.js +46 -4
  84. package/dist/runstore/sqlite/schema.d.ts +1 -1
  85. package/dist/runstore/sqlite/schema.js +7 -1
  86. package/dist/runtime/credentialBinding.d.ts +8 -6
  87. package/dist/runtime/credentialBinding.js +18 -24
  88. package/dist/runtime/pipelineRunner.d.ts +11 -0
  89. package/dist/runtime/pipelineRunner.js +25 -1
  90. package/dist/runtime/pipelineScheduler.d.ts +3 -0
  91. package/dist/runtime/pipelineScheduler.js +70 -14
  92. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  93. package/dist/runtime/reloadRunCatalog.js +44 -0
  94. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  95. package/dist/runtime/resumeReconstruct.js +27 -10
  96. package/dist/runtime/runManager.d.ts +8 -0
  97. package/dist/runtime/runManager.js +49 -18
  98. package/dist/runtime/settingsFile.d.ts +12 -0
  99. package/dist/runtime/settingsFile.js +87 -6
  100. package/dist/runtime/stageAttemptBootstrap.js +3 -0
  101. package/dist/runtime/stageProcessLauncher.d.ts +1 -0
  102. package/dist/runtime/stageProcessLauncher.js +6 -0
  103. package/dist/runtime/stageRecovery.js +3 -1
  104. package/dist/runtime/stageRunner.d.ts +2 -0
  105. package/dist/runtime/stageRunner.js +9 -2
  106. package/dist/runtime/stageWorker.js +4 -6
  107. package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
  108. package/dist/server/http.js +33 -9
  109. package/dist/tools/emitStageEnvelope.d.ts +3 -1
  110. package/dist/tools/emitStageEnvelope.js +11 -1
  111. package/dist/types/envelope.d.ts +1 -0
  112. package/dist/types/forkChoice.d.ts +8 -0
  113. package/dist/types/forkChoice.js +1 -0
  114. package/dist/types/pipeline.d.ts +51 -2
  115. package/dist/types/stageflowManifest.d.ts +23 -0
  116. package/dist/types/stageflowManifest.js +1 -0
  117. package/dist/ui/assets/index-CFSzDZje.js +118 -0
  118. package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
  119. package/dist/ui/index.html +3 -2
  120. package/dist/ui/stageflow-icon.svg +12 -0
  121. package/package.json +7 -5
  122. package/dist/agent/cursorExtension.d.ts +0 -12
  123. package/dist/agent/cursorExtension.js +0 -88
  124. package/dist/runstore/catalog.d.ts +0 -8
  125. package/dist/runstore/catalog.js +0 -16
  126. package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
  127. package/dist/runstore/disk/DiskRunStore.js +0 -238
  128. package/dist/runstore/layout.d.ts +0 -22
  129. package/dist/runstore/layout.js +0 -58
  130. package/dist/runtime/hitlSeams.d.ts +0 -38
  131. package/dist/runtime/hitlSeams.js +0 -2
  132. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,7 +1,8 @@
1
+ import type { LoadedPipeline } from "../types/pipeline.js";
1
2
  import type { LoadIssue } from "./loadOutcome.js";
2
3
  export type ValidationSeverity = "error" | "warning";
3
- export type ValidationCategory = "pipeline" | "stage" | "catalog";
4
- export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "stage.load_error" | "stage.id_filename_mismatch" | "catalog.duplicate_pipeline_id" | "catalog.orphan_stage";
4
+ export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
5
+ export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error";
5
6
  export type ValidationFinding = {
6
7
  severity: ValidationSeverity;
7
8
  code: ValidationFindingCode | string;
@@ -11,12 +12,12 @@ export type ValidationFinding = {
11
12
  pipelineId?: string;
12
13
  stageId?: string;
13
14
  };
14
- export type ValidationScope = "full" | "pipeline";
15
+ export type ValidationScope = "full" | "pipeline" | "task";
15
16
  export type ValidateCatalogOptions = {
16
17
  cwd?: string;
17
- stagesDir?: string;
18
18
  scope: ValidationScope;
19
19
  pipeline?: string;
20
+ task?: string;
20
21
  strict?: boolean;
21
22
  };
22
23
  export type ValidationResult = {
@@ -33,4 +34,21 @@ export declare function effectiveSeverity(finding: ValidationFinding, strict: bo
33
34
  export declare function buildValidationResult(scope: ValidationScope, findings: ValidationFinding[], strict: boolean): ValidationResult;
34
35
  export declare function findingsFromLoadIssues(cwd: string, absPath: string, issues: LoadIssue[]): ValidationFinding[];
35
36
  export declare function findingStageIdFilenameMismatch(cwd: string, absPath: string, fileStem: string, declaredId: string): ValidationFinding;
37
+ export type ValidatePipelineOptions = {
38
+ cwd?: string;
39
+ validateStages?: boolean;
40
+ strict?: boolean;
41
+ };
42
+ export declare function validatePipeline(nameOrPath: string, options?: ValidatePipelineOptions): Promise<ValidationResult>;
43
+ export type LoadPipelineValidatedResult = {
44
+ ok: true;
45
+ loaded: LoadedPipeline;
46
+ } | {
47
+ ok: false;
48
+ findings: ValidationFinding[];
49
+ };
50
+ export declare function loadPipelineValidated(nameOrPath: string, options?: {
51
+ cwd?: string;
52
+ validateStages?: boolean;
53
+ }): Promise<LoadPipelineValidatedResult>;
36
54
  export declare function validateCatalog(options: ValidateCatalogOptions): Promise<ValidationResult>;
@@ -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
  }
@@ -25,6 +25,13 @@ export function assertRequiredEnvelope(value) {
25
25
  summary: record.summary,
26
26
  artifacts: record.artifacts,
27
27
  };
28
+ if (record.fork_choice !== undefined) {
29
+ if (!Array.isArray(record.fork_choice) ||
30
+ !record.fork_choice.every((item) => typeof item === "string")) {
31
+ throw new EnvelopeError("fork_choice must be an array of strings");
32
+ }
33
+ envelope.fork_choice = record.fork_choice;
34
+ }
28
35
  if (record.payload !== undefined) {
29
36
  if (record.payload === null ||
30
37
  typeof record.payload !== "object" ||
@@ -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}`);
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";
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";
@@ -19,6 +19,9 @@ export type McpRunProjection = {
19
19
  run_id: string;
20
20
  pipeline_id: string;
21
21
  task_id?: string;
22
+ pipeline_path?: string;
23
+ task_path?: string;
24
+ project_root?: string;
22
25
  status: RunDetail["status"];
23
26
  created_at: string;
24
27
  updated_at?: string;
@@ -3,6 +3,13 @@ export function projectRunForMcp(detail) {
3
3
  run_id: detail.run_id,
4
4
  pipeline_id: detail.pipeline_id,
5
5
  task_id: detail.task_id,
6
+ ...(detail.pipeline_path !== undefined
7
+ ? { pipeline_path: detail.pipeline_path }
8
+ : {}),
9
+ ...(detail.task_path !== undefined ? { task_path: detail.task_path } : {}),
10
+ ...(detail.project_root !== undefined
11
+ ? { project_root: detail.project_root }
12
+ : {}),
6
13
  status: detail.status,
7
14
  created_at: detail.created_at,
8
15
  updated_at: detail.updated_at,
@@ -4,7 +4,7 @@ import { registerMcpTools } from "./tools.js";
4
4
  export function createStageflowMcpServer(deps) {
5
5
  const server = new McpServer({
6
6
  name: "stageflow",
7
- version: "0.1.0",
7
+ version: "0.2.0",
8
8
  });
9
9
  registerMcpTools(server, deps);
10
10
  return server;
package/dist/mcp/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { listPipelines } from "../config/listConfig.js";
2
+ import { browseCatalog } from "../config/browseCatalog.js";
3
3
  import { projectRunForMcp } from "./projectRun.js";
4
4
  import { readRunArtifact } from "./readArtifact.js";
5
5
  const taskFileSchema = z.object({
@@ -9,6 +9,15 @@ const taskFileSchema = z.object({
9
9
  constraints: z.string().optional(),
10
10
  checkout: z.string().optional(),
11
11
  });
12
+ const startRunSchema = z
13
+ .object({
14
+ pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
15
+ task_path: z.string().optional(),
16
+ task: taskFileSchema.optional(),
17
+ })
18
+ .refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
19
+ message: "Exactly one of task_path or task is required",
20
+ });
12
21
  function textResult(data, isError = false) {
13
22
  return {
14
23
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -18,11 +27,18 @@ function textResult(data, isError = false) {
18
27
  export function registerMcpTools(server, deps) {
19
28
  const { manager, store, cwd } = deps;
20
29
  server.registerTool("list_pipelines", {
21
- description: "List runnable pipeline ids from the factory cwd",
30
+ description: "List manifest-declared pipeline paths from the project catalog",
31
+ inputSchema: z.object({}),
32
+ }, async () => {
33
+ const catalog = await browseCatalog(cwd);
34
+ return textResult({ pipelines: catalog.pipelines });
35
+ });
36
+ server.registerTool("list_tasks", {
37
+ description: "List manifest-declared task paths from the project catalog",
22
38
  inputSchema: z.object({}),
23
39
  }, async () => {
24
- const pipelines = await listPipelines(cwd);
25
- return textResult({ pipelines });
40
+ const catalog = await browseCatalog(cwd);
41
+ return textResult({ tasks: catalog.tasks });
26
42
  });
27
43
  server.registerTool("list_runs", {
28
44
  description: "List known pipeline runs",
@@ -36,16 +52,17 @@ export function registerMcpTools(server, deps) {
36
52
  inputSchema: z.object({}),
37
53
  }, async () => textResult(manager.getHealth()));
38
54
  server.registerTool("start_run", {
39
- description: "Start a pipeline with an inline TaskFile (not written under tasks/). 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.",
40
- inputSchema: z.object({
41
- pipeline: z.string(),
42
- task: taskFileSchema,
43
- }),
44
- }, async ({ pipeline, task }) => {
55
+ 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.",
56
+ inputSchema: startRunSchema,
57
+ }, async ({ pipeline, task_path, task }) => {
45
58
  if (!pipeline.trim()) {
46
59
  return textResult({ error: "pipeline is required" }, true);
47
60
  }
48
- const result = await manager.startRun({ pipeline, task });
61
+ const taskInput = task_path ?? task;
62
+ if (taskInput === undefined) {
63
+ return textResult({ error: "Exactly one of task_path or task is required" }, true);
64
+ }
65
+ const result = await manager.startRun({ pipeline, task: taskInput });
49
66
  if (!result.ok) {
50
67
  const { ok: _ok, reason, ...rest } = result;
51
68
  return textResult({ error: reason, ...rest }, true);
@@ -0,0 +1,2 @@
1
+ export declare function findProjectRoot(startDir: string): string | null;
2
+ export declare function clearFindProjectRootCacheForTests(): void;