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
@@ -390,10 +390,10 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
390
390
  if (successor.cloneInputSchema === undefined) {
391
391
  return header;
392
392
  }
393
- return `${header}\n Assignment schema (clone_input_schema):\n${JSON.stringify(successor.cloneInputSchema, null, 2)}`;
393
+ return `${header}\n Assignment schema (io.input.schema):\n${JSON.stringify(successor.cloneInputSchema, null, 2)}`;
394
394
  });
395
395
  emitHint += `\nClonable successors — emit clone_forks covering each of these ids exactly once:\n${successorLines.join("\n")}`;
396
- emitHint += `\nEach once or fanout envelope is a full StageEnvelope — it requires status ("success" or "failure"), summary (non-empty string), and artifacts (array of strings). clone_input_schema fields belong in envelope.payload, not at the top level of the clone_forks item.`;
396
+ emitHint += `\nEach once or fanout envelope is a full StageEnvelope — it requires status ("success" or "failure"), summary (non-empty string), and artifacts (array of strings). io.input.schema fields belong in envelope.payload, not at the top level of the clone_forks item.`;
397
397
  emitHint += `\nAllowed clone actions: ${allowedActions.join(", ")}`;
398
398
  }
399
399
  if (input.feedbackLoopEmitContext !== undefined) {
@@ -431,6 +431,9 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
431
431
  `Goal: ${input.task.goal}`,
432
432
  input.task.context ? `Context: ${input.task.context}` : "",
433
433
  input.task.constraints ? `Constraints: ${input.task.constraints}` : "",
434
+ input.task.input !== undefined
435
+ ? `Input: ${JSON.stringify(input.task.input, null, 2)}`
436
+ : "",
434
437
  formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes, input.priorEnvelopesByStage),
435
438
  input.feedbackLoopContext !== undefined
436
439
  ? formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode)
@@ -1,7 +1,7 @@
1
1
  import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
2
2
  import type { CompletionContract } from "../types/completion.js";
3
3
  import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
4
- import type { StageConfig } from "../types/stage.js";
4
+ import type { LoadedStageConfig } from "../types/stage.js";
5
5
  import type { FeedbackLoopConfig } from "../types/pipeline.js";
6
6
  import type { TaskFile } from "../types/task.js";
7
7
  import type { StageActivityEvent } from "./activity.js";
@@ -45,7 +45,7 @@ export type FeedbackLoopContext = {
45
45
  };
