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
@@ -1,6 +1,13 @@
1
+ /**
2
+ * Normalize a pipeline `stages:` entry. Target YAML (`io` / `verify` /
3
+ * `on_verify_fail`) compiles onto IR `completion` / `recovery` / body
4
+ * `payload_schema`. Legacy YAML keys are dual-read via legacyYaml.ts.
5
+ */
1
6
  import path from "node:path";
2
7
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
3
8
  import { parseStageMcp } from "./loadStage.js";
9
+ import { legacyAuthoringRejected, presentLegacyKeys } from "./legacyYaml.js";
10
+ import { applyCompiledBody, compileTargetContract, dialectFromKeys, mixedDialectIssue, } from "./yamlDialect.js";
4
11
  import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
5
12
  import { parseExecutionPolicy } from "./parseCompletionContract.js";
6
13
  import { parsePipelineNeeds } from "./pipelineNeeds.js";
@@ -90,6 +97,10 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
90
97
  ]);
91
98
  }
92
99
  }
100
+ const dialect = dialectFromKeys(Object.keys(raw));
101
+ if (dialect === "invalid") {
102
+ return loadFailure([mixedDialectIssue()]);
103
+ }
93
104
  const uses = typeof raw.uses === "string" ? raw.uses : undefined;
94
105
  const hasBody = hasBodyKey(raw);
95
106
  const skillOutcome = readSkill(raw);
@@ -138,7 +149,35 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
138
149
  },
139
150
  ]);
140
151
  }
141
- const policyOutcome = parseExecutionPolicy(raw, id);
152
+ let compiledBody;
153
+ let policyOutcome;
154
+ if (dialect === "target") {
155
+ const compiled = compileTargetContract(raw, {
156
+ stageId: id,
157
+ label: `entry at index ${index} in ${declaringPath}`,
158
+ category: "pipeline",
159
+ deferSchemaRefs: true,
160
+ });
161
+ if (!compiled.ok)
162
+ return compiled;
163
+ policyOutcome = loadSuccess({
164
+ ...(compiled.value.completion !== undefined
165
+ ? { completion: compiled.value.completion }
166
+ : {}),
167
+ ...(compiled.value.recovery !== undefined
168
+ ? { recovery: compiled.value.recovery }
169
+ : {}),
170
+ });
171
+ if (!uses) {
172
+ compiledBody = applyCompiledBody(extractBodyRaw(raw), compiled.value);
173
+ }
174
+ }
175
+ else {
176
+ const rejected = legacyAuthoringRejected(dialect, `stage "${id}" in ${declaringPath}`, presentLegacyKeys(Object.keys(raw)));
177
+ if (rejected)
178
+ return loadFailure([rejected]);
179
+ policyOutcome = parseExecutionPolicy(raw, id);
180
+ }
142
181
  if (!policyOutcome.ok)
143
182
  return policyOutcome;
144
183
  let needs;
@@ -202,7 +241,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
202
241
  body = { kind: "uses", path: uses, absolutePath };
203
242
  }
204
243
  else {
205
- body = { kind: "inline", raw: extractBodyRaw(raw) };
244
+ body = { kind: "inline", raw: compiledBody ?? extractBodyRaw(raw) };
206
245
  }
