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,68 +1,38 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
- import { loadStageOutcome } from "./loadStage.js";
5
- import { readYamlObject } from "./readYamlObject.js";
6
- import { extractPipelineStageIds, resolvePipelineDag } from "./resolvePipelineDag.js";
7
- import { findingStageIdFilenameMismatch, findingsFromLoadIssues, } from "./validateCatalog.js";
8
- async function resolvePipelinePath(nameOrPath, cwd) {
9
- const direct = path.resolve(cwd, nameOrPath);
4
+ import { mergePipelineStages } from "./mergePipelineIncludes.js";
5
+ import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
6
+ import { loadStageFromObjectOutcome, loadStageOutcome } from "./loadStage.js";
7
+ import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
8
+ export async function resolvePipelinePath(pipelinePath, cwd) {
9
+ const resolved = path.normalize(path.resolve(cwd, pipelinePath));
10
10
  try {
11
- await access(direct);
12
- return direct;
11
+ await access(resolved);
12
+ return resolved;
13
13
  }
14
14
  catch {
15
- const candidate = path.resolve(cwd, "pipelines", `${nameOrPath}.yaml`);
16
- await access(candidate);
17
- return candidate;
15
+ throw new Error(`Pipeline file not found: ${resolved}. Pass a filesystem path to --pipeline (e.g. pipelines/hello.pipeline.yaml), not a bare pipeline id.`);
18
16
  }
19
17
  }
20
- export async function loadPipelineOutcome(nameOrPath, options = {}) {
21
- const cwd = options.cwd ?? process.cwd();
22
- const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
23
- let pipelinePath;
24
- try {
25
- pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
26
- }
27
- catch (err) {
28
- const message = err instanceof Error ? err.message : String(err);
29
- return loadFailure([
30
- {
31
- code: "pipeline.load_error",
32
- message,
33
- category: "pipeline",
34
- },
35
- ]);
36
- }
37
- let raw;
38
- try {
39
- raw = await readYamlObject(pipelinePath);
40
- }
41
- catch (err) {
42
- const message = err instanceof Error ? err.message : String(err);
43
- return loadFailure([
44
- {
45
- code: "pipeline.load_error",
46
- message,
47
- category: "pipeline",
48
- },
49
- ]);
50
- }
51
- if (typeof raw?.id !== "string" || !Array.isArray(raw.stages)) {
52
- return loadFailure([
53
- {
54
- code: "pipeline.invalid_shape",
55
- message: `Invalid pipeline ${pipelinePath}: id and stages[] are required`,
56
- category: "pipeline",
57
- },
58
- ]);
59
- }
60
- const pipelineId = raw.id;
61
- const ctx = { pipelineId: raw.id, path: pipelinePath };
18
+ async function loadPipelineFromPath(pipelinePath) {
19
+ const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
20
+ const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
21
+ if (!mergeOutcome.ok) {
22
+ return loadFailure(mergeOutcome.issues);
23
+ }
24
+ const { entries: rawEntries, pipelineId } = mergeOutcome.value;
25
+ const ctx = { pipelineId, path: normalizedPipelinePath };
26
+ const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
27
+ if (!normalizeOutcome.ok) {
28
+ return loadFailure(normalizeOutcome.issues);
29
+ }
30
+ const normalizedEntries = normalizeOutcome.value;
31
+ const wiringRefs = toWiringRefs(normalizedEntries);
62
32
  let stageIds;
63
33
  let dag;
64
34
  try {
65
- const resolved = resolvePipelineDag(raw.stages, ctx);
35
+ const resolved = resolvePipelineDagFromRefs(wiringRefs, ctx);
66
36
  stageIds = resolved.stages;
67
37
  dag = resolved.dag;
68
38
  }
@@ -77,115 +47,87 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
77
47
  },
78
48
  ]);
79
49
  }
80
- const pipeline = {
81
- id: raw.id,
82
- stages: stageIds,
83
- };
50
+ const entryById = new Map(normalizedEntries.map((entry) => [entry.id, entry]));
51
+ const stageSources = {};
84
52
  const stages = [];
