stageflow 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/activity.d.ts +3 -0
- package/dist/agent/claudeAdapter.js +84 -7
- package/dist/agent/claudeSession.d.ts +3 -0
- package/dist/agent/piAdapter.d.ts +2 -0
- package/dist/agent/piAdapter.js +34 -7
- package/dist/agent/port.d.ts +5 -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 +58 -26
- 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 +11 -0
- package/dist/runstore/runProjection.js +9 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +27 -4
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +2 -0
- package/dist/runstore/stageExecution.d.ts +3 -0
- package/dist/runstore/stageExecution.js +6 -2
- package/dist/runstore/stageSnapshot.js +11 -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/stageRunner.js +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/types/usage.d.ts +16 -0
- package/dist/types/usage.js +24 -0
- package/dist/ui/assets/{index-uVHiae8s.css → index-D2Wt8hwH.css} +1 -1
- package/dist/ui/assets/index-XWJovnp7.js +118 -0
- package/dist/ui/index.html +2 -2
- 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
- package/dist/ui/assets/index-Yzh42vhf.js +0 -118
|
@@ -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,
|
package/dist/config/loadTask.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
3
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
4
7
|
export function parseTaskFile(raw, source = "task") {
|
|
5
8
|
const record = raw;
|
|
6
9
|
if (typeof record?.id !== "string" || typeof record?.goal !== "string") {
|
|
@@ -13,12 +16,23 @@ export function parseTaskFile(raw, source = "task") {
|
|
|
13
16
|
},
|
|
14
17
|
]);
|
|
15
18
|
}
|
|
19
|
+
if (record.input !== undefined && !isPlainObject(record.input)) {
|
|
20
|
+
return loadFailure([
|
|
21
|
+
{
|
|
22
|
+
code: "task.invalid_shape",
|
|
23
|
+
message: `Invalid ${source}: input must be an object`,
|
|
24
|
+
category: "task",
|
|
25
|
+
taskId: record.id,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
16
29
|
return loadSuccess({
|
|
17
30
|
id: record.id,
|
|
18
31
|
goal: record.goal,
|
|
19
32
|
context: typeof record.context === "string" ? record.context : undefined,
|
|
20
33
|
constraints: typeof record.constraints === "string" ? record.constraints : undefined,
|
|
21
34
|
checkout: typeof record.checkout === "string" ? record.checkout : undefined,
|
|
35
|
+
...(record.input !== undefined ? { input: record.input } : {}),
|
|
22
36
|
});
|
|
23
37
|
}
|
|
24
38
|
export function loadTaskFromYamlOutcome(yamlText, source = "task yaml") {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LoadedStageConfig, StageConfig } from "../types/stage.js";
|
|
2
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
export declare function materializeStageModels(stages: StageConfig[], options: {
|
|
4
|
+
pipelineModel?: string;
|
|
5
|
+
pipelineId: string;
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
}): Promise<LoadOutcome<LoadedStageConfig[]>>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
|
+
import { loadStageflowManifestOutcome, manifestPathForProject, } from "./loadStageflowManifest.js";
|
|
4
|
+
import { globalModelFromManifest, resolveModelOutcome } from "./resolveModel.js";
|
|
5
|
+
export async function materializeStageModels(stages, options) {
|
|
6
|
+
let globalModel;
|
|
7
|
+
const manifestPath = manifestPathForProject(options.projectRoot);
|
|
8
|
+
let manifestExists = true;
|
|
9
|
+
try {
|
|
10
|
+
await access(manifestPath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
manifestExists = false;
|
|
14
|
+
}
|
|
15
|
+
if (manifestExists) {
|
|
16
|
+
const manifestOutcome = await loadStageflowManifestOutcome(options.projectRoot);
|
|
17
|
+
if (!manifestOutcome.ok) {
|
|
18
|
+
return loadFailure(manifestOutcome.issues);
|
|
19
|
+
}
|
|
20
|
+
globalModel = globalModelFromManifest(manifestOutcome.value);
|
|
21
|
+
}
|
|
22
|
+
const materialized = [];
|
|
23
|
+
for (const stage of stages) {
|
|
24
|
+
const modelOutcome = resolveModelOutcome({
|
|
25
|
+
stage: stage.model,
|
|
26
|
+
pipeline: options.pipelineModel,
|
|
27
|
+
global: globalModel,
|
|
28
|
+
}, { stageId: stage.id, pipelineId: options.pipelineId });
|
|
29
|
+
if (!modelOutcome.ok) {
|
|
30
|
+
return loadFailure(modelOutcome.issues);
|
|
31
|
+
}
|
|
32
|
+
materialized.push({ ...stage, model: modelOutcome.value });
|
|
33
|
+
}
|
|
34
|
+
return loadSuccess(materialized);
|
|
35
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PayloadSchemaMap } from "../envelope/payloadSchema.js";
|
|
2
|
+
import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
|
|
2
3
|
export type RawMergedEntry = {
|
|
3
4
|
raw: unknown;
|
|
4
5
|
declaringPath: string;
|
|
@@ -7,4 +8,7 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
|
|
|
7
8
|
entries: RawMergedEntry[];
|
|
8
9
|
pipelineId: string;
|
|
9
10
|
agent?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
schemas?: PayloadSchemaMap;
|
|
13
|
+
warnings: LoadIssue[];
|
|
10
14
|
}>>;
|