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,6 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a pipeline `stages:` entry. Target YAML (`io` / `verify` /
|
|
3
|
+
* `on_verify_fail`) compiles onto IR `completion` / `recovery` / body
|
|
4
|
+
* `payload_schema`. Legacy YAML keys are dual-read via legacyYaml.ts.
|
|
5
|
+
*/
|
|
1
6
|
import path from "node:path";
|
|
2
7
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
8
|
import { parseStageMcp } from "./loadStage.js";
|
|
9
|
+
import { legacyAuthoringRejected, presentLegacyKeys } from "./legacyYaml.js";
|
|
10
|
+
import { applyCompiledBody, compileTargetContract, dialectFromKeys, mixedDialectIssue, } from "./yamlDialect.js";
|
|
4
11
|
import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
|
|
5
12
|
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
6
13
|
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
@@ -90,6 +97,10 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
90
97
|
]);
|
|
91
98
|
}
|
|
92
99
|
}
|
|
100
|
+
const dialect = dialectFromKeys(Object.keys(raw));
|
|
101
|
+
if (dialect === "invalid") {
|
|
102
|
+
return loadFailure([mixedDialectIssue()]);
|
|
103
|
+
}
|
|
93
104
|
const uses = typeof raw.uses === "string" ? raw.uses : undefined;
|
|
94
105
|
const hasBody = hasBodyKey(raw);
|
|
95
106
|
const skillOutcome = readSkill(raw);
|
|
@@ -138,7 +149,35 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
138
149
|
},
|
|
139
150
|
]);
|
|
140
151
|
}
|
|
141
|
-
|
|
152
|
+
let compiledBody;
|
|
153
|
+
let policyOutcome;
|
|
154
|
+
if (dialect === "target") {
|
|
155
|
+
const compiled = compileTargetContract(raw, {
|
|
156
|
+
stageId: id,
|
|
157
|
+
label: `entry at index ${index} in ${declaringPath}`,
|
|
158
|
+
category: "pipeline",
|
|
159
|
+
deferSchemaRefs: true,
|
|
160
|
+
});
|
|
161
|
+
if (!compiled.ok)
|
|
162
|
+
return compiled;
|
|
163
|
+
policyOutcome = loadSuccess({
|
|
164
|
+
...(compiled.value.completion !== undefined
|
|
165
|
+
? { completion: compiled.value.completion }
|
|
166
|
+
: {}),
|
|
167
|
+
...(compiled.value.recovery !== undefined
|
|
168
|
+
? { recovery: compiled.value.recovery }
|
|
169
|
+
: {}),
|
|
170
|
+
});
|
|
171
|
+
if (!uses) {
|
|
172
|
+
compiledBody = applyCompiledBody(extractBodyRaw(raw), compiled.value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const rejected = legacyAuthoringRejected(dialect, `stage "${id}" in ${declaringPath}`, presentLegacyKeys(Object.keys(raw)));
|
|
177
|
+
if (rejected)
|
|
178
|
+
return loadFailure([rejected]);
|
|
179
|
+
policyOutcome = parseExecutionPolicy(raw, id);
|
|
180
|
+
}
|
|
142
181
|
if (!policyOutcome.ok)
|
|
143
182
|
return policyOutcome;
|
|
144
183
|
let needs;
|
|
@@ -202,7 +241,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
202
241
|
body = { kind: "uses", path: uses, absolutePath };
|
|
203
242
|
}
|
|
204
243
|
else {
|
|
205
|
-
body = { kind: "inline", raw: extractBodyRaw(raw) };
|
|
244
|
+
body = { kind: "inline", raw: compiledBody ?? extractBodyRaw(raw) };
|
|
206
245
|
}
|
|
207
246
|
const entry = {
|
|
208
247
|
id,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
-
import { type LoadOutcome } from "./loadOutcome.js";
|
|
2
|
+
import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
|
|
3
3
|
type ExecutionPolicy = {
|
|
4
4
|
completion?: CompletionContract;
|
|
5
5
|
recovery?: RecoveryPolicy;
|
|
6
6
|
};
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function recoveryRequiresCompletionIssue(stageId: string): LoadIssue;
|
|
8
|
+
export declare function parseExecutionPolicy(raw: Record<string, unknown>, stageId: string, options?: {
|
|
9
|
+
requireCompletionForRecovery?: boolean;
|
|
10
|
+
}): LoadOutcome<ExecutionPolicy>;
|
|
8
11
|
export {};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse IR `completion` / `recovery` objects (DAG wiring after compile, or
|
|
3
|
+
* legacy YAML keys). Target YAML `on_verify_fail` / after-phase `verify` are
|
|
4
|
+
* compiled onto these shapes in yamlDialect.ts before this parser runs.
|
|
5
|
+
*/
|
|
1
6
|
import path from "node:path";
|
|
2
7
|
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
3
8
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
@@ -21,6 +26,13 @@ function hasOnlyKeys(value, allowed) {
|
|
|
21
26
|
function isPositiveInteger(value) {
|
|
22
27
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
23
28
|
}
|
|
29
|
+
export function recoveryRequiresCompletionIssue(stageId) {
|
|
30
|
+
return {
|
|
31
|
+
code: "pipeline.invalid_recovery",
|
|
32
|
+
message: `Stage "${stageId}" recovery: requires a completion contract`,
|
|
33
|
+
category: "pipeline",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
24
36
|
function isGateKind(value) {
|
|
25
37
|
return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
|
|
26
38
|
}
|
|
@@ -167,7 +179,7 @@ function parseRecovery(raw, stageId, hasCompletion) {
|
|
|
167
179
|
if (raw === undefined)
|
|
168
180
|
return loadSuccess(undefined);
|
|
169
181
|
if (!hasCompletion)
|
|
170
|
-
return
|
|
182
|
+
return loadFailure([recoveryRequiresCompletionIssue(stageId)]);
|
|
171
183
|
if (!isPlainObject(raw))
|
|
172
184
|
return failure(stageId, "recovery", "must be an object");
|
|
173
185
|
const unknown = hasOnlyKeys(raw, ["mode", "max_attempts", "retry_safety", "include_failed_checks"]);
|
|
@@ -204,11 +216,12 @@ function parseRecovery(raw, stageId, hasCompletion) {
|
|
|
204
216
|
: {}),
|
|
205
217
|
});
|
|
206
218
|
}
|
|
207
|
-
export function parseExecutionPolicy(raw, stageId) {
|
|
219
|
+
export function parseExecutionPolicy(raw, stageId, options) {
|
|
208
220
|
const completion = parseCompletion(raw.completion, stageId);
|
|
209
221
|
if (!completion.ok)
|
|
210
222
|
return completion;
|
|
211
|
-
const
|
|
223
|
+
const requireCompletion = options?.requireCompletionForRecovery ?? true;
|
|
224
|
+
const recovery = parseRecovery(raw.recovery, stageId, requireCompletion ? completion.value !== undefined : true);
|
|
212
225
|
if (!recovery.ok)
|
|
213
226
|
return recovery;
|
|
214
227
|
return loadSuccess({
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse IR `pre_emit_checks` (emit-phase). YAML `verify` emit items are compiled
|
|
3
|
+
* onto this list in yamlDialect.ts before this parser runs.
|
|
4
|
+
*/
|
|
1
5
|
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
2
6
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
7
|
function isPlainObject(value) {
|
|
@@ -41,13 +41,13 @@ export function parsePipelineNeeds(raw, stageId) {
|
|
|
41
41
|
if (!Array.isArray(raw)) {
|
|
42
42
|
return {
|
|
43
43
|
ok: false,
|
|
44
|
-
message: `stage "${stageId}": needs must be a non-empty string or
|
|
44
|
+
message: `stage "${stageId}": needs must be a non-empty string or a non-empty array`,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
if (raw.length <
|
|
47
|
+
if (raw.length < 1) {
|
|
48
48
|
return {
|
|
49
49
|
ok: false,
|
|
50
|
-
message: `stage "${stageId}": needs array must contain at least
|
|
50
|
+
message: `stage "${stageId}": needs array must contain at least one item`,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
const edges = [];
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowed keys on a pipeline stage YAML entry.
|
|
3
|
+
*
|
|
4
|
+
* Target authoring: `io`, `verify`, `on_verify_fail`.
|
|
5
|
+
* IR / legacy YAML (same strings): `payload_schema`, `pre_emit_checks`,
|
|
6
|
+
* `completion`, `recovery`, `clone_input_schema`.
|
|
7
|
+
*
|
|
8
|
+
* New catalog fields go on the target keys (and compileTargetContract). Dual-read
|
|
9
|
+
* still lists the IR names so old files load.
|
|
10
|
+
*/
|
|
1
11
|
export declare const WIRING_KEYS: Set<string>;
|
|
12
|
+
/** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
|
|
2
13
|
export declare const BODY_KEYS: Set<string>;
|
|
3
14
|
export declare function isPipelineStageBodyKey(key: string): boolean;
|
|
4
15
|
export declare function isAllowedPipelineStageEntryKey(key: string): boolean;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowed keys on a pipeline stage YAML entry.
|
|
3
|
+
*
|
|
4
|
+
* Target authoring: `io`, `verify`, `on_verify_fail`.
|
|
5
|
+
* IR / legacy YAML (same strings): `payload_schema`, `pre_emit_checks`,
|
|
6
|
+
* `completion`, `recovery`, `clone_input_schema`.
|
|
7
|
+
*
|
|
8
|
+
* New catalog fields go on the target keys (and compileTargetContract). Dual-read
|
|
9
|
+
* still lists the IR names so old files load.
|
|
10
|
+
*/
|
|
1
11
|
export const WIRING_KEYS = new Set([
|
|
2
12
|
"id",
|
|
3
13
|
"needs",
|
|
@@ -7,9 +17,11 @@ export const WIRING_KEYS = new Set([
|
|
|
7
17
|
"clone_cap",
|
|
8
18
|
"completion",
|
|
9
19
|
"recovery",
|
|
20
|
+
"on_verify_fail",
|
|
10
21
|
"feedback_loop",
|
|
11
22
|
"replay_safe",
|
|
12
23
|
]);
|
|
24
|
+
/** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
|
|
13
25
|
export const BODY_KEYS = new Set([
|
|
14
26
|
"system_prompt",
|
|
15
27
|
"model",
|
|
@@ -21,6 +33,8 @@ export const BODY_KEYS = new Set([
|
|
|
21
33
|
"timeout_ms",
|
|
22
34
|
"skill",
|
|
23
35
|
"mcp",
|
|
36
|
+
"io",
|
|
37
|
+
"verify",
|
|
24
38
|
]);
|
|
25
39
|
export function isPipelineStageBodyKey(key) {
|
|
26
40
|
return BODY_KEYS.has(key);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CompletionCheck } from "../types/completion.js";
|
|
2
|
+
import type { PreEmitCheck } from "../types/preEmitCheck.js";
|
|
3
|
+
import type { StageIoYaml } from "../types/stage.js";
|
|
4
|
+
export type VerifyPhase = "emit" | "after";
|
|
5
|
+
export declare function buildVerifyItems(preEmit: PreEmitCheck[] | undefined, afterChecks: CompletionCheck[] | undefined): {
|
|
6
|
+
ok: true;
|
|
7
|
+
items: Record<string, unknown>[];
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function ioFromSchemas(stage: {
|
|
13
|
+
payload_schema?: unknown;
|
|
14
|
+
clone_input_schema?: unknown;
|
|
15
|
+
}): StageIoYaml | undefined;
|
|
16
|
+
export declare function ioFromRawDocument(raw: Record<string, unknown>): StageIoYaml | undefined;
|
|
17
|
+
export declare function rewriteStageDocument(raw: Record<string, unknown>, compiled: {
|
|
18
|
+
io?: StageIoYaml;
|
|
19
|
+
verify?: Record<string, unknown>[];
|
|
20
|
+
}): Record<string, unknown>;
|
|
21
|
+
export declare function rewritePipelineStageEntry(raw: Record<string, unknown>, compiled: {
|
|
22
|
+
uses?: string;
|
|
23
|
+
io?: StageIoYaml;
|
|
24
|
+
verify?: Record<string, unknown>[];
|
|
25
|
+
on_verify_fail?: unknown;
|
|
26
|
+
}): Record<string, unknown>;
|
|
27
|
+
export declare function rewriteCatalogDocument(raw: Record<string, unknown>, rewriteEntry: (entry: Record<string, unknown>, index: number) => Record<string, unknown>): Record<string, unknown>;
|
|
28
|
+
export declare function stringifyTargetYaml(doc: unknown): string;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print target YAML (`io` / `verify` / `on_verify_fail`) from compiled IR.
|
|
3
|
+
* Used by `sf migrate-yaml`. IR field names stay payload_schema / completion.
|
|
4
|
+
*/
|
|
5
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
6
|
+
import { LEGACY_CONTRACT_KEYS } from "./legacyYaml.js";
|
|
7
|
+
import { STAGE_FILE_WIRING_KEYS, TARGET_CONTRACT_KEYS } from "./yamlDialect.js";
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && Array.isArray(value) === false;
|
|
10
|
+
}
|
|
11
|
+
function uniquePhases(phases) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const next = [];
|
|
14
|
+
for (const phase of phases) {
|
|
15
|
+
if (seen.has(phase))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(phase);
|
|
18
|
+
next.push(phase);
|
|
19
|
+
}
|
|
20
|
+
return next;
|
|
21
|
+
}
|
|
22
|
+
function artifactTypesCompatible(a, b) {
|
|
23
|
+
const artifact = a === "artifact" || a === "artifact_declared";
|
|
24
|
+
const other = b === "artifact" || b === "artifact_declared";
|
|
25
|
+
return artifact && other;
|
|
26
|
+
}
|
|
27
|
+
function typesCompatible(a, b) {
|
|
28
|
+
if (a === b)
|
|
29
|
+
return true;
|
|
30
|
+
return artifactTypesCompatible(a, b);
|
|
31
|
+
}
|
|
32
|
+
function preEmitToItem(check) {
|
|
33
|
+
if (check.type === "artifact_declared") {
|
|
34
|
+
return {
|
|
35
|
+
id: check.id,
|
|
36
|
+
type: "artifact",
|
|
37
|
+
basename: check.basename,
|
|
38
|
+
when: ["emit"],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { ...check, when: ["emit"] };
|
|
42
|
+
}
|
|
43
|
+
function afterCheckToItem(check) {
|
|
44
|
+
return { ...check, when: ["after"] };
|
|
45
|
+
}
|
|
46
|
+
function mergeVerifyItems(existing, incoming) {
|
|
47
|
+
const existingType = String(existing.type);
|
|
48
|
+
const incomingType = String(incoming.type);
|
|
49
|
+
if (!typesCompatible(existingType, incomingType)) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: `verify id "${String(existing.id)}" cannot merge types "${existingType}" and "${incomingType}"`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const existingWhen = Array.isArray(existing.when) ? existing.when : [];
|
|
56
|
+
const incomingWhen = Array.isArray(incoming.when) ? incoming.when : [];
|
|
57
|
+
const merged = { ...existing };
|
|
58
|
+
if (artifactTypesCompatible(existingType, incomingType)) {
|
|
59
|
+
merged.type = "artifact";
|
|
60
|
+
if (typeof incoming.path === "string")
|
|
61
|
+
merged.path = incoming.path;
|
|
62
|
+
if (typeof incoming.basename === "string" && merged.path === undefined) {
|
|
63
|
+
merged.basename = incoming.basename;
|
|
64
|
+
}
|
|
65
|
+
if (typeof incoming.nonempty === "boolean")
|
|
66
|
+
merged.nonempty = incoming.nonempty;
|
|
67
|
+
if (typeof existing.basename === "string" && typeof merged.path === "string") {
|
|
68
|
+
const base = merged.path.split(/[\\/]/).pop();
|
|
69
|
+
if (base === existing.basename)
|
|
70
|
+
delete merged.basename;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
75
|
+
if (key === "when" || key === "id" || key === "type")
|
|
76
|
+
continue;
|
|
77
|
+
if (merged[key] === undefined)
|
|
78
|
+
merged[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
merged.when = uniquePhases([...existingWhen, ...incomingWhen]);
|
|
82
|
+
return { ok: true, item: merged };
|
|
83
|
+
}
|
|
84
|
+
export function buildVerifyItems(preEmit, afterChecks) {
|
|
85
|
+
const byId = new Map();
|
|
86
|
+
const order = [];
|
|
87
|
+
const add = (item) => {
|
|
88
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
89
|
+
if (!id)
|
|
90
|
+
return { ok: false, error: "verify item is missing id" };
|
|
91
|
+
const existing = byId.get(id);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
byId.set(id, item);
|
|
94
|
+
order.push(id);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const merged = mergeVerifyItems(existing, item);
|
|
98
|
+
if (!merged.ok)
|
|
99
|
+
return { ok: false, error: merged.error };
|
|
100
|
+
byId.set(id, merged.item);
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|
|
103
|
+
for (const check of preEmit ?? []) {
|
|
104
|
+
const failed = add(preEmitToItem(check));
|
|
105
|
+
if (failed)
|
|
106
|
+
return failed;
|
|
107
|
+
}
|
|
108
|
+
for (const check of afterChecks ?? []) {
|
|
109
|
+
const failed = add(afterCheckToItem(check));
|
|
110
|
+
if (failed)
|
|
111
|
+
return failed;
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, items: order.map((id) => byId.get(id)) };
|
|
114
|
+
}
|
|
115
|
+
export function ioFromSchemas(stage) {
|
|
116
|
+
const io = {};
|
|
117
|
+
if (stage.clone_input_schema !== undefined) {
|
|
118
|
+
io.input = { schema: stage.clone_input_schema };
|
|
119
|
+
}
|
|
120
|
+
if (stage.payload_schema !== undefined) {
|
|
121
|
+
io.output = { schema: stage.payload_schema };
|
|
122
|
+
}
|
|
123
|
+
return io.input !== undefined || io.output !== undefined ? io : undefined;
|
|
124
|
+
}
|
|
125
|
+
export function ioFromRawDocument(raw) {
|
|
126
|
+
if (isPlainObject(raw.io))
|
|
127
|
+
return raw.io;
|
|
128
|
+
return ioFromSchemas({
|
|
129
|
+
payload_schema: raw.payload_schema,
|
|
130
|
+
clone_input_schema: raw.clone_input_schema,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function dropContractKeys(raw) {
|
|
134
|
+
const next = {};
|
|
135
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
136
|
+
if (LEGACY_CONTRACT_KEYS.has(key) || TARGET_CONTRACT_KEYS.has(key))
|
|
137
|
+
continue;
|
|
138
|
+
next[key] = value;
|
|
139
|
+
}
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
const STAGE_BODY_ORDER = [
|
|
143
|
+
"id",
|
|
144
|
+
"system_prompt",
|
|
145
|
+
"model",
|
|
146
|
+
"agent",
|
|
147
|
+
"timeout_ms",
|
|
148
|
+
"io",
|
|
149
|
+
"verify",
|
|
150
|
+
"gate_kinds",
|
|
151
|
+
"clone_actions",
|
|
152
|
+
"skill",
|
|
153
|
+
"mcp",
|
|
154
|
+
];
|
|
155
|
+
const PIPELINE_WIRING_ORDER = [
|
|
156
|
+
"id",
|
|
157
|
+
"uses",
|
|
158
|
+
"skill",
|
|
159
|
+
"mcp",
|
|
160
|
+
"needs",
|
|
161
|
+
"fork",
|
|
162
|
+
"clonable",
|
|
163
|
+
"clone_cap",
|
|
164
|
+
"on_verify_fail",
|
|
165
|
+
"feedback_loop",
|
|
166
|
+
"replay_safe",
|
|
167
|
+
];
|
|
168
|
+
function orderKeys(raw, preferred) {
|
|
169
|
+
const next = {};
|
|
170
|
+
for (const key of preferred) {
|
|
171
|
+
if (raw[key] !== undefined)
|
|
172
|
+
next[key] = raw[key];
|
|
173
|
+
}
|
|
174
|
+
for (const key of Object.keys(raw)) {
|
|
175
|
+
if (next[key] !== undefined)
|
|
176
|
+
continue;
|
|
177
|
+
next[key] = raw[key];
|
|
178
|
+
}
|
|
179
|
+
return next;
|
|
180
|
+
}
|
|
181
|
+
export function rewriteStageDocument(raw, compiled) {
|
|
182
|
+
const next = dropContractKeys(raw);
|
|
183
|
+
for (const key of STAGE_FILE_WIRING_KEYS) {
|
|
184
|
+
delete next[key];
|
|
185
|
+
}
|
|
186
|
+
if (compiled.io !== undefined)
|
|
187
|
+
next.io = compiled.io;
|
|
188
|
+
if (compiled.verify !== undefined && compiled.verify.length > 0) {
|
|
189
|
+
next.verify = compiled.verify;
|
|
190
|
+
}
|
|
191
|
+
return orderKeys(next, STAGE_BODY_ORDER);
|
|
192
|
+
}
|
|
193
|
+
export function rewritePipelineStageEntry(raw, compiled) {
|
|
194
|
+
const next = dropContractKeys(raw);
|
|
195
|
+
if (compiled.uses !== undefined) {
|
|
196
|
+
next.uses = compiled.uses;
|
|
197
|
+
delete next.system_prompt;
|
|
198
|
+
delete next.model;
|
|
199
|
+
delete next.agent;
|
|
200
|
+
delete next.timeout_ms;
|
|
201
|
+
delete next.gate_kinds;
|
|
202
|
+
delete next.clone_actions;
|
|
203
|
+
delete next.io;
|
|
204
|
+
delete next.verify;
|
|
205
|
+
if (compiled.on_verify_fail !== undefined)
|
|
206
|
+
next.on_verify_fail = compiled.on_verify_fail;
|
|
207
|
+
else
|
|
208
|
+
delete next.on_verify_fail;
|
|
209
|
+
return orderKeys(next, PIPELINE_WIRING_ORDER);
|
|
210
|
+
}
|
|
211
|
+
if (compiled.io !== undefined)
|
|
212
|
+
next.io = compiled.io;
|
|
213
|
+
else
|
|
214
|
+
delete next.io;
|
|
215
|
+
if (compiled.verify !== undefined && compiled.verify.length > 0)
|
|
216
|
+
next.verify = compiled.verify;
|
|
217
|
+
else
|
|
218
|
+
delete next.verify;
|
|
219
|
+
if (compiled.on_verify_fail !== undefined)
|
|
220
|
+
next.on_verify_fail = compiled.on_verify_fail;
|
|
221
|
+
else
|
|
222
|
+
delete next.on_verify_fail;
|
|
223
|
+
return orderKeys(next, [...STAGE_BODY_ORDER, ...PIPELINE_WIRING_ORDER]);
|
|
224
|
+
}
|
|
225
|
+
export function rewriteCatalogDocument(raw, rewriteEntry) {
|
|
226
|
+
const next = {};
|
|
227
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
228
|
+
if (key === "stages" && Array.isArray(value)) {
|
|
229
|
+
next.stages = value.map((entry, index) => {
|
|
230
|
+
if (!isPlainObject(entry))
|
|
231
|
+
return entry;
|
|
232
|
+
return rewriteEntry(entry, index);
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
next[key] = value;
|
|
237
|
+
}
|
|
238
|
+
return next;
|
|
239
|
+
}
|
|
240
|
+
export function stringifyTargetYaml(doc) {
|
|
241
|
+
const text = stringifyYaml(doc, { indent: 2, lineWidth: 0 });
|
|
242
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
243
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LoadedManifest } from "../types/stageflowManifest.js";
|
|
2
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
export type ModelSelection = {
|
|
4
|
+
/** stageflow.yaml top-level `model`. */
|
|
5
|
+
global?: string;
|
|
6
|
+
/** Pipeline yaml top-level `model`; overrides global. */
|
|
7
|
+
pipeline?: string;
|
|
8
|
+
/** Stage yaml / inline `model`; overrides pipeline/global. */
|
|
9
|
+
stage?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* stage > pipeline > global. No silent hardcoded fallback — unset is a load error.
|
|
13
|
+
* Present-but-empty / whitespace follows parseModelField (load failure, not "").
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveModelOutcome(selection: ModelSelection, ctx: {
|
|
16
|
+
stageId: string;
|
|
17
|
+
pipelineId: string;
|
|
18
|
+
}): LoadOutcome<string>;
|
|
19
|
+
/** The global tier's model id from an already-loaded manifest, or undefined when missing/unset. */
|
|
20
|
+
export declare function globalModelFromManifest(loaded: LoadedManifest | null): string | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
2
|
+
import { parseModelField } from "./modelField.js";
|
|
3
|
+
/**
|
|
4
|
+
* stage > pipeline > global. No silent hardcoded fallback — unset is a load error.
|
|
5
|
+
* Present-but-empty / whitespace follows parseModelField (load failure, not "").
|
|
6
|
+
*/
|
|
7
|
+
export function resolveModelOutcome(selection, ctx) {
|
|
8
|
+
const effective = selection.stage ?? selection.pipeline ?? selection.global;
|
|
9
|
+
const parsed = parseModelField(effective);
|
|
10
|
+
if (!parsed.ok) {
|
|
11
|
+
return loadFailure([
|
|
12
|
+
{
|
|
13
|
+
code: "stage.invalid_model",
|
|
14
|
+
message: `Stage "${ctx.stageId}" in pipeline "${ctx.pipelineId}": ${parsed.message}`,
|
|
15
|
+
category: "stage",
|
|
16
|
+
stageId: ctx.stageId,
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
if (parsed.value === undefined) {
|
|
21
|
+
return loadFailure([
|
|
22
|
+
{
|
|
23
|
+
code: "stage.missing_model",
|
|
24
|
+
message: `Stage "${ctx.stageId}" in pipeline "${ctx.pipelineId}": model is required (set on stage, pipeline, or stageflow.yaml)`,
|
|
25
|
+
category: "stage",
|
|
26
|
+
stageId: ctx.stageId,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
return loadSuccess(parsed.value);
|
|
31
|
+
}
|
|
32
|
+
/** The global tier's model id from an already-loaded manifest, or undefined when missing/unset. */
|
|
33
|
+
export function globalModelFromManifest(loaded) {
|
|
34
|
+
return loaded?.manifest.model;
|
|
35
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { LoadedPipeline } from "../types/pipeline.js";
|
|
|
2
2
|
import type { LoadIssue } from "./loadOutcome.js";
|
|
3
3
|
export type ValidationSeverity = "error" | "warning";
|
|
4
4
|
export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
|
|
5
|
-
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp";
|
|
5
|
+
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml";
|
|
6
6
|
export type ValidationFinding = {
|
|
7
7
|
severity: ValidationSeverity;
|
|
8
8
|
code: ValidationFindingCode | string;
|
|
@@ -15,6 +15,7 @@ export type ValidationFinding = {
|
|
|
15
15
|
export type ValidationScope = "full" | "pipeline" | "task";
|
|
16
16
|
export type ValidateCatalogOptions = {
|
|
17
17
|
cwd?: string;
|
|
18
|
+
projectRoot?: string;
|
|
18
19
|
scope: ValidationScope;
|
|
19
20
|
pipeline?: string;
|
|
20
21
|
task?: string;
|
|
@@ -36,6 +37,7 @@ export declare function findingsFromLoadIssues(cwd: string, absPath: string, iss
|
|
|
36
37
|
export declare function findingStageIdFilenameMismatch(cwd: string, absPath: string, fileStem: string, declaredId: string): ValidationFinding;
|
|
37
38
|
export type ValidatePipelineOptions = {
|
|
38
39
|
cwd?: string;
|
|
40
|
+
projectRoot?: string;
|
|
39
41
|
validateStages?: boolean;
|
|
40
42
|
strict?: boolean;
|
|
41
43
|
};
|
|
@@ -43,12 +45,14 @@ export declare function validatePipeline(nameOrPath: string, options?: ValidateP
|
|
|
43
45
|
export type LoadPipelineValidatedResult = {
|
|
44
46
|
ok: true;
|
|
45
47
|
loaded: LoadedPipeline;
|
|
48
|
+
findings: ValidationFinding[];
|
|
46
49
|
} | {
|
|
47
50
|
ok: false;
|
|
48
51
|
findings: ValidationFinding[];
|
|
49
52
|
};
|
|
50
53
|
export declare function loadPipelineValidated(nameOrPath: string, options?: {
|
|
51
54
|
cwd?: string;
|
|
55
|
+
projectRoot?: string;
|
|
52
56
|
validateStages?: boolean;
|
|
53
57
|
}): Promise<LoadPipelineValidatedResult>;
|
|
54
58
|
export declare function validateCatalog(options: ValidateCatalogOptions): Promise<ValidationResult>;
|