stageflow 0.12.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 (115) hide show
  1. package/README.md +5 -1
  2. package/dist/agent/claudeAdapter.js +31 -1
  3. package/dist/agent/piAdapter.d.ts +6 -4
  4. package/dist/agent/piAdapter.js +71 -26
  5. package/dist/agent/piIsolatedMcp.d.ts +23 -0
  6. package/dist/agent/piIsolatedMcp.js +223 -0
  7. package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
  8. package/dist/agent/piIsolatedMcpProbe.js +292 -0
  9. package/dist/agent/port.d.ts +4 -2
  10. package/dist/agent/port.js +1 -1
  11. package/dist/cli/initTemplates.d.ts +1 -1
  12. package/dist/cli/initTemplates.js +1 -1
  13. package/dist/cli/migrateYamlCommand.d.ts +18 -0
  14. package/dist/cli/migrateYamlCommand.js +180 -0
  15. package/dist/cli/runOutput.js +26 -18
  16. package/dist/cli/validateCommand.d.ts +1 -0
  17. package/dist/cli/validateCommand.js +2 -0
  18. package/dist/cli.js +15 -1
  19. package/dist/config/browseCatalog.js +1 -1
  20. package/dist/config/createPipeline.d.ts +1 -1
  21. package/dist/config/createPipeline.js +23 -11
  22. package/dist/config/createStage.d.ts +1 -1
  23. package/dist/config/createStage.js +10 -5
  24. package/dist/config/legacyYaml.d.ts +28 -0
  25. package/dist/config/legacyYaml.js +98 -0
  26. package/dist/config/loadOutcome.d.ts +2 -1
  27. package/dist/config/loadOutcome.js +4 -1
  28. package/dist/config/loadPipeline.d.ts +2 -0
  29. package/dist/config/loadPipeline.js +151 -8
  30. package/dist/config/loadStage.d.ts +9 -1
  31. package/dist/config/loadStage.js +207 -29
  32. package/dist/config/loadStageflowManifest.js +10 -0
  33. package/dist/config/loadTask.js +14 -0
  34. package/dist/config/materializeStageModels.d.ts +7 -0
  35. package/dist/config/materializeStageModels.js +35 -0
  36. package/dist/config/mergePipelineIncludes.d.ts +5 -1
  37. package/dist/config/mergePipelineIncludes.js +82 -4
  38. package/dist/config/migrateYaml.d.ts +19 -0
  39. package/dist/config/migrateYaml.js +627 -0
  40. package/dist/config/modelField.d.ts +13 -0
  41. package/dist/config/modelField.js +16 -0
  42. package/dist/config/normalizePipelineStageEntry.js +48 -3
  43. package/dist/config/parseCompletionContract.d.ts +5 -2
  44. package/dist/config/parseCompletionContract.js +16 -3
  45. package/dist/config/parsePreEmitChecks.js +4 -0
  46. package/dist/config/pipelineNeeds.js +3 -3
  47. package/dist/config/pipelineStageKeys.d.ts +11 -0
  48. package/dist/config/pipelineStageKeys.js +15 -0
  49. package/dist/config/printTargetYaml.d.ts +28 -0
  50. package/dist/config/printTargetYaml.js +243 -0
  51. package/dist/config/resolveModel.d.ts +20 -0
  52. package/dist/config/resolveModel.js +35 -0
  53. package/dist/config/resolveStageMcpServers.d.ts +35 -0
  54. package/dist/config/resolveStageMcpServers.js +207 -0
  55. package/dist/config/validateCatalog.d.ts +5 -1
  56. package/dist/config/validateCatalog.js +87 -6
  57. package/dist/config/validateCompletionContract.js +5 -0
  58. package/dist/config/yamlDialect.d.ts +29 -0
  59. package/dist/config/yamlDialect.js +327 -0
  60. package/dist/envelope/payloadSchema.d.ts +37 -5
  61. package/dist/envelope/payloadSchema.js +272 -22
  62. package/dist/mcp/catalogTools.js +1 -0
  63. package/dist/mcp/readArtifact.d.ts +2 -0
  64. package/dist/mcp/readArtifact.js +25 -1
  65. package/dist/package-meta.d.ts +1 -1
  66. package/dist/package-meta.js +1 -1
  67. package/dist/runstore/pipelineDagSnapshot.js +2 -0
  68. package/dist/runstore/port.d.ts +1 -0
  69. package/dist/runtime/completionCheckRunner.d.ts +3 -2
  70. package/dist/runtime/completionCheckRunner.js +3 -2
  71. package/dist/runtime/envelopeRouting.d.ts +2 -2
  72. package/dist/runtime/pipelineRunner.d.ts +3 -0
  73. package/dist/runtime/pipelineRunner.js +23 -3
  74. package/dist/runtime/runManager.js +3 -2
  75. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  76. package/dist/runtime/stageAttemptBootstrap.js +38 -2
  77. package/dist/runtime/stageRunner.d.ts +2 -2
  78. package/dist/runtime/taskInput.d.ts +7 -0
  79. package/dist/runtime/taskInput.js +60 -0
  80. package/dist/runtime/verifiedStageExecution.d.ts +2 -0
  81. package/dist/runtime/verifiedStageExecution.js +13 -2
  82. package/dist/server/http.js +23 -4
  83. package/dist/server/projectMcpRoutes.d.ts +6 -0
  84. package/dist/server/projectMcpRoutes.js +38 -0
  85. package/dist/types/completion.d.ts +6 -0
  86. package/dist/types/pipeline.d.ts +23 -2
  87. package/dist/types/preEmitCheck.d.ts +4 -6
  88. package/dist/types/stage.d.ts +24 -4
  89. package/dist/types/stageflowManifest.d.ts +2 -0
  90. package/dist/types/task.d.ts +1 -0
  91. package/dist/ui/assets/index-GtyiWiZg.js +118 -0
  92. package/dist/ui/index.html +1 -1
  93. package/package.json +4 -1
  94. package/skills/stageflow-author/SKILL.md +20 -2
  95. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
  96. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
  97. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
  98. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
  99. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
  100. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
  101. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
  102. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
  103. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
  104. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
  105. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
  106. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
  107. package/skills/stageflow-author/references/catalog-mapping.md +80 -17
  108. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  109. package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
  110. package/skills/stageflow-session-capture/SKILL.md +1 -1
  111. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
  112. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
  113. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
  114. package/skills/stageflow-session-capture/references/catalog-authoring.md +6 -3
  115. package/dist/ui/assets/index-CXGz4nL7.js +0 -118
