stageflow 0.16.0 → 0.18.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 (96) hide show
  1. package/README.md +2 -4
  2. package/dist/agent/claudeTools.d.ts +1 -4
  3. package/dist/agent/claudeTools.js +1 -6
  4. package/dist/agent/fakeAgent.js +4 -5
  5. package/dist/agent/piAdapter.js +1 -14
  6. package/dist/config/cloneChain.d.ts +12 -0
  7. package/dist/config/cloneChain.js +322 -0
  8. package/dist/config/createPipeline.js +101 -21
  9. package/dist/config/createStage.js +7 -0
  10. package/dist/config/loadOutcome.d.ts +1 -0
  11. package/dist/config/loadPipeline.d.ts +2 -0
  12. package/dist/config/loadPipeline.js +34 -22
  13. package/dist/config/loadStage.d.ts +2 -0
  14. package/dist/config/loadStage.js +135 -125
  15. package/dist/config/migrateYaml.js +14 -3
  16. package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
  17. package/dist/config/normalizePipelineStageEntry.js +133 -60
  18. package/dist/config/parsePreEmitChecks.js +2 -2
  19. package/dist/config/pipelineNeeds.d.ts +15 -2
  20. package/dist/config/pipelineNeeds.js +86 -4
  21. package/dist/config/pipelineRoute.d.ts +46 -0
  22. package/dist/config/pipelineRoute.js +215 -0
  23. package/dist/config/pipelineStageKeys.d.ts +2 -0
  24. package/dist/config/pipelineStageKeys.js +15 -4
  25. package/dist/config/printTargetYaml.js +18 -12
  26. package/dist/config/resolveForkEmitContext.d.ts +1 -1
  27. package/dist/config/resolveForkEmitContext.js +3 -26
  28. package/dist/config/resolvePipelineDag.d.ts +1 -3
  29. package/dist/config/resolvePipelineDag.js +143 -186
  30. package/dist/config/routeIf.d.ts +24 -0
  31. package/dist/config/routeIf.js +247 -0
  32. package/dist/config/validateCatalog.d.ts +1 -1
  33. package/dist/config/validateCatalog.js +5 -3
  34. package/dist/config/yamlDialect.d.ts +5 -8
  35. package/dist/config/yamlDialect.js +37 -29
  36. package/dist/envelope/check.d.ts +0 -2
  37. package/dist/envelope/check.js +1 -57
  38. package/dist/envelope/feedbackLoop.js +0 -3
  39. package/dist/envelope/forkChoice.js +1 -1
  40. package/dist/envelope/payloadSchema.d.ts +14 -0
  41. package/dist/envelope/payloadSchema.js +87 -2
  42. package/dist/mcp/catalogTools.js +1 -2
  43. package/dist/package-meta.d.ts +1 -1
  44. package/dist/package-meta.js +1 -1
  45. package/dist/projection/projectRun.d.ts +0 -2
  46. package/dist/projection/projectRun.js +0 -3
  47. package/dist/prompt/priorEnvelope.js +1 -1
  48. package/dist/runstore/stageInstanceId.d.ts +1 -0
  49. package/dist/runstore/stageInstanceId.js +9 -0
  50. package/dist/runtime/cloneSchedule.d.ts +16 -33
  51. package/dist/runtime/cloneSchedule.js +155 -236
  52. package/dist/runtime/envelopeRouting.js +43 -94
  53. package/dist/runtime/forkGeneration.d.ts +2 -2
  54. package/dist/runtime/forkGeneration.js +29 -15
  55. package/dist/runtime/joinReadiness.d.ts +16 -0
  56. package/dist/runtime/joinReadiness.js +156 -0
  57. package/dist/runtime/pipelineScheduler.d.ts +0 -3
  58. package/dist/runtime/pipelineScheduler.js +81 -65
  59. package/dist/runtime/routeIfEval.d.ts +3 -0
  60. package/dist/runtime/routeIfEval.js +94 -0
  61. package/dist/runtime/runRetryCoordinator.js +0 -4
  62. package/dist/runtime/stageAttemptBootstrap.js +60 -11
  63. package/dist/runtime/taskInput.js +1 -13
  64. package/dist/tools/emitStageEnvelope.d.ts +2 -3
  65. package/dist/tools/emitStageEnvelope.js +1 -65
  66. package/dist/types/envelope.d.ts +0 -2
  67. package/dist/types/forkChoice.d.ts +0 -16
  68. package/dist/types/pipeline.d.ts +86 -27
  69. package/dist/types/stage.d.ts +11 -7
  70. package/dist/ui/assets/{index-D2Wt8hwH.css → index-DsgVjLUS.css} +1 -1
  71. package/dist/ui/assets/{index-XWJovnp7.js → index-DwKGaQvQ.js} +13 -12
  72. package/dist/ui/index.html +2 -2
  73. package/package.json +2 -1
  74. package/skills/stageflow-author/SKILL.md +4 -4
  75. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
  76. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
  77. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
  78. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
  79. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
  80. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
  81. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
  82. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
  83. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
  84. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
  85. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
  86. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
  87. package/skills/stageflow-author/references/catalog-mapping.md +159 -50
  88. package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
  89. package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
  90. package/skills/stageflow-session-capture/SKILL.md +2 -2
  91. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
  92. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
  93. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
  94. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
  95. package/dist/envelope/cloneForks.d.ts +0 -2
  96. package/dist/envelope/cloneForks.js +0 -77