85
- for (const stageId of pipeline.stages) {
86
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
87
- const stageOutcome = await loadStageOutcome(stagePath);
53
+ for (const stageId of stageIds) {
54
+ const entry = entryById.get(stageId);
55
+ if (!entry) {
56
+ return loadFailure([
57
+ {
58
+ code: "pipeline.invalid_shape",
59
+ message: `Pipeline ${pipelineId}: missing normalized entry for stage "${stageId}"`,
60
+ category: "pipeline",
61
+ pipelineId,
62
+ },
63
+ ]);
64
+ }
65
+ if (entry.body.kind === "inline") {
66
+ const inlineOutcome = loadStageFromObjectOutcome(entry.body.raw, {
67
+ entryId: entry.id,
68
+ declaringPath: entry.declaringPath,
69
+ });
70
+ if (!inlineOutcome.ok) {
71
+ return loadFailure(inlineOutcome.issues);
72
+ }
73
+ stages.push(inlineOutcome.value);
74
+ stageSources[stageId] = { kind: "inline" };
75
+ continue;
76
+ }
77
+ const stageOutcome = await loadStageOutcome(entry.body.absolutePath);
88
78
  if (!stageOutcome.ok) {
89
- const message = `Pipeline ${pipeline.id} references missing stage "${stageId}" at ${stagePath}`;
90
79
  return loadFailure([
91
80
  {
92
81
  code: "pipeline.missing_stage",
93
- message,
82
+ message: `Pipeline ${pipelineId} references missing stage "${stageId}" at ${entry.body.absolutePath}`,
94
83
  category: "pipeline",
95
84
  pipelineId,
96
85
  },
86
+ ...stageOutcome.issues,
97
87
  ]);
98
88
  }
99
- stages.push(stageOutcome.value);
100
- }
101
- return loadSuccess({ pipeline, stages, dag });
102
- }
103
- async function checkStageIdFilename(cwd, stagePath) {
104
- const fileStem = path.basename(stagePath, ".yaml");
105
- try {
106
- const raw = await readYamlObject(stagePath);
107
- if (typeof raw?.id === "string" && raw.id !== fileStem) {
108
- return findingStageIdFilenameMismatch(cwd, stagePath, fileStem, raw.id);
89
+ if (stageOutcome.value.id !== entry.id) {
90
+ return loadFailure([
91
+ {
92
+ code: "pipeline.stage_id_mismatch",
93
+ message: `Pipeline ${pipelineId}: stage entry "${entry.id}" in ${entry.declaringPath} uses ${entry.body.path} which declares id "${stageOutcome.value.id}"`,
94
+ category: "pipeline",
95
+ pipelineId,
96
+ },
97
+ ]);
109
98
  }
99
+ stages.push(stageOutcome.value);
100
+ stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
110
101
  }
111
- catch {
112
- // loadStageOutcome will report read errors
113
- }
114
- return null;
115
- }
116
- async function validateStageFile(cwd, stagePath) {
117
- const findings = [];
118
- const mismatch = await checkStageIdFilename(cwd, stagePath);
119
- if (mismatch)
120
- findings.push(mismatch);
121
- const outcome = await loadStageOutcome(stagePath);
122
- if (!outcome.ok) {
123
- findings.push(...findingsFromLoadIssues(cwd, stagePath, outcome.issues));
124
- return findings;
125
- }
126
- if (!mismatch && path.basename(stagePath, ".yaml") !== outcome.value.id) {
127
- findings.push(findingStageIdFilenameMismatch(cwd, stagePath, path.basename(stagePath, ".yaml"), outcome.value.id));
128
- }
129
- return findings;
102
+ const pipeline = {
103
+ id: pipelineId,
104
+ stages: stageIds,
105
+ };
106
+ return loadSuccess({
107
+ pipeline,
108
+ stages,
109
+ dag,
110
+ pipelinePath: normalizedPipelinePath,
111
+ stageSources,
112
+ });
130
113
  }
131
- export async function loadPipelineValidated(nameOrPath, options = {}) {
114
+ export async function loadPipelineOutcome(nameOrPath, options = {}) {
132
115
  const cwd = options.cwd ?? process.cwd();
133
- const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
134
- const validateStages = options.validateStages ?? true;
135
116
  let pipelinePath;
136
117
  try {
137
118
  pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
138
119
  }
139
120
  catch (err) {
140
121
  const message = err instanceof Error ? err.message : String(err);
141
- return {
142
- ok: false,
143
- findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
144
- {
145
- code: "pipeline.load_error",
146
- message,
147
- category: "pipeline",
148
- },
149
- ]),
150
- };
151
- }
152
- const referencedStageIds = new Set();
153
- try {
154
- const raw = await readYamlObject(pipelinePath);
155
- if (typeof raw?.id === "string" && Array.isArray(raw.stages)) {
156
- const stageIds = extractPipelineStageIds(raw.stages);
157
- if (stageIds) {
158
- for (const stageId of stageIds) {
159
- referencedStageIds.add(stageId);
160
- }
161
- }
162
- }
163
- }
164
- catch {
165
- // loadPipelineOutcome will report read errors
166
- }
167
- const outcome = await loadPipelineOutcome(nameOrPath, { cwd, stagesDir });
168
- const findings = [];
169
- if (!outcome.ok) {
170
- findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
171
- if (validateStages) {
172
- for (const stageId of referencedStageIds) {
173
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
174
- findings.push(...(await validateStageFile(cwd, stagePath)));
175
- }
176
- }
177
- return { ok: false, findings };
178
- }
179
- if (validateStages) {
180
- for (const stageId of outcome.value.pipeline.stages) {
181
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
182
- findings.push(...(await validateStageFile(cwd, stagePath)));
183
- }
184
- }
185
- if (findings.some((finding) => finding.severity === "error")) {
186
- return { ok: false, findings };
122
+ return loadFailure([
123
+ {
124
+ code: "pipeline.load_error",
125
+ message,
126
+ category: "pipeline",
127
+ },
128
+ ]);
187
129
  }
188
- return { ok: true, loaded: outcome.value };
130
+ return loadPipelineFromPath(pipelinePath);
189
131
  }
