stageflow 0.13.0 → 0.16.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 (113) hide show
  1. package/dist/agent/activity.d.ts +3 -0
  2. package/dist/agent/claudeAdapter.js +84 -7
  3. package/dist/agent/claudeSession.d.ts +3 -0
  4. package/dist/agent/piAdapter.d.ts +2 -0
  5. package/dist/agent/piAdapter.js +34 -7
  6. package/dist/agent/port.d.ts +5 -2
  7. package/dist/cli/initTemplates.d.ts +1 -1
  8. package/dist/cli/initTemplates.js +1 -1
  9. package/dist/cli/migrateYamlCommand.d.ts +18 -0
  10. package/dist/cli/migrateYamlCommand.js +180 -0
  11. package/dist/cli/runOutput.js +58 -26
  12. package/dist/cli/validateCommand.d.ts +1 -0
  13. package/dist/cli/validateCommand.js +2 -0
  14. package/dist/cli.js +15 -1
  15. package/dist/config/browseCatalog.js +1 -1
  16. package/dist/config/createPipeline.d.ts +1 -1
  17. package/dist/config/createPipeline.js +23 -11
  18. package/dist/config/createStage.d.ts +1 -1
  19. package/dist/config/createStage.js +10 -5
  20. package/dist/config/legacyYaml.d.ts +28 -0
  21. package/dist/config/legacyYaml.js +98 -0
  22. package/dist/config/loadOutcome.d.ts +2 -1
  23. package/dist/config/loadOutcome.js +4 -1
  24. package/dist/config/loadPipeline.d.ts +2 -0
  25. package/dist/config/loadPipeline.js +150 -8
  26. package/dist/config/loadStage.d.ts +8 -1
  27. package/dist/config/loadStage.js +138 -29
  28. package/dist/config/loadStageflowManifest.js +10 -0
  29. package/dist/config/loadTask.js +14 -0
  30. package/dist/config/materializeStageModels.d.ts +7 -0
  31. package/dist/config/materializeStageModels.js +35 -0
  32. package/dist/config/mergePipelineIncludes.d.ts +5 -1
  33. package/dist/config/mergePipelineIncludes.js +82 -4
  34. package/dist/config/migrateYaml.d.ts +19 -0
  35. package/dist/config/migrateYaml.js +627 -0
  36. package/dist/config/modelField.d.ts +13 -0
  37. package/dist/config/modelField.js +16 -0
  38. package/dist/config/normalizePipelineStageEntry.js +41 -2
  39. package/dist/config/parseCompletionContract.d.ts +5 -2
  40. package/dist/config/parseCompletionContract.js +16 -3
  41. package/dist/config/parsePreEmitChecks.js +4 -0
  42. package/dist/config/pipelineNeeds.js +3 -3
  43. package/dist/config/pipelineStageKeys.d.ts +11 -0
  44. package/dist/config/pipelineStageKeys.js +14 -0
  45. package/dist/config/printTargetYaml.d.ts +28 -0
  46. package/dist/config/printTargetYaml.js +243 -0
  47. package/dist/config/resolveModel.d.ts +20 -0
  48. package/dist/config/resolveModel.js +35 -0
  49. package/dist/config/validateCatalog.d.ts +5 -1
  50. package/dist/config/validateCatalog.js +36 -6
  51. package/dist/config/validateCompletionContract.js +5 -0
  52. package/dist/config/yamlDialect.d.ts +29 -0
  53. package/dist/config/yamlDialect.js +327 -0
  54. package/dist/envelope/payloadSchema.d.ts +37 -5
  55. package/dist/envelope/payloadSchema.js +272 -22
  56. package/dist/mcp/catalogTools.js +1 -0
  57. package/dist/package-meta.d.ts +1 -1
  58. package/dist/package-meta.js +1 -1
  59. package/dist/runstore/pipelineDagSnapshot.js +2 -0
  60. package/dist/runstore/port.d.ts +11 -0
  61. package/dist/runstore/runProjection.js +9 -0
  62. package/dist/runstore/sqlite/SqliteRunStore.js +27 -4
  63. package/dist/runstore/sqlite/schema.d.ts +1 -1
  64. package/dist/runstore/sqlite/schema.js +2 -0
  65. package/dist/runstore/stageExecution.d.ts +3 -0
  66. package/dist/runstore/stageExecution.js +6 -2
  67. package/dist/runstore/stageSnapshot.js +11 -0
  68. package/dist/runtime/completionCheckRunner.d.ts +3 -2
  69. package/dist/runtime/completionCheckRunner.js +3 -2
  70. package/dist/runtime/envelopeRouting.d.ts +2 -2
  71. package/dist/runtime/pipelineRunner.d.ts +3 -0
  72. package/dist/runtime/pipelineRunner.js +23 -3
  73. package/dist/runtime/runManager.js +3 -2
  74. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  75. package/dist/runtime/stageRunner.d.ts +2 -2
  76. package/dist/runtime/stageRunner.js +2 -2
  77. package/dist/runtime/taskInput.d.ts +7 -0
  78. package/dist/runtime/taskInput.js +60 -0
  79. package/dist/runtime/verifiedStageExecution.js +3 -0
  80. package/dist/types/completion.d.ts +6 -0
  81. package/dist/types/pipeline.d.ts +21 -2
  82. package/dist/types/preEmitCheck.d.ts +4 -6
  83. package/dist/types/stage.d.ts +22 -3
  84. package/dist/types/stageflowManifest.d.ts +2 -0
  85. package/dist/types/task.d.ts +1 -0
  86. package/dist/types/usage.d.ts +16 -0
  87. package/dist/types/usage.js +24 -0
  88. package/dist/ui/assets/{index-uVHiae8s.css → index-D2Wt8hwH.css} +1 -1
  89. package/dist/ui/assets/index-XWJovnp7.js +118 -0
  90. package/dist/ui/index.html +2 -2
  91. package/package.json +1 -1
  92. package/skills/stageflow-author/SKILL.md +7 -7
  93. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
  94. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
  95. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
  96. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
  97. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
  98. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
  99. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
  100. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
  101. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
  102. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
  103. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
  104. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
  105. package/skills/stageflow-author/references/catalog-mapping.md +30 -28
  106. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  107. package/skills/stageflow-author/references/stage-prompt-template.md +5 -5
  108. package/skills/stageflow-session-capture/SKILL.md +1 -1
  109. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
  110. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
  111. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
  112. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -2
  113. package/dist/ui/assets/index-Yzh42vhf.js +0 -118
