stageflow 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/agent/claudeAdapter.js +31 -1
- package/dist/agent/piAdapter.d.ts +6 -4
- package/dist/agent/piAdapter.js +71 -26
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +4 -2
- package/dist/agent/port.js +1 -1
- package/dist/cli/initTemplates.d.ts +1 -1
- package/dist/cli/initTemplates.js +1 -1
- package/dist/cli/migrateYamlCommand.d.ts +18 -0
- package/dist/cli/migrateYamlCommand.js +180 -0
- package/dist/cli/runOutput.js +26 -18
- package/dist/cli/validateCommand.d.ts +1 -0
- package/dist/cli/validateCommand.js +2 -0
- package/dist/cli.js +15 -1
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +1 -1
- package/dist/config/createPipeline.js +23 -11
- package/dist/config/createStage.d.ts +1 -1
- package/dist/config/createStage.js +10 -5
- package/dist/config/legacyYaml.d.ts +28 -0
- package/dist/config/legacyYaml.js +98 -0
- package/dist/config/loadOutcome.d.ts +2 -1
- package/dist/config/loadOutcome.js +4 -1
- package/dist/config/loadPipeline.d.ts +2 -0
- package/dist/config/loadPipeline.js +151 -8
- package/dist/config/loadStage.d.ts +9 -1
- package/dist/config/loadStage.js +207 -29
- package/dist/config/loadStageflowManifest.js +10 -0
- package/dist/config/loadTask.js +14 -0
- package/dist/config/materializeStageModels.d.ts +7 -0
- package/dist/config/materializeStageModels.js +35 -0
- package/dist/config/mergePipelineIncludes.d.ts +5 -1
- package/dist/config/mergePipelineIncludes.js +82 -4
- package/dist/config/migrateYaml.d.ts +19 -0
- package/dist/config/migrateYaml.js +627 -0
- package/dist/config/modelField.d.ts +13 -0
- package/dist/config/modelField.js +16 -0
- package/dist/config/normalizePipelineStageEntry.js +48 -3
- package/dist/config/parseCompletionContract.d.ts +5 -2
- package/dist/config/parseCompletionContract.js +16 -3
- package/dist/config/parsePreEmitChecks.js +4 -0
- package/dist/config/pipelineNeeds.js +3 -3
- package/dist/config/pipelineStageKeys.d.ts +11 -0
- package/dist/config/pipelineStageKeys.js +15 -0
- package/dist/config/printTargetYaml.d.ts +28 -0
- package/dist/config/printTargetYaml.js +243 -0
- package/dist/config/resolveModel.d.ts +20 -0
- package/dist/config/resolveModel.js +35 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +5 -1
- package/dist/config/validateCatalog.js +87 -6
- package/dist/config/validateCompletionContract.js +5 -0
- package/dist/config/yamlDialect.d.ts +29 -0
- package/dist/config/yamlDialect.js +327 -0
- package/dist/envelope/payloadSchema.d.ts +37 -5
- package/dist/envelope/payloadSchema.js +272 -22
- package/dist/mcp/catalogTools.js +1 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/pipelineDagSnapshot.js +2 -0
- package/dist/runstore/port.d.ts +1 -0
- package/dist/runtime/completionCheckRunner.d.ts +3 -2
- package/dist/runtime/completionCheckRunner.js +3 -2
- package/dist/runtime/envelopeRouting.d.ts +2 -2
- package/dist/runtime/pipelineRunner.d.ts +3 -0
- package/dist/runtime/pipelineRunner.js +23 -3
- package/dist/runtime/runManager.js +3 -2
- package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
- package/dist/runtime/stageAttemptBootstrap.js +38 -2
- package/dist/runtime/stageRunner.d.ts +2 -2
- package/dist/runtime/taskInput.d.ts +7 -0
- package/dist/runtime/taskInput.js +60 -0
- package/dist/runtime/verifiedStageExecution.d.ts +2 -0
- package/dist/runtime/verifiedStageExecution.js +13 -2
- package/dist/server/http.js +23 -4
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/types/completion.d.ts +6 -0
- package/dist/types/pipeline.d.ts +23 -2
- package/dist/types/preEmitCheck.d.ts +4 -6
- package/dist/types/stage.d.ts +24 -4
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/types/task.d.ts +1 -0
- package/dist/ui/assets/index-GtyiWiZg.js +118 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
- package/skills/stageflow-author/references/catalog-mapping.md +80 -17
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/SKILL.md +1 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
- package/skills/stageflow-session-capture/references/catalog-authoring.md +6 -3
- package/dist/ui/assets/index-CXGz4nL7.js +0 -118
|
@@ -1,5 +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";
|
|
8
|
+
import { parseStageMcp } from "./loadStage.js";
|
|
9
|
+
import { legacyAuthoringRejected, presentLegacyKeys } from "./legacyYaml.js";
|
|
10
|
+
import { applyCompiledBody, compileTargetContract, dialectFromKeys, mixedDialectIssue, } from "./yamlDialect.js";
|
|
3
11
|
import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
|
|
4
12
|
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
5
13
|
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
@@ -31,7 +39,7 @@ function extractBodyRaw(raw) {
|
|
|
31
39
|
return body;
|
|
32
40
|
}
|
|
33
41
|
function hasBodyKey(raw) {
|
|
34
|
-
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill");
|
|
42
|
+
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill" && key !== "mcp");
|
|
35
43
|
}
|
|
36
44
|
function readSkill(raw) {
|
|
37
45
|
if (raw.skill === undefined)
|
|
@@ -89,6 +97,10 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
89
97
|
]);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
const dialect = dialectFromKeys(Object.keys(raw));
|
|
101
|
+
if (dialect === "invalid") {
|
|
102
|
+
return loadFailure([mixedDialectIssue()]);
|
|
103
|
+
}
|
|
92
104
|
const uses = typeof raw.uses === "string" ? raw.uses : undefined;
|
|
93
105
|
const hasBody = hasBodyKey(raw);
|
|
94
106
|
const skillOutcome = readSkill(raw);
|
|
@@ -96,6 +108,10 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
96
108
|
return skillOutcome;
|
|
97
109
|
}
|
|
98
110
|
const skill = skillOutcome?.ok ? skillOutcome.value : undefined;
|
|
111
|
+
const mcpOutcome = parseStageMcp(raw.mcp, `entry at index ${index} in ${declaringPath}`);
|
|
112
|
+
if (!mcpOutcome.ok)
|
|
113
|
+
return mcpOutcome;
|
|
114
|
+
const mcp = mcpOutcome.value;
|
|
99
115
|
if (uses && hasBody) {
|
|
100
116
|
return loadFailure([
|
|
101
117
|
{
|
|
@@ -133,7 +149,35 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
133
149
|
},
|
|
134
150
|
]);
|
|
135
151
|
}
|
|
136
|
-
|
|
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
|
+
}
|
|
137
181
|
if (!policyOutcome.ok)
|
|
138
182
|
return policyOutcome;
|
|
139
183
|
let needs;
|
|
@@ -197,7 +241,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
197
241
|
body = { kind: "uses", path: uses, absolutePath };
|
|
198
242
|
}
|
|
199
243
|
else {
|
|
200
|
-
body = { kind: "inline", raw: extractBodyRaw(raw) };
|
|
244
|
+
body = { kind: "inline", raw: compiledBody ?? extractBodyRaw(raw) };
|
|
201
245
|
}
|
|
202
246
|
const entry = {
|
|
203
247
|
id,
|
|
@@ -220,6 +264,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
220
264
|
? { replay_safe: raw.replay_safe }
|
|
221
265
|
: {}),
|
|
222
266
|
...(skill !== undefined ? { skill } : {}),
|
|
267
|
+
...(mcp !== undefined ? { mcp } : {}),
|
|
223
268
|
};
|
|
224
269
|
const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
|
|
225
270
|
if (priorPath) {
|
|
@@ -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",
|
|
@@ -20,6 +32,9 @@ export const BODY_KEYS = new Set([
|
|
|
20
32
|
"clone_actions",
|
|
21
33
|
"timeout_ms",
|
|
22
34
|
"skill",
|
|
35
|
+
"mcp",
|
|
36
|
+
"io",
|
|
37
|
+
"verify",
|
|
23
38
|
]);
|
|
24
39
|
export function isPipelineStageBodyKey(key) {
|
|
25
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "connect_failed";
|
|
2
|
+
export declare class StageMcpError extends Error {
|
|
3
|
+
readonly code: StageMcpErrorCode;
|
|
4
|
+
constructor(message: string, code: StageMcpErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export type ResolvedMcpServerConfig = Record<string, unknown>;
|
|
7
|
+
export type ResolvedMcpServers = Record<string, ResolvedMcpServerConfig>;
|
|
8
|
+
export declare const MCP_CATALOG_FILENAME = ".mcp.json";
|
|
9
|
+
export declare const STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV = "STAGEFLOW_STAGE_ARTIFACTS_DIR";
|
|
10
|
+
export declare function stampStagePromptArtifactsDir(prompt: string, artifactsDir: string): string;
|
|
11
|
+
export type McpCatalogServers = Record<string, Record<string, unknown>>;
|
|
12
|
+
export type InspectedMcpCatalog = {
|
|
13
|
+
path: string;
|
|
14
|
+
servers: McpCatalogServers;
|
|
15
|
+
};
|
|
16
|
+
export declare function mcpCatalogPath(projectRoot: string): string;
|
|
17
|
+
export declare function parseMcpCatalog(raw: string, catalogPath?: string): McpCatalogServers;
|
|
18
|
+
export declare function loadMcpCatalog(projectRoot: string): Promise<InspectedMcpCatalog>;
|
|
19
|
+
export type ProjectMcpCatalogTransport = "stdio" | "http";
|
|
20
|
+
export type ProjectMcpCatalogEntry = {
|
|
21
|
+
name: string;
|
|
22
|
+
transport: ProjectMcpCatalogTransport;
|
|
23
|
+
};
|
|
24
|
+
export type ProjectMcpCatalogListStatus = "ok" | "missing_catalog" | "invalid_config";
|
|
25
|
+
export type ProjectMcpCatalogList = {
|
|
26
|
+
status: ProjectMcpCatalogListStatus;
|
|
27
|
+
servers: ProjectMcpCatalogEntry[];
|
|
28
|
+
};
|
|
29
|
+
export declare function listProjectMcpCatalog(projectRoot: string): Promise<ProjectMcpCatalogList>;
|
|
30
|
+
export declare function assertMcpAllowlistKnown(servers: McpCatalogServers, allowlist: readonly string[]): void;
|
|
31
|
+
export declare function resolveStageMcpServers(options: {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
allowlist?: readonly string[];
|
|
34
|
+
env: NodeJS.ProcessEnv;
|
|
35
|
+
}): Promise<ResolvedMcpServers>;
|