207
246
  const entry = {
208
247
  id,
@@ -1,8 +1,11 @@
1
1
  import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
2
- import { type LoadOutcome } from "./loadOutcome.js";
2
+ import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
3
3
  type ExecutionPolicy = {
4
4
  completion?: CompletionContract;
5
5
  recovery?: RecoveryPolicy;
6
6
  };
7
- export declare function parseExecutionPolicy(raw: Record<string, unknown>, stageId: string): LoadOutcome<ExecutionPolicy>;
7
+ export declare function recoveryRequiresCompletionIssue(stageId: string): LoadIssue;
8
+ export declare function parseExecutionPolicy(raw: Record<string, unknown>, stageId: string, options?: {
9
+ requireCompletionForRecovery?: boolean;
10
+ }): LoadOutcome<ExecutionPolicy>;
8
11
  export {};
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Parse IR `completion` / `recovery` objects (DAG wiring after compile, or
3
+ * legacy YAML keys). Target YAML `on_verify_fail` / after-phase `verify` are
4
+ * compiled onto these shapes in yamlDialect.ts before this parser runs.
5
+ */
1
6
  import path from "node:path";
2
7
  import { STAGE_GATE_KINDS } from "../types/stage.js";
3
8
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
@@ -21,6 +26,13 @@ function hasOnlyKeys(value, allowed) {
21
26
  function isPositiveInteger(value) {
22
27
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
23
28
  }
29
+ export function recoveryRequiresCompletionIssue(stageId) {
30
+ return {
31
+ code: "pipeline.invalid_recovery",
32
+ message: `Stage "${stageId}" recovery: requires a completion contract`,
33
+ category: "pipeline",
34
+ };
35
+ }
24
36
  function isGateKind(value) {
25
37
  return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
26
38
  }
@@ -167,7 +179,7 @@ function parseRecovery(raw, stageId, hasCompletion) {
167
179
  if (raw === undefined)
168
180
  return loadSuccess(undefined);
169
181
  if (!hasCompletion)
170
- return failure(stageId, "recovery", "requires a completion contract");
182
+ return loadFailure([recoveryRequiresCompletionIssue(stageId)]);
171
183
  if (!isPlainObject(raw))
172
184
  return failure(stageId, "recovery", "must be an object");
173
185
  const unknown = hasOnlyKeys(raw, ["mode", "max_attempts", "retry_safety", "include_failed_checks"]);
@@ -204,11 +216,12 @@ function parseRecovery(raw, stageId, hasCompletion) {
204
216
  : {}),
205
217
  });
206
218
  }
