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,292 @@
|
|
|
1
|
+
import { StageMcpError, resolveStageMcpServers, } from "../config/resolveStageMcpServers.js";
|
|
2
|
+
import * as piIsolatedMcp from "./piIsolatedMcp.js";
|
|
3
|
+
export const PROJECT_MCP_PROBE_TIMEOUT_MS = 30_000;
|
|
4
|
+
const MCP_STATUS_EVENT = "pi-mcp-adapter/status/v1";
|
|
5
|
+
function scrub(message, secrets) {
|
|
6
|
+
let out = message;
|
|
7
|
+
for (const secret of secrets) {
|
|
8
|
+
if (secret.length === 0)
|
|
9
|
+
continue;
|
|
10
|
+
out = out.split(secret).join("[redacted]");
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function collectInterpolatedSecrets(entry) {
|
|
15
|
+
const secrets = [];
|
|
16
|
+
for (const field of ["env", "headers"]) {
|
|
17
|
+
const value = entry[field];
|
|
18
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const item of Object.values(value)) {
|
|
22
|
+
if (typeof item === "string" && item.length > 0) {
|
|
23
|
+
secrets.push(item);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return secrets;
|
|
28
|
+
}
|
|
29
|
+
function catalogErrorResult(name, err) {
|
|
30
|
+
if (!(err instanceof StageMcpError))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (err.code === "unresolved_var") {
|
|
33
|
+
return { name, status: "unresolved_var", error: err.message };
|
|
34
|
+
}
|
|
35
|
+
if (err.code === "missing_catalog") {
|
|
36
|
+
return { name, status: "missing_catalog" };
|
|
37
|
+
}
|
|
38
|
+
if (err.code === "invalid_config" ||
|
|
39
|
+
err.code === "reserved_name" ||
|
|
40
|
+
err.code === "unknown_server") {
|
|
41
|
+
return { name, status: "invalid_config" };
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function asStatusServers(data) {
|
|
46
|
+
if (data === null || typeof data !== "object")
|
|
47
|
+
return undefined;
|
|
48
|
+
const servers = data.servers;
|
|
49
|
+
if (!Array.isArray(servers))
|
|
50
|
+
return undefined;
|
|
51
|
+
const parsed = [];
|
|
52
|
+
for (const entry of servers) {
|
|
53
|
+
if (entry === null || typeof entry !== "object")
|
|
54
|
+
continue;
|
|
55
|
+
const name = entry.name;
|
|
56
|
+
const status = entry.status;
|
|
57
|
+
if (typeof name === "string" && typeof status === "string") {
|
|
58
|
+
parsed.push({ name, status });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
function mapRuntimeStatus(status) {
|
|
64
|
+
if (status === "connected" || status === "cached")
|
|
65
|
+
return "connected";
|
|
66
|
+
if (status === "needs-auth")
|
|
67
|
+
return "needs_auth";
|
|
68
|
+
if (status === "failed" || status === "disabled")
|
|
69
|
+
return "connect_failed";
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function createProbeExtensionApi(eventBus) {
|
|
73
|
+
const handlers = new Map();
|
|
74
|
+
const api = {
|
|
75
|
+
events: eventBus,
|
|
76
|
+
on(event, handler) {
|
|
77
|
+
const list = handlers.get(event) ?? [];
|
|
78
|
+
list.push(handler);
|
|
79
|
+
handlers.set(event, list);
|
|
80
|
+
},
|
|
81
|
+
registerTool() { },
|
|
82
|
+
registerCommand() { },
|
|
83
|
+
registerShortcut() { },
|
|
84
|
+
registerFlag() { },
|
|
85
|
+
getFlag() {
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
registerMessageRenderer() { },
|
|
89
|
+
registerMarkdownTransformer() { },
|
|
90
|
+
registerEntryRenderer() { },
|
|
91
|
+
sendMessage() { },
|
|
92
|
+
sendUserMessage() { },
|
|
93
|
+
appendEntry() { },
|
|
94
|
+
setSessionName() { },
|
|
95
|
+
getSessionName() {
|
|
96
|
+
return undefined;
|
|
97
|
+
},
|
|
98
|
+
setLabel() { },
|
|
99
|
+
async exec() {
|
|
100
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
101
|
+
},
|
|
102
|
+
getActiveTools() {
|
|
103
|
+
return [];
|
|
104
|
+
},
|
|
105
|
+
setActiveTools() { },
|
|
106
|
+
getAllTools() {
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
async function emit(event, ...args) {
|
|
111
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
112
|
+
await handler(...args);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
api: api,
|
|
117
|
+
emitSessionStart(cwd) {
|
|
118
|
+
void emit("session_start", {}, { mode: "print", hasUI: false, cwd });
|
|
119
|
+
},
|
|
120
|
+
emitSessionShutdown() {
|
|
121
|
+
return emit("session_shutdown", {});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function instantiateProbeFactories(attached, projectRoot) {
|
|
126
|
+
const factories = attached.extensionFactories ?? [];
|
|
127
|
+
const eventBus = attached.eventBus;
|
|
128
|
+
if (factories.length === 0 || eventBus === undefined) {
|
|
129
|
+
return async () => { };
|
|
130
|
+
}
|
|
131
|
+
const probeApi = createProbeExtensionApi(eventBus);
|
|
132
|
+
for (const ext of factories) {
|
|
133
|
+
const factory = typeof ext === "function" ? ext : ext.factory;
|
|
134
|
+
factory(probeApi.api);
|
|
135
|
+
}
|
|
136
|
+
probeApi.emitSessionStart(projectRoot);
|
|
137
|
+
return () => probeApi.emitSessionShutdown();
|
|
138
|
+
}
|
|
139
|
+
function mapConnectingError(err, aborted) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
if (aborted || /cancelled/i.test(message)) {
|
|
142
|
+
return { kind: "cancelled" };
|
|
143
|
+
}
|
|
144
|
+
if (/needs-auth/.test(message)) {
|
|
145
|
+
return { kind: "mapped", status: "needs_auth" };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
kind: "mapped",
|
|
149
|
+
status: "connect_failed",
|
|
150
|
+
error: message.length > 0 ? message : undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function waitForProbeStatus(eventBus, serverName, connecting, options) {
|
|
154
|
+
if (options.signal?.aborted) {
|
|
155
|
+
return { kind: "cancelled" };
|
|
156
|
+
}
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
let unsubscribe;
|
|
160
|
+
let timer;
|
|
161
|
+
const finish = (result) => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
settled = true;
|
|
165
|
+
unsubscribe?.();
|
|
166
|
+
if (timer !== undefined)
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
169
|
+
resolve(result);
|
|
170
|
+
};
|
|
171
|
+
const onAbort = () => finish({ kind: "cancelled" });
|
|
172
|
+
const consider = (data) => {
|
|
173
|
+
const servers = asStatusServers(data);
|
|
174
|
+
if (servers === undefined)
|
|
175
|
+
return;
|
|
176
|
+
const entry = servers.find((server) => server.name === serverName);
|
|
177
|
+
if (entry === undefined)
|
|
178
|
+
return;
|
|
179
|
+
const mapped = mapRuntimeStatus(entry.status);
|
|
180
|
+
if (mapped === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (mapped === "connect_failed") {
|
|
183
|
+
finish({
|
|
184
|
+
kind: "mapped",
|
|
185
|
+
status: "connect_failed",
|
|
186
|
+
error: `MCP server "${serverName}" failed to connect (status: ${entry.status})`,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
finish({ kind: "mapped", status: mapped });
|
|
191
|
+
};
|
|
192
|
+
const handler = (data) => consider(data);
|
|
193
|
+
const maybeUnsub = eventBus.on(MCP_STATUS_EVENT, handler);
|
|
194
|
+
unsubscribe = typeof maybeUnsub === "function" ? maybeUnsub : undefined;
|
|
195
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
196
|
+
void (connecting ?? Promise.resolve()).then(() => finish({ kind: "mapped", status: "connected" }), (err) => finish(mapConnectingError(err, options.signal?.aborted === true)));
|
|
197
|
+
timer = setTimeout(() => {
|
|
198
|
+
finish({
|
|
199
|
+
kind: "mapped",
|
|
200
|
+
status: "connect_failed",
|
|
201
|
+
error: `MCP server "${serverName}" failed to connect (status: timed out)`,
|
|
202
|
+
});
|
|
203
|
+
}, options.timeoutMs);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export async function probeProjectMcpServer(options) {
|
|
207
|
+
const name = options.name;
|
|
208
|
+
const timeoutMs = options.timeoutMs ?? PROJECT_MCP_PROBE_TIMEOUT_MS;
|
|
209
|
+
const env = options.env ?? process.env;
|
|
210
|
+
if (options.signal?.aborted) {
|
|
211
|
+
return { name, status: "cancelled" };
|
|
212
|
+
}
|
|
213
|
+
let snapshot;
|
|
214
|
+
try {
|
|
215
|
+
snapshot = await resolveStageMcpServers({
|
|
216
|
+
projectRoot: options.projectRoot,
|
|
217
|
+
allowlist: [name],
|
|
218
|
+
env,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return (catalogErrorResult(name, err) ?? {
|
|
223
|
+
name,
|
|
224
|
+
status: "invalid_config",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const resolved = snapshot[name];
|
|
228
|
+
if (resolved === undefined) {
|
|
229
|
+
return { name, status: "invalid_config" };
|
|
230
|
+
}
|
|
231
|
+
const secrets = collectInterpolatedSecrets(resolved);
|
|
232
|
+
let attached;
|
|
233
|
+
let shutdownFactories;
|
|
234
|
+
const dispose = async () => {
|
|
235
|
+
if (attached?.connecting !== undefined) {
|
|
236
|
+
void attached.connecting.then(() => undefined, () => undefined);
|
|
237
|
+
}
|
|
238
|
+
attached?.cancel?.();
|
|
239
|
+
if (shutdownFactories !== undefined) {
|
|
240
|
+
const stop = shutdownFactories;
|
|
241
|
+
shutdownFactories = undefined;
|
|
242
|
+
await stop();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
if (options.signal?.aborted) {
|
|
247
|
+
return { name, status: "cancelled" };
|
|
248
|
+
}
|
|
249
|
+
attached = await piIsolatedMcp.attachIsolatedMcp(snapshot, {
|
|
250
|
+
timeoutMs,
|
|
251
|
+
lifecycle: "eager",
|
|
252
|
+
});
|
|
253
|
+
if (attached.eventBus === undefined) {
|
|
254
|
+
return { name, status: "invalid_config" };
|
|
255
|
+
}
|
|
256
|
+
shutdownFactories = instantiateProbeFactories(attached, options.projectRoot);
|
|
257
|
+
const waited = await waitForProbeStatus(attached.eventBus, name, attached.connecting, { timeoutMs, signal: options.signal });
|
|
258
|
+
if (waited.kind === "cancelled") {
|
|
259
|
+
return { name, status: "cancelled" };
|
|
260
|
+
}
|
|
261
|
+
if (waited.status === "connect_failed") {
|
|
262
|
+
return {
|
|
263
|
+
name,
|
|
264
|
+
status: "connect_failed",
|
|
265
|
+
...(waited.error !== undefined
|
|
266
|
+
? { error: scrub(waited.error, secrets) }
|
|
267
|
+
: {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { name, status: waited.status };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (options.signal?.aborted) {
|
|
274
|
+
return { name, status: "cancelled" };
|
|
275
|
+
}
|
|
276
|
+
const catalog = catalogErrorResult(name, err);
|
|
277
|
+
if (catalog)
|
|
278
|
+
return catalog;
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
if (/cancelled/i.test(message)) {
|
|
281
|
+
return { name, status: "cancelled" };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
name,
|
|
285
|
+
status: "connect_failed",
|
|
286
|
+
error: scrub(message, secrets),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
await dispose();
|
|
291
|
+
}
|
|
292
|
+
}
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
|
|
2
2
|
import type { CompletionContract } from "../types/completion.js";
|
|
3
3
|
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { LoadedStageConfig } from "../types/stage.js";
|
|
5
5
|
import type { FeedbackLoopConfig } from "../types/pipeline.js";
|
|
6
6
|
import type { TaskFile } from "../types/task.js";
|
|
7
7
|
import type { StageActivityEvent } from "./activity.js";
|
|
8
|
+
import type { ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
|
|
8
9
|
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
9
10
|
import type { QaExchange } from "../hitl/qaTrail.js";
|
|
10
11
|
/** Opaque to runtime; adapters interpret. */
|
|
@@ -44,7 +45,7 @@ export type FeedbackLoopContext = {
|
|
|
44
45
|
};
|
|
45
46
|
export type StageRunInput = {
|
|
46
47
|
roots: StageRoots;
|
|
47
|
-
stage:
|
|
48
|
+
stage: LoadedStageConfig;
|
|
48
49
|
/** Runtime instance id (`work~2`). Defaults to `stage.id` when omitted. */
|
|
49
50
|
stageId?: string;
|
|
50
51
|
task: TaskFile;
|
|
@@ -52,6 +53,7 @@ export type StageRunInput = {
|
|
|
52
53
|
priorEnvelopes?: StageEnvelope[];
|
|
53
54
|
priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>;
|
|
54
55
|
timeoutMs?: number;
|
|
56
|
+
resolvedMcpServers?: ResolvedMcpServers;
|
|
55
57
|
resumeToken?: StageResumeToken;
|
|
56
58
|
/** Launch/session mode; omitted is treated as `"fresh"`. */
|
|
57
59
|
sessionMode?: StageSessionMode;
|
package/dist/agent/port.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare const STAGEFLOW_YAML = "version: 1\ncatalog:\n pipelines:\n - pipelines\n tasks:\n - tasks\n patterns:\n pipeline: \"*.pipeline.yaml\"\n task: \"*.task.yaml\"\n";
|
|
2
|
-
export declare const HELLO_PIPELINE_YAML = "id: hello\nstages:\n - id: hello\n system_prompt: Say hello and emit a success envelope.\n
|
|
2
|
+
export declare const HELLO_PIPELINE_YAML = "id: hello\nmodel: anthropic/claude-sonnet-4-5\nstages:\n - id: hello\n system_prompt: Say hello and emit a success envelope.\n";
|
|
3
3
|
export declare const HELLO_TASK_YAML = "id: hello\ngoal: Run the hello pipeline scaffold.\n";
|
|
@@ -9,10 +9,10 @@ catalog:
|
|
|
9
9
|
task: "*.task.yaml"
|
|
10
10
|
`;
|
|
11
11
|
export const HELLO_PIPELINE_YAML = `id: hello
|
|
12
|
+
model: anthropic/claude-sonnet-4-5
|
|
12
13
|
stages:
|
|
13
14
|
- id: hello
|
|
14
15
|
system_prompt: Say hello and emit a success envelope.
|
|
15
|
-
model: anthropic/claude-sonnet-4-5
|
|
16
16
|
`;
|
|
17
17
|
export const HELLO_TASK_YAML = `id: hello
|
|
18
18
|
goal: Run the hello pipeline scaffold.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const MIGRATE_YAML_USAGE = "Usage:\n sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]\n\n Convert legacy catalog YAML to target YAML (io / verify / on_verify_fail).\n Dry-run is the default. --write applies changes. Path may be a pipeline,\n stage, task, or catalog root. Does not rewrite .stageflow snapshots.";
|
|
2
|
+
export type MigrateYamlCommandIo = {
|
|
3
|
+
log: (line: string) => void;
|
|
4
|
+
error: (line: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export type MigrateYamlJson = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
write: boolean;
|
|
9
|
+
planned: string[];
|
|
10
|
+
written: string[];
|
|
11
|
+
skipped: string[];
|
|
12
|
+
errors: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function runMigrateYamlCommand(args: string[], options?: {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
projectRoot?: string;
|
|
17
|
+
io?: Partial<MigrateYamlCommandIo>;
|
|
18
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { applyMigrateYamlPlan, fileDirtyVsHead, planMigrateYaml, } from "../config/migrateYaml.js";
|
|
3
|
+
export const MIGRATE_YAML_USAGE = `Usage:
|
|
4
|
+
sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
|
|
5
|
+
|
|
6
|
+
Convert legacy catalog YAML to target YAML (io / verify / on_verify_fail).
|
|
7
|
+
Dry-run is the default. --write applies changes. Path may be a pipeline,
|
|
8
|
+
stage, task, or catalog root. Does not rewrite .stageflow snapshots.`;
|
|
9
|
+
const defaultIo = {
|
|
10
|
+
log: (line) => console.log(line),
|
|
11
|
+
error: (line) => console.error(line),
|
|
12
|
+
};
|
|
13
|
+
function parseMigrateYamlArgs(args) {
|
|
14
|
+
if (args.length === 0) {
|
|
15
|
+
return { help: false, write: false, json: false, force: false };
|
|
16
|
+
}
|
|
17
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
18
|
+
return { help: true, write: false, json: false, force: false };
|
|
19
|
+
}
|
|
20
|
+
let target;
|
|
21
|
+
let root;
|
|
22
|
+
let write = false;
|
|
23
|
+
let json = false;
|
|
24
|
+
let force = false;
|
|
25
|
+
let help = false;
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const arg = args[i];
|
|
28
|
+
if (arg === "--help" || arg === "-h") {
|
|
29
|
+
help = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === "--write") {
|
|
32
|
+
write = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === "--json") {
|
|
35
|
+
json = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "--force") {
|
|
38
|
+
force = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === "--root") {
|
|
41
|
+
const value = args[++i];
|
|
42
|
+
if (value === undefined || value.length === 0) {
|
|
43
|
+
throw new Error("Missing value for --root");
|
|
44
|
+
}
|
|
45
|
+
root = value;
|
|
46
|
+
}
|
|
47
|
+
else if (arg.startsWith("-")) {
|
|
48
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
else if (target !== undefined) {
|
|
51
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
target = arg;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (target !== undefined && root !== undefined) {
|
|
58
|
+
throw new Error("Use at most one of a positional path or --root");
|
|
59
|
+
}
|
|
60
|
+
return { help, target: target ?? root, write, json, force };
|
|
61
|
+
}
|
|
62
|
+
function planToJson(plan, options) {
|
|
63
|
+
return {
|
|
64
|
+
ok: options.ok,
|
|
65
|
+
write: options.write,
|
|
66
|
+
planned: plan.writes.map((item) => item.file),
|
|
67
|
+
written: options.written,
|
|
68
|
+
skipped: plan.skipped,
|
|
69
|
+
errors: [...plan.errors, ...(options.extraErrors ?? [])],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function formatHuman(plan, write, extraErrors) {
|
|
73
|
+
const lines = [];
|
|
74
|
+
const errors = [...plan.errors, ...extraErrors];
|
|
75
|
+
if (errors.length > 0) {
|
|
76
|
+
lines.push("Migration failed:");
|
|
77
|
+
for (const error of errors)
|
|
78
|
+
lines.push(` ${error}`);
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
if (write) {
|
|
82
|
+
if (plan.writes.length === 0) {
|
|
83
|
+
lines.push("Nothing to write.");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lines.push("Wrote:");
|
|
87
|
+
for (const item of plan.writes)
|
|
88
|
+
lines.push(` ${item.file}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (plan.writes.length === 0) {
|
|
92
|
+
lines.push("Nothing to migrate.");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push("Would write:");
|
|
96
|
+
for (const item of plan.writes)
|
|
97
|
+
lines.push(` ${item.file}`);
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("Dry-run; pass --write to apply.");
|
|
100
|
+
}
|
|
101
|
+
if (plan.skipped.length > 0) {
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push("Skipped:");
|
|
104
|
+
for (const file of plan.skipped)
|
|
105
|
+
lines.push(` ${file}`);
|
|
106
|
+
}
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
export async function runMigrateYamlCommand(args, options = {}) {
|
|
110
|
+
const cwd = options.cwd ?? process.cwd();
|
|
111
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
112
|
+
const out = { ...defaultIo, ...options.io };
|
|
113
|
+
try {
|
|
114
|
+
const parsed = parseMigrateYamlArgs(args);
|
|
115
|
+
if (parsed.help) {
|
|
116
|
+
out.error(MIGRATE_YAML_USAGE);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
const target = path.resolve(cwd, parsed.target ?? ".");
|
|
120
|
+
const plan = await planMigrateYaml(target, { cwd, projectRoot });
|
|
121
|
+
const extraErrors = [];
|
|
122
|
+
if (plan.errors.length > 0) {
|
|
123
|
+
if (parsed.json) {
|
|
124
|
+
out.log(JSON.stringify(planToJson(plan, { write: parsed.write, written: [], ok: false }), null, 2));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
out.error(formatHuman(plan, parsed.write, extraErrors));
|
|
128
|
+
out.error(MIGRATE_YAML_USAGE);
|
|
129
|
+
}
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (parsed.write && !parsed.force) {
|
|
133
|
+
for (const item of plan.writes) {
|
|
134
|
+
if (fileDirtyVsHead(item.absPath)) {
|
|
135
|
+
extraErrors.push(`Refusing to overwrite uncommitted changes in ${item.file} (pass --force)`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (extraErrors.length > 0) {
|
|
139
|
+
if (parsed.json) {
|
|
140
|
+
out.log(JSON.stringify(planToJson(plan, {
|
|
141
|
+
write: true,
|
|
142
|
+
written: [],
|
|
143
|
+
ok: false,
|
|
144
|
+
extraErrors,
|
|
145
|
+
}), null, 2));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
out.error(formatHuman(plan, parsed.write, extraErrors));
|
|
149
|
+
}
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let written = [];
|
|
154
|
+
if (parsed.write) {
|
|
155
|
+
await applyMigrateYamlPlan(plan);
|
|
156
|
+
written = plan.writes.map((item) => item.file);
|
|
157
|
+
}
|
|
158
|
+
if (parsed.json) {
|
|
159
|
+
out.log(JSON.stringify(planToJson(plan, {
|
|
160
|
+
write: parsed.write,
|
|
161
|
+
written,
|
|
162
|
+
ok: true,
|
|
163
|
+
}), null, 2));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
out.log(formatHuman(plan, parsed.write, extraErrors));
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const message = err instanceof Error
|
|
172
|
+
? err.message
|
|
173
|
+
: typeof err === "string"
|
|
174
|
+
? err
|
|
175
|
+
: "Migrate-yaml command failed";
|
|
176
|
+
out.error(message);
|
|
177
|
+
out.error(MIGRATE_YAML_USAGE);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
package/dist/cli/runOutput.js
CHANGED
|
@@ -46,30 +46,38 @@ function formatStartFailureJson(started) {
|
|
|
46
46
|
return formatRunStartFailedJson(started);
|
|
47
47
|
}
|
|
48
48
|
function baseRunCompletionPayload(result) {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const payload = result.outcome === "succeeded"
|
|
50
|
+
? {
|
|
51
51
|
ok: true,
|
|
52
52
|
outcome: "succeeded",
|
|
53
53
|
runId: result.runId,
|
|
54
54
|
runDir: result.runDir,
|
|
55
|
-
}
|
|
55
|
+
}
|
|
56
|
+
: result.outcome === "waiting"
|
|
57
|
+
? {
|
|
58
|
+
ok: false,
|
|
59
|
+
outcome: "waiting",
|
|
60
|
+
runId: result.runId,
|
|
61
|
+
runDir: result.runDir,
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
ok: false,
|
|
65
|
+
outcome: "failed",
|
|
66
|
+
runId: result.runId,
|
|
67
|
+
runDir: result.runDir,
|
|
68
|
+
};
|
|
69
|
+
if (result.outcome === "failed" && result.reason !== undefined) {
|
|
70
|
+
payload.reason = result.reason;
|
|
56
71
|
}
|
|
57
|
-
if (result.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
if (Array.isArray(result.findings) && result.findings.length > 0) {
|
|
73
|
+
payload.findings = result.findings.map((finding) => ({
|
|
74
|
+
severity: finding.severity,
|
|
75
|
+
code: finding.code,
|
|
76
|
+
file: finding.path,
|
|
77
|
+
message: finding.message,
|
|
78
|
+
category: finding.category,
|
|
79
|
+
}));
|
|
64
80
|
}
|
|
65
|
-
const payload = {
|
|
66
|
-
ok: false,
|
|
67
|
-
outcome: "failed",
|
|
68
|
-
runId: result.runId,
|
|
69
|
-
runDir: result.runDir,
|
|
70
|
-
};
|
|
71
|
-
if (result.reason !== undefined)
|
|
72
|
-
payload.reason = result.reason;
|
|
73
81
|
return payload;
|
|
74
82
|
}
|
|
75
83
|
function formatRunCompletionJson(result) {
|
|
@@ -6,6 +6,7 @@ export type ValidateCommandIo = {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function runValidateCommand(args: string[], options?: {
|
|
8
8
|
cwd?: string;
|
|
9
|
+
projectRoot?: string;
|
|
9
10
|
io?: Partial<ValidateCommandIo>;
|
|
10
11
|
validateCatalog?: (options: Parameters<typeof defaultValidateCatalog>[0]) => Promise<ValidationResult>;
|
|
11
12
|
}): Promise<number>;
|
|
@@ -61,6 +61,7 @@ function parseValidateArgs(args) {
|
|
|
61
61
|
}
|
|
62
62
|
export async function runValidateCommand(args, options = {}) {
|
|
63
63
|
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
64
65
|
const out = { ...defaultIo, ...options.io };
|
|
65
66
|
const validateCatalogFn = options.validateCatalog ?? defaultValidateCatalog;
|
|
66
67
|
try {
|
|
@@ -73,6 +74,7 @@ export async function runValidateCommand(args, options = {}) {
|
|
|
73
74
|
const result = await validateCatalogFn({
|
|
74
75
|
scope,
|
|
75
76
|
cwd,
|
|
77
|
+
projectRoot,
|
|
76
78
|
pipeline: parsed.pipeline,
|
|
77
79
|
task: parsed.task,
|
|
78
80
|
strict: parsed.strict,
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
|
|
|
14
14
|
import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
|
|
15
15
|
import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
|
|
16
16
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
17
|
+
import { MIGRATE_YAML_USAGE, runMigrateYamlCommand } from "./cli/migrateYamlCommand.js";
|
|
17
18
|
import { createRunStore } from "./runstore/createStore.js";
|
|
18
19
|
import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
|
|
19
20
|
import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
|
|
@@ -26,6 +27,7 @@ const USAGE = `Usage:
|
|
|
26
27
|
sf init
|
|
27
28
|
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
|
|
28
29
|
sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
|
|
30
|
+
sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
|
|
29
31
|
sf artifact read --run <runId> --path <relPath> [--out <file>]
|
|
30
32
|
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
|
|
31
33
|
sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]
|
|
@@ -64,6 +66,8 @@ ${RUN_USAGE}
|
|
|
64
66
|
|
|
65
67
|
${VALIDATE_USAGE}
|
|
66
68
|
|
|
69
|
+
${MIGRATE_YAML_USAGE}
|
|
70
|
+
|
|
67
71
|
${ARTIFACT_USAGE}
|
|
68
72
|
|
|
69
73
|
${ENVELOPE_USAGE}
|
|
@@ -93,6 +97,7 @@ function parseArgs(argv) {
|
|
|
93
97
|
const command = args[0];
|
|
94
98
|
if (command === "providers" ||
|
|
95
99
|
command === "validate" ||
|
|
100
|
+
command === "migrate-yaml" ||
|
|
96
101
|
command === "run" ||
|
|
97
102
|
command === "init" ||
|
|
98
103
|
command === "artifact" ||
|
|
@@ -275,7 +280,16 @@ async function main(argv) {
|
|
|
275
280
|
return runInitCommand(argv.slice(3), { cwd: ctx.invocationCwd });
|
|
276
281
|
}
|
|
277
282
|
if (parsed.command === "validate") {
|
|
278
|
-
return runValidateCommand(argv.slice(3), {
|
|
283
|
+
return runValidateCommand(argv.slice(3), {
|
|
284
|
+
cwd: ctx.invocationCwd,
|
|
285
|
+
projectRoot: ctx.projectRoot,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (parsed.command === "migrate-yaml") {
|
|
289
|
+
return runMigrateYamlCommand(argv.slice(3), {
|
|
290
|
+
cwd: ctx.invocationCwd,
|
|
291
|
+
projectRoot: ctx.projectRoot,
|
|
292
|
+
});
|
|
279
293
|
}
|
|
280
294
|
if (parsed.command === "run") {
|
|
281
295
|
return runRunCommand(argv.slice(3), {
|
|
@@ -106,7 +106,7 @@ async function listModelsFromManifest(projectRoot, manifest) {
|
|
|
106
106
|
try {
|
|
107
107
|
const loaded = await loadPipeline(filePath, { cwd: projectRoot });
|
|
108
108
|
for (const stage of loaded.stages) {
|
|
109
|
-
if (stage.model
|
|
109
|
+
if (stage.model?.trim()) {
|
|
110
110
|
models.add(stage.model);
|
|
111
111
|
}
|
|
112
112
|
}
|