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.
- package/README.md +5 -1
- package/dist/agent/claudeAdapter.js +31 -1
- package/dist/agent/piAdapter.d.ts +6 -4
- package/dist/agent/piAdapter.js +71 -26
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +4 -2
- package/dist/agent/port.js +1 -1
- package/dist/cli/initTemplates.d.ts +1 -1
- package/dist/cli/initTemplates.js +1 -1
- package/dist/cli/migrateYamlCommand.d.ts +18 -0
- package/dist/cli/migrateYamlCommand.js +180 -0
- package/dist/cli/runOutput.js +26 -18
- package/dist/cli/validateCommand.d.ts +1 -0
- package/dist/cli/validateCommand.js +2 -0
- package/dist/cli.js +15 -1
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +1 -1
- package/dist/config/createPipeline.js +23 -11
- package/dist/config/createStage.d.ts +1 -1
- package/dist/config/createStage.js +10 -5
- package/dist/config/legacyYaml.d.ts +28 -0
- package/dist/config/legacyYaml.js +98 -0
- package/dist/config/loadOutcome.d.ts +2 -1
- package/dist/config/loadOutcome.js +4 -1
- package/dist/config/loadPipeline.d.ts +2 -0
- package/dist/config/loadPipeline.js +151 -8
- package/dist/config/loadStage.d.ts +9 -1
- package/dist/config/loadStage.js +207 -29
- package/dist/config/loadStageflowManifest.js +10 -0
- package/dist/config/loadTask.js +14 -0
- package/dist/config/materializeStageModels.d.ts +7 -0
- package/dist/config/materializeStageModels.js +35 -0
- package/dist/config/mergePipelineIncludes.d.ts +5 -1
- package/dist/config/mergePipelineIncludes.js +82 -4
- package/dist/config/migrateYaml.d.ts +19 -0
- package/dist/config/migrateYaml.js +627 -0
- package/dist/config/modelField.d.ts +13 -0
- package/dist/config/modelField.js +16 -0
- package/dist/config/normalizePipelineStageEntry.js +48 -3
- package/dist/config/parseCompletionContract.d.ts +5 -2
- package/dist/config/parseCompletionContract.js +16 -3
- package/dist/config/parsePreEmitChecks.js +4 -0
- package/dist/config/pipelineNeeds.js +3 -3
- package/dist/config/pipelineStageKeys.d.ts +11 -0
- package/dist/config/pipelineStageKeys.js +15 -0
- package/dist/config/printTargetYaml.d.ts +28 -0
- package/dist/config/printTargetYaml.js +243 -0
- package/dist/config/resolveModel.d.ts +20 -0
- package/dist/config/resolveModel.js +35 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +5 -1
- package/dist/config/validateCatalog.js +87 -6
- package/dist/config/validateCompletionContract.js +5 -0
- package/dist/config/yamlDialect.d.ts +29 -0
- package/dist/config/yamlDialect.js +327 -0
- package/dist/envelope/payloadSchema.d.ts +37 -5
- package/dist/envelope/payloadSchema.js +272 -22
- package/dist/mcp/catalogTools.js +1 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/pipelineDagSnapshot.js +2 -0
- package/dist/runstore/port.d.ts +1 -0
- package/dist/runtime/completionCheckRunner.d.ts +3 -2
- package/dist/runtime/completionCheckRunner.js +3 -2
- package/dist/runtime/envelopeRouting.d.ts +2 -2
- package/dist/runtime/pipelineRunner.d.ts +3 -0
- package/dist/runtime/pipelineRunner.js +23 -3
- package/dist/runtime/runManager.js +3 -2
- package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
- package/dist/runtime/stageAttemptBootstrap.js +38 -2
- package/dist/runtime/stageRunner.d.ts +2 -2
- package/dist/runtime/taskInput.d.ts +7 -0
- package/dist/runtime/taskInput.js +60 -0
- package/dist/runtime/verifiedStageExecution.d.ts +2 -0
- package/dist/runtime/verifiedStageExecution.js +13 -2
- package/dist/server/http.js +23 -4
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/types/completion.d.ts +6 -0
- package/dist/types/pipeline.d.ts +23 -2
- package/dist/types/preEmitCheck.d.ts +4 -6
- package/dist/types/stage.d.ts +24 -4
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/types/task.d.ts +1 -0
- package/dist/ui/assets/index-GtyiWiZg.js +118 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
- package/skills/stageflow-author/references/catalog-mapping.md +80 -17
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/SKILL.md +1 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
- package/skills/stageflow-session-capture/references/catalog-authoring.md +6 -3
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
|
@@ -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
|
-
|
|
67
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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>;
|