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.
Files changed (120) hide show
  1. package/README.md +16 -3
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +93 -0
  5. package/dist/a2a/contracts.js +163 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +44 -0
  9. package/dist/a2a/registry.js +159 -0
  10. package/dist/a2a/server.d.ts +29 -0
  11. package/dist/a2a/server.js +341 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +496 -0
  14. package/dist/a2a/store.d.ts +106 -0
  15. package/dist/a2a/store.js +245 -0
  16. package/dist/agent/activity.d.ts +5 -0
  17. package/dist/agent/activityObserver.d.ts +3 -0
  18. package/dist/agent/activityObserver.js +6 -1
  19. package/dist/agent/piAdapter.js +7 -3
  20. package/dist/agent/port.d.ts +2 -0
  21. package/dist/agent/providerInspect.d.ts +19 -0
  22. package/dist/agent/providerInspect.js +35 -0
  23. package/dist/agent/streamLogRedact.d.ts +2 -0
  24. package/dist/agent/streamLogRedact.js +13 -0
  25. package/dist/cli/a2aCommand.d.ts +8 -0
  26. package/dist/cli/a2aCommand.js +131 -0
  27. package/dist/cli/artifactCommand.js +2 -2
  28. package/dist/cli/envelopeCommand.js +2 -2
  29. package/dist/cli/exportRunCommand.js +2 -2
  30. package/dist/cli/hostClient.d.ts +43 -0
  31. package/dist/cli/hostClient.js +182 -0
  32. package/dist/cli/providersCommand.js +5 -3
  33. package/dist/cli/runCommand.d.ts +6 -5
  34. package/dist/cli/runCommand.js +23 -31
  35. package/dist/cli/runOutput.d.ts +1 -1
  36. package/dist/cli/runStageCommand.d.ts +33 -0
  37. package/dist/cli/runStageCommand.js +407 -0
  38. package/dist/cli/runsCommand.d.ts +3 -7
  39. package/dist/cli/runsCommand.js +33 -94
  40. package/dist/cli.js +20 -5
  41. package/dist/config/describePipeline.d.ts +26 -0
  42. package/dist/config/describePipeline.js +37 -0
  43. package/dist/config/loadPipeline.d.ts +13 -1
  44. package/dist/config/loadPipeline.js +34 -4
  45. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  46. package/dist/config/mergePipelineIncludes.js +75 -0
  47. package/dist/config/validateCatalog.d.ts +2 -2
  48. package/dist/config/validateCatalog.js +24 -17
  49. package/dist/mcp/catalogTools.js +285 -52
  50. package/dist/mcp/controlTools.js +77 -19
  51. package/dist/mcp/deps.d.ts +3 -0
  52. package/dist/mcp/projectMcpTools.d.ts +3 -0
  53. package/dist/mcp/projectMcpTools.js +25 -0
  54. package/dist/mcp/providerTools.d.ts +3 -0
  55. package/dist/mcp/providerTools.js +17 -0
  56. package/dist/mcp/readArtifact.d.ts +9 -0
  57. package/dist/mcp/readArtifact.js +11 -0
  58. package/dist/mcp/server.js +9 -1
  59. package/dist/mcp/tailStreamLog.d.ts +8 -0
  60. package/dist/mcp/tailStreamLog.js +28 -0
  61. package/dist/mcp/toolResults.d.ts +17 -0
  62. package/dist/mcp/toolResults.js +15 -0
  63. package/dist/mcp/tools.js +4 -0
  64. package/dist/mcp/waitRun.js +4 -4
  65. package/dist/mcp/waitingGates.d.ts +1 -0
  66. package/dist/mcp/waitingGates.js +3 -2
  67. package/dist/package-meta.d.ts +1 -1
  68. package/dist/package-meta.js +1 -1
  69. package/dist/projection/projectRun.d.ts +3 -0
  70. package/dist/projection/projectRun.js +7 -0
  71. package/dist/runstore/createStore.d.ts +10 -0
  72. package/dist/runstore/createStore.js +13 -4
  73. package/dist/runstore/port.d.ts +17 -2
  74. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  75. package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
  76. package/dist/runstore/sqlite/schema.d.ts +1 -1
  77. package/dist/runstore/sqlite/schema.js +6 -0
  78. package/dist/runstore/submission.d.ts +11 -0
  79. package/dist/runstore/submission.js +7 -0
  80. package/dist/runstore/workspaceLayout.d.ts +1 -0
  81. package/dist/runstore/workspaceLayout.js +3 -0
  82. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  83. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  84. package/dist/runtime/feedbackLoopDecision.js +33 -1
  85. package/dist/runtime/pipelineRunner.d.ts +5 -3
  86. package/dist/runtime/pipelineRunner.js +6 -1
  87. package/dist/runtime/pipelineScheduler.js +32 -15
  88. package/dist/runtime/reloadRunCatalog.js +7 -1
  89. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  90. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  91. package/dist/runtime/resumeReconstruct.js +2 -2
  92. package/dist/runtime/runManager.d.ts +24 -2
  93. package/dist/runtime/runManager.js +83 -19
  94. package/dist/runtime/settingsFile.d.ts +2 -0
  95. package/dist/runtime/settingsFile.js +19 -0
  96. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  97. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  98. package/dist/runtime/stageStreamLog.d.ts +17 -0
  99. package/dist/runtime/stageStreamLog.js +89 -0
  100. package/dist/runtime/stageWorker.js +2 -1
  101. package/dist/server/bootstrap.d.ts +2 -0
  102. package/dist/server/bootstrap.js +22 -4
  103. package/dist/server/createHttpHost.js +6 -0
  104. package/dist/server/ensureGlobalService.d.ts +35 -0
  105. package/dist/server/ensureGlobalService.js +138 -0
  106. package/dist/server/http.d.ts +20 -1
  107. package/dist/server/http.js +450 -402
  108. package/dist/server/mcpHost.js +7 -8
  109. package/dist/server/operatorResults.d.ts +10 -0
  110. package/dist/server/operatorResults.js +24 -0
  111. package/dist/server/providerRoutes.d.ts +0 -6
  112. package/dist/server/providerRoutes.js +10 -18
  113. package/dist/types/pipeline.d.ts +8 -0
  114. package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
  115. package/dist/ui/index.html +1 -1
  116. package/package.json +2 -1
  117. package/skills/stageflow/references/control-surface.md +2 -2
  118. package/skills/stageflow-run/SKILL.md +27 -9
  119. package/skills/stageflow-run/references/mcp-call.md +1 -1
  120. 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 { RunManager } from "../runtime/runManager.js";
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
- createManager?: (store: RunStore) => RunManager;
16
+ hostBaseUrl?: string;
17
+ ensureService?: () => Promise<EnsureGlobalServiceResult>;
22
18
  }): Promise<number>;
@@ -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 { RunManager } from "../runtime/runManager.js";
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 probeHost = options.probeHost ?? defaultProbeHost;
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: projectRoot });
317
+ resolvedStore = createRunStore({ rootDir: globalStageflowHome() });
335
318
  }
336
319
  return resolvedStore;
337
320
  };
338
321
  const mutatingIo = out;
339
- const agent = await resolveGlobalOnlyAgentPort(projectRoot);
340
- const buildManager = () => {
341
- const store = getStore();
342
- if (options.createManager)
343
- return options.createManager(store);
344
- const operatorCatalog = resolveOperatorCatalog({
345
- flags: {},
346
- env,
347
- defaultCwd: cwd,
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
- return undefined;
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 manager = buildManager();
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 manager = buildManager();
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 manager = buildManager();
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 manager = buildManager();
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 manager.stopManualRecovery(parsed.runId, parsed.stageId);
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 manager.recoverManualStageUntilStop(parsed.runId, parsed.stageId, parsed.guidance);
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 manager = buildManager();
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 manager = buildManager();
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
  }