stageflow 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/piAdapter.js +5 -2
- package/dist/agent/port.d.ts +2 -2
- 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 +150 -8
- package/dist/config/loadStage.d.ts +8 -1
- package/dist/config/loadStage.js +138 -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 +41 -2
- 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 +14 -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/validateCatalog.d.ts +5 -1
- package/dist/config/validateCatalog.js +36 -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/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/stageRunner.d.ts +2 -2
- package/dist/runtime/taskInput.d.ts +7 -0
- package/dist/runtime/taskInput.js +60 -0
- package/dist/runtime/verifiedStageExecution.js +3 -0
- package/dist/types/completion.d.ts +6 -0
- package/dist/types/pipeline.d.ts +21 -2
- package/dist/types/preEmitCheck.d.ts +4 -6
- package/dist/types/stage.d.ts +22 -3
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/types/task.d.ts +1 -0
- package/dist/ui/assets/{index-Yzh42vhf.js → index-GtyiWiZg.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/skills/stageflow-author/SKILL.md +7 -7
- 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 +30 -28
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-author/references/stage-prompt-template.md +5 -5
- 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 +2 -2
|
@@ -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
|
{
|
|
@@ -105,13 +206,53 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
105
206
|
stages.push(stage);
|
|
106
207
|
stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
|
|
107
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;
|
|
108
237
|
const pipeline = {
|
|
109
238
|
id: pipelineId,
|
|
110
239
|
stages: stageIds,
|
|
111
240
|
...(pipelineAgent !== undefined ? { agent: pipelineAgent } : {}),
|
|
241
|
+
...(pipelineModel !== undefined ? { model: pipelineModel } : {}),
|
|
242
|
+
...(pipelineSchemas !== undefined ? { schemas: pipelineSchemas } : {}),
|
|
112
243
|
};
|
|
113
244
|
const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
114
|
-
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) {
|
|
115
256
|
const completion = nodeById.get(stage.id)?.completion;
|
|
116
257
|
const completionOutcome = validateCompletionContractForStage(stage, completion);
|
|
117
258
|
if (!completionOutcome.ok)
|
|
@@ -119,14 +260,15 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
119
260
|
}
|
|
120
261
|
return loadSuccess({
|
|
121
262
|
pipeline,
|
|
122
|
-
stages,
|
|
263
|
+
stages: loadedStages,
|
|
123
264
|
dag,
|
|
124
265
|
pipelinePath: normalizedPipelinePath,
|
|
125
266
|
stageSources,
|
|
126
|
-
});
|
|
267
|
+
}, warnings);
|
|
127
268
|
}
|
|
128
269
|
export async function loadPipelineOutcome(nameOrPath, options = {}) {
|
|
129
270
|
const cwd = options.cwd ?? process.cwd();
|
|
271
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
130
272
|
let pipelinePath;
|
|
131
273
|
try {
|
|
132
274
|
pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
|
|
@@ -141,7 +283,7 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
|
|
|
141
283
|
},
|
|
142
284
|
]);
|
|
143
285
|
}
|
|
144
|
-
return loadPipelineFromPath(pipelinePath);
|
|
286
|
+
return loadPipelineFromPath(pipelinePath, cwd, projectRoot);
|
|
145
287
|
}
|
|
146
288
|
export async function loadPipeline(nameOrPath, options = {}) {
|
|
147
289
|
const outcome = await loadPipelineOutcome(nameOrPath, options);
|
|
@@ -1,9 +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;
|
|
3
6
|
export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
|
|
7
|
+
export type LoadStageOptions = {
|
|
8
|
+
deferSchemaRefs?: boolean;
|
|
9
|
+
};
|
|
4
10
|
export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
|
|
5
11
|
entryId: string;
|
|
6
12
|
declaringPath: string;
|
|
13
|
+
deferSchemaRefs?: boolean;
|
|
7
14
|
}): LoadOutcome<StageConfig>;
|
|
8
|
-
export declare function loadStageOutcome(filePath: string): Promise<LoadOutcome<StageConfig>>;
|
|
15
|
+
export declare function loadStageOutcome(filePath: string, options?: LoadStageOptions): Promise<LoadOutcome<StageConfig>>;
|
|
9
16
|
export declare function loadStage(filePath: string): Promise<StageConfig>;
|
package/dist/config/loadStage.js
CHANGED
|
@@ -2,10 +2,18 @@ import { parseAgentField } from "../agent/agentBackend.js";
|
|
|
2
2
|
import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
|
|
3
3
|
import { CLONE_ACTIONS } from "../types/forkChoice.js";
|
|
4
4
|
import { STAGE_GATE_KINDS, } from "../types/stage.js";
|
|
5
|
-
import { compilePayloadSchema } from "../envelope/payloadSchema.js";
|
|
5
|
+
import { compilePayloadSchema, UnresolvedSchemaRefError } from "../envelope/payloadSchema.js";
|
|
6
6
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
7
|
+
import { parseModelField } from "./modelField.js";
|
|
8
|
+
import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
|
|
7
9
|
import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
|
|
8
10
|
import { readYamlObject } from "./readYamlObject.js";
|
|
11
|
+
import { applyCompiledBody, classifyYamlDocument, compileTargetContract, dialectFromKeys, mixedDialectIssue, STAGE_FILE_WIRING_KEYS, } from "./yamlDialect.js";
|
|
12
|
+
const afterCompletionByStage = new WeakMap();
|
|
13
|
+
/** After-phase IR from target `verify` (`when` includes after). Stamped onto DAG `completion`. */
|
|
14
|
+
export function afterCompletionForStage(stage) {
|
|
15
|
+
return afterCompletionByStage.get(stage);
|
|
16
|
+
}
|
|
9
17
|
function isGateKind(value) {
|
|
10
18
|
return STAGE_GATE_KINDS.includes(value);
|
|
11
19
|
}
|
|
@@ -154,13 +162,14 @@ export function parseStageMcp(raw, label, stageId) {
|
|
|
154
162
|
}
|
|
155
163
|
return loadSuccess(names);
|
|
156
164
|
}
|
|
157
|
-
function parseStageFields(raw, label, entryId) {
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
function parseStageFields(raw, label, entryId, deferSchemaRefs) {
|
|
166
|
+
// Reads IR field names. Target YAML must already be compiled via
|
|
167
|
+
// applyCompiledBody; legacy YAML uses these keys as authoring.
|
|
168
|
+
if (typeof raw.system_prompt !== "string") {
|
|
160
169
|
return loadFailure([
|
|
161
170
|
{
|
|
162
171
|
code: "stage.invalid_shape",
|
|
163
|
-
message: `Invalid stage ${label}: system_prompt
|
|
172
|
+
message: `Invalid stage ${label}: system_prompt is a required string`,
|
|
164
173
|
category: "stage",
|
|
165
174
|
stageId: entryId,
|
|
166
175
|
},
|
|
@@ -169,8 +178,21 @@ function parseStageFields(raw, label, entryId) {
|
|
|
169
178
|
const stage = {
|
|
170
179
|
id: entryId,
|
|
171
180
|
system_prompt: raw.system_prompt,
|
|
172
|
-
model: raw.model,
|
|
173
181
|
};
|
|
182
|
+
const modelField = parseModelField(raw.model);
|
|
183
|
+
if (!modelField.ok) {
|
|
184
|
+
return loadFailure([
|
|
185
|
+
{
|
|
186
|
+
code: "stage.invalid_model",
|
|
187
|
+
message: `Invalid stage ${label}: ${modelField.message}`,
|
|
188
|
+
category: "stage",
|
|
189
|
+
stageId: entryId,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
if (modelField.value !== undefined) {
|
|
194
|
+
stage.model = modelField.value;
|
|
195
|
+
}
|
|
174
196
|
if (raw.payload_schema !== undefined) {
|
|
175
197
|
if (raw.payload_schema === null ||
|
|
176
198
|
typeof raw.payload_schema !== "object" ||
|
|
@@ -188,15 +210,29 @@ function parseStageFields(raw, label, entryId) {
|
|
|
188
210
|
compilePayloadSchema(raw.payload_schema);
|
|
189
211
|
}
|
|
190
212
|
catch (err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
if (err instanceof UnresolvedSchemaRefError) {
|
|
214
|
+
if (!deferSchemaRefs) {
|
|
215
|
+
return loadFailure([
|
|
216
|
+
{
|
|
217
|
+
code: "stage.unresolved_schema_ref",
|
|
218
|
+
message: `Invalid stage ${label}: ${err.message}`,
|
|
219
|
+
category: "stage",
|
|
220
|
+
stageId: entryId,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
return loadFailure([
|
|
228
|
+
{
|
|
229
|
+
code: "stage.invalid_payload_schema",
|
|
230
|
+
message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
|
|
231
|
+
category: "stage",
|
|
232
|
+
stageId: entryId,
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
200
236
|
}
|
|
201
237
|
stage.payload_schema = raw.payload_schema;
|
|
202
238
|
}
|
|
@@ -217,15 +253,29 @@ function parseStageFields(raw, label, entryId) {
|
|
|
217
253
|
compilePayloadSchema(raw.clone_input_schema);
|
|
218
254
|
}
|
|
219
255
|
catch (err) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
256
|
+
if (err instanceof UnresolvedSchemaRefError) {
|
|
257
|
+
if (!deferSchemaRefs) {
|
|
258
|
+
return loadFailure([
|
|
259
|
+
{
|
|
260
|
+
code: "stage.unresolved_schema_ref",
|
|
261
|
+
message: `Invalid stage ${label}: ${err.message}`,
|
|
262
|
+
category: "stage",
|
|
263
|
+
stageId: entryId,
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
270
|
+
return loadFailure([
|
|
271
|
+
{
|
|
272
|
+
code: "stage.invalid_clone_input_schema",
|
|
273
|
+
message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
|
|
274
|
+
category: "stage",
|
|
275
|
+
stageId: entryId,
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
229
279
|
}
|
|
230
280
|
stage.clone_input_schema = raw.clone_input_schema;
|
|
231
281
|
}
|
|
@@ -310,9 +360,28 @@ function parseStageFields(raw, label, entryId) {
|
|
|
310
360
|
}
|
|
311
361
|
export function loadStageFromObjectOutcome(raw, ctx) {
|
|
312
362
|
const label = `${ctx.entryId} (${ctx.declaringPath})`;
|
|
313
|
-
|
|
363
|
+
const deferSchemaRefs = ctx.deferSchemaRefs === true;
|
|
364
|
+
const dialect = dialectFromKeys(Object.keys(raw));
|
|
365
|
+
if (dialect === "invalid") {
|
|
366
|
+
return loadFailure([mixedDialectIssue()]);
|
|
367
|
+
}
|
|
368
|
+
const rejected = legacyAuthoringRejected(dialect, label, presentLegacyKeys(Object.keys(raw)));
|
|
369
|
+
if (rejected)
|
|
370
|
+
return loadFailure([rejected]);
|
|
371
|
+
if (dialect === "target") {
|
|
372
|
+
const compiled = compileTargetContract(raw, {
|
|
373
|
+
stageId: ctx.entryId,
|
|
374
|
+
label,
|
|
375
|
+
category: "stage",
|
|
376
|
+
deferSchemaRefs,
|
|
377
|
+
});
|
|
378
|
+
if (!compiled.ok)
|
|
379
|
+
return compiled;
|
|
380
|
+
return parseStageFields(applyCompiledBody(raw, compiled.value), label, ctx.entryId, deferSchemaRefs);
|
|
381
|
+
}
|
|
382
|
+
return parseStageFields(raw, label, ctx.entryId, deferSchemaRefs);
|
|
314
383
|
}
|
|
315
|
-
export async function loadStageOutcome(filePath) {
|
|
384
|
+
export async function loadStageOutcome(filePath, options = {}) {
|
|
316
385
|
let raw;
|
|
317
386
|
try {
|
|
318
387
|
raw = await readYamlObject(filePath);
|
|
@@ -331,12 +400,47 @@ export async function loadStageOutcome(filePath) {
|
|
|
331
400
|
return loadFailure([
|
|
332
401
|
{
|
|
333
402
|
code: "stage.invalid_shape",
|
|
334
|
-
message: `Invalid stage file ${filePath}: id
|
|
403
|
+
message: `Invalid stage file ${filePath}: id and system_prompt are required strings`,
|
|
335
404
|
category: "stage",
|
|
336
405
|
},
|
|
337
406
|
]);
|
|
338
407
|
}
|
|
339
|
-
const
|
|
408
|
+
const dialect = classifyYamlDocument(raw);
|
|
409
|
+
if (dialect === "invalid") {
|
|
410
|
+
return loadFailure([mixedDialectIssue()]);
|
|
411
|
+
}
|
|
412
|
+
const rejected = legacyAuthoringRejected(dialect, filePath, presentLegacyKeys(Object.keys(raw)));
|
|
413
|
+
if (rejected)
|
|
414
|
+
return loadFailure([rejected]);
|
|
415
|
+
if (dialect === "target") {
|
|
416
|
+
const wiring = STAGE_FILE_WIRING_KEYS.find((key) => raw[key] !== undefined);
|
|
417
|
+
if (wiring) {
|
|
418
|
+
return loadFailure([
|
|
419
|
+
{
|
|
420
|
+
code: "stage.invalid_shape",
|
|
421
|
+
message: `Invalid stage file ${filePath}: new-dialect stage files must not declare wiring key "${wiring}"`,
|
|
422
|
+
category: "stage",
|
|
423
|
+
stageId: raw.id,
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
let parseRaw = raw;
|
|
429
|
+
let afterCompletion;
|
|
430
|
+
const deferSchemaRefs = options.deferSchemaRefs === true;
|
|
431
|
+
if (dialect === "target") {
|
|
432
|
+
const compiled = compileTargetContract(raw, {
|
|
433
|
+
stageId: raw.id,
|
|
434
|
+
label: `file ${filePath}`,
|
|
435
|
+
category: "stage",
|
|
436
|
+
deferSchemaRefs,
|
|
437
|
+
});
|
|
438
|
+
if (!compiled.ok)
|
|
439
|
+
return compiled;
|
|
440
|
+
parseRaw = applyCompiledBody(raw, compiled.value);
|
|
441
|
+
afterCompletion = compiled.value.completion;
|
|
442
|
+
}
|
|
443
|
+
const outcome = parseStageFields(parseRaw, `file ${filePath}`, raw.id, deferSchemaRefs);
|
|
340
444
|
if (!outcome.ok)
|
|
341
445
|
return outcome;
|
|
342
446
|
if (outcome.value.id !== raw.id) {
|
|
@@ -349,7 +453,12 @@ export async function loadStageOutcome(filePath) {
|
|
|
349
453
|
},
|
|
350
454
|
]);
|
|
351
455
|
}
|
|
352
|
-
|
|
456
|
+
if (afterCompletion)
|
|
457
|
+
afterCompletionByStage.set(outcome.value, afterCompletion);
|
|
458
|
+
const warning = allowLegacyYamlAuthoring()
|
|
459
|
+
? dialectWarningForDocument(raw, filePath)
|
|
460
|
+
: undefined;
|
|
461
|
+
return loadSuccess(outcome.value, warning ? [warning] : undefined);
|
|
353
462
|
}
|
|
354
463
|
export async function loadStage(filePath) {
|
|
355
464
|
const outcome = await loadStageOutcome(filePath);
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { parseAgentField } from "../agent/agentBackend.js";
|
|
5
5
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
6
|
+
import { parseModelField } from "./modelField.js";
|
|
6
7
|
const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
|
|
7
8
|
const DEFAULT_TASK_PATTERN = "*.task.yaml";
|
|
8
9
|
export function manifestPathForProject(projectRoot) {
|
|
@@ -117,6 +118,14 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
117
118
|
else {
|
|
118
119
|
agent = agentField.value;
|
|
119
120
|
}
|
|
121
|
+
let model;
|
|
122
|
+
const modelField = parseModelField(record.model);
|
|
123
|
+
if (!modelField.ok) {
|
|
124
|
+
issues.push(catalogIssue("catalog.manifest_invalid", modelField.message));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
model = modelField.value;
|
|
128
|
+
}
|
|
120
129
|
if (issues.length > 0) {
|
|
121
130
|
return loadFailure(issues);
|
|
122
131
|
}
|
|
@@ -140,6 +149,7 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
140
149
|
version: 1,
|
|
141
150
|
catalog,
|
|
142
151
|
...(agent !== undefined ? { agent } : {}),
|
|
152
|
+
...(model !== undefined ? { model } : {}),
|
|
143
153
|
};
|
|
144
154
|
return loadSuccess({
|
|
145
155
|
path: manifestPath,
|