@@ -2,6 +2,21 @@ import { projectRun } from "../projection/projectRun.js";
2
2
  function stringify(payload) {
3
3
  return JSON.stringify(payload, null, 2);
4
4
  }
5
+ function formatUsd(amount) {
6
+ return `$${amount.toFixed(4)}`;
7
+ }
8
+ /** Best-effort total cost for a run; undefined (not thrown) on any read failure or when no stage reported usage. */
9
+ async function tryReadTotalCostUsd(store, runId) {
10
+ if (!store)
11
+ return undefined;
12
+ try {
13
+ const detail = await store.readRun(runId);
14
+ return detail.total_cost_usd;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
5
20
  function isBusyCode(code) {
6
21
  return code === "busy_capacity" || code === "busy_checkout";
7
22
  }
@@ -46,40 +61,53 @@ function formatStartFailureJson(started) {
46
61
  return formatRunStartFailedJson(started);
47
62
  }
48
63
  function baseRunCompletionPayload(result) {
49
- if (result.outcome === "succeeded") {
50
- return {
64
+ const payload = result.outcome === "succeeded"
65
+ ? {
51
66
  ok: true,
52
67
  outcome: "succeeded",
53
68
  runId: result.runId,
54
69
  runDir: result.runDir,
55
- };
70
+ }
71
+ : result.outcome === "waiting"
72
+ ? {
73
+ ok: false,
74
+ outcome: "waiting",
75
+ runId: result.runId,
76
+ runDir: result.runDir,
77
+ }
78
+ : {
79
+ ok: false,
80
+ outcome: "failed",
81
+ runId: result.runId,
82
+ runDir: result.runDir,
83
+ };
84
+ if (result.outcome === "failed" && result.reason !== undefined) {
85
+ payload.reason = result.reason;
56
86
  }
57
- if (result.outcome === "waiting") {
58
- return {
59
- ok: false,
60
- outcome: "waiting",
61
- runId: result.runId,
62
- runDir: result.runDir,
63
- };
87
+ if (Array.isArray(result.findings) && result.findings.length > 0) {
88
+ payload.findings = result.findings.map((finding) => ({
89
+ severity: finding.severity,
90
+ code: finding.code,
91
+ file: finding.path,
92
+ message: finding.message,
93
+ category: finding.category,
94
+ }));
64
95
  }
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
96
  return payload;
74
97
  }
75
- function formatRunCompletionJson(result) {
76
- return stringify(baseRunCompletionPayload(result));
98
+ async function formatRunCompletionJsonWithCost(result, store) {
99
+ const totalCostUsd = await tryReadTotalCostUsd(store, result.runId);
100
+ return stringify({
101
+ ...baseRunCompletionPayload(result),
102
+ ...(totalCostUsd !== undefined ? { total_cost_usd: totalCostUsd } : {}),
103
+ });
77
104
  }
78
105
  async function formatRunCompletionJsonWithStages(result, store) {
79
106
  const detail = await store.readRun(result.runId);
80
107
  const projection = projectRun(detail);
81
108
  return stringify({
82
109
  ...baseRunCompletionPayload(result),
110
+ ...(detail.total_cost_usd !== undefined ? { total_cost_usd: detail.total_cost_usd } : {}),
83
111
  stages: projection.stages,
84
112
  });
85
113
  }
@@ -93,18 +121,21 @@ function writeStartFailureHuman(started, io) {
93
121
  io.error(`conflictingCheckout: ${started.conflictingCheckout}`);
94
122
  }
95
123
  }
96
- function writeCompletionHuman(result, io) {
124
+ function writeCompletionHuman(result, io, totalCostUsd) {
97
125
  switch (result.outcome) {
98
126
  case "waiting":
99
127
  io.error(`Pipeline waiting. Run folder: ${result.runDir} (${result.runId})`);
100
- return;
128
+ break;
101
129
  case "failed":
102
130
  io.error(`Pipeline failed: ${result.reason}`);
103
131
  io.error(`Run folder: ${result.runDir}`);
104
- return;
132
+ break;
105
133
  case "succeeded":
106
134
  io.log(`Pipeline succeeded. Run folder: ${result.runDir}`);
107
- return;
135
+ break;
136
+ }
137
+ if (totalCostUsd !== undefined) {
138
+ io.error(`Cost: ${formatUsd(totalCostUsd)}`);
108
139
  }
109
140
  }
110
141
  function exitCodeForRunOutcome(outcome) {
@@ -143,11 +174,12 @@ export async function reportCliRun(event, options) {
143
174
  }
144
175
  }
145
176
  else {
146
- options.io.log(formatRunCompletionJson(event.result));
177
+ options.io.log(await formatRunCompletionJsonWithCost(event.result, options.store));
147
178
  }
148
179
  }
149
180
  else {
150
- writeCompletionHuman(event.result, options.io);
181
+ const totalCostUsd = await tryReadTotalCostUsd(options.store, event.result.runId);
182
+ writeCompletionHuman(event.result, options.io, totalCostUsd);
151
183
  }
152
184
  return exitCodeForRunOutcome(event.result.outcome);
153
185
  }
@@ -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;
@@ -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>;