@@ -6,8 +6,10 @@ export declare function resolvePipelinePath(pipelinePath: string, cwd: string):
6
6
  export declare function loadPipelineOutcome(nameOrPath: string, options?: {
7
7
  cwd?: string;
8
8
  projectRoot?: string;
9
+ requireIo?: boolean;
9
10
  }): Promise<LoadOutcome<LoadedPipeline>>;
10
11
  export declare function loadPipeline(nameOrPath: string, options?: {
11
12
  cwd?: string;
12
13
  projectRoot?: string;
14
+ requireIo?: boolean;
13
15
  }): Promise<LoadedPipeline>;
@@ -10,6 +10,8 @@ import { predecessorEdges } from "./pipelineNeeds.js";
10
10
  import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
11
11
  import { recoveryRequiresCompletionIssue } from "./parseCompletionContract.js";
12
12
  import { validateCompletionContractForStage } from "./validateCompletionContract.js";
13
+ import { applyCloneChains } from "./cloneChain.js";
14
+ import { collectRouteIfSchemaIssues, collectRouteIfIllegalCombos, collectRouteAllGatedWarnings, } from "./routeIf.js";
13
15
  function schemaCompileIssue(stageId, field, err, pipelineId) {
14
16
  const message = err instanceof Error ? err.message : String(err);
15
17
  if (err instanceof UnresolvedSchemaRefError) {
@@ -59,26 +61,23 @@ function checkSequentialIoCompatibility(stages, dag, pipelineId, schemas) {
59
61
  const node = nodeById.get(child.id);
60
62
  if (!node)
61
63
  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
- ]);
64
+ for (const parentEdge of predecessorEdges(node)) {
65
+ const parentNode = nodeById.get(parentEdge.id);
66
+ if (parentNode?.clone_array_field !== undefined)
67
+ continue;
68
+ const parent = stageById.get(parentEdge.id);
69
+ if (!parent?.payload_schema || child.clone_input_schema === undefined)
70
+ continue;
71
+ if (!isPayloadSchemaSubset(child.clone_input_schema, parent.payload_schema, options)) {
72
+ return loadFailure([
73
+ {
74
+ code: "pipeline.io_incompatible",
75
+ message: `Pipeline ${pipelineId}: stage "${child.id}" io.input is not a structural subset of "${parent.id}" io.output`,
76
+ category: "pipeline",
77
+ pipelineId,
78
+ },
79
+ ]);
80
+ }
82
81
  }
83
82
  }
84
83
  return loadSuccess(undefined);