46
46
  export type StageRunInput = {
47
47
  roots: StageRoots;
48
- stage: StageConfig;
48
+ stage: LoadedStageConfig;
49
49
  /** Runtime instance id (`work~2`). Defaults to `stage.id` when omitted. */
50
50
  stageId?: string;
51
51
  task: TaskFile;
@@ -1,3 +1,3 @@
1
1
  export declare const STAGEFLOW_YAML = "version: 1\ncatalog:\n pipelines:\n - pipelines\n tasks:\n - tasks\n patterns:\n pipeline: \"*.pipeline.yaml\"\n task: \"*.task.yaml\"\n";
2
- export declare const HELLO_PIPELINE_YAML = "id: hello\nstages:\n - id: hello\n system_prompt: Say hello and emit a success envelope.\n model: anthropic/claude-sonnet-4-5\n";
2
+ export declare const HELLO_PIPELINE_YAML = "id: hello\nmodel: anthropic/claude-sonnet-4-5\nstages:\n - id: hello\n system_prompt: Say hello and emit a success envelope.\n";
3
3
  export declare const HELLO_TASK_YAML = "id: hello\ngoal: Run the hello pipeline scaffold.\n";
@@ -9,10 +9,10 @@ catalog:
9
9
  task: "*.task.yaml"
10
10
  `;
11
11
  export const HELLO_PIPELINE_YAML = `id: hello
12
+ model: anthropic/claude-sonnet-4-5
12
13
  stages:
13
14
  - id: hello
14
15
  system_prompt: Say hello and emit a success envelope.
15
- model: anthropic/claude-sonnet-4-5
16
16
  `;
17
17
  export const HELLO_TASK_YAML = `id: hello
18
18
  goal: Run the hello pipeline scaffold.
@@ -0,0 +1,18 @@
1
+ export declare const MIGRATE_YAML_USAGE = "Usage:\n sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]\n\n Convert legacy catalog YAML to target YAML (io / verify / on_verify_fail).\n Dry-run is the default. --write applies changes. Path may be a pipeline,\n stage, task, or catalog root. Does not rewrite .stageflow snapshots.";
2
+ export type MigrateYamlCommandIo = {
3
+ log: (line: string) => void;
4
+ error: (line: string) => void;
5
+ };
6
+ export type MigrateYamlJson = {
7
+ ok: boolean;
8
+ write: boolean;
9
+ planned: string[];
10
+ written: string[];
11
+ skipped: string[];
12
+ errors: string[];
13
+ };
14
+ export declare function runMigrateYamlCommand(args: string[], options?: {
15
+ cwd?: string;
16
+ projectRoot?: string;
17
+ io?: Partial<MigrateYamlCommandIo>;
18
+ }): Promise<number>;
@@ -0,0 +1,180 @@
1
+ import path from "node:path";
2
+ import { applyMigrateYamlPlan, fileDirtyVsHead, planMigrateYaml, } from "../config/migrateYaml.js";
3
+ export const MIGRATE_YAML_USAGE = `Usage:
4
+ sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
5
+
6
+ Convert legacy catalog YAML to target YAML (io / verify / on_verify_fail).
7
+ Dry-run is the default. --write applies changes. Path may be a pipeline,
8
+ stage, task, or catalog root. Does not rewrite .stageflow snapshots.`;
9
+ const defaultIo = {
10
+ log: (line) => console.log(line),
11
+ error: (line) => console.error(line),
12
+ };
13
+ function parseMigrateYamlArgs(args) {
14
+ if (args.length === 0) {
15
+ return { help: false, write: false, json: false, force: false };
16
+ }
17
+ if (args[0] === "--help" || args[0] === "-h") {
18
+ return { help: true, write: false, json: false, force: false };
19
+ }
20
+ let target;
21
+ let root;
22
+ let write = false;
23
+ let json = false;
24
+ let force = false;
25
+ let help = false;
26
+ for (let i = 0; i < args.length; i++) {
27
+ const arg = args[i];
28
+ if (arg === "--help" || arg === "-h") {
29
+ help = true;
30
+ }
31
+ else if (arg === "--write") {
32
+ write = true;
33
+ }
34
+ else if (arg === "--json") {
35
+ json = true;
36
+ }
37
+ else if (arg === "--force") {
38
+ force = true;
39
+ }
40
+ else if (arg === "--root") {
41
+ const value = args[++i];
42
+ if (value === undefined || value.length === 0) {
43
+ throw new Error("Missing value for --root");
44
+ }
45
+ root = value;
46
+ }
47
+ else if (arg.startsWith("-")) {
48
+ throw new Error(`Unknown flag: ${arg}`);
49
+ }
50
+ else if (target !== undefined) {
51
+ throw new Error(`Unexpected argument: ${arg}`);
52
+ }
53
+ else {
54
+ target = arg;
55
+ }
56
+ }
57
+ if (target !== undefined && root !== undefined) {
58
+ throw new Error("Use at most one of a positional path or --root");
59
+ }
60
+ return { help, target: target ?? root, write, json, force };
61
+ }
62
+ function planToJson(plan, options) {
63
+ return {
64
+ ok: options.ok,
65
+ write: options.write,
66
+ planned: plan.writes.map((item) => item.file),
67
+ written: options.written,
68
+ skipped: plan.skipped,
69
+ errors: [...plan.errors, ...(options.extraErrors ?? [])],
70
+ };
71
+ }
72
+ function formatHuman(plan, write, extraErrors) {
73
+ const lines = [];
74
+ const errors = [...plan.errors, ...extraErrors];
75
+ if (errors.length > 0) {
76
+ lines.push("Migration failed:");
77
+ for (const error of errors)
78
+ lines.push(` ${error}`);
79
+ return lines.join("\n");
80
+ }
81
+ if (write) {
82
+ if (plan.writes.length === 0) {
83
+ lines.push("Nothing to write.");
84
+ }
85
+ else {
86
+ lines.push("Wrote:");
87
+ for (const item of plan.writes)
88
+ lines.push(` ${item.file}`);
89
+ }
90
+ }
91
+ else if (plan.writes.length === 0) {
92
+ lines.push("Nothing to migrate.");
93
+ }
94
+ else {
95
+ lines.push("Would write:");
96
+ for (const item of plan.writes)
97
+ lines.push(` ${item.file}`);
98
+ lines.push("");
99
+ lines.push("Dry-run; pass --write to apply.");
100
+ }
101
+ if (plan.skipped.length > 0) {
102
+ lines.push("");
103
+ lines.push("Skipped:");
104
+ for (const file of plan.skipped)
105
+ lines.push(` ${file}`);
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+ export async function runMigrateYamlCommand(args, options = {}) {
110
+ const cwd = options.cwd ?? process.cwd();
111
+ const projectRoot = options.projectRoot ?? cwd;
112
+ const out = { ...defaultIo, ...options.io };
113
+ try {
114
+ const parsed = parseMigrateYamlArgs(args);
115
+ if (parsed.help) {
116
+ out.error(MIGRATE_YAML_USAGE);
117
+ return 0;
118
+ }
119
+ const target = path.resolve(cwd, parsed.target ?? ".");
120
+ const plan = await planMigrateYaml(target, { cwd, projectRoot });
121
+ const extraErrors = [];
122
+ if (plan.errors.length > 0) {
123
+ if (parsed.json) {
124
+ out.log(JSON.stringify(planToJson(plan, { write: parsed.write, written: [], ok: false }), null, 2));
125
+ }
126
+ else {
127
+ out.error(formatHuman(plan, parsed.write, extraErrors));
128
+ out.error(MIGRATE_YAML_USAGE);
129
+ }
130
+ return 1;
131
+ }
132
+ if (parsed.write && !parsed.force) {
133
+ for (const item of plan.writes) {
134
+ if (fileDirtyVsHead(item.absPath)) {
135
+ extraErrors.push(`Refusing to overwrite uncommitted changes in ${item.file} (pass --force)`);
136
+ }
137
+ }
138
+ if (extraErrors.length > 0) {
139
+ if (parsed.json) {
140
+ out.log(JSON.stringify(planToJson(plan, {
141
+ write: true,
142
+ written: [],
143
+ ok: false,
144
+ extraErrors,
145
+ }), null, 2));
146
+ }
147
+ else {
148
+ out.error(formatHuman(plan, parsed.write, extraErrors));
149
+ }
150
+ return 1;
151
+ }
152
+ }
153
+ let written = [];
154
+ if (parsed.write) {
155
+ await applyMigrateYamlPlan(plan);
156
+ written = plan.writes.map((item) => item.file);
157
+ }
158
+ if (parsed.json) {
159
+ out.log(JSON.stringify(planToJson(plan, {
160
+ write: parsed.write,
161
+ written,
162
+ ok: true,
163
+ }), null, 2));
164
+ }
165
+ else {
166
+ out.log(formatHuman(plan, parsed.write, extraErrors));
167
+ }
168
+ return 0;
169
+ }
170
+ catch (err) {
171
+ const message = err instanceof Error
172
+ ? err.message
173
+ : typeof err === "string"
174
+ ? err
175
+ : "Migrate-yaml command failed";
176
+ out.error(message);
177
+ out.error(MIGRATE_YAML_USAGE);
178
+ return 1;
179
+ }
180
+ }
@@ -46,30 +46,38 @@ function formatStartFailureJson(started) {
46
46
  return formatRunStartFailedJson(started);
47
47
  }
48
48
  function baseRunCompletionPayload(result) {
49
- if (result.outcome === "succeeded") {
50
- return {
49
+ const payload = result.outcome === "succeeded"
50
+ ? {
51
51
  ok: true,
52
52
  outcome: "succeeded",
53
53
  runId: result.runId,
54
54
  runDir: result.runDir,
55
- };
55
+ }
56
+ : result.outcome === "waiting"
57
+ ? {
58
+ ok: false,
59
+ outcome: "waiting",
60
+ runId: result.runId,
61
+ runDir: result.runDir,
62
+ }
63
+ : {
64
+ ok: false,
65
+ outcome: "failed",
66
+ runId: result.runId,
67
+ runDir: result.runDir,
68
+ };
69
+ if (result.outcome === "failed" && result.reason !== undefined) {
70
+ payload.reason = result.reason;
56
71
  }
57
- if (result.outcome === "waiting") {
58
- return {
59
- ok: false,
60
- outcome: "waiting",
61
- runId: result.runId,
62
- runDir: result.runDir,
63
- };
72
+ if (Array.isArray(result.findings) && result.findings.length > 0) {
73
+ payload.findings = result.findings.map((finding) => ({
74
+ severity: finding.severity,
75
+ code: finding.code,
76
+ file: finding.path,
77
+ message: finding.message,
78
+ category: finding.category,
79
+ }));
64
80
  }
65
- const payload = {
66
- ok: false,
67
- outcome: "failed",
68
- runId: result.runId,
69
- runDir: result.runDir,
70
- };
71
- if (result.reason !== undefined)
72
- payload.reason = result.reason;
73
81
  return payload;
74
82
  }
75
83
  function formatRunCompletionJson(result) {
@@ -6,6 +6,7 @@ export type ValidateCommandIo = {
6
6
  };
7
7
  export declare function runValidateCommand(args: string[], options?: {
8
8
  cwd?: string;
9
+ projectRoot?: string;
9
10
  io?: Partial<ValidateCommandIo>;
10
11
  validateCatalog?: (options: Parameters<typeof defaultValidateCatalog>[0]) => Promise<ValidationResult>;
11
12
  }): Promise<number>;
@@ -61,6 +61,7 @@ function parseValidateArgs(args) {
61
61
  }
62
62
  export async function runValidateCommand(args, options = {}) {
63
63
  const cwd = options.cwd ?? process.cwd();
64
+ const projectRoot = options.projectRoot ?? cwd;
64
65
  const out = { ...defaultIo, ...options.io };
65
66
  const validateCatalogFn = options.validateCatalog ?? defaultValidateCatalog;
66
67
  try {
@@ -73,6 +74,7 @@ export async function runValidateCommand(args, options = {}) {
73
74
  const result = await validateCatalogFn({
74
75
  scope,
75
76
  cwd,
77
+ projectRoot,
76
78
  pipeline: parsed.pipeline,
77
79
  task: parsed.task,
78
80
  strict: parsed.strict,
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
14
14
  import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
15
15
  import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
16
16
  import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
17
+ import { MIGRATE_YAML_USAGE, runMigrateYamlCommand } from "./cli/migrateYamlCommand.js";
17
18
  import { createRunStore } from "./runstore/createStore.js";
18
19
  import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
19
20
  import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
@@ -26,6 +27,7 @@ const USAGE = `Usage:
26
27
  sf init
27
28
  sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
28
29
  sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
30
+ sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
29
31
  sf artifact read --run <runId> --path <relPath> [--out <file>]
30
32
  sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
31
33
  sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]
@@ -64,6 +66,8 @@ ${RUN_USAGE}
64
66
 
65
67
  ${VALIDATE_USAGE}
66
68
 
69
+ ${MIGRATE_YAML_USAGE}
70
+
67
71
  ${ARTIFACT_USAGE}
68
72
 
69
73
  ${ENVELOPE_USAGE}
@@ -93,6 +97,7 @@ function parseArgs(argv) {
93
97
  const command = args[0];
94
98
  if (command === "providers" ||
95
99
  command === "validate" ||
100
+ command === "migrate-yaml" ||
96
101
  command === "run" ||
97
102
  command === "init" ||
98
103
  command === "artifact" ||
@@ -275,7 +280,16 @@ async function main(argv) {
275
280
  return runInitCommand(argv.slice(3), { cwd: ctx.invocationCwd });
276
281
  }
277
282
  if (parsed.command === "validate") {
278
- return runValidateCommand(argv.slice(3), { cwd: ctx.invocationCwd });
283
+ return runValidateCommand(argv.slice(3), {
284
+ cwd: ctx.invocationCwd,
285
+ projectRoot: ctx.projectRoot,
286
+ });
287
+ }
288
+ if (parsed.command === "migrate-yaml") {
289
+ return runMigrateYamlCommand(argv.slice(3), {
290
+ cwd: ctx.invocationCwd,
291
+ projectRoot: ctx.projectRoot,
292
+ });
279
293
  }
280
294
  if (parsed.command === "run") {
281
295
  return runRunCommand(argv.slice(3), {
@@ -106,7 +106,7 @@ async function listModelsFromManifest(projectRoot, manifest) {
106
106
  try {
107
107
  const loaded = await loadPipeline(filePath, { cwd: projectRoot });
108
108
  for (const stage of loaded.stages) {
109
- if (stage.model.trim()) {
109
+ if (stage.model?.trim()) {
110
110
  models.add(stage.model);
111
111
  }
112
112
  }
@@ -3,7 +3,7 @@ import type { StageGateKind } from "../types/stage.js";
3
3
  import type { PipelineListing } from "./listConfig.js";
4
4
  export type CreatePipelineStageInline = {
5
5
  system_prompt: string;
6
- model: string;
6
+ model?: string;
7
7
  gate_kinds?: StageGateKind[];
8
8
  };
9
9
  export type CreatePipelineStageRef = {
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { parsePipelineNeeds } from "./pipelineNeeds.js";
4
4
  import { STAGE_ID_PATTERN } from "./createStage.js";
5
5
  import { inferIdFromUsesPath, normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
6
+ import { parseModelField } from "./modelField.js";
6
7
  import { readYamlObject } from "./readYamlObject.js";
7
8
  import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
8
9
  import { validatePipeline } from "./validateCatalog.js";
@@ -36,7 +37,9 @@ function stageRefToRaw(ref) {
36
37
  raw.uses = ref.uses;
37
38
  if (ref.inline) {
38
39
  raw.system_prompt = ref.inline.system_prompt;
39
- raw.model = ref.inline.model;
40
+ if (ref.inline.model !== undefined && ref.inline.model.length > 0) {
41
+ raw.model = ref.inline.model;
42
+ }
40
43
  if (ref.inline.gate_kinds !== undefined) {
41
44
  raw.gate_kinds = ref.inline.gate_kinds;
42
45
  }
@@ -61,7 +64,9 @@ function normalizedToCreateRefs(entries) {
61
64
  const raw = entry.body.raw;
62
65
  ref.inline = {
63
66
  system_prompt: String(raw.system_prompt ?? ""),
64
- model: String(raw.model ?? ""),
67
+ ...(typeof raw.model === "string" && raw.model.length > 0
68
+ ? { model: raw.model }
69
+ : {}),
65
70
  ...(Array.isArray(raw.gate_kinds)
66
71
  ? { gate_kinds: raw.gate_kinds }
67
72
  : {}),
@@ -97,17 +102,22 @@ function parseInlineBody(entry, index) {
97
102
  error: `stages[${index}].system_prompt is required for inline stage`,
98
103
  };
99
104
  }
100
- if (typeof entry.model !== "string" || entry.model.length === 0) {
101
- return {
102
- ok: false,
103
- status: 400,
104
- error: `stages[${index}].model is required for inline stage`,
105
- };
106
- }
107
105
  const inline = {
108
106
  system_prompt: entry.system_prompt,
109
- model: entry.model,
110
107
  };
108
+ if (entry.model !== undefined) {
109
+ const modelField = parseModelField(entry.model);
110
+ if (!modelField.ok) {
111
+ return {
112
+ ok: false,
113
+ status: 400,
114
+ error: `stages[${index}].${modelField.message}`,
115
+ };
116
+ }
117
+ if (modelField.value !== undefined) {
118
+ inline.model = modelField.value;
119
+ }
120
+ }
111
121
  if (entry.gate_kinds !== undefined) {
112
122
  if (!Array.isArray(entry.gate_kinds)) {
113
123
  return {
@@ -288,7 +298,9 @@ export function pipelineConfigToYaml(pipeline, options) {
288
298
  else {
289
299
  lines.push(` system_prompt: ${formatInlineYamlScalar(stage.inline.system_prompt)}`);
290
300
  }
291
- lines.push(` model: ${formatInlineYamlScalar(stage.inline.model)}`);
301
+ if (stage.inline.model !== undefined && stage.inline.model.length > 0) {
302
+ lines.push(` model: ${formatInlineYamlScalar(stage.inline.model)}`);
303
+ }
292
304
  }
293
305
  else if (stage.uses) {
294
306
  lines.push(` uses: ${stage.uses}`);
@@ -5,7 +5,7 @@ export type CreateStageInput = {
5
5
  filename: string;
6
6
  id: string;
7
7
  system_prompt: string;
8
- model: string;
8
+ model?: string;
9
9
  gate_kinds?: StageGateKind[];
10
10
  };
11
11
  export type CreatedStageListing = {
@@ -2,6 +2,7 @@ import { access, mkdir, readdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { STAGE_GATE_KINDS, } from "../types/stage.js";
4
4
  import { loadStage } from "./loadStage.js";
5
+ import { parseModelField } from "./modelField.js";
5
6
  import { readYamlObject } from "./readYamlObject.js";
6
7
  export const STAGE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
7
8
  function isGateKind(value) {
@@ -63,9 +64,11 @@ export function parseCreateStageBody(body) {
63
64
  if (typeof body.system_prompt !== "string" || body.system_prompt.length === 0) {
64
65
  return { ok: false, status: 400, error: "system_prompt is required" };
65
66
  }
66
- if (typeof body.model !== "string" || body.model.length === 0) {
67
- return { ok: false, status: 400, error: "model is required" };
67
+ const modelField = parseModelField(body.model);
68
+ if (!modelField.ok) {
69
+ return { ok: false, status: 400, error: modelField.message };
68
70
  }
71
+ const model = modelField.value;
69
72
  const gateKinds = parseGateKinds(body.gate_kinds);
70
73
  if (gateKinds === "invalid") {
71
74
  return {
@@ -79,7 +82,7 @@ export function parseCreateStageBody(body) {
79
82
  filename,
80
83
  id: body.id,
81
84
  system_prompt: body.system_prompt,
82
- model: body.model,
85
+ ...(model !== undefined ? { model } : {}),
83
86
  ...(gateKinds !== undefined ? { gate_kinds: gateKinds } : {}),
84
87
  };
85
88
  }
@@ -116,7 +119,9 @@ export function stageConfigToYaml(stage) {
116
119
  else {
117
120
  lines.push(`system_prompt: ${formatInlineYamlScalar(stage.system_prompt)}`);
118
121
  }
119
- lines.push(`model: ${formatInlineYamlScalar(stage.model)}`);
122
+ if (stage.model !== undefined && stage.model.length > 0) {
123
+ lines.push(`model: ${formatInlineYamlScalar(stage.model)}`);
124
+ }
120
125
  lines.push("");
121
126
  return lines.join("\n");
122
127
  }
@@ -190,7 +195,7 @@ export async function createStage(projectRoot, input) {
190
195
  await writeFile(filePath, stageConfigToYaml({
191
196
  id: input.id,
192
197
  system_prompt: input.system_prompt,
193
- model: input.model,
198
+ ...(input.model !== undefined && input.model.length > 0 ? { model: input.model } : {}),
194
199
  ...(input.gate_kinds !== undefined ? { gate_kinds: input.gate_kinds } : {}),
195
200
  }), "utf8");
196
201
  }
@@ -0,0 +1,28 @@
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 type { LoadIssue } from "./loadOutcome.js";
18
+ import type { YamlDialect } from "./yamlDialect.js";
19
+ export declare const LEGACY_CONTRACT_KEYS: Set<string>;
20
+ export declare const LEGACY_KEY_REPLACEMENTS: Record<string, string>;
21
+ export declare function formatLegacyReplacements(keys: string[]): string;
22
+ export declare function presentLegacyKeys(keys: Iterable<string>): string[];
23
+ export declare function legacyYamlIssue(fileLabel: string, keys: string[]): LoadIssue;
24
+ export declare function dialectWarningForDocument(raw: Record<string, unknown>, fileLabel: string): LoadIssue | undefined;
25
+ export declare function allowLegacyYamlAuthoring(): boolean;
26
+ export declare function withLegacyYamlAllowed<T>(fn: () => T): T;
27
+ export declare function withLegacyYamlAllowedAsync<T>(fn: () => Promise<T>): Promise<T>;
28
+ export declare function legacyAuthoringRejected(dialect: YamlDialect, fileLabel: string, keys: string[]): LoadIssue | undefined;