stageflow 0.19.0 → 0.25.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 +16 -3
- package/dist/a2a/configDiscovery.d.ts +9 -0
- package/dist/a2a/configDiscovery.js +18 -0
- package/dist/a2a/contracts.d.ts +93 -0
- package/dist/a2a/contracts.js +163 -0
- package/dist/a2a/limits.d.ts +14 -0
- package/dist/a2a/limits.js +29 -0
- package/dist/a2a/registry.d.ts +44 -0
- package/dist/a2a/registry.js +159 -0
- package/dist/a2a/server.d.ts +29 -0
- package/dist/a2a/server.js +341 -0
- package/dist/a2a/service.d.ts +97 -0
- package/dist/a2a/service.js +496 -0
- package/dist/a2a/store.d.ts +106 -0
- package/dist/a2a/store.js +245 -0
- package/dist/agent/activity.d.ts +5 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/piAdapter.js +7 -3
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -0
- package/dist/agent/streamLogRedact.d.ts +2 -0
- package/dist/agent/streamLogRedact.js +13 -0
- package/dist/cli/a2aCommand.d.ts +8 -0
- package/dist/cli/a2aCommand.js +131 -0
- package/dist/cli/artifactCommand.js +2 -2
- package/dist/cli/envelopeCommand.js +2 -2
- package/dist/cli/exportRunCommand.js +2 -2
- package/dist/cli/hostClient.d.ts +43 -0
- package/dist/cli/hostClient.js +182 -0
- package/dist/cli/providersCommand.js +5 -3
- package/dist/cli/runCommand.d.ts +6 -5
- package/dist/cli/runCommand.js +23 -31
- package/dist/cli/runOutput.d.ts +1 -1
- package/dist/cli/runStageCommand.d.ts +33 -0
- package/dist/cli/runStageCommand.js +407 -0
- package/dist/cli/runsCommand.d.ts +3 -7
- package/dist/cli/runsCommand.js +33 -94
- package/dist/cli.js +20 -5
- package/dist/config/describePipeline.d.ts +26 -0
- package/dist/config/describePipeline.js +37 -0
- package/dist/config/loadPipeline.d.ts +13 -1
- package/dist/config/loadPipeline.js +34 -4
- package/dist/config/mergePipelineIncludes.d.ts +11 -0
- package/dist/config/mergePipelineIncludes.js +75 -0
- package/dist/config/validateCatalog.d.ts +2 -2
- package/dist/config/validateCatalog.js +24 -17
- package/dist/mcp/catalogTools.js +285 -52
- package/dist/mcp/controlTools.js +77 -19
- package/dist/mcp/deps.d.ts +3 -0
- package/dist/mcp/projectMcpTools.d.ts +3 -0
- package/dist/mcp/projectMcpTools.js +25 -0
- package/dist/mcp/providerTools.d.ts +3 -0
- package/dist/mcp/providerTools.js +17 -0
- package/dist/mcp/readArtifact.d.ts +9 -0
- package/dist/mcp/readArtifact.js +11 -0
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/tailStreamLog.d.ts +8 -0
- package/dist/mcp/tailStreamLog.js +28 -0
- package/dist/mcp/toolResults.d.ts +17 -0
- package/dist/mcp/toolResults.js +15 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/waitRun.js +4 -4
- package/dist/mcp/waitingGates.d.ts +1 -0
- package/dist/mcp/waitingGates.js +3 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +3 -0
- package/dist/projection/projectRun.js +7 -0
- package/dist/runstore/createStore.d.ts +10 -0
- package/dist/runstore/createStore.js +13 -4
- package/dist/runstore/port.d.ts +17 -2
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +6 -0
- package/dist/runstore/submission.d.ts +11 -0
- package/dist/runstore/submission.js +7 -0
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +3 -0
- package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
- package/dist/runtime/feedbackLoopCoordinator.js +50 -20
- package/dist/runtime/feedbackLoopDecision.js +33 -1
- package/dist/runtime/pipelineRunner.d.ts +5 -3
- package/dist/runtime/pipelineRunner.js +6 -1
- package/dist/runtime/pipelineScheduler.js +32 -15
- package/dist/runtime/reloadRunCatalog.js +7 -1
- package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
- package/dist/runtime/resolveEnvelopeRef.js +79 -0
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/runManager.d.ts +24 -2
- package/dist/runtime/runManager.js +83 -19
- package/dist/runtime/settingsFile.d.ts +2 -0
- package/dist/runtime/settingsFile.js +19 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
- package/dist/runtime/stageAttemptBootstrap.js +8 -2
- package/dist/runtime/stageStreamLog.d.ts +17 -0
- package/dist/runtime/stageStreamLog.js +89 -0
- package/dist/runtime/stageWorker.js +2 -1
- package/dist/server/bootstrap.d.ts +2 -0
- package/dist/server/bootstrap.js +22 -4
- package/dist/server/createHttpHost.js +6 -0
- package/dist/server/ensureGlobalService.d.ts +35 -0
- package/dist/server/ensureGlobalService.js +138 -0
- package/dist/server/http.d.ts +20 -1
- package/dist/server/http.js +450 -402
- package/dist/server/mcpHost.js +7 -8
- package/dist/server/operatorResults.d.ts +10 -0
- package/dist/server/operatorResults.js +24 -0
- package/dist/server/providerRoutes.d.ts +0 -6
- package/dist/server/providerRoutes.js +10 -18
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
- package/dist/ui/index.html +1 -1
- package/package.json +2 -1
- package/skills/stageflow/references/control-surface.md +2 -2
- package/skills/stageflow-run/SKILL.md +27 -9
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
2
|
+
import { ensureGlobalService, hostBaseUrl, } from "../server/ensureGlobalService.js";
|
|
3
|
+
import { resolveAbsolute } from "./hostClient.js";
|
|
4
|
+
export const RUN_STAGE_USAGE = `Usage:
|
|
5
|
+
sf run-stage (--stage <path> | --stage-inline '<json>') (--task <path> | --task-inline '<json>' | --envelope-ref <runId>:<stageId>[:<attempt>] [--envelope-ref ...]) [--checkout <path>] [--model <id>] [--blocking] [--timeout-ms <n>] [--json]
|
|
6
|
+
|
|
7
|
+
Run a single stage directly against the shared Stageflow service, without authoring a pipeline file. Distinct from the internal-only, worker-process-only \`sf internal run-stage\`.
|
|
8
|
+
--stage <path> Filesystem path to a catalog stage YAML file
|
|
9
|
+
--stage-inline <json> Inline stage body object ({ id, system_prompt, io, ... } — no uses:/route/pipeline wrapper)
|
|
10
|
+
--task <path> Filesystem path to a catalog task YAML file
|
|
11
|
+
--task-inline <json> Inline task object ({ id, goal, ... })
|
|
12
|
+
--envelope-ref <ref> Resolve a previously stored StageEnvelope as this stage's input instead of a task: <runId>:<stageId>[:<attempt>]. Repeat to pass more than one — each resolved payload is namespaced under its stageId in input (disambiguated by runId on a stageId collision), and summaries are combined into goal.
|
|
13
|
+
--checkout <path> Optional checkout to use with --envelope-ref (--task/--task-inline carry their own checkout)
|
|
14
|
+
--model <id> Override the model/backend for this call only, ahead of the stage's own declared model
|
|
15
|
+
--blocking Wait for the run to reach a terminal or waiting state and print the result in this same call, instead of returning immediately with just the run id
|
|
16
|
+
--timeout-ms <n> Wait budget in ms when --blocking is set
|
|
17
|
+
--json Machine-readable JSON output`;
|
|
18
|
+
const defaultIo = {
|
|
19
|
+
log: (line) => console.log(line),
|
|
20
|
+
error: (line) => console.error(line),
|
|
21
|
+
};
|
|
22
|
+
function parseRunStageCliArgs(args) {
|
|
23
|
+
if (args.length === 0) {
|
|
24
|
+
return { help: false, json: false, blocking: false };
|
|
25
|
+
}
|
|
26
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
27
|
+
return { help: true, json: false, blocking: false };
|
|
28
|
+
}
|
|
29
|
+
let stage;
|
|
30
|
+
let stageInline;
|
|
31
|
+
let task;
|
|
32
|
+
let taskInline;
|
|
33
|
+
let envelopeRef;
|
|
34
|
+
let checkout;
|
|
35
|
+
let model;
|
|
36
|
+
let timeoutMs;
|
|
37
|
+
let json = false;
|
|
38
|
+
let blocking = false;
|
|
39
|
+
let help = false;
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg === "--help" || arg === "-h") {
|
|
43
|
+
help = true;
|
|
44
|
+
}
|
|
45
|
+
else if (arg === "--stage") {
|
|
46
|
+
const value = args[++i];
|
|
47
|
+
if (value === undefined || value.length === 0) {
|
|
48
|
+
throw new Error("Missing value for --stage");
|
|
49
|
+
}
|
|
50
|
+
stage = value;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--stage-inline") {
|
|
53
|
+
const value = args[++i];
|
|
54
|
+
if (value === undefined || value.length === 0) {
|
|
55
|
+
throw new Error("Missing value for --stage-inline");
|
|
56
|
+
}
|
|
57
|
+
stageInline = value;
|
|
58
|
+
}
|
|
59
|
+
else if (arg === "--task") {
|
|
60
|
+
const value = args[++i];
|
|
61
|
+
if (value === undefined || value.length === 0) {
|
|
62
|
+
throw new Error("Missing value for --task");
|
|
63
|
+
}
|
|
64
|
+
task = value;
|
|
65
|
+
}
|
|
66
|
+
else if (arg === "--task-inline") {
|
|
67
|
+
const value = args[++i];
|
|
68
|
+
if (value === undefined || value.length === 0) {
|
|
69
|
+
throw new Error("Missing value for --task-inline");
|
|
70
|
+
}
|
|
71
|
+
taskInline = value;
|
|
72
|
+
}
|
|
73
|
+
else if (arg === "--envelope-ref") {
|
|
74
|
+
const value = args[++i];
|
|
75
|
+
if (value === undefined || value.length === 0) {
|
|
76
|
+
throw new Error("Missing value for --envelope-ref");
|
|
77
|
+
}
|
|
78
|
+
envelopeRef = [...(envelopeRef ?? []), value];
|
|
79
|
+
}
|
|
80
|
+
else if (arg === "--checkout") {
|
|
81
|
+
const value = args[++i];
|
|
82
|
+
if (value === undefined || value.length === 0) {
|
|
83
|
+
throw new Error("Missing value for --checkout");
|
|
84
|
+
}
|
|
85
|
+
checkout = value;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === "--model") {
|
|
88
|
+
const value = args[++i];
|
|
89
|
+
if (value === undefined || value.length === 0) {
|
|
90
|
+
throw new Error("Missing value for --model");
|
|
91
|
+
}
|
|
92
|
+
model = value;
|
|
93
|
+
}
|
|
94
|
+
else if (arg === "--timeout-ms") {
|
|
95
|
+
const value = args[++i];
|
|
96
|
+
if (value === undefined || value.length === 0) {
|
|
97
|
+
throw new Error("Missing value for --timeout-ms");
|
|
98
|
+
}
|
|
99
|
+
const parsed = Number(value);
|
|
100
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
101
|
+
throw new Error(`Invalid --timeout-ms: ${value}`);
|
|
102
|
+
}
|
|
103
|
+
timeoutMs = parsed;
|
|
104
|
+
}
|
|
105
|
+
else if (arg === "--blocking") {
|
|
106
|
+
blocking = true;
|
|
107
|
+
}
|
|
108
|
+
else if (arg === "--json") {
|
|
109
|
+
json = true;
|
|
110
|
+
}
|
|
111
|
+
else if (arg.startsWith("-")) {
|
|
112
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
help,
|
|
120
|
+
json,
|
|
121
|
+
blocking,
|
|
122
|
+
stage,
|
|
123
|
+
stageInline,
|
|
124
|
+
task,
|
|
125
|
+
taskInline,
|
|
126
|
+
envelopeRef,
|
|
127
|
+
checkout,
|
|
128
|
+
model,
|
|
129
|
+
timeoutMs,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parseEnvelopeRefFlag(raw) {
|
|
133
|
+
const parts = raw.split(":");
|
|
134
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
135
|
+
throw new Error("--envelope-ref must be <runId>:<stageId>[:<attempt>]");
|
|
136
|
+
}
|
|
137
|
+
const [runId, stageId, attemptRaw] = parts;
|
|
138
|
+
if (!runId || !stageId) {
|
|
139
|
+
throw new Error("--envelope-ref must be <runId>:<stageId>[:<attempt>]");
|
|
140
|
+
}
|
|
141
|
+
const ref = { runId, stageId };
|
|
142
|
+
if (attemptRaw !== undefined) {
|
|
143
|
+
const attempt = Number.parseInt(attemptRaw, 10);
|
|
144
|
+
if (!Number.isFinite(attempt) || attempt < 1) {
|
|
145
|
+
throw new Error("--envelope-ref attempt must be a positive integer");
|
|
146
|
+
}
|
|
147
|
+
ref.attempt = attempt;
|
|
148
|
+
}
|
|
149
|
+
return ref;
|
|
150
|
+
}
|
|
151
|
+
function parseJsonFlag(raw, flagName) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(raw);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new Error(`${flagName} must be valid JSON`);
|
|
158
|
+
}
|
|
159
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
160
|
+
throw new Error(`${flagName} must be a JSON object`);
|
|
161
|
+
}
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Streamable HTTP MCP is stateful by default (see docs/mcp.md): a bare
|
|
166
|
+
* tools/call POST with no session id is rejected with "Session ID
|
|
167
|
+
* required". Do the same initialize -> notifications/initialized ->
|
|
168
|
+
* tools/call handshake a real MCP client would, against the shared global
|
|
169
|
+
* service (started/ensured by the caller), then close the session.
|
|
170
|
+
*/
|
|
171
|
+
async function mcpInitializeSession(base) {
|
|
172
|
+
const res = await fetch(`${base}/mcp`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
Accept: "application/json, text/event-stream",
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
jsonrpc: "2.0",
|
|
180
|
+
id: 0,
|
|
181
|
+
method: "initialize",
|
|
182
|
+
params: {
|
|
183
|
+
protocolVersion: "2025-03-26",
|
|
184
|
+
capabilities: {},
|
|
185
|
+
clientInfo: { name: "sf-cli", version: PACKAGE_VERSION },
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
const sessionId = res.headers.get("mcp-session-id");
|
|
190
|
+
await res.text().catch(() => undefined);
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
throw new Error(`Failed to initialize MCP session against ${base}/mcp (status ${res.status})`);
|
|
193
|
+
}
|
|
194
|
+
await fetch(`${base}/mcp`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
Accept: "application/json, text/event-stream",
|
|
199
|
+
"mcp-session-id": sessionId,
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
method: "notifications/initialized",
|
|
204
|
+
}),
|
|
205
|
+
}).catch(() => undefined);
|
|
206
|
+
return sessionId;
|
|
207
|
+
}
|
|
208
|
+
async function mcpCloseSession(base, sessionId) {
|
|
209
|
+
await fetch(`${base}/mcp`, {
|
|
210
|
+
method: "DELETE",
|
|
211
|
+
headers: { "mcp-session-id": sessionId },
|
|
212
|
+
}).catch(() => undefined);
|
|
213
|
+
}
|
|
214
|
+
async function mcpToolCall(base, sessionId, name, args) {
|
|
215
|
+
const res = await fetch(`${base}/mcp`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
"Content-Type": "application/json",
|
|
219
|
+
Accept: "application/json, text/event-stream",
|
|
220
|
+
"mcp-session-id": sessionId,
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
jsonrpc: "2.0",
|
|
224
|
+
id: 1,
|
|
225
|
+
method: "tools/call",
|
|
226
|
+
params: { name, arguments: args },
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
const text = await res.text();
|
|
230
|
+
const dataLine = text.split("\n").find((line) => line.startsWith("data: "));
|
|
231
|
+
if (!dataLine) {
|
|
232
|
+
throw new Error(`Unexpected MCP response from ${base}/mcp (status ${res.status}): ${text.slice(0, 200)}`);
|
|
233
|
+
}
|
|
234
|
+
const message = JSON.parse(dataLine.slice("data: ".length));
|
|
235
|
+
if (message.error) {
|
|
236
|
+
throw new Error(message.error.message ?? "MCP tool call failed");
|
|
237
|
+
}
|
|
238
|
+
const contentText = message.result?.content?.[0]?.text ?? "";
|
|
239
|
+
return {
|
|
240
|
+
isError: Boolean(message.result?.isError),
|
|
241
|
+
payload: contentText ? JSON.parse(contentText) : null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function defaultCallTool(base, ensureService) {
|
|
245
|
+
return async (args) => {
|
|
246
|
+
const ensured = await ensureService();
|
|
247
|
+
if (!ensured.ok) {
|
|
248
|
+
throw new Error(ensured.message);
|
|
249
|
+
}
|
|
250
|
+
const sessionId = await mcpInitializeSession(base);
|
|
251
|
+
try {
|
|
252
|
+
return await mcpToolCall(base, sessionId, "run_stage", args);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await mcpCloseSession(base, sessionId);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function reportRunStageResult(result, options) {
|
|
260
|
+
const { io, json } = options;
|
|
261
|
+
const payload = result.payload ?? {};
|
|
262
|
+
if (result.isError) {
|
|
263
|
+
if (json) {
|
|
264
|
+
io.log(JSON.stringify(payload, null, 2));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const message = typeof payload.error === "string" ? payload.error : "run_stage failed";
|
|
268
|
+
io.error(message);
|
|
269
|
+
}
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
const status = payload.status;
|
|
273
|
+
if (status === undefined) {
|
|
274
|
+
// Async mode: { runId, stageId }.
|
|
275
|
+
if (json) {
|
|
276
|
+
io.log(JSON.stringify(payload, null, 2));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
io.log(`Stage run started: ${payload.runId} (stage: ${payload.stageId})`);
|
|
280
|
+
}
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
// Blocking mode: { runId, stageId, status, envelope? | pending_prompt? }.
|
|
284
|
+
if (json) {
|
|
285
|
+
io.log(JSON.stringify(payload, null, 2));
|
|
286
|
+
}
|
|
287
|
+
else if (status === "completed") {
|
|
288
|
+
const envelope = payload.envelope;
|
|
289
|
+
if (envelope?.status === "failure") {
|
|
290
|
+
io.error(`Stage failed: ${envelope.summary ?? ""}`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
io.log(`Stage completed: ${envelope?.summary ?? ""}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if (status === "needs_input") {
|
|
297
|
+
io.error(`Stage waiting for input (run ${payload.runId}, stage ${payload.stageId}).`);
|
|
298
|
+
}
|
|
299
|
+
else if (status === "timeout") {
|
|
300
|
+
io.error(`Timed out waiting for stage to finish (run ${payload.runId}).`);
|
|
301
|
+
}
|
|
302
|
+
if (status === "completed") {
|
|
303
|
+
const envelope = payload.envelope;
|
|
304
|
+
return envelope?.status === "failure" ? 1 : 0;
|
|
305
|
+
}
|
|
306
|
+
if (status === "needs_input" || status === "timeout") {
|
|
307
|
+
return 2;
|
|
308
|
+
}
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
export async function runRunStageCommand(args, options = {}) {
|
|
312
|
+
const cwd = options.cwd ?? process.cwd();
|
|
313
|
+
const out = { ...defaultIo, ...options.io };
|
|
314
|
+
const base = options.hostBaseUrl ?? hostBaseUrl();
|
|
315
|
+
let parsed;
|
|
316
|
+
try {
|
|
317
|
+
parsed = parseRunStageCliArgs(args);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
+
out.error(message);
|
|
322
|
+
out.error(RUN_STAGE_USAGE);
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
if (parsed.help) {
|
|
326
|
+
out.error(RUN_STAGE_USAGE);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
if (Boolean(parsed.stage) === Boolean(parsed.stageInline)) {
|
|
330
|
+
out.error("Exactly one of --stage or --stage-inline is required");
|
|
331
|
+
out.error(RUN_STAGE_USAGE);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
const inputFlagCount = [parsed.task, parsed.taskInline, parsed.envelopeRef].filter((v) => v !== undefined).length;
|
|
335
|
+
if (inputFlagCount !== 1) {
|
|
336
|
+
out.error("Exactly one of --task, --task-inline, or --envelope-ref is required");
|
|
337
|
+
out.error(RUN_STAGE_USAGE);
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
let stage;
|
|
341
|
+
if (parsed.stage !== undefined) {
|
|
342
|
+
stage = resolveAbsolute(cwd, parsed.stage);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
try {
|
|
346
|
+
stage = parseJsonFlag(parsed.stageInline, "--stage-inline");
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
out.error(err instanceof Error ? err.message : String(err));
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let taskPath;
|
|
354
|
+
let task;
|
|
355
|
+
let envelopeRef;
|
|
356
|
+
if (parsed.task !== undefined) {
|
|
357
|
+
taskPath = resolveAbsolute(cwd, parsed.task);
|
|
358
|
+
}
|
|
359
|
+
else if (parsed.taskInline !== undefined) {
|
|
360
|
+
try {
|
|
361
|
+
task = parseJsonFlag(parsed.taskInline, "--task-inline");
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
out.error(err instanceof Error ? err.message : String(err));
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
try {
|
|
370
|
+
const refs = parsed.envelopeRef.map(parseEnvelopeRefFlag);
|
|
371
|
+
envelopeRef = refs.length === 1 ? refs[0] : refs;
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
out.error(err instanceof Error ? err.message : String(err));
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const toolArgs = {
|
|
379
|
+
stage,
|
|
380
|
+
...(taskPath !== undefined ? { task_path: taskPath } : {}),
|
|
381
|
+
...(task !== undefined ? { task } : {}),
|
|
382
|
+
...(envelopeRef !== undefined ? { envelope_ref: envelopeRef } : {}),
|
|
383
|
+
...(parsed.checkout !== undefined
|
|
384
|
+
? { checkout: resolveAbsolute(cwd, parsed.checkout) }
|
|
385
|
+
: {}),
|
|
386
|
+
...(parsed.model !== undefined ? { model: parsed.model } : {}),
|
|
387
|
+
...(parsed.blocking ? { blocking: true } : {}),
|
|
388
|
+
...(parsed.timeoutMs !== undefined ? { timeout_ms: parsed.timeoutMs } : {}),
|
|
389
|
+
};
|
|
390
|
+
const callTool = options.callTool ??
|
|
391
|
+
defaultCallTool(base, options.ensureService ?? (() => ensureGlobalService()));
|
|
392
|
+
let result;
|
|
393
|
+
try {
|
|
394
|
+
result = await callTool(toolArgs);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
398
|
+
if (parsed.json) {
|
|
399
|
+
out.log(JSON.stringify({ error: message }));
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
out.error(message);
|
|
403
|
+
}
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
return reportRunStageResult(result, { json: parsed.json, io: out });
|
|
407
|
+
}
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import type { RunStore } from "../runstore/port.js";
|
|
2
|
-
import {
|
|
2
|
+
import { type EnsureGlobalServiceResult } from "../server/ensureGlobalService.js";
|
|
3
3
|
export declare const RUNS_USAGE = "Usage:\n sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]\n sf runs show --run <runId> [--from <sf-run.json>] [--json]\n sf runs verify --run <runId> --stage <stageId> [--json]\n sf runs recover --run <runId> --stage <stageId> [--guidance <text>] [--stop] [--json]\n sf runs waiting [--run <runId>] [--json]\n sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]\n sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]\n sf runs feedback-decide --run <runId> --stage <sourceStageId> [--loop <loopId>] --decision extend|continue|abandon [--reason <text>] [--json]\n sf runs retry --run <runId> --stage <stageId> [--json]\n sf runs resume --run <runId> --stage <stageId> [--json]\n sf runs abandon --run <runId> --stage <stageId> [--json]\n sf runs rerun --run <runId> [--json]";
|
|
4
4
|
export type RunsCommandIo = {
|
|
5
5
|
log: (line: string) => void;
|
|
6
6
|
error: (line: string) => void;
|
|
7
7
|
};
|
|
8
|
-
export type HostProbeResult = "up" | "down";
|
|
9
|
-
export declare function defaultProbeHost(): Promise<HostProbeResult>;
|
|
10
8
|
export declare function runRunsCommand(args: string[], options?: {
|
|
11
9
|
cwd?: string;
|
|
12
|
-
projectRoot?: string;
|
|
13
|
-
isGitProject?: boolean;
|
|
14
10
|
io?: Partial<RunsCommandIo>;
|
|
15
11
|
store?: RunStore;
|
|
16
|
-
probeHost?: () => Promise<HostProbeResult>;
|
|
17
12
|
stdinIsTTY?: boolean;
|
|
18
13
|
readStdin?: () => string;
|
|
19
14
|
waitSignal?: AbortSignal;
|
|
20
15
|
env?: NodeJS.ProcessEnv;
|
|
21
|
-
|
|
16
|
+
hostBaseUrl?: string;
|
|
17
|
+
ensureService?: () => Promise<EnsureGlobalServiceResult>;
|
|
22
18
|
}): Promise<number>;
|
package/dist/cli/runsCommand.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolveGlobalOnlyAgentPort } from "../agent/resolveAgentPort.js";
|
|
4
3
|
import { projectRun } from "../projection/projectRun.js";
|
|
5
4
|
import { waitRun } from "../mcp/waitRun.js";
|
|
6
5
|
import { projectWaitingGates } from "../mcp/waitingGates.js";
|
|
7
|
-
import { resolveOperatorCatalog } from "./operatorCatalog.js";
|
|
8
6
|
import { completeCliRun } from "./runCommand.js";
|
|
9
7
|
import { reportCliRun } from "./runOutput.js";
|
|
8
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
10
9
|
import { createRunStore } from "../runstore/createStore.js";
|
|
11
10
|
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
12
|
-
import {
|
|
13
|
-
import { readStageExecutionMode } from "../runtime/stageConcurrency.js";
|
|
14
|
-
import { DEFAULT_PORT } from "../server/createHttpHost.js";
|
|
11
|
+
import { ensureGlobalService, hostBaseUrl, } from "../server/ensureGlobalService.js";
|
|
15
12
|
import { mapRetryStageFailure } from "../server/operatorResults.js";
|
|
13
|
+
import { httpAbandonStage, httpDecideFeedbackLoop, httpDeliverAnswer, httpRecoverManualStageUntilStop, httpRerun, httpResumeTimedOutStage, httpRetryStageUntilStop, httpStopManualRecovery, } from "./hostClient.js";
|
|
16
14
|
import { AskOperatorError, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
17
15
|
export const RUNS_USAGE = `Usage:
|
|
18
16
|
sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]
|
|
@@ -255,28 +253,6 @@ function resolveRunIdFromFile(fromPath, cwd) {
|
|
|
255
253
|
}
|
|
256
254
|
return runId;
|
|
257
255
|
}
|
|
258
|
-
function hostBaseUrl() {
|
|
259
|
-
return `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
260
|
-
}
|
|
261
|
-
export async function defaultProbeHost() {
|
|
262
|
-
const ac = new AbortController();
|
|
263
|
-
const timer = setTimeout(() => ac.abort(), 1500);
|
|
264
|
-
try {
|
|
265
|
-
const res = await fetch(`${hostBaseUrl()}/api/health`, {
|
|
266
|
-
signal: ac.signal,
|
|
267
|
-
});
|
|
268
|
-
if (res.status !== 200)
|
|
269
|
-
return "down";
|
|
270
|
-
JSON.parse(await res.text());
|
|
271
|
-
return "up";
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
return "down";
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
clearTimeout(timer);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
256
|
function printJson(io, payload) {
|
|
281
257
|
io.log(JSON.stringify(payload, null, 2));
|
|
282
258
|
}
|
|
@@ -304,15 +280,22 @@ async function resolveShowWaitRunId(parsed, cwd, io) {
|
|
|
304
280
|
}
|
|
305
281
|
return parsed.runId;
|
|
306
282
|
}
|
|
283
|
+
const MUTATING_SUBCOMMANDS = new Set([
|
|
284
|
+
"answer",
|
|
285
|
+
"feedback-decide",
|
|
286
|
+
"retry",
|
|
287
|
+
"resume",
|
|
288
|
+
"recover",
|
|
289
|
+
"abandon",
|
|
290
|
+
"rerun",
|
|
291
|
+
]);
|
|
307
292
|
export async function runRunsCommand(args, options = {}) {
|
|
308
293
|
const cwd = options.cwd ?? process.cwd();
|
|
309
|
-
const projectRoot = options.projectRoot ?? cwd;
|
|
310
|
-
const isGitProject = options.isGitProject ?? false;
|
|
311
294
|
const out = { ...defaultIo, ...options.io };
|
|
312
|
-
const env = options.env ?? process.env;
|
|
313
295
|
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
|
|
314
296
|
const readStdin = options.readStdin ?? (() => readFileSync(0, "utf8"));
|
|
315
|
-
const
|
|
297
|
+
const base = options.hostBaseUrl ?? hostBaseUrl();
|
|
298
|
+
const ensureService = options.ensureService ?? (() => ensureGlobalService());
|
|
316
299
|
let parsed;
|
|
317
300
|
try {
|
|
318
301
|
parsed = parseRunsArgs(args);
|
|
@@ -331,39 +314,23 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
331
314
|
let resolvedStore = options.store;
|
|
332
315
|
const getStore = () => {
|
|
333
316
|
if (resolvedStore === undefined) {
|
|
334
|
-
resolvedStore = createRunStore({ rootDir:
|
|
317
|
+
resolvedStore = createRunStore({ rootDir: globalStageflowHome() });
|
|
335
318
|
}
|
|
336
319
|
return resolvedStore;
|
|
337
320
|
};
|
|
338
321
|
const mutatingIo = out;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
349
|
-
return new RunManager({
|
|
350
|
-
agent,
|
|
351
|
-
store,
|
|
352
|
-
cwd,
|
|
353
|
-
projectRoot,
|
|
354
|
-
isGitProject,
|
|
355
|
-
operatorCatalog,
|
|
356
|
-
executionMode: readStageExecutionMode(env, "process"),
|
|
357
|
-
});
|
|
358
|
-
};
|
|
359
|
-
const guardHost = async () => {
|
|
360
|
-
const status = await probeHost();
|
|
361
|
-
if (status === "up") {
|
|
362
|
-
out.error(`A Stageflow host is already running at ${hostBaseUrl()}. Use the operator console or MCP instead of mutating the store from the CLI.`);
|
|
322
|
+
if (MUTATING_SUBCOMMANDS.has(parsed.subcommand)) {
|
|
323
|
+
const ensured = await ensureService();
|
|
324
|
+
if (!ensured.ok) {
|
|
325
|
+
if (parsed.json) {
|
|
326
|
+
printJson(out, { error: ensured.message, reason: ensured.reason });
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
out.error(ensured.message);
|
|
330
|
+
}
|
|
363
331
|
return 1;
|
|
364
332
|
}
|
|
365
|
-
|
|
366
|
-
};
|
|
333
|
+
}
|
|
367
334
|
switch (parsed.subcommand) {
|
|
368
335
|
case "list": {
|
|
369
336
|
if (parsed.status === "waiting") {
|
|
@@ -527,9 +494,6 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
527
494
|
}
|
|
528
495
|
}
|
|
529
496
|
case "answer": {
|
|
530
|
-
const blocked = await guardHost();
|
|
531
|
-
if (blocked !== undefined)
|
|
532
|
-
return blocked;
|
|
533
497
|
if (!parsed.runId) {
|
|
534
498
|
return usageError(out, "Missing --run");
|
|
535
499
|
}
|
|
@@ -578,8 +542,7 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
578
542
|
out.error(message);
|
|
579
543
|
return 1;
|
|
580
544
|
}
|
|
581
|
-
const
|
|
582
|
-
const result = await manager.deliverAnswer(parsed.runId, parsed.stageId, answer);
|
|
545
|
+
const result = await httpDeliverAnswer(base, parsed.runId, parsed.stageId, parsedJson);
|
|
583
546
|
if (!result.ok) {
|
|
584
547
|
const payload = { error: result.reason };
|
|
585
548
|
if (result.status !== undefined)
|
|
@@ -601,9 +564,6 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
601
564
|
return 0;
|
|
602
565
|
}
|
|
603
566
|
case "feedback-decide": {
|
|
604
|
-
const blocked = await guardHost();
|
|
605
|
-
if (blocked !== undefined)
|
|
606
|
-
return blocked;
|
|
607
567
|
if (!parsed.runId) {
|
|
608
568
|
return usageError(out, "Missing --run");
|
|
609
569
|
}
|
|
@@ -625,8 +585,7 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
625
585
|
}
|
|
626
586
|
return 1;
|
|
627
587
|
}
|
|
628
|
-
const
|
|
629
|
-
const result = await manager.decideFeedbackLoop(parsed.runId, parsed.stageId, {
|
|
588
|
+
const result = await httpDecideFeedbackLoop(base, parsed.runId, parsed.stageId, {
|
|
630
589
|
decision: parsed.decision,
|
|
631
590
|
...(parsed.loopId !== undefined ? { loopId: parsed.loopId } : {}),
|
|
632
591
|
...(parsed.reason !== undefined ? { reason: parsed.reason } : {}),
|
|
@@ -656,14 +615,10 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
656
615
|
return 0;
|
|
657
616
|
}
|
|
658
617
|
case "retry": {
|
|
659
|
-
const blocked = await guardHost();
|
|
660
|
-
if (blocked !== undefined)
|
|
661
|
-
return blocked;
|
|
662
618
|
if (!parsed.runId || !parsed.stageId) {
|
|
663
619
|
return usageError(out, "Missing --run and/or --stage");
|
|
664
620
|
}
|
|
665
|
-
const
|
|
666
|
-
const result = await manager.retryStageUntilStop(parsed.runId, parsed.stageId);
|
|
621
|
+
const result = await httpRetryStageUntilStop(base, parsed.runId, parsed.stageId);
|
|
667
622
|
if (!result.ok) {
|
|
668
623
|
if (parsed.json) {
|
|
669
624
|
printJson(out, {
|
|
@@ -679,14 +634,10 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
679
634
|
return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
|
|
680
635
|
}
|
|
681
636
|
case "resume": {
|
|
682
|
-
const blocked = await guardHost();
|
|
683
|
-
if (blocked !== undefined)
|
|
684
|
-
return blocked;
|
|
685
637
|
if (!parsed.runId || !parsed.stageId) {
|
|
686
638
|
return usageError(out, "Missing --run and/or --stage");
|
|
687
639
|
}
|
|
688
|
-
const
|
|
689
|
-
const result = await manager.resumeTimedOutStage(parsed.runId, parsed.stageId);
|
|
640
|
+
const result = await httpResumeTimedOutStage(base, parsed.runId, parsed.stageId);
|
|
690
641
|
if (!result.ok) {
|
|
691
642
|
if (parsed.json) {
|
|
692
643
|
printJson(out, {
|
|
@@ -713,18 +664,14 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
713
664
|
return 0;
|
|
714
665
|
}
|
|
715
666
|
case "recover": {
|
|
716
|
-
const blocked = await guardHost();
|
|
717
|
-
if (blocked !== undefined)
|
|
718
|
-
return blocked;
|
|
719
667
|
if (!parsed.runId || !parsed.stageId) {
|
|
720
668
|
return usageError(out, "Missing --run and/or --stage");
|
|
721
669
|
}
|
|
722
|
-
const manager = buildManager();
|
|
723
670
|
if (parsed.stop) {
|
|
724
671
|
if (parsed.guidance !== undefined) {
|
|
725
672
|
return usageError(out, "--guidance cannot be used with --stop");
|
|
726
673
|
}
|
|
727
|
-
const result = await
|
|
674
|
+
const result = await httpStopManualRecovery(base, parsed.runId, parsed.stageId);
|
|
728
675
|
if (!result.ok) {
|
|
729
676
|
if (parsed.json) {
|
|
730
677
|
printJson(out, { error: result.reason, status: result.status });
|
|
@@ -742,7 +689,7 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
742
689
|
}
|
|
743
690
|
return 0;
|
|
744
691
|
}
|
|
745
|
-
const result = await
|
|
692
|
+
const result = await httpRecoverManualStageUntilStop(base, parsed.runId, parsed.stageId, parsed.guidance);
|
|
746
693
|
if (!result.ok) {
|
|
747
694
|
if (parsed.json) {
|
|
748
695
|
printJson(out, { ...mapRetryStageFailure(result), status: result.status });
|
|
@@ -755,14 +702,10 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
755
702
|
return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
|
|
756
703
|
}
|
|
757
704
|
case "abandon": {
|
|
758
|
-
const blocked = await guardHost();
|
|
759
|
-
if (blocked !== undefined)
|
|
760
|
-
return blocked;
|
|
761
705
|
if (!parsed.runId || !parsed.stageId) {
|
|
762
706
|
return usageError(out, "Missing --run and/or --stage");
|
|
763
707
|
}
|
|
764
|
-
const
|
|
765
|
-
const result = await manager.abandonStage(parsed.runId, parsed.stageId);
|
|
708
|
+
const result = await httpAbandonStage(base, parsed.runId, parsed.stageId);
|
|
766
709
|
if (!result.ok) {
|
|
767
710
|
const payload = { error: result.reason };
|
|
768
711
|
if (result.status !== undefined)
|
|
@@ -788,14 +731,10 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
788
731
|
return 0;
|
|
789
732
|
}
|
|
790
733
|
case "rerun": {
|
|
791
|
-
const blocked = await guardHost();
|
|
792
|
-
if (blocked !== undefined)
|
|
793
|
-
return blocked;
|
|
794
734
|
if (!parsed.runId) {
|
|
795
735
|
return usageError(out, "Missing --run");
|
|
796
736
|
}
|
|
797
|
-
const
|
|
798
|
-
const started = await manager.rerun(parsed.runId);
|
|
737
|
+
const started = await httpRerun(base, parsed.runId);
|
|
799
738
|
if (!started.ok) {
|
|
800
739
|
return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: mutatingIo });
|
|
801
740
|
}
|