stageflow 0.9.0 → 0.13.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 +6 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +331 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +12 -7
- package/dist/agent/piAdapter.js +194 -44
- 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 +52 -1
- package/dist/agent/port.js +1 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +11 -1
- package/dist/config/loadStage.d.ts +1 -0
- package/dist/config/loadStage.js +207 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +56 -7
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +17 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +51 -0
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +117 -3
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +31 -0
- package/dist/runtime/verifiedStageExecution.js +127 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +137 -4
- package/dist/server/operatorResults.js +2 -0
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +41 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-Yzh42vhf.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +5 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/references/catalog-mapping.md +88 -3
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isAllowedPipelineStageEntryKey } from "./pipelineStageKeys.js";
|
|
2
|
+
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
3
|
+
import { parsePipelineNeeds, predecessorEdges, toNeedEdges } from "./pipelineNeeds.js";
|
|
2
4
|
const ALLOWED_FORK_KEYS = new Set(["select", "allow_none"]);
|
|
3
5
|
function pipelineLabel(ctx) {
|
|
4
6
|
return `Pipeline ${ctx.pipelineId} (${ctx.path})`;
|
|
@@ -9,6 +11,44 @@ function formatError(ctx, message) {
|
|
|
9
11
|
function isPlainObject(value) {
|
|
10
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
13
|
}
|
|
14
|
+
const ALLOWED_FEEDBACK_LOOP_KEYS = new Set([
|
|
15
|
+
"target",
|
|
16
|
+
"max_replays",
|
|
17
|
+
"on_max_replays",
|
|
18
|
+
"replay_session",
|
|
19
|
+
]);
|
|
20
|
+
export function parseFeedbackLoopConfig(value, stageId, ctx) {
|
|
21
|
+
if (!isPlainObject(value)) {
|
|
22
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop must be an object`));
|
|
23
|
+
}
|
|
24
|
+
for (const key of Object.keys(value)) {
|
|
25
|
+
if (!ALLOWED_FEEDBACK_LOOP_KEYS.has(key)) {
|
|
26
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop: unknown key "${key}"`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value.target)) {
|
|
30
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop.target must be a single stage id string, not an array`));
|
|
31
|
+
}
|
|
32
|
+
if (typeof value.target !== "string" || value.target.trim() === "") {
|
|
33
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop.target must be a non-empty stage id`));
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isInteger(value.max_replays) || value.max_replays < 1) {
|
|
36
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop.max_replays must be a positive integer`));
|
|
37
|
+
}
|
|
38
|
+
if (value.on_max_replays !== "require_continue" &&
|
|
39
|
+
value.on_max_replays !== "wait_for_human") {
|
|
40
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop.on_max_replays must be "require_continue" or "wait_for_human"`));
|
|
41
|
+
}
|
|
42
|
+
if (value.replay_session !== "resume" && value.replay_session !== "new_session") {
|
|
43
|
+
throw new Error(formatError(ctx, `stage "${stageId}": feedback_loop.replay_session must be "resume" or "new_session"`));
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
target: value.target,
|
|
47
|
+
max_replays: value.max_replays,
|
|
48
|
+
on_max_replays: value.on_max_replays,
|
|
49
|
+
replay_session: value.replay_session,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
12
52
|
export function parsePipelineStageEntries(raw, ctx) {
|
|
13
53
|
if (!Array.isArray(raw)) {
|
|
14
54
|
throw new Error(formatError(ctx, "stages[] is required"));
|
|
@@ -53,41 +93,69 @@ export function parsePipelineStageEntries(raw, ctx) {
|
|
|
53
93
|
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
54
94
|
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
55
95
|
};
|
|
96
|
+
const policyOutcome = parseExecutionPolicy(entry, entry.id);
|
|
97
|
+
if (!policyOutcome.ok) {
|
|
98
|
+
throw new Error(formatError(ctx, policyOutcome.issues[0]?.message ?? "invalid execution policy"));
|
|
99
|
+
}
|
|
100
|
+
const policyFields = {
|
|
101
|
+
...(policyOutcome.value.completion !== undefined
|
|
102
|
+
? { completion: policyOutcome.value.completion }
|
|
103
|
+
: {}),
|
|
104
|
+
...(policyOutcome.value.recovery !== undefined
|
|
105
|
+
? { recovery: policyOutcome.value.recovery }
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
108
|
+
const feedbackLoopFields = entry.feedback_loop !== undefined
|
|
109
|
+
? { feedback_loop: parseFeedbackLoopConfig(entry.feedback_loop, entry.id, ctx) }
|
|
110
|
+
: {};
|
|
111
|
+
if (entry.replay_safe !== undefined && typeof entry.replay_safe !== "boolean") {
|
|
112
|
+
throw new Error(formatError(ctx, `stage "${entry.id}": replay_safe must be a boolean`));
|
|
113
|
+
}
|
|
114
|
+
const replaySafetyFields = entry.replay_safe !== undefined ? { replay_safe: entry.replay_safe } : {};
|
|
56
115
|
if (entry.needs === undefined) {
|
|
57
116
|
entries.push({
|
|
58
117
|
id: entry.id,
|
|
59
118
|
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
60
119
|
...clonableFields,
|
|
120
|
+
...policyFields,
|
|
121
|
+
...feedbackLoopFields,
|
|
122
|
+
...replaySafetyFields,
|
|
61
123
|
});
|
|
62
124
|
continue;
|
|
63
125
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a string, not an array`));
|
|
69
|
-
}
|
|
70
|
-
if (typeof entry.needs !== "string" || !entry.needs) {
|
|
71
|
-
throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a non-empty string`));
|
|
126
|
+
const parsedNeeds = parsePipelineNeeds(entry.needs, entry.id);
|
|
127
|
+
if (!parsedNeeds.ok) {
|
|
128
|
+
throw new Error(formatError(ctx, parsedNeeds.message));
|
|
72
129
|
}
|
|
73
130
|
entries.push({
|
|
74
131
|
id: entry.id,
|
|
75
|
-
needs:
|
|
132
|
+
needs: parsedNeeds.value,
|
|
76
133
|
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
77
134
|
...clonableFields,
|
|
135
|
+
...policyFields,
|
|
136
|
+
...feedbackLoopFields,
|
|
137
|
+
...replaySafetyFields,
|
|
78
138
|
});
|
|
79
139
|
}
|
|
80
140
|
return entries;
|
|
81
141
|
}
|
|
82
142
|
function normalizeToEdges(entries) {
|
|
83
|
-
return entries.map((entry, index) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
return entries.map((entry, index) => {
|
|
144
|
+
const needsEdges = toNeedEdges(entry.needs);
|
|
145
|
+
return {
|
|
146
|
+
id: entry.id,
|
|
147
|
+
needs: needsEdges.length === 1 ? needsEdges[0].id : null,
|
|
148
|
+
needsEdges,
|
|
149
|
+
stageIndex: index,
|
|
150
|
+
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
151
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
152
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
153
|
+
...(entry.completion !== undefined ? { completion: entry.completion } : {}),
|
|
154
|
+
...(entry.recovery !== undefined ? { recovery: entry.recovery } : {}),
|
|
155
|
+
...(entry.feedback_loop !== undefined ? { feedback_loop: entry.feedback_loop } : {}),
|
|
156
|
+
...(entry.replay_safe !== undefined ? { replay_safe: entry.replay_safe } : {}),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
91
159
|
}
|
|
92
160
|
function detectDuplicateIds(edges, ctx) {
|
|
93
161
|
const seen = new Set();
|
|
@@ -101,8 +169,10 @@ function detectDuplicateIds(edges, ctx) {
|
|
|
101
169
|
function validateNeedsTargets(edges, ctx) {
|
|
102
170
|
const declared = new Set(edges.map((edge) => edge.id));
|
|
103
171
|
for (const edge of edges) {
|
|
104
|
-
|
|
105
|
-
|
|
172
|
+
for (const parent of edge.needsEdges) {
|
|
173
|
+
if (!declared.has(parent.id)) {
|
|
174
|
+
throw new Error(formatError(ctx, `stage "${edge.id}" has unknown needs "${parent.id}"`));
|
|
175
|
+
}
|
|
106
176
|
}
|
|
107
177
|
}
|
|
108
178
|
}
|
|
@@ -114,10 +184,10 @@ function detectCycle(edges, ctx) {
|
|
|
114
184
|
children.set(edge.id, []);
|
|
115
185
|
}
|
|
116
186
|
for (const edge of edges) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
for (const parent of edge.needsEdges) {
|
|
188
|
+
indegree.set(edge.id, (indegree.get(edge.id) ?? 0) + 1);
|
|
189
|
+
children.get(parent.id)?.push(edge.id);
|
|
190
|
+
}
|
|
121
191
|
}
|
|
122
192
|
const queue = [];
|
|
123
193
|
for (const [id, degree] of indegree) {
|
|
@@ -140,19 +210,31 @@ function detectCycle(edges, ctx) {
|
|
|
140
210
|
}
|
|
141
211
|
}
|
|
142
212
|
function computeAncestors(edges) {
|
|
143
|
-
const
|
|
213
|
+
const parentsById = new Map(edges.map((edge) => [edge.id, edge.needsEdges]));
|
|
144
214
|
const ancestorsById = new Map();
|
|
145
215
|
function ancestorsFor(id) {
|
|
146
216
|
const cached = ancestorsById.get(id);
|
|
147
217
|
if (cached)
|
|
148
218
|
return cached;
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
219
|
+
const parents = parentsById.get(id) ?? [];
|
|
220
|
+
if (parents.length === 0) {
|
|
151
221
|
ancestorsById.set(id, []);
|
|
152
222
|
return [];
|
|
153
223
|
}
|
|
154
|
-
const
|
|
155
|
-
const ancestors = [
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
const ancestors = [];
|
|
226
|
+
for (const parent of parents) {
|
|
227
|
+
for (const ancestor of ancestorsFor(parent.id)) {
|
|
228
|
+
if (seen.has(ancestor))
|
|
229
|
+
continue;
|
|
230
|
+
seen.add(ancestor);
|
|
231
|
+
ancestors.push(ancestor);
|
|
232
|
+
}
|
|
233
|
+
if (!seen.has(parent.id)) {
|
|
234
|
+
seen.add(parent.id);
|
|
235
|
+
ancestors.push(parent.id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
156
238
|
ancestorsById.set(id, ancestors);
|
|
157
239
|
return ancestors;
|
|
158
240
|
}
|
|
@@ -170,16 +252,16 @@ function topologicalSort(edges) {
|
|
|
170
252
|
children.set(edge.id, []);
|
|
171
253
|
}
|
|
172
254
|
for (const edge of edges) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
255
|
+
for (const parent of edge.needsEdges) {
|
|
256
|
+
indegree.set(edge.id, (indegree.get(edge.id) ?? 0) + 1);
|
|
257
|
+
children.get(parent.id)?.push(edge.id);
|
|
258
|
+
}
|
|
177
259
|
}
|
|
178
260
|
for (const [, childIds] of children) {
|
|
179
261
|
childIds.sort((a, b) => (byId.get(a)?.stageIndex ?? 0) - (byId.get(b)?.stageIndex ?? 0));
|
|
180
262
|
}
|
|
181
263
|
const roots = edges
|
|
182
|
-
.filter((edge) => edge.
|
|
264
|
+
.filter((edge) => edge.needsEdges.length === 0)
|
|
183
265
|
.sort((a, b) => a.stageIndex - b.stageIndex)
|
|
184
266
|
.map((edge) => edge.id);
|
|
185
267
|
const sorted = [];
|
|
@@ -206,8 +288,8 @@ function buildResolvedPipelineDag(edges) {
|
|
|
206
288
|
childrenOf[edge.id] = [];
|
|
207
289
|
}
|
|
208
290
|
for (const edge of edges) {
|
|
209
|
-
|
|
210
|
-
childrenOf[
|
|
291
|
+
for (const parent of edge.needsEdges) {
|
|
292
|
+
childrenOf[parent.id].push(edge.id);
|
|
211
293
|
}
|
|
212
294
|
}
|
|
213
295
|
for (const id of Object.keys(childrenOf)) {
|
|
@@ -215,12 +297,13 @@ function buildResolvedPipelineDag(edges) {
|
|
|
215
297
|
(edges.find((edge) => edge.id === b)?.stageIndex ?? 0));
|
|
216
298
|
}
|
|
217
299
|
const roots = edges
|
|
218
|
-
.filter((edge) => edge.
|
|
300
|
+
.filter((edge) => edge.needsEdges.length === 0)
|
|
219
301
|
.sort((a, b) => a.stageIndex - b.stageIndex)
|
|
220
302
|
.map((edge) => edge.id);
|
|
221
303
|
const nodes = sortedEdges.map((edge) => ({
|
|
222
304
|
id: edge.id,
|
|
223
305
|
needs: edge.needs,
|
|
306
|
+
needsEdges: edge.needsEdges,
|
|
224
307
|
ancestors: ancestorsById.get(edge.id) ?? [],
|
|
225
308
|
stageIndex: edge.stageIndex,
|
|
226
309
|
...(edge.fork !== undefined
|
|
@@ -229,6 +312,10 @@ function buildResolvedPipelineDag(edges) {
|
|
|
229
312
|
...(edge.clonable === true
|
|
230
313
|
? { clonable: true, clone_cap: edge.clone_cap ?? 5 }
|
|
231
314
|
: {}),
|
|
315
|
+
...(edge.completion !== undefined ? { completion: edge.completion } : {}),
|
|
316
|
+
...(edge.recovery !== undefined ? { recovery: edge.recovery } : {}),
|
|
317
|
+
...(edge.feedback_loop !== undefined ? { feedback_loop: edge.feedback_loop } : {}),
|
|
318
|
+
...(edge.replay_safe !== undefined ? { replay_safe: edge.replay_safe } : {}),
|
|
232
319
|
}));
|
|
233
320
|
return { nodes, roots, childrenOf };
|
|
234
321
|
}
|
|
@@ -259,6 +346,34 @@ function validateClonableFields(edges, dag, ctx) {
|
|
|
259
346
|
}
|
|
260
347
|
}
|
|
261
348
|
}
|
|
349
|
+
function validateFeedbackLoopFields(dag, ctx) {
|
|
350
|
+
const byId = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
351
|
+
for (const source of dag.nodes) {
|
|
352
|
+
const policy = source.feedback_loop;
|
|
353
|
+
if (!policy)
|
|
354
|
+
continue;
|
|
355
|
+
if (source.clonable === true) {
|
|
356
|
+
throw new Error(formatError(ctx, `stage "${source.id}": feedback_loop source cannot be clonable`));
|
|
357
|
+
}
|
|
358
|
+
const targetId = policy.target;
|
|
359
|
+
const target = byId.get(targetId);
|
|
360
|
+
if (!target) {
|
|
361
|
+
throw new Error(formatError(ctx, `stage "${source.id}": feedback_loop target "${targetId}" is not declared`));
|
|
362
|
+
}
|
|
363
|
+
if (!source.ancestors.includes(targetId)) {
|
|
364
|
+
throw new Error(formatError(ctx, `stage "${source.id}": feedback_loop target "${targetId}" must be an earlier ancestor`));
|
|
365
|
+
}
|
|
366
|
+
if (target.clonable === true) {
|
|
367
|
+
throw new Error(formatError(ctx, `stage "${source.id}": feedback_loop target "${targetId}" cannot be clonable`));
|
|
368
|
+
}
|
|
369
|
+
const targetIndex = source.ancestors.indexOf(targetId);
|
|
370
|
+
const route = [...source.ancestors.slice(targetIndex), source.id];
|
|
371
|
+
const unsafeStage = route.find((stageId) => byId.get(stageId)?.replay_safe === false);
|
|
372
|
+
if (unsafeStage !== undefined) {
|
|
373
|
+
throw new Error(formatError(ctx, `stage "${source.id}": feedback_loop target "${targetId}" replays replay_safe: false stage "${unsafeStage}"`));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
262
377
|
export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
263
378
|
const edges = normalizeToEdges(refs);
|
|
264
379
|
detectDuplicateIds(edges, ctx);
|
|
@@ -271,6 +386,7 @@ export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
|
271
386
|
const dag = buildResolvedPipelineDag(edges);
|
|
272
387
|
validateForkFields(edges, dag, ctx);
|
|
273
388
|
validateClonableFields(edges, dag, ctx);
|
|
389
|
+
validateFeedbackLoopFields(dag, ctx);
|
|
274
390
|
return { stages, dag };
|
|
275
391
|
}
|
|
276
392
|
export function resolvePipelineDag(rawStages, ctx) {
|
|
@@ -293,12 +409,40 @@ export function areResolvedDagsEquivalent(a, b) {
|
|
|
293
409
|
const nodeB = byIdB.get(id);
|
|
294
410
|
if (nodeA.needs !== nodeB.needs)
|
|
295
411
|
return false;
|
|
412
|
+
const edgesA = predecessorEdges(nodeA);
|
|
413
|
+
const edgesB = predecessorEdges(nodeB);
|
|
414
|
+
if (edgesA.length !== edgesB.length)
|
|
415
|
+
return false;
|
|
416
|
+
for (let i = 0; i < edgesA.length; i++) {
|
|
417
|
+
if (edgesA[i].id !== edgesB[i].id)
|
|
418
|
+
return false;
|
|
419
|
+
if (edgesA[i].on.length !== edgesB[i].on.length)
|
|
420
|
+
return false;
|
|
421
|
+
for (let j = 0; j < edgesA[i].on.length; j++) {
|
|
422
|
+
if (edgesA[i].on[j] !== edgesB[i].on[j])
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
296
426
|
if (nodeA.ancestors.length !== nodeB.ancestors.length)
|
|
297
427
|
return false;
|
|
298
428
|
for (let i = 0; i < nodeA.ancestors.length; i++) {
|
|
299
429
|
if (nodeA.ancestors[i] !== nodeB.ancestors[i])
|
|
300
430
|
return false;
|
|
301
431
|
}
|
|
432
|
+
if ((nodeA.replay_safe ?? true) !== (nodeB.replay_safe ?? true))
|
|
433
|
+
return false;
|
|
434
|
+
if (nodeA.feedback_loop?.max_replays !== nodeB.feedback_loop?.max_replays) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if (nodeA.feedback_loop?.on_max_replays !==
|
|
438
|
+
nodeB.feedback_loop?.on_max_replays) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (nodeA.feedback_loop?.replay_session !== nodeB.feedback_loop?.replay_session) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (nodeA.feedback_loop?.target !== nodeB.feedback_loop?.target)
|
|
445
|
+
return false;
|
|
302
446
|
}
|
|
303
447
|
return true;
|
|
304
448
|
}
|
|
@@ -321,9 +465,8 @@ export function extractPipelineStageIds(rawStages) {
|
|
|
321
465
|
if (typeof entry.id !== "string" || !entry.id)
|
|
322
466
|
return null;
|
|
323
467
|
if (entry.needs !== undefined) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (typeof entry.needs !== "string" || !entry.needs)
|
|
468
|
+
const parsedNeeds = parsePipelineNeeds(entry.needs, entry.id);
|
|
469
|
+
if (!parsedNeeds.ok)
|
|
327
470
|
return null;
|
|
328
471
|
}
|
|
329
472
|
stageIds.push(entry.id);
|
|
@@ -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>;
|
|
@@ -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.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "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" | "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";
|
|
6
6
|
export type ValidationFinding = {
|
|
7
7
|
severity: ValidationSeverity;
|
|
8
8
|
code: ValidationFindingCode | string;
|