207
- export function parseExecutionPolicy(raw, stageId) {
219
+ export function parseExecutionPolicy(raw, stageId, options) {
208
220
  const completion = parseCompletion(raw.completion, stageId);
209
221
  if (!completion.ok)
210
222
  return completion;
211
- const recovery = parseRecovery(raw.recovery, stageId, completion.value !== undefined);
223
+ const requireCompletion = options?.requireCompletionForRecovery ?? true;
224
+ const recovery = parseRecovery(raw.recovery, stageId, requireCompletion ? completion.value !== undefined : true);
212
225
  if (!recovery.ok)
213
226
  return recovery;
214
227
  return loadSuccess({
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Parse IR `pre_emit_checks` (emit-phase). YAML `verify` emit items are compiled
3
+ * onto this list in yamlDialect.ts before this parser runs.
4
+ */
1
5
  import { STAGE_GATE_KINDS } from "../types/stage.js";
2
6
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
3
7
  function isPlainObject(value) {
@@ -41,13 +41,13 @@ export function parsePipelineNeeds(raw, stageId) {
41
41
  if (!Array.isArray(raw)) {
42
42
  return {
43
43
  ok: false,
44
- message: `stage "${stageId}": needs must be a non-empty string or an array of at least two items`,
44
+ message: `stage "${stageId}": needs must be a non-empty string or a non-empty array`,
45
45
  };
46
46
  }
47
- if (raw.length < 2) {
47
+ if (raw.length < 1) {
48
48
  return {
49
49
  ok: false,
50
- message: `stage "${stageId}": needs array must contain at least two items`,
50
+ message: `stage "${stageId}": needs array must contain at least one item`,
51
51
  };
52
52
  }
53
53
  const edges = [];
@@ -1,4 +1,15 @@
1
+ /**
2
+ * Allowed keys on a pipeline stage YAML entry.
3
+ *
4
+ * Target authoring: `io`, `verify`, `on_verify_fail`.
5
+ * IR / legacy YAML (same strings): `payload_schema`, `pre_emit_checks`,
6
+ * `completion`, `recovery`, `clone_input_schema`.
7
+ *
8
+ * New catalog fields go on the target keys (and compileTargetContract). Dual-read
9
+ * still lists the IR names so old files load.
10
+ */
1
11
  export declare const WIRING_KEYS: Set<string>;
12
+ /** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
2
13
  export declare const BODY_KEYS: Set<string>;
3
14
  export declare function isPipelineStageBodyKey(key: string): boolean;
4
15
  export declare function isAllowedPipelineStageEntryKey(key: string): boolean;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Allowed keys on a pipeline stage YAML entry.
3
+ *
4
+ * Target authoring: `io`, `verify`, `on_verify_fail`.
5
+ * IR / legacy YAML (same strings): `payload_schema`, `pre_emit_checks`,
6
+ * `completion`, `recovery`, `clone_input_schema`.
7
+ *
8
+ * New catalog fields go on the target keys (and compileTargetContract). Dual-read
9
+ * still lists the IR names so old files load.
10
+ */
1
11
  export const WIRING_KEYS = new Set([
2
12
  "id",
3
13
  "needs",
@@ -7,9 +17,11 @@ export const WIRING_KEYS = new Set([
7
17
  "clone_cap",
8
18
  "completion",
9
19
  "recovery",
20
+ "on_verify_fail",
10
21
  "feedback_loop",
11
22
  "replay_safe",
12
23
  ]);
24
+ /** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
13
25
  export const BODY_KEYS = new Set([
14
26
  "system_prompt",
15
27
  "model",
@@ -21,6 +33,8 @@ export const BODY_KEYS = new Set([
21
33
  "timeout_ms",
22
34
  "skill",
23
35
  "mcp",
36
+ "io",
37
+ "verify",
24
38
  ]);
25
39
  export function isPipelineStageBodyKey(key) {
26
40
  return BODY_KEYS.has(key);
@@ -0,0 +1,28 @@
1
+ import type { CompletionCheck } from "../types/completion.js";
2
+ import type { PreEmitCheck } from "../types/preEmitCheck.js";
3
+ import type { StageIoYaml } from "../types/stage.js";
4
+ export type VerifyPhase = "emit" | "after";
5
+ export declare function buildVerifyItems(preEmit: PreEmitCheck[] | undefined, afterChecks: CompletionCheck[] | undefined): {
6
+ ok: true;
7
+ items: Record<string, unknown>[];
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ export declare function ioFromSchemas(stage: {
13
+ payload_schema?: unknown;
14
+ clone_input_schema?: unknown;
15
+ }): StageIoYaml | undefined;
16
+ export declare function ioFromRawDocument(raw: Record<string, unknown>): StageIoYaml | undefined;
17
+ export declare function rewriteStageDocument(raw: Record<string, unknown>, compiled: {
18
+ io?: StageIoYaml;
19
+ verify?: Record<string, unknown>[];
20
+ }): Record<string, unknown>;
21
+ export declare function rewritePipelineStageEntry(raw: Record<string, unknown>, compiled: {
22
+ uses?: string;
23
+ io?: StageIoYaml;
24
+ verify?: Record<string, unknown>[];
25
+ on_verify_fail?: unknown;
26
+ }): Record<string, unknown>;
27
+ export declare function rewriteCatalogDocument(raw: Record<string, unknown>, rewriteEntry: (entry: Record<string, unknown>, index: number) => Record<string, unknown>): Record<string, unknown>;
28
+ export declare function stringifyTargetYaml(doc: unknown): string;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Print target YAML (`io` / `verify` / `on_verify_fail`) from compiled IR.
3
+ * Used by `sf migrate-yaml`. IR field names stay payload_schema / completion.
4
+ */
5
+ import { stringify as stringifyYaml } from "yaml";
6
+ import { LEGACY_CONTRACT_KEYS } from "./legacyYaml.js";
7
+ import { STAGE_FILE_WIRING_KEYS, TARGET_CONTRACT_KEYS } from "./yamlDialect.js";
8
+ function isPlainObject(value) {
9
+ return typeof value === "object" && value !== null && Array.isArray(value) === false;
10
+ }
11
+ function uniquePhases(phases) {
12
+ const seen = new Set();
13
+ const next = [];
14
+ for (const phase of phases) {
15
+ if (seen.has(phase))
16
+ continue;
17
+ seen.add(phase);
18
+ next.push(phase);
19
+ }
20
+ return next;
21
+ }
22
+ function artifactTypesCompatible(a, b) {
23
+ const artifact = a === "artifact" || a === "artifact_declared";
24
+ const other = b === "artifact" || b === "artifact_declared";
25
+ return artifact && other;
26
+ }
27
+ function typesCompatible(a, b) {
28
+ if (a === b)
29
+ return true;
30
+ return artifactTypesCompatible(a, b);
31
+ }
32
+ function preEmitToItem(check) {
33
+ if (check.type === "artifact_declared") {
34
+ return {
35
+ id: check.id,
36
+ type: "artifact",
37
+ basename: check.basename,
38
+ when: ["emit"],
39
+ };
40
+ }
41
+ return { ...check, when: ["emit"] };
42
+ }
43
+ function afterCheckToItem(check) {
44
+ return { ...check, when: ["after"] };
45
+ }
46
+ function mergeVerifyItems(existing, incoming) {
47
+ const existingType = String(existing.type);
48
+ const incomingType = String(incoming.type);
49
+ if (!typesCompatible(existingType, incomingType)) {
50
+ return {
51
+ ok: false,
52
+ error: `verify id "${String(existing.id)}" cannot merge types "${existingType}" and "${incomingType}"`,
53
+ };
54
+ }
55
+ const existingWhen = Array.isArray(existing.when) ? existing.when : [];
56
+ const incomingWhen = Array.isArray(incoming.when) ? incoming.when : [];
57
+ const merged = { ...existing };
58
+ if (artifactTypesCompatible(existingType, incomingType)) {
59
+ merged.type = "artifact";
60
+ if (typeof incoming.path === "string")
61
+ merged.path = incoming.path;
62
+ if (typeof incoming.basename === "string" && merged.path === undefined) {
63
+ merged.basename = incoming.basename;
64
+ }
65
+ if (typeof incoming.nonempty === "boolean")
66
+ merged.nonempty = incoming.nonempty;
67
+ if (typeof existing.basename === "string" && typeof merged.path === "string") {
68
+ const base = merged.path.split(/[\\/]/).pop();
69
+ if (base === existing.basename)
70
+ delete merged.basename;
71
+ }
72
+ }
73
+ else {
74
+ for (const [key, value] of Object.entries(incoming)) {
75
+ if (key === "when" || key === "id" || key === "type")
76
+ continue;
77
+ if (merged[key] === undefined)
78
+ merged[key] = value;
79
+ }
80
+ }
81
+ merged.when = uniquePhases([...existingWhen, ...incomingWhen]);
82
+ return { ok: true, item: merged };
83
+ }
84
+ export function buildVerifyItems(preEmit, afterChecks) {
85
+ const byId = new Map();
86
+ const order = [];
87
+ const add = (item) => {
88
+ const id = typeof item.id === "string" ? item.id : "";
89
+ if (!id)
90
+ return { ok: false, error: "verify item is missing id" };
91
+ const existing = byId.get(id);
92
+ if (!existing) {
93
+ byId.set(id, item);
94
+ order.push(id);
95
+ return undefined;
96
+ }
97
+ const merged = mergeVerifyItems(existing, item);
98
+ if (!merged.ok)
99
+ return { ok: false, error: merged.error };
100
+ byId.set(id, merged.item);
101
+ return undefined;
102
+ };
103
+ for (const check of preEmit ?? []) {
104
+ const failed = add(preEmitToItem(check));
105
+ if (failed)
106
+ return failed;
107
+ }
108
+ for (const check of afterChecks ?? []) {
109
+ const failed = add(afterCheckToItem(check));
110
+ if (failed)
111
+ return failed;
112
+ }
113
+ return { ok: true, items: order.map((id) => byId.get(id)) };
114
+ }
115
+ export function ioFromSchemas(stage) {
116
+ const io = {};
117
+ if (stage.clone_input_schema !== undefined) {
118
+ io.input = { schema: stage.clone_input_schema };
119
+ }
120
+ if (stage.payload_schema !== undefined) {
121
+ io.output = { schema: stage.payload_schema };
122
+ }
123
+ return io.input !== undefined || io.output !== undefined ? io : undefined;
124
+ }
125
+ export function ioFromRawDocument(raw) {
126
+ if (isPlainObject(raw.io))
127
+ return raw.io;
128
+ return ioFromSchemas({
129
+ payload_schema: raw.payload_schema,
130
+ clone_input_schema: raw.clone_input_schema,
131
+ });
132
+ }
133
+ function dropContractKeys(raw) {
134
+ const next = {};
135
+ for (const [key, value] of Object.entries(raw)) {
136
+ if (LEGACY_CONTRACT_KEYS.has(key) || TARGET_CONTRACT_KEYS.has(key))
137
+ continue;
138
+ next[key] = value;
139
+ }
140
+ return next;
141
+ }
142
+ const STAGE_BODY_ORDER = [
143
+ "id",
144
+ "system_prompt",
145
+ "model",
146
+ "agent",
147
+ "timeout_ms",
148
+ "io",
149
+ "verify",
150
+ "gate_kinds",
151
+ "clone_actions",
152
+ "skill",
153
+ "mcp",
154
+ ];
155
+ const PIPELINE_WIRING_ORDER = [
156
+ "id",
157
+ "uses",
158
+ "skill",
159
+ "mcp",
160
+ "needs",
161
+ "fork",
162
+ "clonable",
163
+ "clone_cap",
164
+ "on_verify_fail",
165
+ "feedback_loop",
166
+ "replay_safe",
167
+ ];
168
+ function orderKeys(raw, preferred) {
169
+ const next = {};
170
+ for (const key of preferred) {
171
+ if (raw[key] !== undefined)
172
+ next[key] = raw[key];
173
+ }
174
+ for (const key of Object.keys(raw)) {
175
+ if (next[key] !== undefined)
176
+ continue;
177
+ next[key] = raw[key];
178
+ }
179
+ return next;
180
+ }
181
+ export function rewriteStageDocument(raw, compiled) {
182
+ const next = dropContractKeys(raw);
183
+ for (const key of STAGE_FILE_WIRING_KEYS) {
184
+ delete next[key];
185
+ }
186
+ if (compiled.io !== undefined)
187
+ next.io = compiled.io;
188
+ if (compiled.verify !== undefined && compiled.verify.length > 0) {
189
+ next.verify = compiled.verify;
190
+ }
191
+ return orderKeys(next, STAGE_BODY_ORDER);
192
+ }
193
+ export function rewritePipelineStageEntry(raw, compiled) {
194
+ const next = dropContractKeys(raw);
195
+ if (compiled.uses !== undefined) {
196
+ next.uses = compiled.uses;
197
+ delete next.system_prompt;
198
+ delete next.model;
199
+ delete next.agent;
200
+ delete next.timeout_ms;
201
+ delete next.gate_kinds;
202
+ delete next.clone_actions;
203
+ delete next.io;
204
+ delete next.verify;
205
+ if (compiled.on_verify_fail !== undefined)
206
+ next.on_verify_fail = compiled.on_verify_fail;
207
+ else
208
+ delete next.on_verify_fail;
209
+ return orderKeys(next, PIPELINE_WIRING_ORDER);
210
+ }
211
+ if (compiled.io !== undefined)
212
+ next.io = compiled.io;
213
+ else
214
+ delete next.io;
215
+ if (compiled.verify !== undefined && compiled.verify.length > 0)
216
+ next.verify = compiled.verify;
217
+ else
218
+ delete next.verify;
219
+ if (compiled.on_verify_fail !== undefined)
220
+ next.on_verify_fail = compiled.on_verify_fail;
221
+ else
222
+ delete next.on_verify_fail;
223
+ return orderKeys(next, [...STAGE_BODY_ORDER, ...PIPELINE_WIRING_ORDER]);
224
+ }
225
+ export function rewriteCatalogDocument(raw, rewriteEntry) {
226
+ const next = {};
227
+ for (const [key, value] of Object.entries(raw)) {
228
+ if (key === "stages" && Array.isArray(value)) {
229
+ next.stages = value.map((entry, index) => {
230
+ if (!isPlainObject(entry))
231
+ return entry;
232
+ return rewriteEntry(entry, index);
233
+ });
234
+ continue;
235
+ }
236
+ next[key] = value;
237
+ }
238
+ return next;
239
+ }
240
+ export function stringifyTargetYaml(doc) {
241
+ const text = stringifyYaml(doc, { indent: 2, lineWidth: 0 });
242
+ return text.endsWith("\n") ? text : `${text}\n`;
243
+ }
@@ -0,0 +1,20 @@
1
+ import type { LoadedManifest } from "../types/stageflowManifest.js";
2
+ import { type LoadOutcome } from "./loadOutcome.js";
3
+ export type ModelSelection = {
4
+ /** stageflow.yaml top-level `model`. */
5
+ global?: string;
6
+ /** Pipeline yaml top-level `model`; overrides global. */
7
+ pipeline?: string;
8
+ /** Stage yaml / inline `model`; overrides pipeline/global. */
9
+ stage?: string;
10
+ };
11
+ /**
12
+ * stage > pipeline > global. No silent hardcoded fallback — unset is a load error.
13
+ * Present-but-empty / whitespace follows parseModelField (load failure, not "").
14
+ */
15
+ export declare function resolveModelOutcome(selection: ModelSelection, ctx: {
16
+ stageId: string;
17
+ pipelineId: string;
18
+ }): LoadOutcome<string>;
19
+ /** The global tier's model id from an already-loaded manifest, or undefined when missing/unset. */
20
+ export declare function globalModelFromManifest(loaded: LoadedManifest | null): string | undefined;
@@ -0,0 +1,35 @@
1
+ import { loadFailure, loadSuccess } from "./loadOutcome.js";
2
+ import { parseModelField } from "./modelField.js";
3
+ /**
4
+ * stage > pipeline > global. No silent hardcoded fallback — unset is a load error.
5
+ * Present-but-empty / whitespace follows parseModelField (load failure, not "").
6
+ */
7
+ export function resolveModelOutcome(selection, ctx) {
8
+ const effective = selection.stage ?? selection.pipeline ?? selection.global;
9
+ const parsed = parseModelField(effective);
10
+ if (!parsed.ok) {
11
+ return loadFailure([
12
+ {
13
+ code: "stage.invalid_model",
14
+ message: `Stage "${ctx.stageId}" in pipeline "${ctx.pipelineId}": ${parsed.message}`,
15
+ category: "stage",
16
+ stageId: ctx.stageId,
17
+ },
18
+ ]);
19
+ }
20
+ if (parsed.value === undefined) {
21
+ return loadFailure([
22
+ {
23
+ code: "stage.missing_model",
24
+ message: `Stage "${ctx.stageId}" in pipeline "${ctx.pipelineId}": model is required (set on stage, pipeline, or stageflow.yaml)`,
25
+ category: "stage",
26
+ stageId: ctx.stageId,
27
+ },
28
+ ]);
29
+ }
30
+ return loadSuccess(parsed.value);
31
+ }
32
+ /** The global tier's model id from an already-loaded manifest, or undefined when missing/unset. */
33
+ export function globalModelFromManifest(loaded) {
34
+ return loaded?.manifest.model;
35
+ }
@@ -2,7 +2,7 @@ import type { LoadedPipeline } from "../types/pipeline.js";
2
2
  import type { LoadIssue } from "./loadOutcome.js";
3
3
  export type ValidationSeverity = "error" | "warning";
4
4
  export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
5
- export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp";
5
+ export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml";
6
6
  export type ValidationFinding = {
7
7
  severity: ValidationSeverity;
8
8
  code: ValidationFindingCode | string;
@@ -15,6 +15,7 @@ export type ValidationFinding = {
15
15
  export type ValidationScope = "full" | "pipeline" | "task";
16
16
  export type ValidateCatalogOptions = {
17
17
  cwd?: string;
18
+ projectRoot?: string;
18
19
  scope: ValidationScope;
19
20
  pipeline?: string;
20
21
  task?: string;
@@ -36,6 +37,7 @@ export declare function findingsFromLoadIssues(cwd: string, absPath: string, iss
36
37
  export declare function findingStageIdFilenameMismatch(cwd: string, absPath: string, fileStem: string, declaredId: string): ValidationFinding;
37
38
  export type ValidatePipelineOptions = {
38
39
  cwd?: string;
40
+ projectRoot?: string;
39
41
  validateStages?: boolean;
40
42
  strict?: boolean;
41
43
  };
@@ -43,12 +45,14 @@ export declare function validatePipeline(nameOrPath: string, options?: ValidateP
43
45
  export type LoadPipelineValidatedResult = {
44
46
  ok: true;
45
47
  loaded: LoadedPipeline;
48
+ findings: ValidationFinding[];
46
49
  } | {
47
50
  ok: false;
48
51
  findings: ValidationFinding[];
49
52
  };
50
53
  export declare function loadPipelineValidated(nameOrPath: string, options?: {
51
54
  cwd?: string;
55
+ projectRoot?: string;
52
56
  validateStages?: boolean;
53
57
  }): Promise<LoadPipelineValidatedResult>;
54
58
  export declare function validateCatalog(options: ValidateCatalogOptions): Promise<ValidationResult>;