stageflow 0.13.0 → 0.15.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 (97) hide show
  1. package/dist/agent/piAdapter.js +5 -2
  2. package/dist/agent/port.d.ts +2 -2
  3. package/dist/cli/initTemplates.d.ts +1 -1
  4. package/dist/cli/initTemplates.js +1 -1
  5. package/dist/cli/migrateYamlCommand.d.ts +18 -0
  6. package/dist/cli/migrateYamlCommand.js +180 -0
  7. package/dist/cli/runOutput.js +26 -18
  8. package/dist/cli/validateCommand.d.ts +1 -0
  9. package/dist/cli/validateCommand.js +2 -0
  10. package/dist/cli.js +15 -1
  11. package/dist/config/browseCatalog.js +1 -1
  12. package/dist/config/createPipeline.d.ts +1 -1
  13. package/dist/config/createPipeline.js +23 -11
  14. package/dist/config/createStage.d.ts +1 -1
  15. package/dist/config/createStage.js +10 -5
  16. package/dist/config/legacyYaml.d.ts +28 -0
  17. package/dist/config/legacyYaml.js +98 -0
  18. package/dist/config/loadOutcome.d.ts +2 -1
  19. package/dist/config/loadOutcome.js +4 -1
  20. package/dist/config/loadPipeline.d.ts +2 -0
  21. package/dist/config/loadPipeline.js +150 -8
  22. package/dist/config/loadStage.d.ts +8 -1
  23. package/dist/config/loadStage.js +138 -29
  24. package/dist/config/loadStageflowManifest.js +10 -0
  25. package/dist/config/loadTask.js +14 -0
  26. package/dist/config/materializeStageModels.d.ts +7 -0
  27. package/dist/config/materializeStageModels.js +35 -0
  28. package/dist/config/mergePipelineIncludes.d.ts +5 -1
  29. package/dist/config/mergePipelineIncludes.js +82 -4
  30. package/dist/config/migrateYaml.d.ts +19 -0
  31. package/dist/config/migrateYaml.js +627 -0
  32. package/dist/config/modelField.d.ts +13 -0
  33. package/dist/config/modelField.js +16 -0
  34. package/dist/config/normalizePipelineStageEntry.js +41 -2
  35. package/dist/config/parseCompletionContract.d.ts +5 -2
  36. package/dist/config/parseCompletionContract.js +16 -3
  37. package/dist/config/parsePreEmitChecks.js +4 -0
  38. package/dist/config/pipelineNeeds.js +3 -3
  39. package/dist/config/pipelineStageKeys.d.ts +11 -0
  40. package/dist/config/pipelineStageKeys.js +14 -0
  41. package/dist/config/printTargetYaml.d.ts +28 -0
  42. package/dist/config/printTargetYaml.js +243 -0
  43. package/dist/config/resolveModel.d.ts +20 -0
  44. package/dist/config/resolveModel.js +35 -0
  45. package/dist/config/validateCatalog.d.ts +5 -1
  46. package/dist/config/validateCatalog.js +36 -6
  47. package/dist/config/validateCompletionContract.js +5 -0
  48. package/dist/config/yamlDialect.d.ts +29 -0
  49. package/dist/config/yamlDialect.js +327 -0
  50. package/dist/envelope/payloadSchema.d.ts +37 -5
  51. package/dist/envelope/payloadSchema.js +272 -22
  52. package/dist/mcp/catalogTools.js +1 -0
  53. package/dist/package-meta.d.ts +1 -1
  54. package/dist/package-meta.js +1 -1
  55. package/dist/runstore/pipelineDagSnapshot.js +2 -0
  56. package/dist/runstore/port.d.ts +1 -0
  57. package/dist/runtime/completionCheckRunner.d.ts +3 -2
  58. package/dist/runtime/completionCheckRunner.js +3 -2
  59. package/dist/runtime/envelopeRouting.d.ts +2 -2
  60. package/dist/runtime/pipelineRunner.d.ts +3 -0
  61. package/dist/runtime/pipelineRunner.js +23 -3
  62. package/dist/runtime/runManager.js +3 -2
  63. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  64. package/dist/runtime/stageRunner.d.ts +2 -2
  65. package/dist/runtime/taskInput.d.ts +7 -0
  66. package/dist/runtime/taskInput.js +60 -0
  67. package/dist/runtime/verifiedStageExecution.js +3 -0
  68. package/dist/types/completion.d.ts +6 -0
  69. package/dist/types/pipeline.d.ts +21 -2
  70. package/dist/types/preEmitCheck.d.ts +4 -6
  71. package/dist/types/stage.d.ts +22 -3
  72. package/dist/types/stageflowManifest.d.ts +2 -0
  73. package/dist/types/task.d.ts +1 -0
  74. package/dist/ui/assets/{index-Yzh42vhf.js → index-GtyiWiZg.js} +1 -1
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +1 -1
  77. package/skills/stageflow-author/SKILL.md +7 -7
  78. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
  79. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
  80. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
  81. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
  82. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
  83. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
  84. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
  85. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
  86. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
  87. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
  88. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
  89. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
  90. package/skills/stageflow-author/references/catalog-mapping.md +30 -28
  91. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  92. package/skills/stageflow-author/references/stage-prompt-template.md +5 -5
  93. package/skills/stageflow-session-capture/SKILL.md +1 -1
  94. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
  95. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
  96. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
  97. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -2
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Dual-read adapter: accept old YAML authoring keys and warn (`catalog.legacy_yaml`).
3
+ *
4
+ * This is not the runtime IR. Stage workers, snapshots, emit, and VSE already
5
+ * use those same strings as TypeScript fields (`StageConfig.payload_schema`,
6
+ * DAG `completion` / `recovery`). Target YAML compiles onto that IR in
7
+ * `yamlDialect.ts`. New catalog fields go on `io` / `verify` / `on_verify_fail`,
8
+ * not here.
9
+ *
10
+ * Off: STAGEFLOW_LEGACY_YAML=0. migrate-yaml still reads legacy via
11
+ * withLegacyYamlAllowedAsync.
12
+ *
13
+ * Delete this file plus migrateYaml.ts, printTargetYaml.ts,
14
+ * migrateYamlCommand.ts, and tests/fixtures/dual-read to stop accepting
15
+ * payload_schema / pre_emit_checks / completion / recovery as authoring keys.
16
+ */
17
+ import { AsyncLocalStorage } from "node:async_hooks";
18
+ export const LEGACY_CONTRACT_KEYS = new Set([
19
+ "payload_schema",
20
+ "clone_input_schema",
21
+ "pre_emit_checks",
22
+ "completion",
23
+ "recovery",
24
+ ]);
25
+ export const LEGACY_KEY_REPLACEMENTS = {
26
+ payload_schema: "io.output.schema",
27
+ clone_input_schema: "io.input.schema",
28
+ pre_emit_checks: "verify items with when including emit",
29
+ completion: "verify items with when including after",
30
+ recovery: "on_verify_fail",
31
+ };
32
+ const TARGET_AUTHORING_KEYS = new Set(["io", "verify", "on_verify_fail"]);
33
+ const legacyYamlOverride = new AsyncLocalStorage();
34
+ function collectDocumentKeys(raw) {
35
+ const keys = new Set(Object.keys(raw));
36
+ if (Array.isArray(raw.stages)) {
37
+ for (const entry of raw.stages) {
38
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
39
+ for (const key of Object.keys(entry))
40
+ keys.add(key);
41
+ }
42
+ }
43
+ }
44
+ return keys;
45
+ }
46
+ export function formatLegacyReplacements(keys) {
47
+ return keys
48
+ .map((key) => {
49
+ const replacement = LEGACY_KEY_REPLACEMENTS[key];
50
+ return replacement ? `${key} → ${replacement}` : key;
51
+ })
52
+ .join(", ");
53
+ }
54
+ export function presentLegacyKeys(keys) {
55
+ const set = keys instanceof Set ? keys : new Set(keys);
56
+ return [...LEGACY_CONTRACT_KEYS].filter((key) => set.has(key));
57
+ }
58
+ export function legacyYamlIssue(fileLabel, keys) {
59
+ return {
60
+ code: "catalog.legacy_yaml",
61
+ message: `${fileLabel} uses legacy YAML keys (${formatLegacyReplacements(keys)})`,
62
+ category: "catalog",
63
+ };
64
+ }
65
+ export function dialectWarningForDocument(raw, fileLabel) {
66
+ const keys = collectDocumentKeys(raw);
67
+ const legacyKeys = presentLegacyKeys(keys);
68
+ if (legacyKeys.length === 0)
69
+ return undefined;
70
+ for (const key of keys) {
71
+ if (TARGET_AUTHORING_KEYS.has(key))
72
+ return undefined;
73
+ }
74
+ return legacyYamlIssue(fileLabel, legacyKeys);
75
+ }
76
+ export function allowLegacyYamlAuthoring() {
77
+ if (legacyYamlOverride.getStore() === true)
78
+ return true;
79
+ const raw = process.env.STAGEFLOW_LEGACY_YAML;
80
+ if (raw === "0" || raw === "false")
81
+ return false;
82
+ return true;
83
+ }
84
+ export function withLegacyYamlAllowed(fn) {
85
+ return legacyYamlOverride.run(true, fn);
86
+ }
87
+ export function withLegacyYamlAllowedAsync(fn) {
88
+ return legacyYamlOverride.run(true, fn);
89
+ }
90
+ export function legacyAuthoringRejected(dialect, fileLabel, keys) {
91
+ if (dialect !== "legacy" || allowLegacyYamlAuthoring())
92
+ return undefined;
93
+ return {
94
+ code: "catalog.legacy_yaml",
95
+ message: `${fileLabel}: dual-read is disabled (STAGEFLOW_LEGACY_YAML=0). Run sf migrate-yaml or author io / verify / on_verify_fail (${formatLegacyReplacements(keys)}).`,
96
+ category: "catalog",
97
+ };
98
+ }
@@ -23,9 +23,10 @@ export type LoadIssue = {
23
23
  export type LoadOutcome<T> = {
24
24
  ok: true;
25
25
  value: T;
26
+ issues?: LoadIssue[];
26
27
  } | {
27
28
  ok: false;
28
29
  issues: LoadIssue[];
29
30
  };
30
31
  export declare function loadFailure<T>(issues: LoadIssue[]): LoadOutcome<T>;
31
- export declare function loadSuccess<T>(value: T): LoadOutcome<T>;
32
+ export declare function loadSuccess<T>(value: T, issues?: LoadIssue[]): LoadOutcome<T>;
@@ -1,6 +1,9 @@
1
1
  export function loadFailure(issues) {
2
2
  return { ok: false, issues };
3
3
  }
4
- export function loadSuccess(value) {
4
+ export function loadSuccess(value, issues) {
5
+ if (issues && issues.length > 0) {
6
+ return { ok: true, value, issues };
7
+ }
5
8
  return { ok: true, value };
6
9
  }
@@ -5,7 +5,9 @@ export type { LoadIssue, LoadOutcome } from "./loadOutcome.js";
5
5
  export declare function resolvePipelinePath(pipelinePath: string, cwd: string): Promise<string>;
6
6
  export declare function loadPipelineOutcome(nameOrPath: string, options?: {
7
7
  cwd?: string;
8
+ projectRoot?: string;
8
9
  }): Promise<LoadOutcome<LoadedPipeline>>;
9
10
  export declare function loadPipeline(nameOrPath: string, options?: {
10
11
  cwd?: string;
12
+ projectRoot?: string;
11
13
  }): Promise<LoadedPipeline>;
@@ -1,11 +1,88 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { compilePayloadSchema, expandPayloadSchemaRefs, isPayloadSchemaSubset, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
3
4
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
5
  import { mergePipelineStages } from "./mergePipelineIncludes.js";
5
6
  import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
6
- import { loadStageFromObjectOutcome, loadStageOutcome } from "./loadStage.js";
7
+ import { loadStageFromObjectOutcome, loadStageOutcome, afterCompletionForStage } from "./loadStage.js";
8
+ import { materializeStageModels } from "./materializeStageModels.js";
9
+ import { predecessorEdges } from "./pipelineNeeds.js";
7
10
  import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
11
+ import { recoveryRequiresCompletionIssue } from "./parseCompletionContract.js";
8
12
  import { validateCompletionContractForStage } from "./validateCompletionContract.js";
13
+ function schemaCompileIssue(stageId, field, err, pipelineId) {
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ if (err instanceof UnresolvedSchemaRefError) {
16
+ return {
17
+ code: "stage.unresolved_schema_ref",
18
+ message: `Pipeline ${pipelineId}: stage "${stageId}" ${field}: ${message}`,
19
+ category: "stage",
20
+ stageId,
21
+ };
22
+ }
23
+ return {
24
+ code: field === "payload_schema" ? "stage.invalid_payload_schema" : "stage.invalid_clone_input_schema",
25
+ message: `Pipeline ${pipelineId}: stage "${stageId}" ${field}: ${message}`,
26
+ category: "stage",
27
+ stageId,
28
+ };
29
+ }
30
+ function attachPipelineSchemas(stages, schemas, pipelineId) {
31
+ const options = schemas !== undefined ? { schemas } : undefined;
32
+ for (const stage of stages) {
33
+ if (stage.payload_schema !== undefined) {
34
+ try {
35
+ compilePayloadSchema(stage.payload_schema, options);
36
+ stage.payload_schema = expandPayloadSchemaRefs(stage.payload_schema, options);
37
+ }
38
+ catch (err) {
39
+ return loadFailure([schemaCompileIssue(stage.id, "payload_schema", err, pipelineId)]);
40
+ }
41
+ }
42
+ if (stage.clone_input_schema !== undefined) {
43
+ try {
44
+ compilePayloadSchema(stage.clone_input_schema, options);
45
+ stage.clone_input_schema = expandPayloadSchemaRefs(stage.clone_input_schema, options);
46
+ }
47
+ catch (err) {
48
+ return loadFailure([schemaCompileIssue(stage.id, "clone_input_schema", err, pipelineId)]);
49
+ }
50
+ }
51
+ }
52
+ return loadSuccess(undefined);
53
+ }
54
+ function checkSequentialIoCompatibility(stages, dag, pipelineId, schemas) {
55
+ const stageById = new Map(stages.map((stage) => [stage.id, stage]));
56
+ const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
57
+ const options = schemas !== undefined ? { schemas } : undefined;
58
+ for (const child of stages) {
59
+ const node = nodeById.get(child.id);
60
+ if (!node)
61
+ continue;
62
+ const parents = predecessorEdges(node);
63
+ if (parents.length !== 1)
64
+ continue;
65
+ if (node.clonable)
66
+ continue;
67
+ const parentNode = nodeById.get(parents[0].id);
68
+ if (parentNode?.clonable)
69
+ continue;
70
+ const parent = stageById.get(parents[0].id);
71
+ if (!parent?.payload_schema || child.clone_input_schema === undefined)
72
+ continue;
73
+ if (!isPayloadSchemaSubset(child.clone_input_schema, parent.payload_schema, options)) {
74
+ return loadFailure([
75
+ {
76
+ code: "pipeline.io_incompatible",
77
+ message: `Pipeline ${pipelineId}: stage "${child.id}" io.input is not a structural subset of "${parent.id}" io.output`,
78
+ category: "pipeline",
79
+ pipelineId,
80
+ },
81
+ ]);
82
+ }
83
+ }
84
+ return loadSuccess(undefined);
85
+ }
9
86
  export async function resolvePipelinePath(pipelinePath, cwd) {
10
87
  const resolved = path.normalize(path.resolve(cwd, pipelinePath));
11
88
  try {
@@ -16,14 +93,15 @@ export async function resolvePipelinePath(pipelinePath, cwd) {
16
93
  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.`);
17
94
  }
18
95
  }
19
- async function loadPipelineFromPath(pipelinePath) {
96
+ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
20
97
  const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
21
98
  const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
22
99
  if (!mergeOutcome.ok) {
23
100
  return loadFailure(mergeOutcome.issues);
24
101
  }
25
- const { entries: rawEntries, pipelineId, agent: pipelineAgent } = mergeOutcome.value;
102
+ const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, warnings: mergeWarnings, } = mergeOutcome.value;
26
103
  const ctx = { pipelineId, path: normalizedPipelinePath };
104
+ const warnings = [...mergeWarnings];
27
105
  const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
28
106
  if (!normalizeOutcome.ok) {
29
107
  return loadFailure(normalizeOutcome.issues);
@@ -51,6 +129,7 @@ async function loadPipelineFromPath(pipelinePath) {
51
129
  const entryById = new Map(normalizedEntries.map((entry) => [entry.id, entry]));
52
130
  const stageSources = {};
53
131
  const stages = [];
132
+ const fileAfterById = new Map();
54
133
  for (const stageId of stageIds) {
55
134
  const entry = entryById.get(stageId);
56
135
  if (!entry) {
@@ -67,6 +146,7 @@ async function loadPipelineFromPath(pipelinePath) {
67
146
  const inlineOutcome = loadStageFromObjectOutcome(entry.body.raw, {
68
147
  entryId: entry.id,
69
148
  declaringPath: entry.declaringPath,
149
+ deferSchemaRefs: true,
70
150
  });
71
151
  if (!inlineOutcome.ok) {
72
152
  return loadFailure(inlineOutcome.issues);
@@ -75,8 +155,13 @@ async function loadPipelineFromPath(pipelinePath) {
75
155
  stageSources[stageId] = { kind: "inline" };
76
156
  continue;
77
157
  }
78
- const stageOutcome = await loadStageOutcome(entry.body.absolutePath);
158
+ const stageOutcome = await loadStageOutcome(entry.body.absolutePath, {
159
+ deferSchemaRefs: true,
160
+ });
79
161
  if (!stageOutcome.ok) {
162
+ if (stageOutcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
163
+ return loadFailure(stageOutcome.issues);
164
+ }
80
165
  return loadFailure([
81
166
  {
82
167
  code: "pipeline.missing_stage",
@@ -87,6 +172,22 @@ async function loadPipelineFromPath(pipelinePath) {
87
172
  ...stageOutcome.issues,
88
173
  ]);
89
174
  }
175
+ if (stageOutcome.issues)
176
+ warnings.push(...stageOutcome.issues);
177
+ const fileAfter = afterCompletionForStage(stageOutcome.value);
178
+ if (fileAfter && entry.completion) {
179
+ return loadFailure([
180
+ {
181
+ code: "pipeline.invalid_completion",
182
+ message: `Pipeline ${pipelineId}: stage "${stageId}" after-checks come from both body verify and wrapper completion`,
183
+ category: "pipeline",
184
+ pipelineId,
185
+ },
186
+ ]);
187
+ }
188
+ if (fileAfter && !entry.completion) {
189
+ fileAfterById.set(stageId, fileAfter);
190
+ }
90
191
  if (stageOutcome.value.id !== entry.id) {
91
192
  return loadFailure([
92
193
  {
@@ -105,13 +206,53 @@ async function loadPipelineFromPath(pipelinePath) {
105
206
  stages.push(stage);
106
207
  stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
107
208
  }
209
+ const schemaOutcome = attachPipelineSchemas(stages, pipelineSchemas, pipelineId);
210
+ if (!schemaOutcome.ok)
211
+ return schemaOutcome;
212
+ const ioOutcome = checkSequentialIoCompatibility(stages, dag, pipelineId, pipelineSchemas);
213
+ if (!ioOutcome.ok)
214
+ return ioOutcome;
215
+ if (pipelineModel !== undefined) {
216
+ const inheritingIds = stages
217
+ .filter((stage) => stage.model === undefined)
218
+ .map((stage) => stage.id);
219
+ if (inheritingIds.length > 0) {
220
+ warnings.push({
221
+ code: "pipeline.model_applies",
222
+ message: `Pipeline ${pipelineId}: pipeline-root model now applies to stages that omit model (${inheritingIds.join(", ")})`,
223
+ category: "pipeline",
224
+ pipelineId,
225
+ });
226
+ }
227
+ }
228
+ const materializeOutcome = await materializeStageModels(stages, {
229
+ pipelineModel,
230
+ pipelineId,
231
+ projectRoot,
232
+ });
233
+ if (!materializeOutcome.ok) {
234
+ return loadFailure(materializeOutcome.issues);
235
+ }
236
+ const loadedStages = materializeOutcome.value;
108
237
  const pipeline = {
109
238
  id: pipelineId,
110
239
  stages: stageIds,
111
240
  ...(pipelineAgent !== undefined ? { agent: pipelineAgent } : {}),
241
+ ...(pipelineModel !== undefined ? { model: pipelineModel } : {}),
242
+ ...(pipelineSchemas !== undefined ? { schemas: pipelineSchemas } : {}),
112
243
  };
113
244
  const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
114
- for (const stage of stages) {
245
+ for (const [stageId, after] of fileAfterById) {
246
+ const node = nodeById.get(stageId);
247
+ if (node)
248
+ node.completion = after;
249
+ }
250
+ for (const node of dag.nodes) {
251
+ if (node.recovery !== undefined && node.completion === undefined) {
252
+ return loadFailure([recoveryRequiresCompletionIssue(node.id)]);
253
+ }
254
+ }
255
+ for (const stage of loadedStages) {
115
256
  const completion = nodeById.get(stage.id)?.completion;
116
257
  const completionOutcome = validateCompletionContractForStage(stage, completion);
117
258
  if (!completionOutcome.ok)
@@ -119,14 +260,15 @@ async function loadPipelineFromPath(pipelinePath) {
119
260
  }
120
261
  return loadSuccess({
121
262
  pipeline,
122
- stages,
263
+ stages: loadedStages,
123
264
  dag,
124
265
  pipelinePath: normalizedPipelinePath,
125
266
  stageSources,
126
- });
267
+ }, warnings);
127
268
  }
128
269
  export async function loadPipelineOutcome(nameOrPath, options = {}) {
129
270
  const cwd = options.cwd ?? process.cwd();
271
+ const projectRoot = options.projectRoot ?? cwd;
130
272
  let pipelinePath;
131
273
  try {
132
274
  pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
@@ -141,7 +283,7 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
141
283
  },
142
284
  ]);
143
285
  }
144
- return loadPipelineFromPath(pipelinePath);
286
+ return loadPipelineFromPath(pipelinePath, cwd, projectRoot);
145
287
  }
146
288
  export async function loadPipeline(nameOrPath, options = {}) {
147
289
  const outcome = await loadPipelineOutcome(nameOrPath, options);
@@ -1,9 +1,16 @@
1
+ import type { CompletionContract } from "../types/completion.js";
1
2
  import { type StageConfig } from "../types/stage.js";
2
3
  import { type LoadOutcome } from "./loadOutcome.js";
4
+ /** After-phase IR from target `verify` (`when` includes after). Stamped onto DAG `completion`. */
5
+ export declare function afterCompletionForStage(stage: StageConfig): CompletionContract | undefined;
3
6
  export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
7
+ export type LoadStageOptions = {
8
+ deferSchemaRefs?: boolean;
9
+ };
4
10
  export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
5
11
  entryId: string;
6
12
  declaringPath: string;
13
+ deferSchemaRefs?: boolean;
7
14
  }): LoadOutcome<StageConfig>;
8
- export declare function loadStageOutcome(filePath: string): Promise<LoadOutcome<StageConfig>>;
15
+ export declare function loadStageOutcome(filePath: string, options?: LoadStageOptions): Promise<LoadOutcome<StageConfig>>;
9
16
  export declare function loadStage(filePath: string): Promise<StageConfig>;
@@ -2,10 +2,18 @@ import { parseAgentField } from "../agent/agentBackend.js";
2
2
  import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
3
3
  import { CLONE_ACTIONS } from "../types/forkChoice.js";
4
4
  import { STAGE_GATE_KINDS, } from "../types/stage.js";
5
- import { compilePayloadSchema } from "../envelope/payloadSchema.js";
5
+ import { compilePayloadSchema, UnresolvedSchemaRefError } from "../envelope/payloadSchema.js";
6
6
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
7
+ import { parseModelField } from "./modelField.js";
8
+ import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
7
9
  import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
8
10
  import { readYamlObject } from "./readYamlObject.js";
11
+ import { applyCompiledBody, classifyYamlDocument, compileTargetContract, dialectFromKeys, mixedDialectIssue, STAGE_FILE_WIRING_KEYS, } from "./yamlDialect.js";
12
+ const afterCompletionByStage = new WeakMap();
13
+ /** After-phase IR from target `verify` (`when` includes after). Stamped onto DAG `completion`. */
14
+ export function afterCompletionForStage(stage) {
15
+ return afterCompletionByStage.get(stage);
16
+ }
9
17
  function isGateKind(value) {
10
18
  return STAGE_GATE_KINDS.includes(value);
11
19
  }
@@ -154,13 +162,14 @@ export function parseStageMcp(raw, label, stageId) {
154
162
  }
155
163
  return loadSuccess(names);
156
164
  }
157
- function parseStageFields(raw, label, entryId) {
158
- if (typeof raw.system_prompt !== "string" ||
159
- typeof raw.model !== "string") {
165
+ function parseStageFields(raw, label, entryId, deferSchemaRefs) {
166
+ // Reads IR field names. Target YAML must already be compiled via
167
+ // applyCompiledBody; legacy YAML uses these keys as authoring.
168
+ if (typeof raw.system_prompt !== "string") {
160
169
  return loadFailure([
161
170
  {
162
171
  code: "stage.invalid_shape",
163
- message: `Invalid stage ${label}: system_prompt and model are required strings`,
172
+ message: `Invalid stage ${label}: system_prompt is a required string`,
164
173
  category: "stage",
165
174
  stageId: entryId,
166
175
  },
@@ -169,8 +178,21 @@ function parseStageFields(raw, label, entryId) {
169
178
  const stage = {
170
179
  id: entryId,
171
180
  system_prompt: raw.system_prompt,
172
- model: raw.model,
173
181
  };
182
+ const modelField = parseModelField(raw.model);
183
+ if (!modelField.ok) {
184
+ return loadFailure([
185
+ {
186
+ code: "stage.invalid_model",
187
+ message: `Invalid stage ${label}: ${modelField.message}`,
188
+ category: "stage",
189
+ stageId: entryId,
190
+ },
191
+ ]);
192
+ }
193
+ if (modelField.value !== undefined) {
194
+ stage.model = modelField.value;
195
+ }
174
196
  if (raw.payload_schema !== undefined) {
175
197
  if (raw.payload_schema === null ||
176
198
  typeof raw.payload_schema !== "object" ||
@@ -188,15 +210,29 @@ function parseStageFields(raw, label, entryId) {
188
210
  compilePayloadSchema(raw.payload_schema);
189
211
  }
190
212
  catch (err) {
191
- const message = err instanceof Error ? err.message : String(err);
192
- return loadFailure([
193
- {
194
- code: "stage.invalid_payload_schema",
195
- message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
196
- category: "stage",
197
- stageId: entryId,
198
- },
199
- ]);
213
+ if (err instanceof UnresolvedSchemaRefError) {
214
+ if (!deferSchemaRefs) {
215
+ return loadFailure([
216
+ {
217
+ code: "stage.unresolved_schema_ref",
218
+ message: `Invalid stage ${label}: ${err.message}`,
219
+ category: "stage",
220
+ stageId: entryId,
221
+ },
222
+ ]);
223
+ }
224
+ }
225
+ else {
226
+ const message = err instanceof Error ? err.message : String(err);
227
+ return loadFailure([
228
+ {
229
+ code: "stage.invalid_payload_schema",
230
+ message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
231
+ category: "stage",
232
+ stageId: entryId,
233
+ },
234
+ ]);
235
+ }
200
236
  }
201
237
  stage.payload_schema = raw.payload_schema;
202
238
  }
@@ -217,15 +253,29 @@ function parseStageFields(raw, label, entryId) {
217
253
  compilePayloadSchema(raw.clone_input_schema);
218
254
  }
219
255
  catch (err) {
220
- const message = err instanceof Error ? err.message : String(err);
221
- return loadFailure([
222
- {
223
- code: "stage.invalid_clone_input_schema",
224
- message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
225
- category: "stage",
226
- stageId: entryId,
227
- },
228
- ]);
256
+ if (err instanceof UnresolvedSchemaRefError) {
257
+ if (!deferSchemaRefs) {
258
+ return loadFailure([
259
+ {
260
+ code: "stage.unresolved_schema_ref",
261
+ message: `Invalid stage ${label}: ${err.message}`,
262
+ category: "stage",
263
+ stageId: entryId,
264
+ },
265
+ ]);
266
+ }
267
+ }
268
+ else {
269
+ const message = err instanceof Error ? err.message : String(err);
270
+ return loadFailure([
271
+ {
272
+ code: "stage.invalid_clone_input_schema",
273
+ message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
274
+ category: "stage",
275
+ stageId: entryId,
276
+ },
277
+ ]);
278
+ }
229
279
  }
230
280
  stage.clone_input_schema = raw.clone_input_schema;
231
281
  }
@@ -310,9 +360,28 @@ function parseStageFields(raw, label, entryId) {
310
360
  }
311
361
  export function loadStageFromObjectOutcome(raw, ctx) {
312
362
  const label = `${ctx.entryId} (${ctx.declaringPath})`;
313
- return parseStageFields(raw, label, ctx.entryId);
363
+ const deferSchemaRefs = ctx.deferSchemaRefs === true;
364
+ const dialect = dialectFromKeys(Object.keys(raw));
365
+ if (dialect === "invalid") {
366
+ return loadFailure([mixedDialectIssue()]);
367
+ }
368
+ const rejected = legacyAuthoringRejected(dialect, label, presentLegacyKeys(Object.keys(raw)));
369
+ if (rejected)
370
+ return loadFailure([rejected]);
371
+ if (dialect === "target") {
372
+ const compiled = compileTargetContract(raw, {
373
+ stageId: ctx.entryId,
374
+ label,
375
+ category: "stage",
376
+ deferSchemaRefs,
377
+ });
378
+ if (!compiled.ok)
379
+ return compiled;
380
+ return parseStageFields(applyCompiledBody(raw, compiled.value), label, ctx.entryId, deferSchemaRefs);
381
+ }
382
+ return parseStageFields(raw, label, ctx.entryId, deferSchemaRefs);
314
383
  }
315
- export async function loadStageOutcome(filePath) {
384
+ export async function loadStageOutcome(filePath, options = {}) {
316
385
  let raw;
317
386
  try {
318
387
  raw = await readYamlObject(filePath);
@@ -331,12 +400,47 @@ export async function loadStageOutcome(filePath) {
331
400
  return loadFailure([
332
401
  {
333
402
  code: "stage.invalid_shape",
334
- message: `Invalid stage file ${filePath}: id, system_prompt, and model are required strings`,
403
+ message: `Invalid stage file ${filePath}: id and system_prompt are required strings`,
335
404
  category: "stage",
336
405
  },
337
406
  ]);
338
407
  }
339
- const outcome = parseStageFields(raw, `file ${filePath}`, raw.id);
408
+ const dialect = classifyYamlDocument(raw);
409
+ if (dialect === "invalid") {
410
+ return loadFailure([mixedDialectIssue()]);
411
+ }
412
+ const rejected = legacyAuthoringRejected(dialect, filePath, presentLegacyKeys(Object.keys(raw)));
413
+ if (rejected)
414
+ return loadFailure([rejected]);
415
+ if (dialect === "target") {
416
+ const wiring = STAGE_FILE_WIRING_KEYS.find((key) => raw[key] !== undefined);
417
+ if (wiring) {
418
+ return loadFailure([
419
+ {
420
+ code: "stage.invalid_shape",
421
+ message: `Invalid stage file ${filePath}: new-dialect stage files must not declare wiring key "${wiring}"`,
422
+ category: "stage",
423
+ stageId: raw.id,
424
+ },
425
+ ]);
426
+ }
427
+ }
428
+ let parseRaw = raw;
429
+ let afterCompletion;
430
+ const deferSchemaRefs = options.deferSchemaRefs === true;
431
+ if (dialect === "target") {
432
+ const compiled = compileTargetContract(raw, {
433
+ stageId: raw.id,
434
+ label: `file ${filePath}`,
435
+ category: "stage",
436
+ deferSchemaRefs,
437
+ });
438
+ if (!compiled.ok)
439
+ return compiled;
440
+ parseRaw = applyCompiledBody(raw, compiled.value);
441
+ afterCompletion = compiled.value.completion;
442
+ }
443
+ const outcome = parseStageFields(parseRaw, `file ${filePath}`, raw.id, deferSchemaRefs);
340
444
  if (!outcome.ok)
341
445
  return outcome;
342
446
  if (outcome.value.id !== raw.id) {
@@ -349,7 +453,12 @@ export async function loadStageOutcome(filePath) {
349
453
  },
350
454
  ]);
351
455
  }
352
- return outcome;
456
+ if (afterCompletion)
457
+ afterCompletionByStage.set(outcome.value, afterCompletion);
458
+ const warning = allowLegacyYamlAuthoring()
459
+ ? dialectWarningForDocument(raw, filePath)
460
+ : undefined;
461
+ return loadSuccess(outcome.value, warning ? [warning] : undefined);
353
462
  }
354
463
  export async function loadStage(filePath) {
355
464
  const outcome = await loadStageOutcome(filePath);
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import { parseAgentField } from "../agent/agentBackend.js";
5
5
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
6
+ import { parseModelField } from "./modelField.js";
6
7
  const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
7
8
  const DEFAULT_TASK_PATTERN = "*.task.yaml";
8
9
  export function manifestPathForProject(projectRoot) {
@@ -117,6 +118,14 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
117
118
  else {
118
119
  agent = agentField.value;
119
120
  }
121
+ let model;
122
+ const modelField = parseModelField(record.model);
123
+ if (!modelField.ok) {
124
+ issues.push(catalogIssue("catalog.manifest_invalid", modelField.message));
125
+ }
126
+ else {
127
+ model = modelField.value;
128
+ }
120
129
  if (issues.length > 0) {
121
130
  return loadFailure(issues);
122
131
  }
@@ -140,6 +149,7 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
140
149
  version: 1,
141
150
  catalog,
142
151
  ...(agent !== undefined ? { agent } : {}),
152
+ ...(model !== undefined ? { model } : {}),
143
153
  };
144
154
  return loadSuccess({
145
155
  path: manifestPath,