190
132
  export async function loadPipeline(nameOrPath, options = {}) {
191
133
  const outcome = await loadPipelineOutcome(nameOrPath, options);
@@ -1,4 +1,8 @@
1
1
  import { type StageConfig } from "../types/stage.js";
2
2
  import { type LoadOutcome } from "./loadOutcome.js";
3
+ export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
4
+ entryId: string;
5
+ declaringPath: string;
6
+ }): LoadOutcome<StageConfig>;
3
7
  export declare function loadStageOutcome(filePath: string): Promise<LoadOutcome<StageConfig>>;
4
8
  export declare function loadStage(filePath: string): Promise<StageConfig>;
@@ -5,14 +5,14 @@ import { readYamlObject } from "./readYamlObject.js";
5
5
  function isGateKind(value) {
6
6
  return STAGE_GATE_KINDS.includes(value);
7
7
  }
8
- function parseGateKinds(raw, filePath) {
8
+ function parseGateKinds(raw, label) {
9
9
  if (raw === undefined)
10
10
  return loadSuccess(undefined);
11
11
  if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
12
12
  return loadFailure([
13
13
  {
14
14
  code: "stage.invalid_gate_kinds",
15
- message: `Invalid stage file ${filePath}: gate_kinds must be an array of strings`,
15
+ message: `Invalid stage ${label}: gate_kinds must be an array of strings`,
16
16
  category: "stage",
17
17
  },
18
18
  ]);
@@ -23,7 +23,7 @@ function parseGateKinds(raw, filePath) {
23
23
  return loadFailure([
24
24
  {
25
25
  code: "stage.invalid_gate_kinds",
26
- message: `Invalid stage file ${filePath}: unsupported gate kind "${item}" (allowed: ${STAGE_GATE_KINDS.join(", ")})`,
26
+ message: `Invalid stage ${label}: unsupported gate kind "${item}" (allowed: ${STAGE_GATE_KINDS.join(", ")})`,
27
27
  category: "stage",
28
28
  },
29
29
  ]);
@@ -32,35 +32,20 @@ function parseGateKinds(raw, filePath) {
32
32
  }
33
33
  return loadSuccess(kinds.length > 0 ? kinds : undefined);
34
34
  }
35
- export async function loadStageOutcome(filePath) {
36
- let raw;
37
- try {
38
- raw = await readYamlObject(filePath);
39
- }
40
- catch (err) {
41
- const message = err instanceof Error ? err.message : String(err);
42
- return loadFailure([
43
- {
44
- code: "stage.load_error",
45
- message,
46
- category: "stage",
47
- },
48
- ]);
49
- }
50
- if (typeof raw?.id !== "string" ||
51
- typeof raw?.system_prompt !== "string" ||
52
- typeof raw?.model !== "string") {
35
+ function parseStageFields(raw, label, entryId) {
36
+ if (typeof raw.system_prompt !== "string" ||
37
+ typeof raw.model !== "string") {
53
38
  return loadFailure([
54
39
  {
55
40
  code: "stage.invalid_shape",
56
- message: `Invalid stage file ${filePath}: id, system_prompt, and model are required strings`,
41
+ message: `Invalid stage ${label}: system_prompt and model are required strings`,
57
42
  category: "stage",
58
- stageId: typeof raw?.id === "string" ? raw.id : undefined,
43
+ stageId: entryId,
59
44
  },
60
45
  ]);
61
46
  }
62
47
  const stage = {
63
- id: raw.id,
48
+ id: entryId,
64
49
  system_prompt: raw.system_prompt,
65
50
  model: raw.model,
66
51
  };
@@ -71,9 +56,9 @@ export async function loadStageOutcome(filePath) {
71
56
  return loadFailure([
72
57
  {
73
58
  code: "stage.invalid_payload_schema",
74
- message: `Invalid stage file ${filePath}: payload_schema must be an object`,
59
+ message: `Invalid stage ${label}: payload_schema must be an object`,
75
60
  category: "stage",
76
- stageId: raw.id,
61
+ stageId: entryId,
77
62
  },
78
63
  ]);
79
64
  }
@@ -85,19 +70,19 @@ export async function loadStageOutcome(filePath) {
85
70
  return loadFailure([
86
71
  {
87
72
  code: "stage.invalid_payload_schema",
88
- message: `Invalid stage file ${filePath}: invalid payload_schema: ${message}`,
73
+ message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
89
74
  category: "stage",
90
- stageId: raw.id,
75
+ stageId: entryId,
91
76
  },
92
77
  ]);
93
78
  }
94
79
  stage.payload_schema = raw.payload_schema;
95
80
  }
96
- const gateKindsOutcome = parseGateKinds(raw.gate_kinds, filePath);
81
+ const gateKindsOutcome = parseGateKinds(raw.gate_kinds, label);
97
82
  if (!gateKindsOutcome.ok) {
98
83
  const issues = gateKindsOutcome.issues.map((issue) => ({
99
84
  ...issue,
100
- stageId: stage.id,
85
+ stageId: entryId,
101
86
  }));
102
87
  return loadFailure(issues);
103
88
  }
@@ -108,9 +93,9 @@ export async function loadStageOutcome(filePath) {
108
93
  return loadFailure([
109
94
  {
110
95
  code: "stage.invalid_skill",
111
- message: `Invalid stage file ${filePath}: skill must be a non-empty string`,
96
+ message: `Invalid stage ${label}: skill must be a non-empty string`,
112
97
  category: "stage",
113
- stageId: stage.id,
98
+ stageId: entryId,
114
99
  },
115
100
  ]);
116
101
  }
@@ -118,6 +103,49 @@ export async function loadStageOutcome(filePath) {
118
103
  }
119
104
  return loadSuccess(stage);
120
105
  }
106
+ export function loadStageFromObjectOutcome(raw, ctx) {
107
+ const label = `${ctx.entryId} (${ctx.declaringPath})`;
108
+ return parseStageFields(raw, label, ctx.entryId);
109
+ }
110
+ export async function loadStageOutcome(filePath) {
111
+ let raw;
112
+ try {
113
+ raw = await readYamlObject(filePath);
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ return loadFailure([
118
+ {
119
+ code: "stage.load_error",
120
+ message,
121
+ category: "stage",
122
+ },
123
+ ]);
124
+ }
125
+ if (typeof raw?.id !== "string") {
126
+ return loadFailure([
127
+ {
128
+ code: "stage.invalid_shape",
129
+ message: `Invalid stage file ${filePath}: id, system_prompt, and model are required strings`,
130
+ category: "stage",
131
+ },
132
+ ]);
133
+ }
134
+ const outcome = parseStageFields(raw, `file ${filePath}`, raw.id);
135
+ if (!outcome.ok)
136
+ return outcome;
137
+ if (outcome.value.id !== raw.id) {
138
+ return loadFailure([
139
+ {
140
+ code: "stage.invalid_shape",
141
+ message: `Invalid stage file ${filePath}: id mismatch`,
142
+ category: "stage",
143
+ stageId: raw.id,
144
+ },
145
+ ]);
146
+ }
147
+ return outcome;
148
+ }
121
149
  export async function loadStage(filePath) {
122
150
  const outcome = await loadStageOutcome(filePath);
123
151
  if (!outcome.ok) {
@@ -0,0 +1,5 @@
1
+ import type { LoadedManifest } from "../types/stageflowManifest.js";
2
+ import { type LoadOutcome } from "./loadOutcome.js";
3
+ export declare function manifestPathForProject(projectRoot: string): string;
4
+ export declare function parseStageflowManifestOutcome(yamlText: string, manifestPath: string, projectRoot: string): LoadOutcome<LoadedManifest>;
5
+ export declare function loadStageflowManifestOutcome(projectRoot: string): Promise<LoadOutcome<LoadedManifest>>;
@@ -0,0 +1,157 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { loadFailure, loadSuccess } from "./loadOutcome.js";
5
+ const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
6
+ const DEFAULT_TASK_PATTERN = "*.task.yaml";
7
+ export function manifestPathForProject(projectRoot) {
8
+ return path.join(projectRoot, "stageflow.yaml");
9
+ }
10
+ function catalogIssue(code, message) {
11
+ return { code, message, category: "catalog" };
12
+ }
13
+ function normalizeEntryPath(entry) {
14
+ return entry.replace(/\\/g, "/").replace(/^\.\/+/, "");
15
+ }
16
+ function parseStringArray(raw, field, issues) {
17
+ if (!Array.isArray(raw)) {
18
+ issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} must be an array`));
19
+ return null;
20
+ }
21
+ const entries = [];
22
+ for (const item of raw) {
23
+ if (typeof item !== "string" || item.trim().length === 0) {
24
+ issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} entries must be non-empty strings`));
25
+ return null;
26
+ }
27
+ const normalized = normalizeEntryPath(item.trim());
28
+ if (path.isAbsolute(normalized)) {
29
+ issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} entries must be repo-relative paths`));
30
+ return null;
31
+ }
32
+ entries.push(normalized);
33
+ }
34
+ return entries;
35
+ }
36
+ function parsePatterns(raw, issues) {
37
+ const defaults = {
38
+ pipeline: DEFAULT_PIPELINE_PATTERN,
39
+ task: DEFAULT_TASK_PATTERN,
40
+ };
41
+ if (raw === undefined || raw === null) {
42
+ return defaults;
43
+ }
44
+ if (typeof raw !== "object" || Array.isArray(raw)) {
45
+ issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns must be an object"));
46
+ return defaults;
47
+ }
48
+ const record = raw;
49
+ const pipeline = record.pipeline === undefined
50
+ ? defaults.pipeline
51
+ : typeof record.pipeline === "string" && record.pipeline.trim().length > 0
52
+ ? record.pipeline.trim()
53
+ : null;
54
+ const task = record.task === undefined
55
+ ? defaults.task
56
+ : typeof record.task === "string" && record.task.trim().length > 0
57
+ ? record.task.trim()
58
+ : null;
59
+ if (pipeline === null) {
60
+ issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns.pipeline must be a string"));
61
+ }
62
+ if (task === null) {
63
+ issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns.task must be a string"));
64
+ }
65
+ return {
66
+ pipeline: pipeline ?? defaults.pipeline,
67
+ task: task ?? defaults.task,
68
+ };
69
+ }
70
+ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoot) {
71
+ let raw;
72
+ try {
73
+ raw = parseYaml(yamlText);
74
+ }
75
+ catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ return loadFailure([catalogIssue("catalog.manifest_load_error", message)]);
78
+ }
79
+ const issues = [];
80
+ const record = raw;
81
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
82
+ return loadFailure([
83
+ catalogIssue("catalog.manifest_invalid", "stageflow.yaml must be a mapping"),
84
+ ]);
85
+ }
86
+ if (record.version !== 1) {
87
+ issues.push(catalogIssue("catalog.manifest_invalid", `Unsupported manifest version: ${record.version}`));
88
+ }
89
+ const catalogRaw = record.catalog;
90
+ if (catalogRaw === undefined ||
91
+ typeof catalogRaw !== "object" ||
92
+ catalogRaw === null ||
93
+ Array.isArray(catalogRaw)) {
94
+ issues.push(catalogIssue("catalog.manifest_invalid", "catalog object is required"));
95
+ if (issues.length > 0) {
96
+ return loadFailure(issues);
97
+ }
98
+ }
99
+ const catalogRecord = catalogRaw;
100
+ const pipelines = parseStringArray(catalogRecord.pipelines, "pipelines", issues);
101
+ const tasks = parseStringArray(catalogRecord.tasks, "tasks", issues);
102
+ const excludeRaw = catalogRecord.exclude;
103
+ let exclude;
104
+ if (excludeRaw !== undefined) {
105
+ const parsed = parseStringArray(excludeRaw, "exclude", issues);
106
+ if (parsed !== null) {
107
+ exclude = parsed;
108
+ }
109
+ }
110
+ const patterns = parsePatterns(catalogRecord.patterns, issues);
111
+ if (issues.length > 0) {
112
+ return loadFailure(issues);
113
+ }
114
+ if (pipelines === null || tasks === null) {
115
+ return loadFailure(issues);
116
+ }
117
+ const catalog = { pipelines, tasks };
118
+ if (exclude !== undefined) {
119
+ catalog.exclude = exclude;
120
+ }
121
+ if (catalogRecord.patterns !== undefined) {
122
+ catalog.patterns = {};
123
+ if (typeof catalogRecord.patterns.pipeline === "string") {
124
+ catalog.patterns.pipeline = patterns.pipeline;
125
+ }
126
+ if (typeof catalogRecord.patterns.task === "string") {
127
+ catalog.patterns.task = patterns.task;
128
+ }
129
+ }
130
+ const manifest = { version: 1, catalog };
131
+ return loadSuccess({
132
+ path: manifestPath,
133
+ projectRoot,
134
+ manifest,
135
+ patterns,
136
+ });
137
+ }
138
+ export async function loadStageflowManifestOutcome(projectRoot) {
139
+ const manifestPath = manifestPathForProject(projectRoot);
140
+ try {
141
+ await access(manifestPath);
142
+ }
143
+ catch {
144
+ return loadFailure([
145
+ catalogIssue("catalog.manifest_load_error", `Manifest not found: ${manifestPath}`),
146
+ ]);
147
+ }
148
+ let yamlText;
149
+ try {
150
+ yamlText = await readFile(manifestPath, "utf8");
151
+ }
152
+ catch (err) {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ return loadFailure([catalogIssue("catalog.manifest_load_error", message)]);
155
+ }
156
+ return parseStageflowManifestOutcome(yamlText, manifestPath, projectRoot);
157
+ }
@@ -0,0 +1,9 @@
1
+ import { type LoadOutcome } from "./loadOutcome.js";
2
+ export type RawMergedEntry = {
3
+ raw: unknown;
4
+ declaringPath: string;
5
+ };
6
+ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutcome<{
7
+ entries: RawMergedEntry[];
8
+ pipelineId: string;
9
+ }>>;