@@ -1,10 +1,19 @@
1
1
  import { parseAgentField } from "../agent/agentBackend.js";
2
+ import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
2
3
  import { CLONE_ACTIONS } from "../types/forkChoice.js";
3
4
  import { STAGE_GATE_KINDS, } from "../types/stage.js";
4
- import { compilePayloadSchema } from "../envelope/payloadSchema.js";
5
+ import { compilePayloadSchema, UnresolvedSchemaRefError } from "../envelope/payloadSchema.js";
5
6
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
7
+ import { parseModelField } from "./modelField.js";
8
+ import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
6
9
  import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
7
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
+ }
8
17
  function isGateKind(value) {
9
18
  return STAGE_GATE_KINDS.includes(value);
10
19
  }
@@ -91,13 +100,76 @@ function parseGateKinds(raw, label) {
91
100
  }
92
101
  return loadSuccess(kinds);
93
102
  }
94
- function parseStageFields(raw, label, entryId) {
95
- if (typeof raw.system_prompt !== "string" ||
96
- typeof raw.model !== "string") {
103
+ export function parseStageMcp(raw, label, stageId) {
104
+ if (raw === undefined)
105
+ return loadSuccess(undefined);
106
+ if (!Array.isArray(raw)) {
107
+ return loadFailure([
108
+ {
109
+ code: "stage.invalid_mcp",
110
+ message: `Invalid stage ${label}: mcp must be an array of server names`,
111
+ category: "stage",
112
+ stageId,
113
+ },
114
+ ]);
115
+ }
116
+ const names = [];
117
+ const seen = new Set();
118
+ for (const item of raw) {
119
+ if (typeof item !== "string") {
120
+ return loadFailure([
121
+ {
122
+ code: "stage.invalid_mcp",
123
+ message: `Invalid stage ${label}: mcp entries must be strings`,
124
+ category: "stage",
125
+ stageId,
126
+ },
127
+ ]);
128
+ }
129
+ const name = item.trim();
130
+ if (name === "") {
131
+ return loadFailure([
132
+ {
133
+ code: "stage.invalid_mcp",
134
+ message: `Invalid stage ${label}: mcp entries must be non-empty strings`,
135
+ category: "stage",
136
+ stageId,
137
+ },
138
+ ]);
139
+ }
140
+ if (name === STAGEFLOW_MCP_SERVER_NAME) {
141
+ return loadFailure([
142
+ {
143
+ code: "stage.invalid_mcp",
144
+ message: `Invalid stage ${label}: mcp must not include reserved name "${STAGEFLOW_MCP_SERVER_NAME}"`,
145
+ category: "stage",
146
+ stageId,
147
+ },
148
+ ]);
149
+ }
150
+ if (seen.has(name)) {
151
+ return loadFailure([
152
+ {
153
+ code: "stage.invalid_mcp",
154
+ message: `Invalid stage ${label}: mcp contains duplicate name "${name}"`,
155
+ category: "stage",
156
+ stageId,
157
+ },
158
+ ]);
159
+ }
160
+ seen.add(name);
161
+ names.push(name);
162
+ }
163
+ return loadSuccess(names);
164
+ }
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") {
97
169
  return loadFailure([
98
170
  {
99
171
  code: "stage.invalid_shape",
100
- message: `Invalid stage ${label}: system_prompt and model are required strings`,
172
+ message: `Invalid stage ${label}: system_prompt is a required string`,
101
173
  category: "stage",
102
174
  stageId: entryId,
103
175
  },
@@ -106,8 +178,21 @@ function parseStageFields(raw, label, entryId) {
106
178
  const stage = {
107
179
  id: entryId,
108
180
  system_prompt: raw.system_prompt,
109
- model: raw.model,
110
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
+ }
111
196
  if (raw.payload_schema !== undefined) {
112
197
  if (raw.payload_schema === null ||
113
198
  typeof raw.payload_schema !== "object" ||
@@ -125,15 +210,29 @@ function parseStageFields(raw, label, entryId) {
125
210
  compilePayloadSchema(raw.payload_schema);
126
211
  }
127
212
  catch (err) {
128
- const message = err instanceof Error ? err.message : String(err);
129
- return loadFailure([
130
- {
131
- code: "stage.invalid_payload_schema",
132
- message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
133
- category: "stage",
134
- stageId: entryId,
135
- },
136
- ]);
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
+ }
137
236
  }
138
237
  stage.payload_schema = raw.payload_schema;
139
238
  }
@@ -154,15 +253,29 @@ function parseStageFields(raw, label, entryId) {
154
253
  compilePayloadSchema(raw.clone_input_schema);
155
254
  }
156
255
  catch (err) {
157
- const message = err instanceof Error ? err.message : String(err);
158
- return loadFailure([
159
- {
160
- code: "stage.invalid_clone_input_schema",
161
- message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
162
- category: "stage",
163
- stageId: entryId,
164
- },
165
- ]);
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
+ }
166
279
  }
167
280
  stage.clone_input_schema = raw.clone_input_schema;
168
281
  }
@@ -223,6 +336,12 @@ function parseStageFields(raw, label, entryId) {
223
336
  }
224
337
  stage.skill = raw.skill.trim();
225
338
  }
339
+ const mcpOutcome = parseStageMcp(raw.mcp, label, entryId);
340
+ if (!mcpOutcome.ok)
341
+ return mcpOutcome;
342
+ if (mcpOutcome.value !== undefined) {
343
+ stage.mcp = mcpOutcome.value;
344
+ }
226
345
  const agentField = parseAgentField(raw.agent);
227
346
  if (!agentField.ok) {
228
347
  return loadFailure([
@@ -241,9 +360,28 @@ function parseStageFields(raw, label, entryId) {
241
360
  }
242
361
  export function loadStageFromObjectOutcome(raw, ctx) {
243
362
  const label = `${ctx.entryId} (${ctx.declaringPath})`;
244
- 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);
245
383
  }
246
- export async function loadStageOutcome(filePath) {
384
+ export async function loadStageOutcome(filePath, options = {}) {
247
385
  let raw;
248
386
  try {
249
387
  raw = await readYamlObject(filePath);
@@ -262,12 +400,47 @@ export async function loadStageOutcome(filePath) {
262
400
  return loadFailure([
263
401
  {
264
402
  code: "stage.invalid_shape",
265
- 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`,
266
404
  category: "stage",
267
405
  },
268
406
  ]);
269
407
  }
270
- 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);
271
444
  if (!outcome.ok)
272
445
  return outcome;
273
446
  if (outcome.value.id !== raw.id) {
@@ -280,7 +453,12 @@ export async function loadStageOutcome(filePath) {
280
453
  },
281
454
  ]);
282
455
  }
283
- 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);
284
462
  }
285
463
  export async function loadStage(filePath) {
286
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,
@@ -1,6 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parse as parseYaml } from "yaml";
3
3
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
+ function isPlainObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
4
7
  export function parseTaskFile(raw, source = "task") {
5
8
  const record = raw;
6
9
  if (typeof record?.id !== "string" || typeof record?.goal !== "string") {
@@ -13,12 +16,23 @@ export function parseTaskFile(raw, source = "task") {
13
16
  },
14
17
  ]);
15
18
  }
19
+ if (record.input !== undefined && !isPlainObject(record.input)) {
20
+ return loadFailure([
21
+ {
22
+ code: "task.invalid_shape",
23
+ message: `Invalid ${source}: input must be an object`,
24
+ category: "task",
25
+ taskId: record.id,
26
+ },
27
+ ]);
28
+ }
16
29
  return loadSuccess({
17
30
  id: record.id,
18
31
  goal: record.goal,
19
32
  context: typeof record.context === "string" ? record.context : undefined,
20
33
  constraints: typeof record.constraints === "string" ? record.constraints : undefined,
21
34
  checkout: typeof record.checkout === "string" ? record.checkout : undefined,
35
+ ...(record.input !== undefined ? { input: record.input } : {}),
22
36
  });
23
37
  }
24
38
  export function loadTaskFromYamlOutcome(yamlText, source = "task yaml") {
@@ -0,0 +1,7 @@
1
+ import type { LoadedStageConfig, StageConfig } from "../types/stage.js";
2
+ import { type LoadOutcome } from "./loadOutcome.js";
3
+ export declare function materializeStageModels(stages: StageConfig[], options: {
4
+ pipelineModel?: string;
5
+ pipelineId: string;
6
+ projectRoot: string;
7
+ }): Promise<LoadOutcome<LoadedStageConfig[]>>;
@@ -0,0 +1,35 @@
1
+ import { access } from "node:fs/promises";
2
+ import { loadFailure, loadSuccess } from "./loadOutcome.js";
3
+ import { loadStageflowManifestOutcome, manifestPathForProject, } from "./loadStageflowManifest.js";
4
+ import { globalModelFromManifest, resolveModelOutcome } from "./resolveModel.js";
5
+ export async function materializeStageModels(stages, options) {
6
+ let globalModel;
7
+ const manifestPath = manifestPathForProject(options.projectRoot);
8
+ let manifestExists = true;
9
+ try {
10
+ await access(manifestPath);
11
+ }
12
+ catch {
13
+ manifestExists = false;
14
+ }
15
+ if (manifestExists) {
16
+ const manifestOutcome = await loadStageflowManifestOutcome(options.projectRoot);
17
+ if (!manifestOutcome.ok) {
18
+ return loadFailure(manifestOutcome.issues);
19
+ }
20
+ globalModel = globalModelFromManifest(manifestOutcome.value);
21
+ }
22
+ const materialized = [];
23
+ for (const stage of stages) {
24
+ const modelOutcome = resolveModelOutcome({
25
+ stage: stage.model,
26
+ pipeline: options.pipelineModel,
27
+ global: globalModel,
28
+ }, { stageId: stage.id, pipelineId: options.pipelineId });
29
+ if (!modelOutcome.ok) {
30
+ return loadFailure(modelOutcome.issues);
31
+ }
32
+ materialized.push({ ...stage, model: modelOutcome.value });
33
+ }
34
+ return loadSuccess(materialized);
35
+ }
@@ -1,4 +1,5 @@
1
- import { type LoadOutcome } from "./loadOutcome.js";
1
+ import type { PayloadSchemaMap } from "../envelope/payloadSchema.js";
2
+ import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
2
3
  export type RawMergedEntry = {
3
4
  raw: unknown;
4
5
  declaringPath: string;
@@ -7,4 +8,7 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
7
8
  entries: RawMergedEntry[];
8
9
  pipelineId: string;
9
10
  agent?: string;
11
+ model?: string;
12
+ schemas?: PayloadSchemaMap;
13
+ warnings: LoadIssue[];
10
14
  }>>;
@@ -1,7 +1,10 @@
1
1
  import path from "node:path";
2
2
  import { parseAgentField } from "../agent/agentBackend.js";
3
3
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
+ import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
5
+ import { parseModelField } from "./modelField.js";
4
6
  import { readYamlObject } from "./readYamlObject.js";
7
+ import { classifyYamlDocument, collectDocumentKeys, mixedDialectIssue, } from "./yamlDialect.js";
5
8
  function isPlainObject(value) {
6
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
10
  }
@@ -12,7 +15,7 @@ function formatIncludeCycle(stack, nextPath) {
12
15
  const chain = [...stack, nextPath].map((p) => path.basename(p));
13
16
  return chain.join(" → ");
14
17
  }
15
- async function visitPipelineFile(filePath, stack, idLocations, entries) {
18
+ async function visitPipelineFile(filePath, stack, idLocations, entries, warnings) {
16
19
  const absPath = normalizePath(filePath);
17
20
  if (stack.some((p) => normalizePath(p) === absPath)) {
18
21
  const cycleChain = formatIncludeCycle(stack, absPath);
@@ -38,6 +41,28 @@ async function visitPipelineFile(filePath, stack, idLocations, entries) {
38
41
  },
39
42
  ]);
40
43
  }
44
+ const dialect = classifyYamlDocument(raw);
45
+ if (dialect === "invalid") {
46
+ return loadFailure([mixedDialectIssue()]);
47
+ }
48
+ const rejected = legacyAuthoringRejected(dialect, absPath, presentLegacyKeys(collectDocumentKeys(raw)));
49
+ if (rejected)
50
+ return loadFailure([rejected]);
51
+ if (allowLegacyYamlAuthoring()) {
52
+ const warning = dialectWarningForDocument(raw, absPath);
53
+ if (warning)
54
+ warnings.push(warning);
55
+ }
56
+ const isFragment = stack.length > 0;
57
+ if (isFragment && raw.schemas !== undefined) {
58
+ return loadFailure([
59
+ {
60
+ code: "pipeline.include_invalid",
61
+ message: `Invalid include in ${absPath}: schemas is only allowed on the pipeline file, not on include fragments`,
62
+ category: "pipeline",
63
+ },
64
+ ]);
65
+ }
41
66
  const nextStack = [...stack, absPath];
42
67
  if (raw.include !== undefined) {
43
68
  if (!Array.isArray(raw.include)) {
@@ -61,7 +86,7 @@ async function visitPipelineFile(filePath, stack, idLocations, entries) {
61
86
  ]);
62
87
  }
63
88
  const includePath = normalizePath(path.resolve(path.dirname(absPath), item.local));
64
- const includeResult = await visitPipelineFile(includePath, nextStack, idLocations, entries);
89
+ const includeResult = await visitPipelineFile(includePath, nextStack, idLocations, entries, warnings);
65
90
  if (!includeResult.ok)
66
91
  return includeResult;
67
92
  }
@@ -136,9 +161,26 @@ export async function mergePipelineStages(rootPath) {
136
161
  ]);
137
162
  }
138
163
  const agent = agentField.value;
164
+ const modelField = parseModelField(raw.model);
165
+ if (!modelField.ok) {
166
+ return loadFailure([
167
+ {
168
+ code: "pipeline.invalid_model",
169
+ message: `Invalid pipeline ${absRoot}: ${modelField.message}`,
170
+ category: "pipeline",
171
+ pipelineId,
172
+ },
173
+ ]);
174
+ }
175
+ const model = modelField.value;
176
+ const schemasOutcome = parsePipelineSchemas(raw.schemas, absRoot, pipelineId);
177
+ if (!schemasOutcome.ok)
178
+ return schemasOutcome;
179
+ const schemas = schemasOutcome.value;
139
180
  const idLocations = new Map();
140
181
  const entries = [];
141
- const mergeResult = await visitPipelineFile(absRoot, [], idLocations, entries);
182
+ const warnings = [];
183
+ const mergeResult = await visitPipelineFile(absRoot, [], idLocations, entries, warnings);
142
184
  if (!mergeResult.ok)
143
185
  return mergeResult;
144
186
  if (entries.length === 0) {
@@ -150,5 +192,41 @@ export async function mergePipelineStages(rootPath) {
150
192
  },
151
193
  ]);
152
194
  }
153
- return loadSuccess({ entries, pipelineId, ...(agent !== undefined ? { agent } : {}) });
195
+ return loadSuccess({
196
+ entries,
197
+ pipelineId,
198
+ ...(agent !== undefined ? { agent } : {}),
199
+ ...(model !== undefined ? { model } : {}),
200
+ ...(schemas !== undefined ? { schemas } : {}),
201
+ warnings,
202
+ });
203
+ }
204
+ function parsePipelineSchemas(raw, absPath, pipelineId) {
205
+ if (raw === undefined)
206
+ return loadSuccess(undefined);
207
+ if (!isPlainObject(raw)) {
208
+ return loadFailure([
209
+ {
210
+ code: "pipeline.invalid_shape",
211
+ message: `Invalid pipeline ${absPath}: schemas must be an object`,
212
+ category: "pipeline",
213
+ pipelineId,
214
+ },
215
+ ]);
216
+ }
217
+ const schemas = {};
218
+ for (const [name, value] of Object.entries(raw)) {
219
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
220
+ return loadFailure([
221
+ {
222
+ code: "pipeline.invalid_shape",
223
+ message: `Invalid pipeline ${absPath}: schemas.${name} must be an object`,
224
+ category: "pipeline",
225
+ pipelineId,
226
+ },
227
+ ]);
228
+ }
229
+ schemas[name] = value;
230
+ }
231
+ return loadSuccess(schemas);
154
232
  }
@@ -0,0 +1,19 @@
1
+ export type MigrateYamlWrite = {
2
+ file: string;
3
+ absPath: string;
4
+ content: string;
5
+ };
6
+ export type MigrateYamlPlan = {
7
+ writes: MigrateYamlWrite[];
8
+ skipped: string[];
9
+ errors: string[];
10
+ };
11
+ export declare function catalogKind(raw: Record<string, unknown>, filePath: string): "pipeline" | "stage" | "task" | "unknown";
12
+ export declare function fileDirtyVsHead(absPath: string): boolean;
13
+ export declare function planMigrateYaml(targetPath: string, options?: {
14
+ cwd: string;
15
+ projectRoot?: string;
16
+ }): Promise<MigrateYamlPlan>;
17
+ export declare function applyMigrateYamlPlan(plan: MigrateYamlPlan, testOnly?: {
18
+ beforeRename?: () => Promise<void>;
19
+ }): Promise<void>;