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
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { EnvelopeError } from "../types/envelope.js";
|
|
4
|
+
export class UnresolvedSchemaRefError extends Error {
|
|
5
|
+
ref;
|
|
6
|
+
constructor(path, ref) {
|
|
7
|
+
super(`${path}: unresolved $ref "${ref}"`);
|
|
8
|
+
this.name = "UnresolvedSchemaRefError";
|
|
9
|
+
this.ref = ref;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const SCHEMA_REF_PREFIX = "#/schemas/";
|
|
13
|
+
function readSchemaRefName(ref, path) {
|
|
14
|
+
if (typeof ref !== "string" || !ref.startsWith(SCHEMA_REF_PREFIX)) {
|
|
15
|
+
throw new Error(`${path}: $ref must be a JSON Pointer of the form #/schemas/<name>`);
|
|
16
|
+
}
|
|
17
|
+
const name = ref.slice(SCHEMA_REF_PREFIX.length);
|
|
18
|
+
if (!name || name.includes("/")) {
|
|
19
|
+
throw new Error(`${path}: $ref must be a JSON Pointer of the form #/schemas/<name>`);
|
|
20
|
+
}
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
function derefSchemaNode(node, path, ctx) {
|
|
24
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
25
|
+
throw new Error(`${path}: schema must be an object`);
|
|
26
|
+
}
|
|
27
|
+
const schema = node;
|
|
28
|
+
if (schema.$ref === undefined) {
|
|
29
|
+
return { schema, stack: ctx.stack };
|
|
30
|
+
}
|
|
31
|
+
const name = readSchemaRefName(schema.$ref, path);
|
|
32
|
+
if (ctx.stack.includes(name)) {
|
|
33
|
+
const cycle = [...ctx.stack, name]
|
|
34
|
+
.map((item) => `${SCHEMA_REF_PREFIX}${item}`)
|
|
35
|
+
.join(" → ");
|
|
36
|
+
throw new Error(`${path}: $ref cycle involving ${cycle}`);
|
|
37
|
+
}
|
|
38
|
+
const target = ctx.schemas?.[name];
|
|
39
|
+
if (target === undefined) {
|
|
40
|
+
throw new UnresolvedSchemaRefError(path, `${SCHEMA_REF_PREFIX}${name}`);
|
|
41
|
+
}
|
|
42
|
+
return derefSchemaNode(target, path, {
|
|
43
|
+
schemas: ctx.schemas,
|
|
44
|
+
stack: [...ctx.stack, name],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
4
47
|
function readEnum(schema, path, valueType) {
|
|
5
48
|
if (schema.enum === undefined) {
|
|
6
49
|
return undefined;
|
|
@@ -84,11 +127,9 @@ function stringOptions(schema, path) {
|
|
|
84
127
|
...(maxLength !== undefined ? { maxLength } : {}),
|
|
85
128
|
};
|
|
86
129
|
}
|
|
87
|
-
function compileNode(node, path) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
const schema = node;
|
|
130
|
+
function compileNode(node, path, ctx) {
|
|
131
|
+
const { schema, stack } = derefSchemaNode(node, path, ctx);
|
|
132
|
+
const childCtx = { schemas: ctx.schemas, stack };
|
|
92
133
|
if (typeof schema.type !== "string") {
|
|
93
134
|
throw new Error(`${path}: type is required`);
|
|
94
135
|
}
|
|
@@ -138,7 +179,7 @@ function compileNode(node, path) {
|
|
|
138
179
|
schema.minItems < 0)) {
|
|
139
180
|
throw new Error(`${path}: minItems must be a non-negative integer when present`);
|
|
140
181
|
}
|
|
141
|
-
const items = compileNode(schema.items, `${path}.items
|
|
182
|
+
const items = compileNode(schema.items, `${path}.items`, childCtx);
|
|
142
183
|
base =
|
|
143
184
|
schema.minItems !== undefined
|
|
144
185
|
? Type.Array(items, { minItems: schema.minItems })
|
|
@@ -164,7 +205,7 @@ function compileNode(node, path) {
|
|
|
164
205
|
}
|
|
165
206
|
const compiledProps = {};
|
|
166
207
|
for (const [key, value] of Object.entries(properties)) {
|
|
167
|
-
const propSchema = compileNode(value, `${path}.properties.${key}
|
|
208
|
+
const propSchema = compileNode(value, `${path}.properties.${key}`, childCtx);
|
|
168
209
|
compiledProps[key] = required.has(key)
|
|
169
210
|
? propSchema
|
|
170
211
|
: Type.Optional(propSchema);
|
|
@@ -181,28 +222,227 @@ function compileNode(node, path) {
|
|
|
181
222
|
return nullable ? Type.Union([base, Type.Null()]) : base;
|
|
182
223
|
}
|
|
183
224
|
/**
|
|
184
|
-
* Compile a JSON Schema subset
|
|
225
|
+
* Compile a JSON Schema subset for IR `payload_schema` / `clone_input_schema`
|
|
226
|
+
* (YAML: `io.output.schema` / `io.input.schema`).
|
|
185
227
|
* Supported: type object/string/number/integer/boolean/array,
|
|
186
228
|
* properties, required, items, additionalProperties (boolean),
|
|
187
229
|
* minItems, enum, minimum, maximum. String nodes also accept
|
|
188
230
|
* pattern (a JS RegExp validated with the unicode flag), minLength,
|
|
189
231
|
* and maxLength (non-negative integers). Nested nodes accept
|
|
190
232
|
* nullable (boolean), compiling to a union of that type with null;
|
|
191
|
-
* the root object cannot be nullable.
|
|
233
|
+
* the root object cannot be nullable. `$ref` is resolved against
|
|
234
|
+
* `options.schemas` using JSON Pointer `#/schemas/<name>`. Unknown
|
|
235
|
+
* keywords other than `$ref` are ignored.
|
|
192
236
|
*/
|
|
193
|
-
export function compilePayloadSchema(raw) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
raw.nullable === true) {
|
|
237
|
+
export function compilePayloadSchema(raw, options) {
|
|
238
|
+
const ctx = { schemas: options?.schemas, stack: [] };
|
|
239
|
+
const { schema: resolved } = derefSchemaNode(raw, "payload_schema", ctx);
|
|
240
|
+
if (resolved.nullable === true) {
|
|
198
241
|
throw new Error("payload_schema: root cannot be nullable");
|
|
199
242
|
}
|
|
200
|
-
const compiled = compileNode(raw, "payload_schema");
|
|
243
|
+
const compiled = compileNode(raw, "payload_schema", ctx);
|
|
201
244
|
if (!Type.IsObject(compiled)) {
|
|
202
245
|
throw new Error("payload_schema: root type must be object");
|
|
203
246
|
}
|
|
204
247
|
return compiled;
|
|
205
248
|
}
|
|
249
|
+
function isPlainSchemaObject(value) {
|
|
250
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
251
|
+
}
|
|
252
|
+
function readRequiredKeys(schema) {
|
|
253
|
+
if (schema.required === undefined)
|
|
254
|
+
return [];
|
|
255
|
+
if (!Array.isArray(schema.required) ||
|
|
256
|
+
!schema.required.every((key) => typeof key === "string")) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
return schema.required;
|
|
260
|
+
}
|
|
261
|
+
function readProperties(schema) {
|
|
262
|
+
if (!isPlainSchemaObject(schema.properties))
|
|
263
|
+
return {};
|
|
264
|
+
return schema.properties;
|
|
265
|
+
}
|
|
266
|
+
function typesCompatible(consumerType, producerType) {
|
|
267
|
+
if (consumerType === producerType)
|
|
268
|
+
return true;
|
|
269
|
+
return consumerType === "number" && producerType === "integer";
|
|
270
|
+
}
|
|
271
|
+
function readEnumValues(schema) {
|
|
272
|
+
if (!Array.isArray(schema.enum) || schema.enum.length === 0)
|
|
273
|
+
return undefined;
|
|
274
|
+
return schema.enum;
|
|
275
|
+
}
|
|
276
|
+
function enumCompatible(consumer, producer, type) {
|
|
277
|
+
if (type !== "string" && type !== "integer")
|
|
278
|
+
return true;
|
|
279
|
+
const consumerEnum = readEnumValues(consumer);
|
|
280
|
+
const producerEnum = readEnumValues(producer);
|
|
281
|
+
if (consumerEnum === undefined)
|
|
282
|
+
return true;
|
|
283
|
+
if (producerEnum === undefined)
|
|
284
|
+
return false;
|
|
285
|
+
const allowed = new Set(consumerEnum);
|
|
286
|
+
return producerEnum.every((value) => allowed.has(value));
|
|
287
|
+
}
|
|
288
|
+
function patternCompatible(consumer, producer) {
|
|
289
|
+
const consumerPattern = typeof consumer.pattern === "string" ? consumer.pattern : undefined;
|
|
290
|
+
const producerPattern = typeof producer.pattern === "string" ? producer.pattern : undefined;
|
|
291
|
+
if (consumerPattern === undefined)
|
|
292
|
+
return true;
|
|
293
|
+
if (producerPattern === undefined)
|
|
294
|
+
return false;
|
|
295
|
+
return consumerPattern === producerPattern;
|
|
296
|
+
}
|
|
297
|
+
function numericBound(value) {
|
|
298
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
299
|
+
return undefined;
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
function minBoundFits(consumerBound, producerBound) {
|
|
303
|
+
const consumer = numericBound(consumerBound);
|
|
304
|
+
if (consumer === undefined)
|
|
305
|
+
return true;
|
|
306
|
+
const producer = numericBound(producerBound);
|
|
307
|
+
if (producer === undefined)
|
|
308
|
+
return false;
|
|
309
|
+
return producer >= consumer;
|
|
310
|
+
}
|
|
311
|
+
function maxBoundFits(consumerBound, producerBound) {
|
|
312
|
+
const consumer = numericBound(consumerBound);
|
|
313
|
+
if (consumer === undefined)
|
|
314
|
+
return true;
|
|
315
|
+
const producer = numericBound(producerBound);
|
|
316
|
+
if (producer === undefined)
|
|
317
|
+
return false;
|
|
318
|
+
return producer <= consumer;
|
|
319
|
+
}
|
|
320
|
+
function isSubsetNode(consumer, producer, path, ctx) {
|
|
321
|
+
const consumerDeref = derefSchemaNode(consumer, path, ctx);
|
|
322
|
+
const producerDeref = derefSchemaNode(producer, path, ctx);
|
|
323
|
+
const consumerNode = consumerDeref.schema;
|
|
324
|
+
const producerNode = producerDeref.schema;
|
|
325
|
+
if (typeof consumerNode.type !== "string" || typeof producerNode.type !== "string") {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if (!typesCompatible(consumerNode.type, producerNode.type)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (producerNode.nullable === true && consumerNode.nullable !== true) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (consumerNode.type === "array") {
|
|
335
|
+
if (consumerNode.items === undefined || producerNode.items === undefined) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
if (!isSubsetNode(consumerNode.items, producerNode.items, `${path}.items`, {
|
|
339
|
+
schemas: ctx.schemas,
|
|
340
|
+
stack: consumerDeref.stack,
|
|
341
|
+
})) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return minBoundFits(consumerNode.minItems, producerNode.minItems);
|
|
345
|
+
}
|
|
346
|
+
if (consumerNode.type === "object") {
|
|
347
|
+
const consumerRequired = readRequiredKeys(consumerNode);
|
|
348
|
+
const producerRequired = new Set(readRequiredKeys(producerNode));
|
|
349
|
+
const producerProps = readProperties(producerNode);
|
|
350
|
+
const consumerProps = readProperties(consumerNode);
|
|
351
|
+
for (const key of consumerRequired) {
|
|
352
|
+
if (!(key in producerProps) || !producerRequired.has(key)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const [key, consumerProp] of Object.entries(consumerProps)) {
|
|
357
|
+
if (producerProps[key] === undefined)
|
|
358
|
+
continue;
|
|
359
|
+
if (!isSubsetNode(consumerProp, producerProps[key], `${path}.properties.${key}`, {
|
|
360
|
+
schemas: ctx.schemas,
|
|
361
|
+
stack: consumerDeref.stack,
|
|
362
|
+
})) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (consumerNode.additionalProperties === false) {
|
|
367
|
+
if (producerNode.additionalProperties !== false) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
for (const key of Object.keys(producerProps)) {
|
|
371
|
+
if (!(key in consumerProps)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (!enumCompatible(consumerNode, producerNode, consumerNode.type)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
if (consumerNode.type === "string") {
|
|
382
|
+
if (!patternCompatible(consumerNode, producerNode))
|
|
383
|
+
return false;
|
|
384
|
+
if (!minBoundFits(consumerNode.minLength, producerNode.minLength))
|
|
385
|
+
return false;
|
|
386
|
+
if (!maxBoundFits(consumerNode.maxLength, producerNode.maxLength))
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (consumerNode.type === "number" || consumerNode.type === "integer") {
|
|
390
|
+
if (!minBoundFits(consumerNode.minimum, producerNode.minimum))
|
|
391
|
+
return false;
|
|
392
|
+
if (!maxBoundFits(consumerNode.maximum, producerNode.maximum))
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* After `$ref` resolve, true when every success payload that satisfies the
|
|
399
|
+
* producer schema also satisfies the consumer schema (producer output
|
|
400
|
+
* assignable to consumer input). Extra producer fields are allowed unless
|
|
401
|
+
* the consumer sets `additionalProperties: false`, which also requires the
|
|
402
|
+
* producer to close extras. A number consumer accepts an integer producer;
|
|
403
|
+
* the reverse is incompatible. Producer enums must be subsets of consumer
|
|
404
|
+
* enums; a consumer enum with an unconstrained producer is incompatible.
|
|
405
|
+
* String `pattern` values must be identical when the consumer sets one.
|
|
406
|
+
* Producer `minimum`/`maximum`, `minLength`/`maxLength`, and array `minItems`
|
|
407
|
+
* must fit inside the consumer bounds (a consumer bound with an omitted
|
|
408
|
+
* producer bound is incompatible). A nullable producer is incompatible unless
|
|
409
|
+
* the consumer is also nullable.
|
|
410
|
+
*/
|
|
411
|
+
export function isPayloadSchemaSubset(consumer, producer, options) {
|
|
412
|
+
return isSubsetNode(consumer, producer, "payload_schema", {
|
|
413
|
+
schemas: options?.schemas,
|
|
414
|
+
stack: [],
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
export function expandPayloadSchemaRefs(raw, options) {
|
|
418
|
+
return expandSchemaNode(raw, "payload_schema", {
|
|
419
|
+
schemas: options?.schemas,
|
|
420
|
+
stack: [],
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function expandSchemaNode(node, path, ctx) {
|
|
424
|
+
const { schema, stack } = derefSchemaNode(node, path, ctx);
|
|
425
|
+
const childCtx = { schemas: ctx.schemas, stack };
|
|
426
|
+
const clone = {};
|
|
427
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
428
|
+
if (key === "$ref")
|
|
429
|
+
continue;
|
|
430
|
+
if (key === "properties" && isPlainSchemaObject(value)) {
|
|
431
|
+
const props = {};
|
|
432
|
+
for (const [pk, pv] of Object.entries(value)) {
|
|
433
|
+
props[pk] = expandSchemaNode(pv, `${path}.properties.${pk}`, childCtx);
|
|
434
|
+
}
|
|
435
|
+
clone.properties = props;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (key === "items") {
|
|
439
|
+
clone.items = expandSchemaNode(value, `${path}.items`, childCtx);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
clone[key] = value;
|
|
443
|
+
}
|
|
444
|
+
return clone;
|
|
445
|
+
}
|
|
206
446
|
function formatPayloadInstancePath(instancePath) {
|
|
207
447
|
const translated = instancePath.replace(/\//g, ".").replace(/^\./, "");
|
|
208
448
|
return translated ? `payload.${translated}` : "payload";
|
|
@@ -212,14 +452,20 @@ function payloadSchemaMismatchDetails(schema, payload) {
|
|
|
212
452
|
.map((err) => `${formatPayloadInstancePath(err.instancePath)} ${err.message}`)
|
|
213
453
|
.join("; ");
|
|
214
454
|
}
|
|
215
|
-
export function
|
|
455
|
+
export function payloadInstanceMismatch(instance, payloadSchema, options) {
|
|
456
|
+
const schema = compilePayloadSchema(payloadSchema, options);
|
|
457
|
+
if (Value.Check(schema, instance))
|
|
458
|
+
return undefined;
|
|
459
|
+
return payloadSchemaMismatchDetails(schema, instance);
|
|
460
|
+
}
|
|
461
|
+
export function assertEnvelopePayload(envelope, payloadSchema, options) {
|
|
216
462
|
if (payloadSchema === undefined) {
|
|
217
463
|
return;
|
|
218
464
|
}
|
|
219
465
|
if (envelope.status !== "success") {
|
|
220
466
|
return;
|
|
221
467
|
}
|
|
222
|
-
const schema = compilePayloadSchema(payloadSchema);
|
|
468
|
+
const schema = compilePayloadSchema(payloadSchema, options);
|
|
223
469
|
if (envelope.payload === undefined) {
|
|
224
470
|
throw new EnvelopeError("payload is required by stage payload_schema");
|
|
225
471
|
}
|
|
@@ -230,19 +476,23 @@ export function assertEnvelopePayload(envelope, payloadSchema) {
|
|
|
230
476
|
: "payload does not match payload_schema");
|
|
231
477
|
}
|
|
232
478
|
}
|
|
233
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Validate clone/fan-out assignment against the successor's IR
|
|
481
|
+
* `clone_input_schema` (YAML: `io.input.schema`).
|
|
482
|
+
*/
|
|
483
|
+
export function assertCloneAssignmentPayload(envelope, cloneInputSchema, successorId, itemPath = "", options) {
|
|
234
484
|
if (envelope.status !== "success") {
|
|
235
485
|
return;
|
|
236
486
|
}
|
|
237
|
-
const schema = compilePayloadSchema(cloneInputSchema);
|
|
487
|
+
const schema = compilePayloadSchema(cloneInputSchema, options);
|
|
238
488
|
const pathLead = itemPath ? `${itemPath}: ` : "";
|
|
239
489
|
if (envelope.payload === undefined) {
|
|
240
|
-
throw new EnvelopeError(`${pathLead}clone assignment payload is required by
|
|
490
|
+
throw new EnvelopeError(`${pathLead}clone assignment payload is required by io.input.schema for ${successorId}`);
|
|
241
491
|
}
|
|
242
492
|
if (!Value.Check(schema, envelope.payload)) {
|
|
243
493
|
const details = payloadSchemaMismatchDetails(schema, envelope.payload);
|
|
244
494
|
throw new EnvelopeError(details
|
|
245
|
-
? `${pathLead}clone assignment payload does not match
|
|
246
|
-
: `${pathLead}clone assignment payload does not match
|
|
495
|
+
? `${pathLead}clone assignment payload does not match io.input.schema for ${successorId}: ${details}`
|
|
496
|
+
: `${pathLead}clone assignment payload does not match io.input.schema for ${successorId}`);
|
|
247
497
|
}
|
|
248
498
|
}
|
package/dist/mcp/catalogTools.js
CHANGED
package/dist/package-meta.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.15.0";
|
package/dist/package-meta.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export const PACKAGE_VERSION = "0.
|
|
2
|
+
export const PACKAGE_VERSION = "0.15.0";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { hydrateResolvedNeeds } from "../config/pipelineNeeds.js";
|
|
2
2
|
import { mintCloneInstanceIds } from "./stageInstanceId.js";
|
|
3
3
|
export function buildPipelineDagSnapshotFromLoaded(loaded) {
|
|
4
|
+
// Snapshot JSON keeps IR keys (`completion`, `recovery`, `clone_input_schema`).
|
|
5
|
+
// Do not rewrite these from target YAML names. Resume hydrates this shape.
|
|
4
6
|
const gate_kinds = {};
|
|
5
7
|
const clone_input_schema = {};
|
|
6
8
|
for (const stage of loaded.stages) {
|
package/dist/runstore/port.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type RunStatus = "created" | "running" | "succeeded" | "failed";
|
|
|
8
8
|
export type RunPipelineDagSnapshot = ResolvedPipelineDag & {
|
|
9
9
|
stage_ids: string[];
|
|
10
10
|
gate_kinds?: Record<string, StageGateKind[]>;
|
|
11
|
+
/** IR: per-stage clone assignment schema. YAML: `io.input.schema`. */
|
|
11
12
|
clone_input_schema?: Record<string, unknown>;
|
|
12
13
|
};
|
|
13
14
|
export type StageReadiness = "blocked" | "ready" | "running" | "waiting" | "succeeded" | "failed" | "skipped";
|
|
@@ -131,7 +131,8 @@ export type CompletionVerificationResult = {
|
|
|
131
131
|
/** Default Node implementation; callers may substitute a sandboxed executor. */
|
|
132
132
|
export declare const nodeCommandExecutor: CommandExecutor;
|
|
133
133
|
/**
|
|
134
|
-
* Run every
|
|
135
|
-
* or prevents later checks from producing their own
|
|
134
|
+
* Run every after-phase IR check in declaration order. YAML: `verify` when after.
|
|
135
|
+
* A failed check never throws or prevents later checks from producing their own
|
|
136
|
+
* repair evidence.
|
|
136
137
|
*/
|
|
137
138
|
export declare function runCompletionContract(input: CompletionCheckRunnerInput): Promise<CompletionVerificationResult>;
|
|
@@ -343,8 +343,9 @@ async function runCheck(check, input) {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
/**
|
|
346
|
-
* Run every
|
|
347
|
-
* or prevents later checks from producing their own
|
|
346
|
+
* Run every after-phase IR check in declaration order. YAML: `verify` when after.
|
|
347
|
+
* A failed check never throws or prevents later checks from producing their own
|
|
348
|
+
* repair evidence.
|
|
348
349
|
*/
|
|
349
350
|
export async function runCompletionContract(input) {
|
|
350
351
|
const checks = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { RunStore, StageSnapshot } from "../runstore/port.js";
|
|
2
2
|
import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
|
|
3
3
|
import type { LoadedPipeline, ResolvedPipelineDag } from "../types/pipeline.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { LoadedStageConfig } from "../types/stage.js";
|
|
5
5
|
export declare function buildCompletedEnvelopesFromRun(store: RunStore, runId: string, stages?: StageSnapshot[], dag?: ResolvedPipelineDag): Promise<Map<string, StageEnvelope>>;
|
|
6
|
-
export declare function buildStageConfigById(loaded: LoadedPipeline): Map<string,
|
|
6
|
+
export declare function buildStageConfigById(loaded: LoadedPipeline): Map<string, LoadedStageConfig>;
|
|
7
7
|
export type ResolvePriorEnvelopeResult = {
|
|
8
8
|
ok: true;
|
|
9
9
|
prior: StageEnvelope | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentPort } from "../agent/port.js";
|
|
2
|
+
import { type ValidationFinding } from "../config/validateCatalog.js";
|
|
2
3
|
import type { RunStore } from "../runstore/port.js";
|
|
3
4
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
4
5
|
import type { StageHitlController } from "./stageHitl.js";
|
|
@@ -15,6 +16,7 @@ export type PipelineRunResult = {
|
|
|
15
16
|
runDir: string;
|
|
16
17
|
runId: string;
|
|
17
18
|
reason?: string;
|
|
19
|
+
findings?: ValidationFinding[];
|
|
18
20
|
};
|
|
19
21
|
export type StartedPipeline = {
|
|
20
22
|
runId: string;
|
|
@@ -38,6 +40,7 @@ export type PreparedPipeline = {
|
|
|
38
40
|
stageProcessLauncher?: StageProcessLauncher;
|
|
39
41
|
operatorCatalog?: OperatorCatalog;
|
|
40
42
|
skipGates?: boolean;
|
|
43
|
+
findings?: ValidationFinding[];
|
|
41
44
|
};
|
|
42
45
|
export type ExecuteStagesOptions = {
|
|
43
46
|
maxActiveStagesPerRun?: number;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { loadPipelineValidated } from "../config/validateCatalog.js";
|
|
3
|
+
import { buildValidationResult, loadPipelineValidated, } from "../config/validateCatalog.js";
|
|
4
4
|
import { loadTaskFromYaml } from "../config/loadTask.js";
|
|
5
|
-
import { buildValidationResult } from "../config/validateCatalog.js";
|
|
6
5
|
import { resolveAndValidateCheckout } from "./stageRoots.js";
|
|
7
6
|
import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
|
|
8
7
|
import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
|
|
@@ -10,6 +9,7 @@ import { runPipelineDag } from "./pipelineScheduler.js";
|
|
|
10
9
|
import { readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
11
10
|
import { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
12
11
|
import { PipelineValidationError } from "./pipelineValidationError.js";
|
|
12
|
+
import { checkTaskEntryInput } from "./taskInput.js";
|
|
13
13
|
export { PipelineValidationError } from "./pipelineValidationError.js";
|
|
14
14
|
let defaultStageProcessLauncher;
|
|
15
15
|
function resolveStageProcessLauncher(executionMode, override) {
|
|
@@ -27,6 +27,7 @@ function resolveStageProcessLauncher(executionMode, override) {
|
|
|
27
27
|
async function preparePipeline(options) {
|
|
28
28
|
const loadResult = await loadPipelineValidated(options.pipeline, {
|
|
29
29
|
cwd: options.cwd,
|
|
30
|
+
projectRoot: options.projectRoot ?? options.cwd,
|
|
30
31
|
validateStages: true,
|
|
31
32
|
});
|
|
32
33
|
if (!loadResult.ok) {
|
|
@@ -41,6 +42,20 @@ async function preparePipeline(options) {
|
|
|
41
42
|
})());
|
|
42
43
|
const label = options.taskPath ?? "task.yaml";
|
|
43
44
|
const task = loadTaskFromYaml(taskYaml, `task file ${label}`);
|
|
45
|
+
const pairing = checkTaskEntryInput(task, loaded, {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
taskPath: options.taskPath,
|
|
48
|
+
});
|
|
49
|
+
if (pairing.some((finding) => finding.severity === "error")) {
|
|
50
|
+
throw new PipelineValidationError(buildValidationResult("pipeline", pairing, false));
|
|
51
|
+
}
|
|
52
|
+
const warningFindings = [
|
|
53
|
+
...loadResult.findings.filter((finding) => finding.severity === "warning"),
|
|
54
|
+
...pairing.filter((finding) => finding.severity === "warning"),
|
|
55
|
+
];
|
|
56
|
+
for (const finding of warningFindings) {
|
|
57
|
+
console.error(`${finding.code}: ${finding.message}`);
|
|
58
|
+
}
|
|
44
59
|
const checkoutRoot = await resolveAndValidateCheckout(task, options.checkoutOverride, options.cwd);
|
|
45
60
|
const pipelinePath = normalizeCatalogPath(loaded.pipelinePath);
|
|
46
61
|
const taskPath = options.taskPath
|
|
@@ -76,6 +91,7 @@ async function preparePipeline(options) {
|
|
|
76
91
|
stageProcessLauncher,
|
|
77
92
|
operatorCatalog: options.operatorCatalog,
|
|
78
93
|
skipGates: options.skipGates,
|
|
94
|
+
...(warningFindings.length > 0 ? { findings: warningFindings } : {}),
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
export async function executeStages(prepared, options) {
|
|
@@ -84,7 +100,7 @@ export async function executeStages(prepared, options) {
|
|
|
84
100
|
prepared.executionMode ??
|
|
85
101
|
readStageExecutionMode(process.env);
|
|
86
102
|
const stageProcessLauncher = resolveStageProcessLauncher(executionMode, options?.stageProcessLauncher ?? prepared.stageProcessLauncher);
|
|
87
|
-
|
|
103
|
+
const result = await runPipelineDag({
|
|
88
104
|
prepared,
|
|
89
105
|
maxActiveStagesPerRun,
|
|
90
106
|
resumeFromStageId: options?.resumeFromStageId,
|
|
@@ -93,6 +109,10 @@ export async function executeStages(prepared, options) {
|
|
|
93
109
|
executionMode,
|
|
94
110
|
stageProcessLauncher,
|
|
95
111
|
});
|
|
112
|
+
if (prepared.findings !== undefined && prepared.findings.length > 0) {
|
|
113
|
+
return { ...result, findings: prepared.findings };
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
96
116
|
}
|
|
97
117
|
export async function runPipeline(options) {
|
|
98
118
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -383,7 +383,7 @@ export class RunManager {
|
|
|
383
383
|
gitSha: input.gitSha,
|
|
384
384
|
ciPrUrl: input.ciPrUrl,
|
|
385
385
|
ciJobUrl: input.ciJobUrl,
|
|
386
|
-
});
|
|
386
|
+
}, undefined, resolved.kind === "path" ? resolved.taskPath : undefined);
|
|
387
387
|
}
|
|
388
388
|
async rerun(runId) {
|
|
389
389
|
const cwd = this.options.cwd ?? process.cwd();
|
|
@@ -1061,7 +1061,7 @@ export class RunManager {
|
|
|
1061
1061
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
1062
1062
|
}
|
|
1063
1063
|
}
|
|
1064
|
-
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot) {
|
|
1064
|
+
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath) {
|
|
1065
1065
|
let checkoutKey;
|
|
1066
1066
|
try {
|
|
1067
1067
|
const task = loadTaskFromYaml(taskYaml, taskLabel);
|
|
@@ -1086,6 +1086,7 @@ export class RunManager {
|
|
|
1086
1086
|
agent: this.options.agent,
|
|
1087
1087
|
store: this.options.store,
|
|
1088
1088
|
taskYaml,
|
|
1089
|
+
taskPath,
|
|
1089
1090
|
pipeline,
|
|
1090
1091
|
cwd,
|
|
1091
1092
|
projectRoot: projectRoot ?? this.projectRoot,
|
|
@@ -2,7 +2,7 @@ import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunInput, StageS
|
|
|
2
2
|
import type { RunStore } from "../runstore/port.js";
|
|
3
3
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
4
4
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { LoadedStageConfig } from "../types/stage.js";
|
|
6
6
|
import type { TaskFile } from "../types/task.js";
|
|
7
7
|
import { type StageAttemptContext } from "./stageAttemptContext.js";
|
|
8
8
|
import { type StageRoots } from "./stageRoots.js";
|
|
@@ -14,7 +14,7 @@ export type StageAttemptOpenInput = {
|
|
|
14
14
|
agent: Pick<AgentPort, "openStage">;
|
|
15
15
|
store: RunStore;
|
|
16
16
|
runId: string;
|
|
17
|
-
stage:
|
|
17
|
+
stage: LoadedStageConfig;
|
|
18
18
|
task: TaskFile;
|
|
19
19
|
dag: ResolvedPipelineDag;
|
|
20
20
|
checkoutRoot?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunResult, StageSessionMode } from "../agent/port.js";
|
|
2
2
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { LoadedStageConfig } from "../types/stage.js";
|
|
4
4
|
import type { TaskFile } from "../types/task.js";
|
|
5
5
|
import type { RunStore } from "../runstore/port.js";
|
|
6
6
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
@@ -21,7 +21,7 @@ export type RunStageOptions = {
|
|
|
21
21
|
agent: AgentPort;
|
|
22
22
|
store: RunStore;
|
|
23
23
|
runId: string;
|
|
24
|
-
stage:
|
|
24
|
+
stage: LoadedStageConfig;
|
|
25
25
|
task: TaskFile;
|
|
26
26
|
dag?: ResolvedPipelineDag;
|
|
27
27
|
priorEnvelope?: StageEnvelope | null;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ValidationFinding } from "../config/validateCatalog.js";
|
|
2
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
1
3
|
import type { TaskFile } from "../types/task.js";
|
|
2
4
|
export type StartTaskInput = {
|
|
3
5
|
task?: string | TaskFile;
|
|
@@ -12,4 +14,9 @@ export type ResolvedTaskInput = {
|
|
|
12
14
|
};
|
|
13
15
|
export declare function isTaskFile(value: unknown): value is TaskFile;
|
|
14
16
|
export declare function taskFileToYaml(task: TaskFile): string;
|
|
17
|
+
/** Validate optional `task.input` against entry-stage IR `clone_input_schema` (YAML: `io.input.schema`). */
|
|
18
|
+
export declare function checkTaskEntryInput(task: TaskFile, loaded: LoadedPipeline, options: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
taskPath?: string;
|
|
21
|
+
}): ValidationFinding[];
|
|
15
22
|
export declare function resolveStartTaskInput(input: StartTaskInput, cwd: string): ResolvedTaskInput;
|