@@ -93,7 +92,7 @@ export async function resolvePipelinePath(pipelinePath, cwd) {
93
92
  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.`);
94
93
  }
95
94
  }
96
- async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
95
+ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd, requireIo = true) {
97
96
  const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
98
97
  const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
99
98
  if (!mergeOutcome.ok) {
@@ -147,6 +146,7 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
147
146
  entryId: entry.id,
148
147
  declaringPath: entry.declaringPath,
149
148
  deferSchemaRefs: true,
149
+ requireIo,
150
150
  });
151
151
  if (!inlineOutcome.ok) {
152
152
  return loadFailure(inlineOutcome.issues);
@@ -157,6 +157,7 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
157
157
  }
158
158
  const stageOutcome = await loadStageOutcome(entry.body.absolutePath, {
159
159
  deferSchemaRefs: true,
160
+ requireIo,
160
161
  });
161
162
  if (!stageOutcome.ok) {
162
163
  if (stageOutcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
@@ -206,9 +207,20 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
206
207
  stages.push(stage);
207
208
  stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
208
209
  }
210
+ const cloneChainOutcome = applyCloneChains(stages, wiringRefs, dag, pipelineId);
211
+ if (!cloneChainOutcome.ok)
212
+ return cloneChainOutcome;
209
213
  const schemaOutcome = attachPipelineSchemas(stages, pipelineSchemas, pipelineId);
210
214
  if (!schemaOutcome.ok)
211
215
  return schemaOutcome;
216
+ const routeIfIssues = [
217
+ ...collectRouteIfSchemaIssues(stages, wiringRefs, pipelineId),
218
+ ...collectRouteIfIllegalCombos(wiringRefs, pipelineId),
219
+ ];
220
+ if (routeIfIssues.length > 0) {
221
+ return loadFailure(routeIfIssues);
222
+ }
223
+ warnings.push(...collectRouteAllGatedWarnings(wiringRefs, pipelineId));
212
224
  const ioOutcome = checkSequentialIoCompatibility(stages, dag, pipelineId, pipelineSchemas);
213
225
  if (!ioOutcome.ok)
214
226
  return ioOutcome;
@@ -283,7 +295,7 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
283
295
  },
284
296
  ]);
285
297
  }
286
- return loadPipelineFromPath(pipelinePath, cwd, projectRoot);
298
+ return loadPipelineFromPath(pipelinePath, cwd, projectRoot, options.requireIo !== false);
287
299
  }
288
300
  export async function loadPipeline(nameOrPath, options = {}) {
289
301
  const outcome = await loadPipelineOutcome(nameOrPath, options);
@@ -6,11 +6,13 @@ export declare function afterCompletionForStage(stage: StageConfig): CompletionC
6
6
  export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
7
7
  export type LoadStageOptions = {
8
8
  deferSchemaRefs?: boolean;
9
+ requireIo?: boolean;
9
10
  };
10
11
  export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
11
12
  entryId: string;
12
13
  declaringPath: string;
13
14
  deferSchemaRefs?: boolean;
15
+ requireIo?: boolean;
14
16
  }): LoadOutcome<StageConfig>;
15
17
  export declare function loadStageOutcome(filePath: string, options?: LoadStageOptions): Promise<LoadOutcome<StageConfig>>;
16
18
  export declare function loadStage(filePath: string): Promise<StageConfig>;
@@ -1,6 +1,5 @@
1
1
  import { parseAgentField } from "../agent/agentBackend.js";
2
2
  import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
3
- import { CLONE_ACTIONS } from "../types/forkChoice.js";
4
3
  import { STAGE_GATE_KINDS, } from "../types/stage.js";
5
4
  import { compilePayloadSchema, UnresolvedSchemaRefError } from "../envelope/payloadSchema.js";
6
5
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
@@ -8,7 +7,7 @@ import { parseModelField } from "./modelField.js";
8
7
  import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
9
8
  import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
10
9
  import { readYamlObject } from "./readYamlObject.js";
11
- import { applyCompiledBody, classifyYamlDocument, compileTargetContract, dialectFromKeys, mixedDialectIssue, STAGE_FILE_WIRING_KEYS, } from "./yamlDialect.js";
10
+ import { classifyYamlDocument, compileTargetContract, dialectFromKeys, mixedDialectIssue, STAGE_FILE_WIRING_KEYS, } from "./yamlDialect.js";
12
11
  const afterCompletionByStage = new WeakMap();
13
12
  /** After-phase IR from target `verify` (`when` includes after). Stamped onto DAG `completion`. */
14
13
  export function afterCompletionForStage(stage) {
@@ -17,45 +16,6 @@ export function afterCompletionForStage(stage) {
17
16
  function isGateKind(value) {
18
17
  return STAGE_GATE_KINDS.includes(value);
19
18
  }
20
- function isCloneAction(value) {
21
- return CLONE_ACTIONS.includes(value);
22
- }
23
- function parseCloneActions(raw, label) {
24
- if (raw === undefined)
25
- return loadSuccess(undefined);
26
- if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
27
- return loadFailure([
28
- {
29
- code: "stage.invalid_clone_actions",
30
- message: `Invalid stage ${label}: clone_actions must be a non-empty array of strings`,
31
- category: "stage",
32
- },
33
- ]);
34
- }
35
- if (raw.length === 0) {
36
- return loadFailure([
37
- {
38
- code: "stage.invalid_clone_actions",
39
- message: `Invalid stage ${label}: clone_actions must not be empty`,
40
- category: "stage",
41
- },
42
- ]);
43
- }
44
- const actions = [];
45
- for (const item of raw) {
46
- if (!isCloneAction(item)) {
47
- return loadFailure([
48
- {
49
- code: "stage.invalid_clone_actions",
50
- message: `Invalid stage ${label}: unsupported clone_actions value "${item}" (allowed: ${CLONE_ACTIONS.join(", ")})`,
51
- category: "stage",
52
- },
53
- ]);
54
- }
55
- actions.push(item);
56
- }
57
- return loadSuccess(actions);
58
- }
59
19
  function parseTimeoutMs(raw, label) {
60
20
  if (raw === undefined)
61
21
  return loadSuccess(undefined);
@@ -162,9 +122,7 @@ export function parseStageMcp(raw, label, stageId) {
162
122
  }
163
123
  return loadSuccess(names);
164
124
  }
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.
125
+ function parseStageFields(raw, label, entryId, deferSchemaRefs, requireIo, compiled) {
168
126
  if (typeof raw.system_prompt !== "string") {
169
127
  return loadFailure([
170
128
  {
@@ -179,6 +137,16 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs) {
179
137
  id: entryId,
180
138
  system_prompt: raw.system_prompt,
181
139
  };
140
+ if (raw.clone_actions !== undefined) {
141
+ return loadFailure([
142
+ {
143
+ code: "stage.invalid_clone_actions",
144
+ message: `Invalid stage ${label}: "clone_actions" is no longer supported — use a Clone Chain instead`,
145
+ category: "stage",
146
+ stageId: entryId,
147
+ },
148
+ ]);
149
+ }
182
150
  const modelField = parseModelField(raw.model);
183
151
  if (!modelField.ok) {
184
152
  return loadFailure([
@@ -193,102 +161,133 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs) {
193
161
  if (modelField.value !== undefined) {
194
162
  stage.model = modelField.value;
195
163
  }
196
- if (raw.payload_schema !== undefined) {
197
- if (raw.payload_schema === null ||
198
- typeof raw.payload_schema !== "object" ||
199
- Array.isArray(raw.payload_schema)) {
200
- return loadFailure([
201
- {
202
- code: "stage.invalid_payload_schema",
203
- message: `Invalid stage ${label}: payload_schema must be an object`,
204
- category: "stage",
205
- stageId: entryId,
206
- },
207
- ]);
164
+ if (compiled) {
165
+ if (compiled.payload_schema !== undefined) {
166
+ stage.payload_schema = compiled.payload_schema;
208
167
  }
209
- try {
210
- compilePayloadSchema(raw.payload_schema);
168
+ if (compiled.clone_input_schema !== undefined) {
169
+ stage.clone_input_schema = compiled.clone_input_schema;
211
170
  }
212
- catch (err) {
213
- if (err instanceof UnresolvedSchemaRefError) {
214
- if (!deferSchemaRefs) {
171
+ }
172
+ else {
173
+ if (raw.payload_schema !== undefined) {
174
+ if (raw.payload_schema === null ||
175
+ typeof raw.payload_schema !== "object" ||
176
+ Array.isArray(raw.payload_schema)) {
177
+ return loadFailure([
178
+ {
179
+ code: "stage.invalid_payload_schema",
180
+ message: `Invalid stage ${label}: io.output.schema must be an object`,
181
+ category: "stage",
182
+ stageId: entryId,
183
+ },
184
+ ]);
185
+ }
186
+ try {
187
+ compilePayloadSchema(raw.payload_schema);
188
+ }
189
+ catch (err) {
190
+ if (err instanceof UnresolvedSchemaRefError) {
191
+ if (!deferSchemaRefs) {
192
+ return loadFailure([
193
+ {
194
+ code: "stage.unresolved_schema_ref",
195
+ message: `Invalid stage ${label}: ${err.message}`,
196
+ category: "stage",
197
+ stageId: entryId,
198
+ },
199
+ ]);
200
+ }
201
+ }
202
+ else {
203
+ const message = err instanceof Error ? err.message : String(err);
215
204
  return loadFailure([
216
205
  {
217
- code: "stage.unresolved_schema_ref",
218
- message: `Invalid stage ${label}: ${err.message}`,
206
+ code: "stage.invalid_payload_schema",
207
+ message: `Invalid stage ${label}: invalid io.output.schema: ${message}`,
219
208
  category: "stage",
220
209
  stageId: entryId,
221
210
  },
222
211
  ]);
223
212
  }
224
213
  }
225
- else {
226
- const message = err instanceof Error ? err.message : String(err);
214
+ stage.payload_schema = raw.payload_schema;
215
+ }
216
+ if (raw.clone_input_schema !== undefined) {
217
+ if (raw.clone_input_schema === null ||
218
+ typeof raw.clone_input_schema !== "object" ||
219
+ Array.isArray(raw.clone_input_schema)) {
227
220
  return loadFailure([
228
221
  {
229
- code: "stage.invalid_payload_schema",
230
- message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
222
+ code: "stage.invalid_clone_input_schema",
223
+ message: `Invalid stage ${label}: io.input.schema must be an object`,
231
224
  category: "stage",
232
225
  stageId: entryId,
233
226
  },
234
227
  ]);
235
228
  }
236
- }
237
- stage.payload_schema = raw.payload_schema;
238
- }
239
- if (raw.clone_input_schema !== undefined) {
240
- if (raw.clone_input_schema === null ||
241
- typeof raw.clone_input_schema !== "object" ||
242
- Array.isArray(raw.clone_input_schema)) {
243
- return loadFailure([
244
- {
245
- code: "stage.invalid_clone_input_schema",
246
- message: `Invalid stage ${label}: clone_input_schema must be an object`,
247
- category: "stage",
248
- stageId: entryId,
249
- },
250
- ]);
251
- }
252
- try {
253
- compilePayloadSchema(raw.clone_input_schema);
254
- }
255
- catch (err) {
256
- if (err instanceof UnresolvedSchemaRefError) {
257
- if (!deferSchemaRefs) {
229
+ try {
230
+ compilePayloadSchema(raw.clone_input_schema);
231
+ }
232
+ catch (err) {
233
+ if (err instanceof UnresolvedSchemaRefError) {
234
+ if (!deferSchemaRefs) {
235
+ return loadFailure([
236
+ {
237
+ code: "stage.unresolved_schema_ref",
238
+ message: `Invalid stage ${label}: ${err.message}`,
239
+ category: "stage",
240
+ stageId: entryId,
241
+ },
242
+ ]);
243
+ }
244
+ }
245
+ else {
246
+ const message = err instanceof Error ? err.message : String(err);
258
247
  return loadFailure([
259
248
  {
260
- code: "stage.unresolved_schema_ref",
261
- message: `Invalid stage ${label}: ${err.message}`,
249
+ code: "stage.invalid_clone_input_schema",
250
+ message: `Invalid stage ${label}: invalid io.input.schema: ${message}`,
262
251
  category: "stage",
263
252
  stageId: entryId,
264
253
  },
265
254
  ]);
266
255
  }
267
256
  }
268
- else {
269
- const message = err instanceof Error ? err.message : String(err);
257
+ stage.clone_input_schema = raw.clone_input_schema;
258
+ }
259
+ if (requireIo) {
260
+ if (raw.payload_schema === undefined && raw.clone_input_schema === undefined) {
270
261
  return loadFailure([
271
262
  {
272
- code: "stage.invalid_clone_input_schema",
273
- message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
263
+ code: "stage.invalid_io",
264
+ message: `Invalid stage ${label}: io is required`,
265
+ category: "stage",
266
+ stageId: entryId,
267
+ },
268
+ ]);
269
+ }
270
+ if (raw.payload_schema === undefined) {
271
+ return loadFailure([
272
+ {
273
+ code: "stage.invalid_io",
274
+ message: `Invalid stage ${label}: io.output.schema is required`,
275
+ category: "stage",
276
+ stageId: entryId,
277
+ },
278
+ ]);
279
+ }
280
+ if (raw.clone_input_schema === undefined) {
281
+ return loadFailure([
282
+ {
283
+ code: "stage.invalid_io",
284
+ message: `Invalid stage ${label}: io.input.schema is required`,
274
285
  category: "stage",
275
286
  stageId: entryId,
276
287
  },
277
288
  ]);
278
289
  }
279
290
  }
280
- stage.clone_input_schema = raw.clone_input_schema;
281
- }
282
- const cloneActionsOutcome = parseCloneActions(raw.clone_actions, label);
283
- if (!cloneActionsOutcome.ok) {
284
- const issues = cloneActionsOutcome.issues.map((issue) => ({
285
- ...issue,
286
- stageId: entryId,
287
- }));
288
- return loadFailure(issues);
289
- }
290
- if (cloneActionsOutcome.value !== undefined) {
291
- stage.clone_actions = cloneActionsOutcome.value;
292
291
  }
293
292
  const gateKindsOutcome = parseGateKinds(raw.gate_kinds, label);
294
293
  if (!gateKindsOutcome.ok) {
@@ -312,16 +311,23 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs) {
312
311
  if (timeoutMsOutcome.value !== undefined) {
313
312
  stage.timeout_ms = timeoutMsOutcome.value;
314
313
  }
315
- const preEmitChecksOutcome = parsePreEmitChecks(raw.pre_emit_checks, label);
316
- if (!preEmitChecksOutcome.ok) {
317
- const issues = preEmitChecksOutcome.issues.map((issue) => ({
318
- ...issue,
319
- stageId: entryId,
320
- }));
321
- return loadFailure(issues);
314
+ if (compiled) {
315
+ if (compiled.pre_emit_checks !== undefined) {
316
+ stage.pre_emit_checks = compiled.pre_emit_checks;
317
+ }
322
318
  }
323
- if (preEmitChecksOutcome.value !== undefined) {
324
- stage.pre_emit_checks = preEmitChecksOutcome.value;
319
+ else {
320
+ const preEmitChecksOutcome = parsePreEmitChecks(raw.pre_emit_checks, label);
321
+ if (!preEmitChecksOutcome.ok) {
322
+ const issues = preEmitChecksOutcome.issues.map((issue) => ({
323
+ ...issue,
324
+ stageId: entryId,
325
+ }));
326
+ return loadFailure(issues);
327
+ }
328
+ if (preEmitChecksOutcome.value !== undefined) {
329
+ stage.pre_emit_checks = preEmitChecksOutcome.value;
330
+ }
325
331
  }
326
332
  if (raw.skill !== undefined) {
327
333
  if (typeof raw.skill !== "string" || raw.skill.trim() === "") {
@@ -361,6 +367,7 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs) {
361
367
  export function loadStageFromObjectOutcome(raw, ctx) {
362
368
  const label = `${ctx.entryId} (${ctx.declaringPath})`;
363
369
  const deferSchemaRefs = ctx.deferSchemaRefs === true;
370
+ const requireIo = ctx.requireIo !== false;
364
371
  const dialect = dialectFromKeys(Object.keys(raw));
365
372
  if (dialect === "invalid") {
366
373
  return loadFailure([mixedDialectIssue()]);
@@ -374,12 +381,13 @@ export function loadStageFromObjectOutcome(raw, ctx) {
374
381
  label,
375
382
  category: "stage",
376
383
  deferSchemaRefs,
384
+ requireIo,
377
385
  });
378
386
  if (!compiled.ok)
379
387
  return compiled;
380
- return parseStageFields(applyCompiledBody(raw, compiled.value), label, ctx.entryId, deferSchemaRefs);
388
+ return parseStageFields(raw, label, ctx.entryId, deferSchemaRefs, requireIo, compiled.value);
381
389
  }
382
- return parseStageFields(raw, label, ctx.entryId, deferSchemaRefs);
390
+ return parseStageFields(raw, label, ctx.entryId, deferSchemaRefs, requireIo);
383
391
  }
384
392
  export async function loadStageOutcome(filePath, options = {}) {
385
393
  let raw;
@@ -425,22 +433,24 @@ export async function loadStageOutcome(filePath, options = {}) {
425
433
  ]);
426
434
  }
427
435
  }
428
- let parseRaw = raw;
436
+ let compiled;
429
437
  let afterCompletion;
430
438
  const deferSchemaRefs = options.deferSchemaRefs === true;
439
+ const requireIo = options.requireIo !== false;
431
440
  if (dialect === "target") {
432
- const compiled = compileTargetContract(raw, {
441
+ const compiledOutcome = compileTargetContract(raw, {
433
442
  stageId: raw.id,
434
443
  label: `file ${filePath}`,
435
444
  category: "stage",
436
445
  deferSchemaRefs,
446
+ requireIo,
437
447
  });
438
- if (!compiled.ok)
439
- return compiled;
440
- parseRaw = applyCompiledBody(raw, compiled.value);
441
- afterCompletion = compiled.value.completion;
448
+ if (!compiledOutcome.ok)
449
+ return compiledOutcome;
450
+ compiled = compiledOutcome.value;
451
+ afterCompletion = compiled.completion;
442
452
  }
443
- const outcome = parseStageFields(parseRaw, `file ${filePath}`, raw.id, deferSchemaRefs);
453
+ const outcome = parseStageFields(raw, `file ${filePath}`, raw.id, deferSchemaRefs, requireIo, compiled);
444
454
  if (!outcome.ok)
445
455
  return outcome;
446
456
  if (outcome.value.id !== raw.id) {
@@ -333,7 +333,11 @@ async function refuseOutOfScopeUsesFolds(usesCompile, scopePipelines, catalogRoo
333
333
  catch {
334
334
  outsideId = undefined;
335
335
  }
336
- const outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
336
+ const outcome = await loadPipelineOutcome(pipelinePath, {
337
+ cwd,
338
+ projectRoot,
339
+ requireIo: false,
340
+ });
337
341
  if (outcome.ok) {
338
342
  const other = new Map();
339
343
  const otherErrors = [];
@@ -415,7 +419,11 @@ async function planMigrateYamlInner(targetPath, options) {
415
419
  skipped.add(relPath(cwd, pipelinePath));
416
420
  continue;
417
421
  }
418
- const outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
422
+ const outcome = await loadPipelineOutcome(pipelinePath, {
423
+ cwd,
424
+ projectRoot,
425
+ requireIo: false,
426
+ });
419
427
  if (!outcome.ok) {
420
428
  if (outcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
421
429
  skipped.add(relPath(cwd, pipelinePath));
@@ -518,7 +526,10 @@ async function planMigrateYamlInner(targetPath, options) {
518
526
  skipped.add(relPath(cwd, stagePath));
519
527
  continue;
520
528
  }
521
- const outcome = await loadStageOutcome(stagePath, { deferSchemaRefs: true });
529
+ const outcome = await loadStageOutcome(stagePath, {
530
+ deferSchemaRefs: true,
531
+ requireIo: false,
532
+ });
522
533
  if (!outcome.ok) {
523
534
  if (outcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
524
535
  skipped.add(relPath(cwd, stagePath));
@@ -1,5 +1,5 @@
1
1
  import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
2
- import type { NormalizedPipelineStageEntry, PipelineNeeds } from "../types/pipeline.js";
2
+ import type { CloneMode, NormalizedPipelineStageEntry, PipelineRouteEntry } from "../types/pipeline.js";
3
3
  import { type LoadOutcome } from "./loadOutcome.js";
4
4
  import type { RawMergedEntry } from "./mergePipelineIncludes.js";
5
5
  export declare function inferIdFromUsesPath(usesPath: string): string | null;
@@ -9,15 +9,11 @@ export declare function normalizePipelineStageEntries(rawEntries: RawMergedEntry
9
9
  }): LoadOutcome<NormalizedPipelineStageEntry[]>;
10
10
  export declare function toWiringRefs(entries: NormalizedPipelineStageEntry[]): Array<{
11
11
  id: string;
12
- needs?: PipelineNeeds;
13
- fork?: {
14
- select: "one" | "subset";
15
- allow_none?: boolean;
16
- };
17
- clonable?: boolean;
18
- clone_cap?: number;
19
12
  completion?: CompletionContract;
20
13
  recovery?: RecoveryPolicy;
21
- feedback_loop?: NormalizedPipelineStageEntry["feedback_loop"];
22
14
  replay_safe?: boolean;
15
+ route?: PipelineRouteEntry[];
16
+ entry?: boolean;
17
+ clone_cap?: number;
18
+ clone_mode?: CloneMode;
23
19
  }>;