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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
|
|
4
|
+
export class StageMcpError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "StageMcpError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const MCP_CATALOG_FILENAME = ".mcp.json";
|
|
13
|
+
export const STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV = "STAGEFLOW_STAGE_ARTIFACTS_DIR";
|
|
14
|
+
const STAGEFLOW_STAGE_ARTIFACTS_DIR_TOKEN = `\${${STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV}}`;
|
|
15
|
+
export function stampStagePromptArtifactsDir(prompt, artifactsDir) {
|
|
16
|
+
return prompt.replaceAll(STAGEFLOW_STAGE_ARTIFACTS_DIR_TOKEN, artifactsDir);
|
|
17
|
+
}
|
|
18
|
+
function isPlainObject(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
export function mcpCatalogPath(projectRoot) {
|
|
22
|
+
return path.join(projectRoot, MCP_CATALOG_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
export function parseMcpCatalog(raw, catalogPath = MCP_CATALOG_FILENAME) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new StageMcpError(`MCP catalog "${catalogPath}" is not valid JSON`, "invalid_config");
|
|
31
|
+
}
|
|
32
|
+
if (!isPlainObject(parsed)) {
|
|
33
|
+
throw new StageMcpError(`MCP catalog "${catalogPath}" must be a JSON object`, "invalid_config");
|
|
34
|
+
}
|
|
35
|
+
if (!Object.hasOwn(parsed, "mcpServers")) {
|
|
36
|
+
throw new StageMcpError(`MCP catalog "${catalogPath}" is missing mcpServers`, "invalid_config");
|
|
37
|
+
}
|
|
38
|
+
const mcpServers = parsed.mcpServers;
|
|
39
|
+
if (!isPlainObject(mcpServers)) {
|
|
40
|
+
throw new StageMcpError(`MCP catalog "${catalogPath}" mcpServers must be an object`, "invalid_config");
|
|
41
|
+
}
|
|
42
|
+
const servers = {};
|
|
43
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
44
|
+
if (name === STAGEFLOW_MCP_SERVER_NAME) {
|
|
45
|
+
throw new StageMcpError(`MCP catalog must not include reserved name "${STAGEFLOW_MCP_SERVER_NAME}"`, "reserved_name");
|
|
46
|
+
}
|
|
47
|
+
if (!isPlainObject(entry)) {
|
|
48
|
+
throw new StageMcpError(`MCP catalog server "${name}" must be an object`, "invalid_config");
|
|
49
|
+
}
|
|
50
|
+
servers[name] = entry;
|
|
51
|
+
}
|
|
52
|
+
return servers;
|
|
53
|
+
}
|
|
54
|
+
export async function loadMcpCatalog(projectRoot) {
|
|
55
|
+
const catalogPath = mcpCatalogPath(projectRoot);
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readFile(catalogPath, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const code = err.code;
|
|
62
|
+
if (code === "ENOENT") {
|
|
63
|
+
throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" is missing`, "missing_catalog");
|
|
64
|
+
}
|
|
65
|
+
throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" could not be read`, "invalid_config");
|
|
66
|
+
}
|
|
67
|
+
return { path: catalogPath, servers: parseMcpCatalog(raw, catalogPath) };
|
|
68
|
+
}
|
|
69
|
+
function coarseMcpTransport(entry) {
|
|
70
|
+
if (Object.hasOwn(entry, "command")) {
|
|
71
|
+
return "stdio";
|
|
72
|
+
}
|
|
73
|
+
if (Object.hasOwn(entry, "url")) {
|
|
74
|
+
return "http";
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
export async function listProjectMcpCatalog(projectRoot) {
|
|
79
|
+
try {
|
|
80
|
+
const catalog = await loadMcpCatalog(projectRoot);
|
|
81
|
+
const servers = [];
|
|
82
|
+
for (const [name, entry] of Object.entries(catalog.servers)) {
|
|
83
|
+
const transport = coarseMcpTransport(entry);
|
|
84
|
+
if (transport === undefined) {
|
|
85
|
+
return { status: "invalid_config", servers: [] };
|
|
86
|
+
}
|
|
87
|
+
servers.push({ name, transport });
|
|
88
|
+
}
|
|
89
|
+
return { status: "ok", servers };
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof StageMcpError) {
|
|
93
|
+
if (err.code === "missing_catalog") {
|
|
94
|
+
return { status: "missing_catalog", servers: [] };
|
|
95
|
+
}
|
|
96
|
+
return { status: "invalid_config", servers: [] };
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function assertMcpAllowlistKnown(servers, allowlist) {
|
|
102
|
+
for (const name of allowlist) {
|
|
103
|
+
if (!Object.hasOwn(servers, name)) {
|
|
104
|
+
throw new StageMcpError(`Unknown MCP server "${name}"`, "unknown_server");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const INTERPOLATION_TOKEN = /\$\{([^}]*)\}/g;
|
|
109
|
+
const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
110
|
+
const ENV_VAR_WITH_DEFAULT = /^([A-Za-z_][A-Za-z0-9_]*):-(.*)$/;
|
|
111
|
+
function interpolateString(value, env, serverName) {
|
|
112
|
+
return value.replace(INTERPOLATION_TOKEN, (_match, inner) => {
|
|
113
|
+
const withDefault = ENV_VAR_WITH_DEFAULT.exec(inner);
|
|
114
|
+
if (withDefault) {
|
|
115
|
+
const found = env[withDefault[1]];
|
|
116
|
+
return found !== undefined ? found : withDefault[2];
|
|
117
|
+
}
|
|
118
|
+
if (ENV_VAR_NAME.test(inner)) {
|
|
119
|
+
const found = env[inner];
|
|
120
|
+
if (found !== undefined) {
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
123
|
+
throw new StageMcpError(`Unresolved MCP catalog variable "${inner}"`, "unresolved_var");
|
|
124
|
+
}
|
|
125
|
+
throw new StageMcpError(`MCP catalog server "${serverName}" has an invalid interpolation form`, "invalid_config");
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function interpolateField(key, value, env, serverName) {
|
|
129
|
+
if ((key === "command" || key === "url" || key === "cwd") && typeof value === "string") {
|
|
130
|
+
return interpolateString(value, env, serverName);
|
|
131
|
+
}
|
|
132
|
+
if (key === "args" && Array.isArray(value)) {
|
|
133
|
+
return value.map((item) => typeof item === "string" ? interpolateString(item, env, serverName) : item);
|
|
134
|
+
}
|
|
135
|
+
if ((key === "env" || key === "headers") && isPlainObject(value)) {
|
|
136
|
+
const copied = {};
|
|
137
|
+
for (const [field, fieldValue] of Object.entries(value)) {
|
|
138
|
+
copied[field] =
|
|
139
|
+
typeof fieldValue === "string"
|
|
140
|
+
? interpolateString(fieldValue, env, serverName)
|
|
141
|
+
: fieldValue;
|
|
142
|
+
}
|
|
143
|
+
return copied;
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function hasPathSeparator(value) {
|
|
148
|
+
return value.includes("/") || value.includes("\\");
|
|
149
|
+
}
|
|
150
|
+
function isRelativeFsPath(value) {
|
|
151
|
+
if (value.length === 0 || path.isAbsolute(value) || value.startsWith("-") || value.startsWith("@")) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return hasPathSeparator(value) || value.startsWith(".");
|
|
155
|
+
}
|
|
156
|
+
function isInsideProjectRoot(projectRoot, candidate) {
|
|
157
|
+
const root = path.resolve(projectRoot);
|
|
158
|
+
const resolved = path.resolve(candidate);
|
|
159
|
+
const rel = path.relative(root, resolved);
|
|
160
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
161
|
+
}
|
|
162
|
+
function stampSpawnRoot(resolved, projectRoot, serverName) {
|
|
163
|
+
const command = resolved.command;
|
|
164
|
+
if (typeof command === "string" && hasPathSeparator(command) && !path.isAbsolute(command)) {
|
|
165
|
+
resolved.command = path.resolve(projectRoot, command);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(resolved.args)) {
|
|
168
|
+
resolved.args = resolved.args.map((item) => typeof item === "string" && isRelativeFsPath(item)
|
|
169
|
+
? path.resolve(projectRoot, item)
|
|
170
|
+
: item);
|
|
171
|
+
}
|
|
172
|
+
const catalogCwd = resolved.cwd;
|
|
173
|
+
if (catalogCwd !== undefined) {
|
|
174
|
+
if (typeof catalogCwd !== "string" || catalogCwd.length === 0 || !path.isAbsolute(catalogCwd)) {
|
|
175
|
+
throw new StageMcpError(`MCP catalog server "${serverName}" cwd must be an absolute path inside the project root`, "invalid_config");
|
|
176
|
+
}
|
|
177
|
+
const canonical = path.resolve(catalogCwd);
|
|
178
|
+
if (!isInsideProjectRoot(projectRoot, canonical)) {
|
|
179
|
+
throw new StageMcpError(`MCP catalog server "${serverName}" cwd is outside the project root`, "invalid_config");
|
|
180
|
+
}
|
|
181
|
+
resolved.cwd = canonical;
|
|
182
|
+
}
|
|
183
|
+
else if (typeof command === "string") {
|
|
184
|
+
resolved.cwd = path.resolve(projectRoot);
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
function interpolateServer(entry, env, serverName, projectRoot) {
|
|
189
|
+
const resolved = {};
|
|
190
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
191
|
+
resolved[key] = interpolateField(key, value, env, serverName);
|
|
192
|
+
}
|
|
193
|
+
return stampSpawnRoot(resolved, projectRoot, serverName);
|
|
194
|
+
}
|
|
195
|
+
export async function resolveStageMcpServers(options) {
|
|
196
|
+
const allowlist = options.allowlist ?? [];
|
|
197
|
+
if (allowlist.length === 0) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
const catalog = await loadMcpCatalog(options.projectRoot);
|
|
201
|
+
assertMcpAllowlistKnown(catalog.servers, allowlist);
|
|
202
|
+
const resolved = {};
|
|
203
|
+
for (const name of allowlist) {
|
|
204
|
+
resolved[name] = interpolateServer(catalog.servers[name], options.env, name, options.projectRoot);
|
|
205
|
+
}
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
@@ -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_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";
|
|
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>;
|
|
@@ -6,6 +6,7 @@ import { readYamlObject } from "./readYamlObject.js";
|
|
|
6
6
|
import { resolveCatalogContext } from "./resolveCatalogContext.js";
|
|
7
7
|
import { getCatalogScanPaths } from "./browseCatalog.js";
|
|
8
8
|
import { manifestPathForProject } from "./loadStageflowManifest.js";
|
|
9
|
+
import { MCP_CATALOG_FILENAME, StageMcpError, assertMcpAllowlistKnown, loadMcpCatalog, mcpCatalogPath, } from "./resolveStageMcpServers.js";
|
|
9
10
|
export function relPath(cwd, absPath) {
|
|
10
11
|
return path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
11
12
|
}
|
|
@@ -109,6 +110,18 @@ function findingCatalog(cwd, absPath, message, code, severity) {
|
|
|
109
110
|
export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
110
111
|
return issues.flatMap((issue) => {
|
|
111
112
|
if (issue.category === "pipeline") {
|
|
113
|
+
if (issue.code === "pipeline.model_applies") {
|
|
114
|
+
return [
|
|
115
|
+
baseFinding({
|
|
116
|
+
cwd,
|
|
117
|
+
absPath,
|
|
118
|
+
message: issue.message,
|
|
119
|
+
code: issue.code,
|
|
120
|
+
category: "pipeline",
|
|
121
|
+
pipelineId: issue.pipelineId,
|
|
122
|
+
}, "warning"),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
112
125
|
return [
|
|
113
126
|
findingPipelineError(cwd, absPath, issue.message, issue.code, issue.pipelineId),
|
|
114
127
|
];
|
|
@@ -125,7 +138,9 @@ export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
|
125
138
|
}
|
|
126
139
|
if (issue.category === "catalog") {
|
|
127
140
|
const code = issue.code;
|
|
128
|
-
const severity = code === "catalog.manifest_missing" ||
|
|
141
|
+
const severity = code === "catalog.manifest_missing" ||
|
|
142
|
+
code === "catalog.empty_catalog" ||
|
|
143
|
+
code === "catalog.legacy_yaml"
|
|
129
144
|
? "warning"
|
|
130
145
|
: "error";
|
|
131
146
|
return [findingCatalog(cwd, absPath, issue.message, code, severity)];
|
|
@@ -171,8 +186,58 @@ async function validateStageFile(cwd, stagePath) {
|
|
|
171
186
|
}
|
|
172
187
|
return findings;
|
|
173
188
|
}
|
|
189
|
+
function collectMcpAllowlistNames(loaded) {
|
|
190
|
+
const names = new Set();
|
|
191
|
+
for (const stage of loaded.stages) {
|
|
192
|
+
for (const name of stage.mcp ?? []) {
|
|
193
|
+
names.add(name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [...names];
|
|
197
|
+
}
|
|
198
|
+
async function findingsForStageMcpCatalog(cwd, loaded) {
|
|
199
|
+
const allowlist = collectMcpAllowlistNames(loaded);
|
|
200
|
+
const ctx = await resolveCatalogContext(cwd);
|
|
201
|
+
const projectRoot = ctx.projectRoot;
|
|
202
|
+
const catalogAbsPath = projectRoot
|
|
203
|
+
? mcpCatalogPath(projectRoot)
|
|
204
|
+
: path.resolve(cwd, MCP_CATALOG_FILENAME);
|
|
205
|
+
if (!projectRoot) {
|
|
206
|
+
if (allowlist.length === 0)
|
|
207
|
+
return [];
|
|
208
|
+
return [
|
|
209
|
+
findingCatalog(cwd, catalogAbsPath, `MCP catalog "${MCP_CATALOG_FILENAME}" is missing`, "catalog.invalid_mcp", "error"),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
let catalog;
|
|
213
|
+
try {
|
|
214
|
+
catalog = await loadMcpCatalog(projectRoot);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (!(err instanceof StageMcpError))
|
|
218
|
+
throw err;
|
|
219
|
+
if (err.code === "missing_catalog" && allowlist.length === 0)
|
|
220
|
+
return [];
|
|
221
|
+
return [
|
|
222
|
+
findingCatalog(cwd, catalogAbsPath, err.message, "catalog.invalid_mcp", "error"),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
assertMcpAllowlistKnown(catalog.servers, allowlist);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (err instanceof StageMcpError) {
|
|
230
|
+
return [
|
|
231
|
+
findingCatalog(cwd, catalogAbsPath, err.message, "catalog.invalid_mcp", "error"),
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
174
238
|
async function runPipelineValidation(nameOrPath, options) {
|
|
175
239
|
const { cwd, validateStages } = options;
|
|
240
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
176
241
|
let pipelinePath;
|
|
177
242
|
try {
|
|
178
243
|
pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
|
|
@@ -190,16 +255,21 @@ async function runPipelineValidation(nameOrPath, options) {
|
|
|
190
255
|
]),
|
|
191
256
|
};
|
|
192
257
|
}
|
|
193
|
-
const outcome = await loadPipelineOutcome(pipelinePath, { cwd });
|
|
258
|
+
const outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
|
|
194
259
|
const findings = [];
|
|
195
260
|
if (!outcome.ok) {
|
|
196
261
|
findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
|
|
197
262
|
return { ok: false, findings };
|
|
198
263
|
}
|
|
264
|
+
if (outcome.issues) {
|
|
265
|
+
findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
|
|
266
|
+
}
|
|
267
|
+
findings.push(...(await findingsForStageMcpCatalog(cwd, outcome.value)));
|
|
199
268
|
if (validateStages && outcome.value.stageSources) {
|
|
200
269
|
for (const source of Object.values(outcome.value.stageSources)) {
|
|
201
270
|
if (source.kind === "file") {
|
|
202
|
-
|
|
271
|
+
const extra = await validateStageFile(cwd, source.path);
|
|
272
|
+
findings.push(...extra.filter((finding) => finding.code !== "stage.unresolved_schema_ref"));
|
|
203
273
|
}
|
|
204
274
|
}
|
|
205
275
|
}
|
|
@@ -210,17 +280,27 @@ async function runPipelineValidation(nameOrPath, options) {
|
|
|
210
280
|
}
|
|
211
281
|
export async function validatePipeline(nameOrPath, options = {}) {
|
|
212
282
|
const cwd = options.cwd ?? process.cwd();
|
|
283
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
213
284
|
const validateStages = options.validateStages ?? true;
|
|
214
285
|
const strict = options.strict ?? false;
|
|
215
|
-
const core = await runPipelineValidation(nameOrPath, {
|
|
286
|
+
const core = await runPipelineValidation(nameOrPath, {
|
|
287
|
+
cwd,
|
|
288
|
+
projectRoot,
|
|
289
|
+
validateStages,
|
|
290
|
+
});
|
|
216
291
|
return buildValidationResult("pipeline", core.findings, strict);
|
|
217
292
|
}
|
|
218
293
|
export async function loadPipelineValidated(nameOrPath, options = {}) {
|
|
219
294
|
const cwd = options.cwd ?? process.cwd();
|
|
295
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
220
296
|
const validateStages = options.validateStages ?? true;
|
|
221
|
-
const core = await runPipelineValidation(nameOrPath, {
|
|
297
|
+
const core = await runPipelineValidation(nameOrPath, {
|
|
298
|
+
cwd,
|
|
299
|
+
projectRoot,
|
|
300
|
+
validateStages,
|
|
301
|
+
});
|
|
222
302
|
if (core.ok) {
|
|
223
|
-
return { ok: true, loaded: core.loaded };
|
|
303
|
+
return { ok: true, loaded: core.loaded, findings: core.findings };
|
|
224
304
|
}
|
|
225
305
|
return { ok: false, findings: core.findings };
|
|
226
306
|
}
|
|
@@ -345,6 +425,7 @@ export async function validateCatalog(options) {
|
|
|
345
425
|
if (options.scope === "pipeline") {
|
|
346
426
|
return validatePipeline(options.pipeline, {
|
|
347
427
|
cwd,
|
|
428
|
+
projectRoot: options.projectRoot ?? cwd,
|
|
348
429
|
validateStages: true,
|
|
349
430
|
strict,
|
|
350
431
|
});
|
|
@@ -30,6 +30,11 @@ function isRequiredStringArrayField(schema, field) {
|
|
|
30
30
|
items.type === "string");
|
|
31
31
|
}
|
|
32
32
|
export function validateCompletionContractForStage(stage, completion) {
|
|
33
|
+
for (const check of stage.pre_emit_checks ?? []) {
|
|
34
|
+
if (check.type === "gate" && !stage.gate_kinds?.includes(check.kind)) {
|
|
35
|
+
return failure(stage.id, `gate check "${check.id}" requires gate_kinds to include "${check.kind}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
33
38
|
if (!completion)
|
|
34
39
|
return loadSuccess(undefined);
|
|
35
40
|
for (const check of completion.checks) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
+
import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
export { LEGACY_CONTRACT_KEYS, LEGACY_KEY_REPLACEMENTS, dialectWarningForDocument, formatLegacyReplacements, legacyYamlIssue, presentLegacyKeys, } from "./legacyYaml.js";
|
|
4
|
+
/** Target authoring keys. New catalog contracts belong here, not on the IR field names. */
|
|
5
|
+
export declare const TARGET_CONTRACT_KEYS: Set<string>;
|
|
6
|
+
/** Target wiring on a stage *file* is forbidden (`on_verify_fail`). `completion`/`recovery` are IR/legacy keys. */
|
|
7
|
+
export declare const STAGE_FILE_WIRING_KEYS: readonly ["needs", "on_verify_fail", "completion", "recovery", "fork", "clonable"];
|
|
8
|
+
export type YamlDialect = "legacy" | "target" | "invalid" | "neutral";
|
|
9
|
+
/** Target YAML compiled onto IR field names (see file comment). */
|
|
10
|
+
export type CompiledTargetContract = {
|
|
11
|
+
payload_schema?: unknown;
|
|
12
|
+
clone_input_schema?: unknown;
|
|
13
|
+
pre_emit_raw?: unknown[];
|
|
14
|
+
completion?: CompletionContract;
|
|
15
|
+
recovery?: RecoveryPolicy;
|
|
16
|
+
};
|
|
17
|
+
export declare function dialectFromKeys(keys: Iterable<string>): YamlDialect;
|
|
18
|
+
export declare function collectDocumentKeys(raw: Record<string, unknown>): Set<string>;
|
|
19
|
+
export declare function classifyYamlDocument(raw: Record<string, unknown>): YamlDialect;
|
|
20
|
+
export declare function mixedDialectIssue(message?: string): LoadIssue;
|
|
21
|
+
/** Compile target YAML (`io` / `verify` / `on_verify_fail`) onto IR fields. */
|
|
22
|
+
export declare function compileTargetContract(raw: Record<string, unknown>, ctx: {
|
|
23
|
+
stageId: string;
|
|
24
|
+
label: string;
|
|
25
|
+
category: "pipeline" | "stage";
|
|
26
|
+
deferSchemaRefs?: boolean;
|
|
27
|
+
}): LoadOutcome<CompiledTargetContract>;
|
|
28
|
+
/** Strip target YAML keys and stamp IR `payload_schema` / `clone_input_schema` / `pre_emit_checks`. */
|
|
29
|
+
export declare function applyCompiledBody(raw: Record<string, unknown>, compiled: CompiledTargetContract): Record<string, unknown>;
|