stageflow 0.12.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +5 -1
  2. package/dist/agent/claudeAdapter.js +31 -1
  3. package/dist/agent/piAdapter.d.ts +6 -4
  4. package/dist/agent/piAdapter.js +71 -26
  5. package/dist/agent/piIsolatedMcp.d.ts +23 -0
  6. package/dist/agent/piIsolatedMcp.js +223 -0
  7. package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
  8. package/dist/agent/piIsolatedMcpProbe.js +292 -0
  9. package/dist/agent/port.d.ts +4 -2
  10. package/dist/agent/port.js +1 -1
  11. package/dist/cli/initTemplates.d.ts +1 -1
  12. package/dist/cli/initTemplates.js +1 -1
  13. package/dist/cli/migrateYamlCommand.d.ts +18 -0
  14. package/dist/cli/migrateYamlCommand.js +180 -0
  15. package/dist/cli/runOutput.js +26 -18
  16. package/dist/cli/validateCommand.d.ts +1 -0
  17. package/dist/cli/validateCommand.js +2 -0
  18. package/dist/cli.js +15 -1
  19. package/dist/config/browseCatalog.js +1 -1
  20. package/dist/config/createPipeline.d.ts +1 -1
  21. package/dist/config/createPipeline.js +23 -11
  22. package/dist/config/createStage.d.ts +1 -1
  23. package/dist/config/createStage.js +10 -5
  24. package/dist/config/legacyYaml.d.ts +28 -0
  25. package/dist/config/legacyYaml.js +98 -0
  26. package/dist/config/loadOutcome.d.ts +2 -1
  27. package/dist/config/loadOutcome.js +4 -1
  28. package/dist/config/loadPipeline.d.ts +2 -0
  29. package/dist/config/loadPipeline.js +151 -8
  30. package/dist/config/loadStage.d.ts +9 -1
  31. package/dist/config/loadStage.js +207 -29
  32. package/dist/config/loadStageflowManifest.js +10 -0
  33. package/dist/config/loadTask.js +14 -0
  34. package/dist/config/materializeStageModels.d.ts +7 -0
  35. package/dist/config/materializeStageModels.js +35 -0
  36. package/dist/config/mergePipelineIncludes.d.ts +5 -1
  37. package/dist/config/mergePipelineIncludes.js +82 -4
  38. package/dist/config/migrateYaml.d.ts +19 -0
  39. package/dist/config/migrateYaml.js +627 -0
  40. package/dist/config/modelField.d.ts +13 -0
  41. package/dist/config/modelField.js +16 -0
  42. package/dist/config/normalizePipelineStageEntry.js +48 -3
  43. package/dist/config/parseCompletionContract.d.ts +5 -2
  44. package/dist/config/parseCompletionContract.js +16 -3
  45. package/dist/config/parsePreEmitChecks.js +4 -0
  46. package/dist/config/pipelineNeeds.js +3 -3
  47. package/dist/config/pipelineStageKeys.d.ts +11 -0
  48. package/dist/config/pipelineStageKeys.js +15 -0
  49. package/dist/config/printTargetYaml.d.ts +28 -0
  50. package/dist/config/printTargetYaml.js +243 -0
  51. package/dist/config/resolveModel.d.ts +20 -0
  52. package/dist/config/resolveModel.js +35 -0
  53. package/dist/config/resolveStageMcpServers.d.ts +35 -0
  54. package/dist/config/resolveStageMcpServers.js +207 -0
  55. package/dist/config/validateCatalog.d.ts +5 -1
  56. package/dist/config/validateCatalog.js +87 -6
  57. package/dist/config/validateCompletionContract.js +5 -0
  58. package/dist/config/yamlDialect.d.ts +29 -0
  59. package/dist/config/yamlDialect.js +327 -0
  60. package/dist/envelope/payloadSchema.d.ts +37 -5
  61. package/dist/envelope/payloadSchema.js +272 -22
  62. package/dist/mcp/catalogTools.js +1 -0
  63. package/dist/mcp/readArtifact.d.ts +2 -0
  64. package/dist/mcp/readArtifact.js +25 -1
  65. package/dist/package-meta.d.ts +1 -1
  66. package/dist/package-meta.js +1 -1
  67. package/dist/runstore/pipelineDagSnapshot.js +2 -0
  68. package/dist/runstore/port.d.ts +1 -0
  69. package/dist/runtime/completionCheckRunner.d.ts +3 -2
  70. package/dist/runtime/completionCheckRunner.js +3 -2
  71. package/dist/runtime/envelopeRouting.d.ts +2 -2
  72. package/dist/runtime/pipelineRunner.d.ts +3 -0
  73. package/dist/runtime/pipelineRunner.js +23 -3
  74. package/dist/runtime/runManager.js +3 -2
  75. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  76. package/dist/runtime/stageAttemptBootstrap.js +38 -2
  77. package/dist/runtime/stageRunner.d.ts +2 -2
  78. package/dist/runtime/taskInput.d.ts +7 -0
  79. package/dist/runtime/taskInput.js +60 -0
  80. package/dist/runtime/verifiedStageExecution.d.ts +2 -0
  81. package/dist/runtime/verifiedStageExecution.js +13 -2
  82. package/dist/server/http.js +23 -4
  83. package/dist/server/projectMcpRoutes.d.ts +6 -0
  84. package/dist/server/projectMcpRoutes.js +38 -0
  85. package/dist/types/completion.d.ts +6 -0
  86. package/dist/types/pipeline.d.ts +23 -2
  87. package/dist/types/preEmitCheck.d.ts +4 -6
  88. package/dist/types/stage.d.ts +24 -4
  89. package/dist/types/stageflowManifest.d.ts +2 -0
  90. package/dist/types/task.d.ts +1 -0
  91. package/dist/ui/assets/index-GtyiWiZg.js +118 -0
  92. package/dist/ui/index.html +1 -1
  93. package/package.json +4 -1
  94. package/skills/stageflow-author/SKILL.md +20 -2
  95. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
  96. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
  97. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
  98. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
  99. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
  100. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
  101. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
  102. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
  103. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
  104. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
  105. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
  106. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
  107. package/skills/stageflow-author/references/catalog-mapping.md +80 -17
  108. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  109. package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
  110. package/skills/stageflow-session-capture/SKILL.md +1 -1
  111. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
  112. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
  113. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
  114. package/skills/stageflow-session-capture/references/catalog-authoring.md +6 -3
  115. package/dist/ui/assets/index-CXGz4nL7.js +0 -118
@@ -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>;
@@ -1,11 +1,88 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { compilePayloadSchema, expandPayloadSchemaRefs, isPayloadSchemaSubset, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
3
4
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
5
  import { mergePipelineStages } from "./mergePipelineIncludes.js";
5
6
  import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
6
- import { loadStageFromObjectOutcome, loadStageOutcome } from "./loadStage.js";
7
+ import { loadStageFromObjectOutcome, loadStageOutcome, afterCompletionForStage } from "./loadStage.js";
8
+ import { materializeStageModels } from "./materializeStageModels.js";
9
+ import { predecessorEdges } from "./pipelineNeeds.js";
7
10
  import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
11
+ import { recoveryRequiresCompletionIssue } from "./parseCompletionContract.js";
8
12
  import { validateCompletionContractForStage } from "./validateCompletionContract.js";
13
+ function schemaCompileIssue(stageId, field, err, pipelineId) {
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ if (err instanceof UnresolvedSchemaRefError) {
16
+ return {
17
+ code: "stage.unresolved_schema_ref",
18
+ message: `Pipeline ${pipelineId}: stage "${stageId}" ${field}: ${message}`,
19
+ category: "stage",
20
+ stageId,
21
+ };
22
+ }
23
+ return {
24
+ code: field === "payload_schema" ? "stage.invalid_payload_schema" : "stage.invalid_clone_input_schema",
25
+ message: `Pipeline ${pipelineId}: stage "${stageId}" ${field}: ${message}`,
26
+ category: "stage",
27
+ stageId,
28
+ };
29
+ }
30
+ function attachPipelineSchemas(stages, schemas, pipelineId) {
31
+ const options = schemas !== undefined ? { schemas } : undefined;
32
+ for (const stage of stages) {
33
+ if (stage.payload_schema !== undefined) {
34
+ try {
35
+ compilePayloadSchema(stage.payload_schema, options);
36
+ stage.payload_schema = expandPayloadSchemaRefs(stage.payload_schema, options);
37
+ }
38
+ catch (err) {
39
+ return loadFailure([schemaCompileIssue(stage.id, "payload_schema", err, pipelineId)]);
40
+ }
41
+ }
42
+ if (stage.clone_input_schema !== undefined) {
43
+ try {
44
+ compilePayloadSchema(stage.clone_input_schema, options);
45
+ stage.clone_input_schema = expandPayloadSchemaRefs(stage.clone_input_schema, options);
46
+ }
47
+ catch (err) {
48
+ return loadFailure([schemaCompileIssue(stage.id, "clone_input_schema", err, pipelineId)]);
49
+ }
50
+ }
51
+ }
52
+ return loadSuccess(undefined);
53
+ }
54
+ function checkSequentialIoCompatibility(stages, dag, pipelineId, schemas) {
55
+ const stageById = new Map(stages.map((stage) => [stage.id, stage]));
56
+ const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
57
+ const options = schemas !== undefined ? { schemas } : undefined;
58
+ for (const child of stages) {
59
+ const node = nodeById.get(child.id);
60
+ if (!node)
61
+ 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
+ ]);
82
+ }
83
+ }
84
+ return loadSuccess(undefined);
85
+ }
9
86
  export async function resolvePipelinePath(pipelinePath, cwd) {
10
87
  const resolved = path.normalize(path.resolve(cwd, pipelinePath));
11
88
  try {
@@ -16,14 +93,15 @@ export async function resolvePipelinePath(pipelinePath, cwd) {
16
93
  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.`);
17
94
  }
18
95
  }
19
- async function loadPipelineFromPath(pipelinePath) {
96
+ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
20
97
  const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
21
98
  const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
22
99
  if (!mergeOutcome.ok) {
23
100
  return loadFailure(mergeOutcome.issues);
24
101
  }
25
- const { entries: rawEntries, pipelineId, agent: pipelineAgent } = mergeOutcome.value;
102
+ const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, warnings: mergeWarnings, } = mergeOutcome.value;
26
103
  const ctx = { pipelineId, path: normalizedPipelinePath };
104
+ const warnings = [...mergeWarnings];
27
105
  const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
28
106
  if (!normalizeOutcome.ok) {
29
107
  return loadFailure(normalizeOutcome.issues);
@@ -51,6 +129,7 @@ async function loadPipelineFromPath(pipelinePath) {
51
129
  const entryById = new Map(normalizedEntries.map((entry) => [entry.id, entry]));
52
130
  const stageSources = {};
53
131
  const stages = [];
132
+ const fileAfterById = new Map();
54
133
  for (const stageId of stageIds) {
55
134
  const entry = entryById.get(stageId);
56
135
  if (!entry) {
@@ -67,6 +146,7 @@ async function loadPipelineFromPath(pipelinePath) {
67
146
  const inlineOutcome = loadStageFromObjectOutcome(entry.body.raw, {
68
147
  entryId: entry.id,
69
148
  declaringPath: entry.declaringPath,
149
+ deferSchemaRefs: true,
70
150
  });
71
151
  if (!inlineOutcome.ok) {
72
152
  return loadFailure(inlineOutcome.issues);
@@ -75,8 +155,13 @@ async function loadPipelineFromPath(pipelinePath) {
75
155
  stageSources[stageId] = { kind: "inline" };
76
156
  continue;
77
157
  }
78
- const stageOutcome = await loadStageOutcome(entry.body.absolutePath);
158
+ const stageOutcome = await loadStageOutcome(entry.body.absolutePath, {
159
+ deferSchemaRefs: true,
160
+ });
79
161
  if (!stageOutcome.ok) {
162
+ if (stageOutcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
163
+ return loadFailure(stageOutcome.issues);
164
+ }
80
165
  return loadFailure([
81
166
  {
82
167
  code: "pipeline.missing_stage",
@@ -87,6 +172,22 @@ async function loadPipelineFromPath(pipelinePath) {
87
172
  ...stageOutcome.issues,
88
173
  ]);
89
174
  }
175
+ if (stageOutcome.issues)
176
+ warnings.push(...stageOutcome.issues);
177
+ const fileAfter = afterCompletionForStage(stageOutcome.value);
178
+ if (fileAfter && entry.completion) {
179
+ return loadFailure([
180
+ {
181
+ code: "pipeline.invalid_completion",
182
+ message: `Pipeline ${pipelineId}: stage "${stageId}" after-checks come from both body verify and wrapper completion`,
183
+ category: "pipeline",
184
+ pipelineId,
185
+ },
186
+ ]);
187
+ }
188
+ if (fileAfter && !entry.completion) {
189
+ fileAfterById.set(stageId, fileAfter);
190
+ }
90
191
  if (stageOutcome.value.id !== entry.id) {
91
192
  return loadFailure([
92
193
  {
@@ -100,17 +201,58 @@ async function loadPipelineFromPath(pipelinePath) {
100
201
  const stage = {
101
202
  ...stageOutcome.value,
102
203
  ...(entry.skill !== undefined ? { skill: entry.skill } : {}),
204
+ ...(entry.mcp !== undefined ? { mcp: entry.mcp } : {}),
103
205
  };
104
206
  stages.push(stage);
105
207
  stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
106
208
  }
209
+ const schemaOutcome = attachPipelineSchemas(stages, pipelineSchemas, pipelineId);
210
+ if (!schemaOutcome.ok)
211
+ return schemaOutcome;
212
+ const ioOutcome = checkSequentialIoCompatibility(stages, dag, pipelineId, pipelineSchemas);
213
+ if (!ioOutcome.ok)
214
+ return ioOutcome;
215
+ if (pipelineModel !== undefined) {
216
+ const inheritingIds = stages
217
+ .filter((stage) => stage.model === undefined)
218
+ .map((stage) => stage.id);
219
+ if (inheritingIds.length > 0) {
220
+ warnings.push({
221
+ code: "pipeline.model_applies",
222
+ message: `Pipeline ${pipelineId}: pipeline-root model now applies to stages that omit model (${inheritingIds.join(", ")})`,
223
+ category: "pipeline",
224
+ pipelineId,
225
+ });
226
+ }
227
+ }
228
+ const materializeOutcome = await materializeStageModels(stages, {
229
+ pipelineModel,
230
+ pipelineId,
231
+ projectRoot,
232
+ });
233
+ if (!materializeOutcome.ok) {
234
+ return loadFailure(materializeOutcome.issues);
235
+ }
236
+ const loadedStages = materializeOutcome.value;
107
237
  const pipeline = {
108
238
  id: pipelineId,
109
239
  stages: stageIds,
110
240
  ...(pipelineAgent !== undefined ? { agent: pipelineAgent } : {}),
241
+ ...(pipelineModel !== undefined ? { model: pipelineModel } : {}),
242
+ ...(pipelineSchemas !== undefined ? { schemas: pipelineSchemas } : {}),
111
243
  };
112
244
  const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
113
- for (const stage of stages) {
245
+ for (const [stageId, after] of fileAfterById) {
246
+ const node = nodeById.get(stageId);
247
+ if (node)
248
+ node.completion = after;
249
+ }
250
+ for (const node of dag.nodes) {
251
+ if (node.recovery !== undefined && node.completion === undefined) {
252
+ return loadFailure([recoveryRequiresCompletionIssue(node.id)]);
253
+ }
254
+ }
255
+ for (const stage of loadedStages) {
114
256
  const completion = nodeById.get(stage.id)?.completion;
115
257
  const completionOutcome = validateCompletionContractForStage(stage, completion);
116
258
  if (!completionOutcome.ok)
@@ -118,14 +260,15 @@ async function loadPipelineFromPath(pipelinePath) {
118
260
  }
119
261
  return loadSuccess({
120
262
  pipeline,
121
- stages,
263
+ stages: loadedStages,
122
264
  dag,
123
265
  pipelinePath: normalizedPipelinePath,
124
266
  stageSources,
125
- });
267
+ }, warnings);
126
268
  }
127
269
  export async function loadPipelineOutcome(nameOrPath, options = {}) {
128
270
  const cwd = options.cwd ?? process.cwd();
271
+ const projectRoot = options.projectRoot ?? cwd;
129
272
  let pipelinePath;
130
273
  try {
131
274
  pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
@@ -140,7 +283,7 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
140
283
  },
141
284
  ]);
142
285
  }
143
- return loadPipelineFromPath(pipelinePath);
286
+ return loadPipelineFromPath(pipelinePath, cwd, projectRoot);
144
287
  }
145
288
  export async function loadPipeline(nameOrPath, options = {}) {
146
289
  const outcome = await loadPipelineOutcome(nameOrPath, options);
@@ -1,8 +1,16 @@
1
+ import type { CompletionContract } from "../types/completion.js";
1
2
  import { type StageConfig } from "../types/stage.js";
2
3
  import { type LoadOutcome } from "./loadOutcome.js";
4
+ /** After-phase IR from target `verify` (`when` includes after). Stamped onto DAG `completion`. */
5
+ export declare function afterCompletionForStage(stage: StageConfig): CompletionContract | undefined;
6
+ export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
7
+ export type LoadStageOptions = {
8
+ deferSchemaRefs?: boolean;
9
+ };
3
10
  export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
4
11
  entryId: string;
5
12
  declaringPath: string;
13
+ deferSchemaRefs?: boolean;
6
14
  }): LoadOutcome<StageConfig>;
7
- export declare function loadStageOutcome(filePath: string): Promise<LoadOutcome<StageConfig>>;
15
+ export declare function loadStageOutcome(filePath: string, options?: LoadStageOptions): Promise<LoadOutcome<StageConfig>>;
8
16
  export declare function loadStage(filePath: string): Promise<StageConfig>;