stageflow 0.8.0 → 0.9.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 +23 -14
- package/dist/cli/runsCommand.d.ts +22 -0
- package/dist/cli/runsCommand.js +591 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.js +32 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/runtime/runManager.d.ts +7 -0
- package/dist/runtime/runManager.js +31 -2
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +4 -1
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow-run/SKILL.md +56 -25
- package/skills/stageflow-run/references/mcp-call.md +2 -2
- package/skills/stageflow-run/references/native-question-ui.md +78 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# <img src="ui/public/stageflow-icon.svg" alt="" width="44" height="44" valign="middle"> Stageflow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Open-source runtime for **configurable multi-stage agent workflows** with typed handoffs, DAG execution, human gates, MCP, CI, and a local operator console.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/stageflow)
|
|
6
6
|
[](https://www.npmjs.com/package/stageflow)
|
|
@@ -9,7 +9,7 @@ CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badl
|
|
|
9
9
|
[](https://github.com/tejasghutukade/stageflow/issues)
|
|
10
10
|
[](https://github.com/tejasghutukade/stageflow/actions/workflows/ci.yml)
|
|
11
11
|
|
|
12
|
-
Author pipeline-owned YAML in your project. Stageflow
|
|
12
|
+
Author pipeline-owned YAML in your project. Stageflow schedules each stage in a fresh agent session, moves context through explicit envelopes and artifacts, and persists run state under `.stageflow/`. [Pi](https://github.com/badlogic/pi-mono) is the current agent execution backend. Bins: **`sf`** and **`stageflow`**.
|
|
13
13
|
|
|
14
14
|
## Why Stageflow?
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ The same pipeline runs three ways without rewriting anything:
|
|
|
21
21
|
|
|
22
22
|
- **Locally** — `sf ui` for triage, provider setup, and gate replies
|
|
23
23
|
- **Headless / CI** — `sf validate` and `sf run --json` with predictable exit codes
|
|
24
|
-
- **Via MCP** — Streamable HTTP tools when
|
|
24
|
+
- **Via MCP** — Streamable HTTP tools when `sf ui` or `sf mcp` is running
|
|
25
25
|
|
|
26
26
|
**Stageflow is not an SDLC tool.** Software delivery is a popular pattern in fixtures and dogfood flows, but stages are user-authored and domain-agnostic. If you can express a multi-step workflow in YAML, Stageflow can run it on Pi.
|
|
27
27
|
|
|
@@ -33,12 +33,20 @@ The same pipeline runs three ways without rewriting anything:
|
|
|
33
33
|
- **Envelope handoffs** — typed stage payloads and artifacts via `write_stage_artifact` / `emit_stage_envelope`
|
|
34
34
|
- **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
|
|
35
35
|
- **Operator console** — triage runs, connect providers, answer gates, inspect transcripts at `http://127.0.0.1:3847`
|
|
36
|
-
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` is running
|
|
36
|
+
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` is running
|
|
37
37
|
- **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
|
|
38
38
|
- **Parallel stages** — pipeline DAG with fan-out and join (see [YAML catalog](docs/yaml-catalog.md))
|
|
39
39
|
- **Clonable fan-out** — clone one successor N times at completion, then join (see [YAML catalog](docs/yaml-catalog.md#clonable-successors))
|
|
40
40
|
- **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
|
|
41
41
|
|
|
42
|
+
## Architecture at a glance
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
The scheduler owns orchestration semantics: DAG readiness, bounded parallelism, fan-out/join, conditional routing, retries, skipped branches, and resumable human gates. Agent execution sits behind `AgentPort`; Pi supplies the current coding-agent session, while Stageflow owns pipeline state, stage workspaces, handoff validation, and the interfaces used by the CLI, console, MCP, and CI.
|
|
47
|
+
|
|
48
|
+
See [Architecture](docs/architecture.md) for component boundaries, execution flow, persistence, recovery behavior, and the tradeoffs behind fresh sessions and explicit handoffs.
|
|
49
|
+
|
|
42
50
|
## Installation
|
|
43
51
|
|
|
44
52
|
Requires **Node.js ≥ 20**.
|
|
@@ -117,7 +125,7 @@ sf providers detect
|
|
|
117
125
|
sf providers source set pi_home # or sf_owned
|
|
118
126
|
```
|
|
119
127
|
|
|
120
|
-
|
|
128
|
+
Pi is Stageflow's current agent execution backend. Stageflow owns the workflow layer around it, and you do not need Pi CLI `/login` as a prerequisite when providers are configured through the console or `sf providers`.
|
|
121
129
|
|
|
122
130
|
Full reference: [docs/providers.md](docs/providers.md)
|
|
123
131
|
|
|
@@ -126,12 +134,12 @@ Full reference: [docs/providers.md](docs/providers.md)
|
|
|
126
134
|
Start the console with `sf ui` (default `http://127.0.0.1:3847`).
|
|
127
135
|
|
|
128
136
|
- **Runs** — active and recent pipeline runs, capacity, and status at a glance
|
|
129
|
-
- **Run detail** — stage
|
|
137
|
+
- **Run detail** — spatial stage map; select a stage for the gated workspace (logs, files, envelopes, HITL). Created runs show **not started** with a **Start run** action until history exists
|
|
130
138
|
- **HITL reply** — answer operator gates (`ask_operator`) without leaving the browser
|
|
131
139
|
- **Pipelines** — browse manifest-declared pipeline definitions
|
|
132
140
|
- **Settings → Providers** — connect model providers (`pi_home` or `sf_owned` credential storage)
|
|
133
141
|
|
|
134
|
-
|
|
142
|
+
Brand assets live under `docs/img/` (`stageflow-og.svg`, `stageflow-icon.svg`).
|
|
135
143
|
|
|
136
144
|
## Headless / CI
|
|
137
145
|
|
|
@@ -141,7 +149,7 @@ The guest actor is the CLI (`sf` / `stageflow`). `sf ui` and MCP are not require
|
|
|
141
149
|
sf validate --strict --json
|
|
142
150
|
```
|
|
143
151
|
|
|
144
|
-
Validate exits `0` or `1` only (no waiting / `2`).
|
|
152
|
+
Validate exits `0` or `1` only (no waiting / `2`). With no flags, `sf validate` checks pipelines and tasks in the manifest (plus stages). `--pipeline` validates that pipeline and its stages only; `--task` validates that task file. It never proves provider auth or checkout paths.
|
|
145
153
|
|
|
146
154
|
```bash
|
|
147
155
|
sf providers login <providerId> --api-key-env <VAR>
|
|
@@ -180,11 +188,9 @@ Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository
|
|
|
180
188
|
|
|
181
189
|
## MCP
|
|
182
190
|
|
|
183
|
-
`sf ui`
|
|
184
|
-
|
|
185
|
-
Available tools: `list_pipelines`, `list_tasks`, `list_runs`, `get_health`, `start_run`, `get_run`, `read_artifact`.
|
|
191
|
+
Host MCP via `sf ui` or `sf mcp` at `http://127.0.0.1:3847/mcp` (URL printed on boot). Sessions are the default. Point a Cursor (or other) MCP client at that URL. Do not run both hosts against the same store.
|
|
186
192
|
|
|
187
|
-
Full
|
|
193
|
+
HITL-aware tools include `wait_run`, `answer_gate`, and `list_waiting`. Full tool list: [docs/mcp.md](docs/mcp.md).
|
|
188
194
|
|
|
189
195
|
## Stageflow vs Conductor
|
|
190
196
|
|
|
@@ -196,7 +202,7 @@ Both projects address multi-step agent workflows. They differ in orchestration m
|
|
|
196
202
|
| **Orchestration** | Pipeline DAG + stage worker | Jinja routing, no LLM in router |
|
|
197
203
|
| **Unit of work** | Task → Pipeline → Stage attempts | Workflow → Agents |
|
|
198
204
|
| **Handoff** | Typed **envelope** + artifacts | Agent output → context |
|
|
199
|
-
| **Human gates** |
|
|
205
|
+
| **Human gates** | Console + MCP + harness native question UI | Dashboard + TUI fleet |
|
|
200
206
|
| **Runtime** | Node.js, Pi coding agent | Python, Copilot/Claude SDKs |
|
|
201
207
|
| **Best for** | Personal/team **multi-stage Pi workflows** you define (releases, research, SDLC, …) | Enterprise multi-agent workflows |
|
|
202
208
|
|
|
@@ -210,6 +216,7 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
|
|
|
210
216
|
| [hello-world](examples/hello-world/) | Single stage, domain-neutral |
|
|
211
217
|
| [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
|
|
212
218
|
| [conditional-fork](examples/conditional-fork/) | Exclusive fork routing with operator branch choice |
|
|
219
|
+
| [clonable-fanout](examples/clonable-fanout/) | Clone one successor N times, then join |
|
|
213
220
|
| [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
|
|
214
221
|
| [ci-validate](examples/ci-validate/) | Strict validate in CI |
|
|
215
222
|
|
|
@@ -222,15 +229,17 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
|
|
|
222
229
|
| Doc | Description |
|
|
223
230
|
|-----|-------------|
|
|
224
231
|
| [docs/README.md](docs/README.md) | Documentation index |
|
|
232
|
+
| [docs/architecture.md](docs/architecture.md) | Runtime components, execution flow, persistence, and design decisions |
|
|
225
233
|
| [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
|
|
226
234
|
| [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
|
|
227
|
-
| [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf envelope`, `sf
|
|
235
|
+
| [docs/cli-reference.md](docs/cli-reference.md) | `sf init`, `sf run`, `sf validate`, `sf ui`, `sf mcp`, `sf envelope`, `sf export-run`, `sf artifact`, `sf skills`, `sf providers` |
|
|
228
236
|
| [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
|
|
229
237
|
| [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
|
|
230
238
|
| [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
|
|
231
239
|
| [docs/mcp.md](docs/mcp.md) | MCP tool reference |
|
|
232
240
|
| [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
|
|
233
241
|
| [docs/operator-console.md](docs/operator-console.md) | Console IA and settings |
|
|
242
|
+
| [docs/skills-suite.md](docs/skills-suite.md) | Harness skills — router + jobs for Cursor, Claude Code, Codex, Pi, OpenCode |
|
|
234
243
|
| [docs/compare-conductor.md](docs/compare-conductor.md) | Positioning deep dive |
|
|
235
244
|
|
|
236
245
|
## Develop from source
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
2
|
+
import { RunManager } from "../runtime/runManager.js";
|
|
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 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 retry --run <runId> --stage <stageId> [--json]\n sf runs abandon --run <runId> --stage <stageId> [--json]\n sf runs rerun --run <runId> [--json]";
|
|
4
|
+
export type RunsCommandIo = {
|
|
5
|
+
log: (line: string) => void;
|
|
6
|
+
error: (line: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export type HostProbeResult = "up" | "down";
|
|
9
|
+
export declare function defaultProbeHost(): Promise<HostProbeResult>;
|
|
10
|
+
export declare function runRunsCommand(args: string[], options?: {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
projectRoot?: string;
|
|
13
|
+
isGitProject?: boolean;
|
|
14
|
+
io?: Partial<RunsCommandIo>;
|
|
15
|
+
store?: RunStore;
|
|
16
|
+
probeHost?: () => Promise<HostProbeResult>;
|
|
17
|
+
stdinIsTTY?: boolean;
|
|
18
|
+
readStdin?: () => string;
|
|
19
|
+
waitSignal?: AbortSignal;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
createManager?: (store: RunStore) => RunManager;
|
|
22
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PiAgentAdapter } from "../agent/piAdapter.js";
|
|
4
|
+
import { projectRun } from "../projection/projectRun.js";
|
|
5
|
+
import { waitRun } from "../mcp/waitRun.js";
|
|
6
|
+
import { projectWaitingGates } from "../mcp/waitingGates.js";
|
|
7
|
+
import { resolveOperatorCatalog } from "./operatorCatalog.js";
|
|
8
|
+
import { completeCliRun } from "./runCommand.js";
|
|
9
|
+
import { reportCliRun } from "./runOutput.js";
|
|
10
|
+
import { createRunStore } from "../runstore/createStore.js";
|
|
11
|
+
import { RunManager } from "../runtime/runManager.js";
|
|
12
|
+
import { readStageExecutionMode } from "../runtime/stageConcurrency.js";
|
|
13
|
+
import { DEFAULT_PORT } from "../server/createHttpHost.js";
|
|
14
|
+
import { mapRetryStageFailure } from "../server/operatorResults.js";
|
|
15
|
+
import { AskOperatorError, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
16
|
+
export const RUNS_USAGE = `Usage:
|
|
17
|
+
sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]
|
|
18
|
+
sf runs show --run <runId> [--from <sf-run.json>] [--json]
|
|
19
|
+
sf runs waiting [--run <runId>] [--json]
|
|
20
|
+
sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]
|
|
21
|
+
sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
|
|
22
|
+
sf runs retry --run <runId> --stage <stageId> [--json]
|
|
23
|
+
sf runs abandon --run <runId> --stage <stageId> [--json]
|
|
24
|
+
sf runs rerun --run <runId> [--json]`;
|
|
25
|
+
const defaultIo = {
|
|
26
|
+
log: (line) => console.log(line),
|
|
27
|
+
error: (line) => console.error(line),
|
|
28
|
+
};
|
|
29
|
+
const RUN_STATUSES = [
|
|
30
|
+
"created",
|
|
31
|
+
"running",
|
|
32
|
+
"succeeded",
|
|
33
|
+
"failed",
|
|
34
|
+
];
|
|
35
|
+
const LIST_FLAGS = new Set([
|
|
36
|
+
"--status",
|
|
37
|
+
"--since",
|
|
38
|
+
"--pipeline",
|
|
39
|
+
"--json",
|
|
40
|
+
"--help",
|
|
41
|
+
"-h",
|
|
42
|
+
]);
|
|
43
|
+
const SHOW_FLAGS = new Set(["--run", "--from", "--json", "--help", "-h"]);
|
|
44
|
+
const WAITING_FLAGS = new Set(["--run", "--json", "--help", "-h"]);
|
|
45
|
+
const WAIT_FLAGS = new Set([
|
|
46
|
+
"--run",
|
|
47
|
+
"--from",
|
|
48
|
+
"--until",
|
|
49
|
+
"--timeout-ms",
|
|
50
|
+
"--json",
|
|
51
|
+
"--help",
|
|
52
|
+
"-h",
|
|
53
|
+
]);
|
|
54
|
+
const ANSWER_FLAGS = new Set([
|
|
55
|
+
"--run",
|
|
56
|
+
"--stage",
|
|
57
|
+
"--answer",
|
|
58
|
+
"--json",
|
|
59
|
+
"--help",
|
|
60
|
+
"-h",
|
|
61
|
+
]);
|
|
62
|
+
const RETRY_FLAGS = new Set(["--run", "--stage", "--json", "--help", "-h"]);
|
|
63
|
+
const ABANDON_FLAGS = new Set(["--run", "--stage", "--json", "--help", "-h"]);
|
|
64
|
+
const RERUN_FLAGS = new Set(["--run", "--json", "--help", "-h"]);
|
|
65
|
+
const VALUE_FLAGS = new Set([
|
|
66
|
+
"--status",
|
|
67
|
+
"--since",
|
|
68
|
+
"--pipeline",
|
|
69
|
+
"--run",
|
|
70
|
+
"--from",
|
|
71
|
+
"--until",
|
|
72
|
+
"--timeout-ms",
|
|
73
|
+
"--stage",
|
|
74
|
+
"--answer",
|
|
75
|
+
]);
|
|
76
|
+
function flagsFor(subcommand) {
|
|
77
|
+
switch (subcommand) {
|
|
78
|
+
case "list":
|
|
79
|
+
return LIST_FLAGS;
|
|
80
|
+
case "show":
|
|
81
|
+
return SHOW_FLAGS;
|
|
82
|
+
case "waiting":
|
|
83
|
+
return WAITING_FLAGS;
|
|
84
|
+
case "wait":
|
|
85
|
+
return WAIT_FLAGS;
|
|
86
|
+
case "answer":
|
|
87
|
+
return ANSWER_FLAGS;
|
|
88
|
+
case "retry":
|
|
89
|
+
return RETRY_FLAGS;
|
|
90
|
+
case "abandon":
|
|
91
|
+
return ABANDON_FLAGS;
|
|
92
|
+
case "rerun":
|
|
93
|
+
return RERUN_FLAGS;
|
|
94
|
+
default:
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function parseRunsArgs(args) {
|
|
99
|
+
if (args.length === 0) {
|
|
100
|
+
return { help: false, json: false };
|
|
101
|
+
}
|
|
102
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
103
|
+
return { help: true, json: false };
|
|
104
|
+
}
|
|
105
|
+
const subcommand = args[0];
|
|
106
|
+
const allowed = flagsFor(subcommand);
|
|
107
|
+
if (allowed === undefined) {
|
|
108
|
+
return { subcommand, help: false, json: false };
|
|
109
|
+
}
|
|
110
|
+
let help = false;
|
|
111
|
+
let json = false;
|
|
112
|
+
let status;
|
|
113
|
+
let since;
|
|
114
|
+
let pipeline;
|
|
115
|
+
let runId;
|
|
116
|
+
let fromPath;
|
|
117
|
+
let until;
|
|
118
|
+
let timeoutMs;
|
|
119
|
+
let stageId;
|
|
120
|
+
let answer;
|
|
121
|
+
for (let i = 1; i < args.length; i++) {
|
|
122
|
+
const arg = args[i];
|
|
123
|
+
if (arg === "--help" || arg === "-h") {
|
|
124
|
+
help = true;
|
|
125
|
+
}
|
|
126
|
+
else if (arg === "--json") {
|
|
127
|
+
json = true;
|
|
128
|
+
}
|
|
129
|
+
else if (VALUE_FLAGS.has(arg)) {
|
|
130
|
+
if (!allowed.has(arg)) {
|
|
131
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
132
|
+
}
|
|
133
|
+
const value = args[++i];
|
|
134
|
+
if (value === undefined || value.length === 0) {
|
|
135
|
+
throw new Error(`Missing value for ${arg}`);
|
|
136
|
+
}
|
|
137
|
+
if (arg === "--status")
|
|
138
|
+
status = value;
|
|
139
|
+
else if (arg === "--since")
|
|
140
|
+
since = value;
|
|
141
|
+
else if (arg === "--pipeline")
|
|
142
|
+
pipeline = value;
|
|
143
|
+
else if (arg === "--run")
|
|
144
|
+
runId = value;
|
|
145
|
+
else if (arg === "--from")
|
|
146
|
+
fromPath = value;
|
|
147
|
+
else if (arg === "--until")
|
|
148
|
+
until = value;
|
|
149
|
+
else if (arg === "--timeout-ms")
|
|
150
|
+
timeoutMs = Number(value);
|
|
151
|
+
else if (arg === "--stage")
|
|
152
|
+
stageId = value;
|
|
153
|
+
else if (arg === "--answer")
|
|
154
|
+
answer = value;
|
|
155
|
+
}
|
|
156
|
+
else if (arg.startsWith("-")) {
|
|
157
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
help,
|
|
165
|
+
json,
|
|
166
|
+
subcommand,
|
|
167
|
+
status,
|
|
168
|
+
since,
|
|
169
|
+
pipeline,
|
|
170
|
+
runId,
|
|
171
|
+
fromPath,
|
|
172
|
+
until,
|
|
173
|
+
timeoutMs,
|
|
174
|
+
stageId,
|
|
175
|
+
answer,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function resolveRunIdFromFile(fromPath, cwd) {
|
|
179
|
+
const resolved = path.resolve(cwd, fromPath);
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
throw new Error(`Failed to read --from file: ${message}`);
|
|
187
|
+
}
|
|
188
|
+
if (!parsed || typeof parsed !== "object") {
|
|
189
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
190
|
+
}
|
|
191
|
+
const runId = parsed.runId;
|
|
192
|
+
if (typeof runId !== "string" || runId.length === 0) {
|
|
193
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
194
|
+
}
|
|
195
|
+
return runId;
|
|
196
|
+
}
|
|
197
|
+
function hostBaseUrl() {
|
|
198
|
+
return `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
199
|
+
}
|
|
200
|
+
export async function defaultProbeHost() {
|
|
201
|
+
const ac = new AbortController();
|
|
202
|
+
const timer = setTimeout(() => ac.abort(), 1500);
|
|
203
|
+
try {
|
|
204
|
+
const res = await fetch(`${hostBaseUrl()}/api/health`, {
|
|
205
|
+
signal: ac.signal,
|
|
206
|
+
});
|
|
207
|
+
if (res.status !== 200)
|
|
208
|
+
return "down";
|
|
209
|
+
JSON.parse(await res.text());
|
|
210
|
+
return "up";
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return "down";
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function printJson(io, payload) {
|
|
220
|
+
io.log(JSON.stringify(payload, null, 2));
|
|
221
|
+
}
|
|
222
|
+
function isRunStatus(value) {
|
|
223
|
+
return RUN_STATUSES.includes(value);
|
|
224
|
+
}
|
|
225
|
+
function isWaitUntil(value) {
|
|
226
|
+
return value === "any" || value === "waiting" || value === "terminal";
|
|
227
|
+
}
|
|
228
|
+
function usageError(io, message) {
|
|
229
|
+
if (message !== undefined)
|
|
230
|
+
io.error(message);
|
|
231
|
+
io.error(RUNS_USAGE);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
async function resolveShowWaitRunId(parsed, cwd, io) {
|
|
235
|
+
if (parsed.fromPath !== undefined) {
|
|
236
|
+
try {
|
|
237
|
+
return resolveRunIdFromFile(parsed.fromPath, cwd);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
io.error(err instanceof Error ? err.message : String(err));
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return parsed.runId;
|
|
245
|
+
}
|
|
246
|
+
export async function runRunsCommand(args, options = {}) {
|
|
247
|
+
const cwd = options.cwd ?? process.cwd();
|
|
248
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
249
|
+
const isGitProject = options.isGitProject ?? false;
|
|
250
|
+
const out = { ...defaultIo, ...options.io };
|
|
251
|
+
const env = options.env ?? process.env;
|
|
252
|
+
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
|
|
253
|
+
const readStdin = options.readStdin ?? (() => readFileSync(0, "utf8"));
|
|
254
|
+
const probeHost = options.probeHost ?? defaultProbeHost;
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
parsed = parseRunsArgs(args);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
return usageError(out, message);
|
|
262
|
+
}
|
|
263
|
+
if (parsed.help) {
|
|
264
|
+
out.error(RUNS_USAGE);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
if (!parsed.subcommand) {
|
|
268
|
+
return usageError(out);
|
|
269
|
+
}
|
|
270
|
+
let resolvedStore = options.store;
|
|
271
|
+
const getStore = () => {
|
|
272
|
+
if (resolvedStore === undefined) {
|
|
273
|
+
resolvedStore = createRunStore({ rootDir: projectRoot });
|
|
274
|
+
}
|
|
275
|
+
return resolvedStore;
|
|
276
|
+
};
|
|
277
|
+
const mutatingIo = out;
|
|
278
|
+
const buildManager = () => {
|
|
279
|
+
const store = getStore();
|
|
280
|
+
if (options.createManager)
|
|
281
|
+
return options.createManager(store);
|
|
282
|
+
const operatorCatalog = resolveOperatorCatalog({
|
|
283
|
+
flags: {},
|
|
284
|
+
env,
|
|
285
|
+
defaultCwd: cwd,
|
|
286
|
+
});
|
|
287
|
+
return new RunManager({
|
|
288
|
+
agent: new PiAgentAdapter(),
|
|
289
|
+
store,
|
|
290
|
+
cwd,
|
|
291
|
+
projectRoot,
|
|
292
|
+
isGitProject,
|
|
293
|
+
operatorCatalog,
|
|
294
|
+
executionMode: readStageExecutionMode(env, "process"),
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
const guardHost = async () => {
|
|
298
|
+
const status = await probeHost();
|
|
299
|
+
if (status === "up") {
|
|
300
|
+
out.error(`A Stageflow host is already running at ${hostBaseUrl()}. Use the operator console or MCP instead of mutating the store from the CLI.`);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
};
|
|
305
|
+
switch (parsed.subcommand) {
|
|
306
|
+
case "list": {
|
|
307
|
+
if (parsed.status === "waiting") {
|
|
308
|
+
out.error("waiting is not a run status; parked HITL runs stay running. Use sf runs waiting.");
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
if (parsed.status !== undefined && !isRunStatus(parsed.status)) {
|
|
312
|
+
out.error("--status must be created, running, succeeded, or failed");
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
if (parsed.since !== undefined && !Number.isFinite(Date.parse(parsed.since))) {
|
|
316
|
+
out.error("since must be a valid date");
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const filter = {};
|
|
320
|
+
if (parsed.status !== undefined)
|
|
321
|
+
filter.status = parsed.status;
|
|
322
|
+
if (parsed.since !== undefined)
|
|
323
|
+
filter.since = parsed.since;
|
|
324
|
+
if (parsed.pipeline !== undefined)
|
|
325
|
+
filter.pipeline = parsed.pipeline;
|
|
326
|
+
const runs = await getStore().listRuns(Object.keys(filter).length > 0 ? filter : undefined);
|
|
327
|
+
if (parsed.json) {
|
|
328
|
+
printJson(out, { runs });
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
for (const run of runs) {
|
|
332
|
+
out.log(`${run.run_id}\t${run.status}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
case "show": {
|
|
338
|
+
const runId = await resolveShowWaitRunId(parsed, cwd, out);
|
|
339
|
+
if (parsed.fromPath !== undefined && runId === undefined) {
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
if (!runId) {
|
|
343
|
+
return usageError(out, "Missing --run (or --from <sf-run.json>)");
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const detail = await getStore().readRun(runId);
|
|
347
|
+
const projection = projectRun(detail);
|
|
348
|
+
if (parsed.json) {
|
|
349
|
+
printJson(out, projection);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
out.log(`${projection.run_id}\t${projection.status}`);
|
|
353
|
+
}
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
out.error(message);
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
case "waiting": {
|
|
363
|
+
const waiting = await projectWaitingGates(getStore(), {
|
|
364
|
+
runId: parsed.runId,
|
|
365
|
+
});
|
|
366
|
+
if (parsed.json) {
|
|
367
|
+
printJson(out, { waiting });
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
for (const item of waiting) {
|
|
371
|
+
const runId = typeof item.runId === "string" ? item.runId : "";
|
|
372
|
+
const stageId = typeof item.stageId === "string" ? item.stageId : "";
|
|
373
|
+
out.log(`${runId}\t${stageId}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
case "wait": {
|
|
379
|
+
const runId = await resolveShowWaitRunId(parsed, cwd, out);
|
|
380
|
+
if (parsed.fromPath !== undefined && runId === undefined) {
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
if (!runId) {
|
|
384
|
+
return usageError(out, "Missing --run (or --from <sf-run.json>)");
|
|
385
|
+
}
|
|
386
|
+
const untilRaw = parsed.until ?? "any";
|
|
387
|
+
if (!isWaitUntil(untilRaw)) {
|
|
388
|
+
return usageError(out, "--until must be any, waiting, or terminal");
|
|
389
|
+
}
|
|
390
|
+
let signal = options.waitSignal;
|
|
391
|
+
let onSigInt;
|
|
392
|
+
if (signal === undefined) {
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
signal = controller.signal;
|
|
395
|
+
onSigInt = () => controller.abort();
|
|
396
|
+
process.once("SIGINT", onSigInt);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const result = await waitRun({
|
|
400
|
+
store: getStore(),
|
|
401
|
+
runId,
|
|
402
|
+
timeoutMs: parsed.timeoutMs,
|
|
403
|
+
until: untilRaw,
|
|
404
|
+
signal,
|
|
405
|
+
});
|
|
406
|
+
if (!result.ok) {
|
|
407
|
+
if (result.code === "aborted") {
|
|
408
|
+
if (parsed.json) {
|
|
409
|
+
printJson(out, { error: result.error, code: "aborted" });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
out.error(result.error);
|
|
413
|
+
}
|
|
414
|
+
return 130;
|
|
415
|
+
}
|
|
416
|
+
if (parsed.json) {
|
|
417
|
+
const payload = { error: result.error };
|
|
418
|
+
if (result.status !== undefined)
|
|
419
|
+
payload.status = result.status;
|
|
420
|
+
printJson(out, payload);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
out.error(result.error);
|
|
424
|
+
}
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
if (parsed.json) {
|
|
428
|
+
printJson(out, result);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
out.log(`${result.reason}\t${result.run.status}`);
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
if (onSigInt !== undefined) {
|
|
437
|
+
process.removeListener("SIGINT", onSigInt);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
case "answer": {
|
|
442
|
+
const blocked = await guardHost();
|
|
443
|
+
if (blocked !== undefined)
|
|
444
|
+
return blocked;
|
|
445
|
+
if (!parsed.runId) {
|
|
446
|
+
return usageError(out, "Missing --run");
|
|
447
|
+
}
|
|
448
|
+
if (!parsed.stageId) {
|
|
449
|
+
return usageError(out, "Missing --stage");
|
|
450
|
+
}
|
|
451
|
+
let answerRaw = parsed.answer;
|
|
452
|
+
if (answerRaw === undefined) {
|
|
453
|
+
if (stdinIsTTY) {
|
|
454
|
+
return usageError(out, "Missing --answer (pass --answer '<json>' or pipe JSON on stdin)");
|
|
455
|
+
}
|
|
456
|
+
answerRaw = readStdin().trim();
|
|
457
|
+
if (answerRaw.length === 0) {
|
|
458
|
+
return usageError(out, "Missing --answer (pass --answer '<json>' or pipe JSON on stdin)");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
let parsedJson;
|
|
462
|
+
try {
|
|
463
|
+
parsedJson = JSON.parse(answerRaw);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
const message = "--answer must be valid JSON";
|
|
467
|
+
if (parsed.json) {
|
|
468
|
+
printJson(out, { error: message, status: 400 });
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
out.error(message);
|
|
472
|
+
}
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
let answer;
|
|
476
|
+
try {
|
|
477
|
+
answer = parseAskOperatorAnswer(parsedJson);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
481
|
+
if (err instanceof AskOperatorError) {
|
|
482
|
+
if (parsed.json) {
|
|
483
|
+
printJson(out, { error: message, status: 400 });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
out.error(message);
|
|
487
|
+
}
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
out.error(message);
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
493
|
+
const manager = buildManager();
|
|
494
|
+
const result = await manager.deliverAnswer(parsed.runId, parsed.stageId, answer);
|
|
495
|
+
if (!result.ok) {
|
|
496
|
+
const payload = { error: result.reason };
|
|
497
|
+
if (result.status !== undefined)
|
|
498
|
+
payload.status = result.status;
|
|
499
|
+
if (parsed.json) {
|
|
500
|
+
printJson(out, payload);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
out.error(result.reason);
|
|
504
|
+
}
|
|
505
|
+
return 1;
|
|
506
|
+
}
|
|
507
|
+
if (parsed.json) {
|
|
508
|
+
printJson(out, { ok: true });
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
out.log("ok");
|
|
512
|
+
}
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
case "retry": {
|
|
516
|
+
const blocked = await guardHost();
|
|
517
|
+
if (blocked !== undefined)
|
|
518
|
+
return blocked;
|
|
519
|
+
if (!parsed.runId || !parsed.stageId) {
|
|
520
|
+
return usageError(out, "Missing --run and/or --stage");
|
|
521
|
+
}
|
|
522
|
+
const manager = buildManager();
|
|
523
|
+
const result = await manager.retryStageUntilStop(parsed.runId, parsed.stageId);
|
|
524
|
+
if (!result.ok) {
|
|
525
|
+
if (parsed.json) {
|
|
526
|
+
printJson(out, {
|
|
527
|
+
...mapRetryStageFailure(result),
|
|
528
|
+
status: result.status,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
out.error(result.reason);
|
|
533
|
+
}
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
|
|
537
|
+
}
|
|
538
|
+
case "abandon": {
|
|
539
|
+
const blocked = await guardHost();
|
|
540
|
+
if (blocked !== undefined)
|
|
541
|
+
return blocked;
|
|
542
|
+
if (!parsed.runId || !parsed.stageId) {
|
|
543
|
+
return usageError(out, "Missing --run and/or --stage");
|
|
544
|
+
}
|
|
545
|
+
const manager = buildManager();
|
|
546
|
+
const result = await manager.abandonStage(parsed.runId, parsed.stageId);
|
|
547
|
+
if (!result.ok) {
|
|
548
|
+
const payload = { error: result.reason };
|
|
549
|
+
if (result.status !== undefined)
|
|
550
|
+
payload.status = result.status;
|
|
551
|
+
if (parsed.json) {
|
|
552
|
+
printJson(out, payload);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
out.error(result.reason);
|
|
556
|
+
}
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
if (parsed.json) {
|
|
560
|
+
printJson(out, {
|
|
561
|
+
ok: true,
|
|
562
|
+
runId: result.runId,
|
|
563
|
+
stageId: result.stageId,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
out.log(`${result.runId}\t${result.stageId}`);
|
|
568
|
+
}
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
case "rerun": {
|
|
572
|
+
const blocked = await guardHost();
|
|
573
|
+
if (blocked !== undefined)
|
|
574
|
+
return blocked;
|
|
575
|
+
if (!parsed.runId) {
|
|
576
|
+
return usageError(out, "Missing --run");
|
|
577
|
+
}
|
|
578
|
+
const manager = buildManager();
|
|
579
|
+
const started = await manager.rerun(parsed.runId);
|
|
580
|
+
if (!started.ok) {
|
|
581
|
+
return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: mutatingIo });
|
|
582
|
+
}
|
|
583
|
+
return completeCliRun(started, mutatingIo, {
|
|
584
|
+
json: parsed.json,
|
|
585
|
+
store: getStore(),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
default:
|
|
589
|
+
return usageError(out, `Unknown runs subcommand: ${parsed.subcommand}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ValidationResult } from "../config/validateCatalog.js";
|
|
2
|
-
export declare const VALIDATION_SCOPE_LINE = "Scope:
|
|
3
|
-
export declare const VALIDATION_CHECKS = "
|
|
2
|
+
export declare const VALIDATION_SCOPE_LINE = "Scope: catalog YAML (pipelines, stages, and tasks as selected by flags). Provider auth and checkout paths are not checked.";
|
|
3
|
+
export declare const VALIDATION_CHECKS = "catalog YAML (pipelines, stages, tasks as selected by flags)";
|
|
4
4
|
export declare function formatValidationHuman(result: ValidationResult, options?: {
|
|
5
5
|
strict?: boolean;
|
|
6
6
|
}): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { effectiveSeverity } from "../config/validateCatalog.js";
|
|
2
|
-
export const VALIDATION_SCOPE_LINE = "Scope:
|
|
3
|
-
export const VALIDATION_CHECKS = "
|
|
2
|
+
export const VALIDATION_SCOPE_LINE = "Scope: catalog YAML (pipelines, stages, and tasks as selected by flags). Provider auth and checkout paths are not checked.";
|
|
3
|
+
export const VALIDATION_CHECKS = "catalog YAML (pipelines, stages, tasks as selected by flags)";
|
|
4
4
|
function effectiveSeverityRank(finding, strict) {
|
|
5
5
|
return effectiveSeverity(finding, strict) === "error" ? 0 : 1;
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { EXPORT_RUN_USAGE, runExportRunCommand, } from "./cli/exportRunCommand.j
|
|
|
10
10
|
import { INIT_USAGE, runInitCommand } from "./cli/initCommand.js";
|
|
11
11
|
import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
|
|
12
12
|
import { RUN_USAGE, runRunCommand } from "./cli/runCommand.js";
|
|
13
|
+
import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
|
|
13
14
|
import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
|
|
14
15
|
import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
|
|
15
16
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
@@ -20,6 +21,7 @@ import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
|
|
|
20
21
|
import { DEFAULT_PORT, startUiServer } from "./server/http.js";
|
|
21
22
|
import { startMcpServer } from "./server/mcpHost.js";
|
|
22
23
|
import { resolveMcpStateless } from "./mcp/server.js";
|
|
24
|
+
import { PACKAGE_VERSION } from "./package-meta.js";
|
|
23
25
|
const USAGE = `Usage:
|
|
24
26
|
sf init
|
|
25
27
|
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>]
|
|
@@ -27,6 +29,14 @@ const USAGE = `Usage:
|
|
|
27
29
|
sf artifact read --run <runId> --path <relPath> [--out <file>]
|
|
28
30
|
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
|
|
29
31
|
sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]
|
|
32
|
+
sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]
|
|
33
|
+
sf runs show --run <runId> [--from <sf-run.json>] [--json]
|
|
34
|
+
sf runs waiting [--run <runId>] [--json]
|
|
35
|
+
sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]
|
|
36
|
+
sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
|
|
37
|
+
sf runs retry --run <runId> --stage <stageId> [--json]
|
|
38
|
+
sf runs abandon --run <runId> --stage <stageId> [--json]
|
|
39
|
+
sf runs rerun --run <runId> [--json]
|
|
30
40
|
sf ui [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
31
41
|
sf mcp [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
32
42
|
sf providers list
|
|
@@ -38,6 +48,8 @@ const USAGE = `Usage:
|
|
|
38
48
|
sf skills list
|
|
39
49
|
sf skills install --from-path <dir> [--skill-name <name>]
|
|
40
50
|
sf skills install --from-zip <url-or-path> [--skill-name <name>] [--checksum sha256:<hex>]
|
|
51
|
+
sf --version
|
|
52
|
+
sf -V
|
|
41
53
|
sf --help
|
|
42
54
|
|
|
43
55
|
Stageflow (sf) runs YAML-defined automatic stage pipelines.
|
|
@@ -56,6 +68,8 @@ ${ENVELOPE_USAGE}
|
|
|
56
68
|
|
|
57
69
|
${EXPORT_RUN_USAGE}
|
|
58
70
|
|
|
71
|
+
${RUNS_USAGE}
|
|
72
|
+
|
|
59
73
|
${PROVIDERS_USAGE}
|
|
60
74
|
|
|
61
75
|
${SKILLS_USAGE}`;
|
|
@@ -68,6 +82,12 @@ function parseArgs(argv) {
|
|
|
68
82
|
args.length === 1) {
|
|
69
83
|
return { help: true };
|
|
70
84
|
}
|
|
85
|
+
if (args[0] === "--version" || args[0] === "-V") {
|
|
86
|
+
if (args.length > 1) {
|
|
87
|
+
throw new Error(`Unexpected argument: ${args[1]}`);
|
|
88
|
+
}
|
|
89
|
+
return { help: false, version: true };
|
|
90
|
+
}
|
|
71
91
|
const command = args[0];
|
|
72
92
|
if (command === "providers" ||
|
|
73
93
|
command === "validate" ||
|
|
@@ -76,6 +96,7 @@ function parseArgs(argv) {
|
|
|
76
96
|
command === "artifact" ||
|
|
77
97
|
command === "envelope" ||
|
|
78
98
|
command === "export-run" ||
|
|
99
|
+
command === "runs" ||
|
|
79
100
|
command === "skills") {
|
|
80
101
|
return { help: false, command };
|
|
81
102
|
}
|
|
@@ -240,6 +261,10 @@ async function main(argv) {
|
|
|
240
261
|
console.log(USAGE);
|
|
241
262
|
return argv.slice(2).length === 0 ? 1 : 0;
|
|
242
263
|
}
|
|
264
|
+
if (parsed.version) {
|
|
265
|
+
console.log(PACKAGE_VERSION);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
243
268
|
const ctx = await resolveStageflowContext(process.cwd());
|
|
244
269
|
if (parsed.command === "init") {
|
|
245
270
|
return runInitCommand(argv.slice(3), { cwd: ctx.invocationCwd });
|
|
@@ -272,6 +297,13 @@ async function main(argv) {
|
|
|
272
297
|
projectRoot: ctx.projectRoot,
|
|
273
298
|
});
|
|
274
299
|
}
|
|
300
|
+
if (parsed.command === "runs") {
|
|
301
|
+
return runRunsCommand(argv.slice(3), {
|
|
302
|
+
cwd: ctx.invocationCwd,
|
|
303
|
+
projectRoot: ctx.projectRoot,
|
|
304
|
+
isGitProject: ctx.isGitProject,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
275
307
|
if (parsed.command === "providers") {
|
|
276
308
|
return runProvidersCommand(argv.slice(3), ctx.invocationCwd);
|
|
277
309
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export type { StageEnvelope } from "./types/envelope.js";
|
|
7
7
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
8
8
|
export type { AgentPort } from "./agent/port.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
7
7
|
export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
|
|
8
8
|
export { createRunStore } from "./runstore/createStore.js";
|
package/dist/package-meta.d.ts
CHANGED
package/dist/package-meta.js
CHANGED
|
@@ -111,6 +111,13 @@ export declare class RunManager {
|
|
|
111
111
|
}): Promise<StartRunResult>;
|
|
112
112
|
rerun(runId: string): Promise<StartRunResult>;
|
|
113
113
|
retryStage(runId: string, stageId: string): Promise<RetryStageResult>;
|
|
114
|
+
retryStageUntilStop(runId: string, stageId: string): Promise<{
|
|
115
|
+
ok: true;
|
|
116
|
+
pipeline: PipelineRunResult;
|
|
117
|
+
} | Extract<RetryStageResult, {
|
|
118
|
+
ok: false;
|
|
119
|
+
}>>;
|
|
120
|
+
private retryStageInternal;
|
|
114
121
|
/**
|
|
115
122
|
* Deliver an operator answer for `(runId, stageId)` (KTD2 / KTD6).
|
|
116
123
|
* Same-process: unparks the live yield loop. After restart: reconstructs
|
|
@@ -389,6 +389,29 @@ export class RunManager {
|
|
|
389
389
|
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined, rerunProjectRoot);
|
|
390
390
|
}
|
|
391
391
|
async retryStage(runId, stageId) {
|
|
392
|
+
return this.retryStageInternal(runId, stageId, true);
|
|
393
|
+
}
|
|
394
|
+
async retryStageUntilStop(runId, stageId) {
|
|
395
|
+
const retryKey = waitKey(runId, stageId);
|
|
396
|
+
const retried = await this.retryStageInternal(runId, stageId, false);
|
|
397
|
+
if (!retried.ok)
|
|
398
|
+
return retried;
|
|
399
|
+
try {
|
|
400
|
+
if (retried.done === undefined) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
reason: `Retry orchestration did not start for run ${runId} stage ${stageId}`,
|
|
404
|
+
status: 500,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const pipeline = await retried.done;
|
|
408
|
+
return { ok: true, pipeline };
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
this.retryInFlight.delete(retryKey);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async retryStageInternal(runId, stageId, awaitRoot) {
|
|
392
415
|
const retryKey = waitKey(runId, stageId);
|
|
393
416
|
if (this.retryInFlight.has(retryKey)) {
|
|
394
417
|
return {
|
|
@@ -401,7 +424,7 @@ export class RunManager {
|
|
|
401
424
|
try {
|
|
402
425
|
const orchestrationConflict = this.active.has(runId) &&
|
|
403
426
|
!this.attachedWaiting.has(waitKey(runId, stageId));
|
|
404
|
-
|
|
427
|
+
const result = await this.retryCoordinator.retryStage({
|
|
405
428
|
runId,
|
|
406
429
|
stageId,
|
|
407
430
|
store: this.options.store,
|
|
@@ -414,10 +437,16 @@ export class RunManager {
|
|
|
414
437
|
hitl: this.hitl,
|
|
415
438
|
orchestrationConflict,
|
|
416
439
|
tracking: this.retryTracking,
|
|
440
|
+
awaitRoot,
|
|
417
441
|
});
|
|
442
|
+
if (!result.ok || awaitRoot !== false) {
|
|
443
|
+
this.retryInFlight.delete(retryKey);
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
418
446
|
}
|
|
419
|
-
|
|
447
|
+
catch (err) {
|
|
420
448
|
this.retryInFlight.delete(retryKey);
|
|
449
|
+
throw err;
|
|
421
450
|
}
|
|
422
451
|
}
|
|
423
452
|
/**
|
|
@@ -29,6 +29,7 @@ export type RetryStageResult = {
|
|
|
29
29
|
runId: string;
|
|
30
30
|
stageId: string;
|
|
31
31
|
attemptIndex: number;
|
|
32
|
+
done?: Promise<PipelineRunResult>;
|
|
32
33
|
} | {
|
|
33
34
|
ok: false;
|
|
34
35
|
reason: string;
|
|
@@ -62,6 +63,7 @@ export type RetryStageRequest = {
|
|
|
62
63
|
hitl: StageHitlController;
|
|
63
64
|
orchestrationConflict: boolean;
|
|
64
65
|
tracking: RetryTrackingPort;
|
|
66
|
+
awaitRoot?: boolean;
|
|
65
67
|
};
|
|
66
68
|
export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
|
|
67
69
|
recoveryActive?: boolean;
|
|
@@ -315,12 +315,15 @@ export class RunRetryCoordinator {
|
|
|
315
315
|
tracking.onOrchestrationStarted(runId, orchestrationPromise);
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
-
|
|
318
|
+
if (req.awaitRoot !== false) {
|
|
319
|
+
await this.waitForRoot(runId, stageId, store);
|
|
320
|
+
}
|
|
319
321
|
return {
|
|
320
322
|
ok: true,
|
|
321
323
|
runId,
|
|
322
324
|
stageId,
|
|
323
325
|
attemptIndex: execution.attempt,
|
|
326
|
+
done: this.getOrchestrationPromise(runId),
|
|
324
327
|
};
|
|
325
328
|
}
|
|
326
329
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ node ../scripts/detect-host.mjs
|
|
|
11
11
|
node ../scripts/detect-host.mjs --base-url http://127.0.0.1:3847
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The script `GET`s `{baseUrl}/api/health` (default `http://127.0.0.1:3847`, 1500 ms timeout). **up** means HTTP 200 and parseable JSON. Non-200, non-JSON, or timeout is **down**.
|
|
14
|
+
The script `GET`s `{baseUrl}/api/health` (default `http://127.0.0.1:3847`, 1500 ms timeout). **up** means HTTP 200 and parseable JSON. Non-200, non-JSON, or timeout is **down**. This probe is Stageflow host up/down only — it does not detect a coding-agent question UI. Gate presentation lives in [`../../stageflow-run/references/native-question-ui.md`](../../stageflow-run/references/native-question-ui.md).
|
|
15
15
|
|
|
16
16
|
Stdout is one line: `up <baseUrl>` or `down <baseUrl>`. Exit `0` when up, `1` when down, `2` on usage error.
|
|
17
17
|
|
|
@@ -23,4 +23,6 @@ Use MCP tools over the Streamable HTTP endpoint at `{baseUrl}/mcp`. Tool names a
|
|
|
23
23
|
|
|
24
24
|
## When the host is down
|
|
25
25
|
|
|
26
|
-
Use the `sf` CLI. Command names and flags live in [docs/cli-reference.md](../../../docs/cli-reference.md). Typical talking-job commands: `sf run`, `sf validate`, `sf envelope get`, `sf artifact read`, `sf providers`.
|
|
26
|
+
Use the `sf` CLI. Command names and flags live in [docs/cli-reference.md](../../../docs/cli-reference.md). Typical talking-job commands: `sf run`, `sf runs waiting`, `sf runs answer`, `sf runs wait`, `sf validate`, `sf envelope get`, `sf artifact read`, `sf providers`.
|
|
27
|
+
|
|
28
|
+
Probe before each mutating `sf runs` verb. If the probe is **up** or the command refuses because a host is up, continue that gate via MCP — do not start a second mutating writer, and do not start `sf mcp` as a disposable bridge.
|
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
name: stageflow-run
|
|
3
3
|
description: >-
|
|
4
4
|
Starts a catalog pipeline from the harness, reports progress, and answers
|
|
5
|
-
every HITL gate
|
|
6
|
-
|
|
5
|
+
every HITL gate on the host native question UI when it can, otherwise in
|
|
6
|
+
this chat. Triggers: run a pipeline, start <pipeline>, check on my run,
|
|
7
|
+
answer the pending question.
|
|
7
8
|
compatibility: Requires the sf CLI on PATH. An MCP host (sf ui or sf mcp) is optional.
|
|
8
9
|
disable-model-invocation: true
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Stageflow run
|
|
12
13
|
|
|
13
|
-
Start a catalog pipeline,
|
|
14
|
+
Start a catalog pipeline, present HITL on the host native question UI when it can, and report the outcome. Author and session-capture own catalog YAML; this job owns the run and any throwaway `*.task.yaml`.
|
|
14
15
|
|
|
15
16
|
Talking jobs cite [`../stageflow/references/control-surface.md`](../stageflow/references/control-surface.md). Probe with [`../stageflow/scripts/detect-host.mjs`](../stageflow/scripts/detect-host.mjs) only. Do not write a second probe.
|
|
16
17
|
|
|
17
|
-
MCP tool shapes: [`docs/mcp.md`](../../docs/mcp.md). CLI flags and exit codes: [`docs/cli-reference.md`](../../docs/cli-reference.md). Direct tool calls: [`references/mcp-call.md`](references/mcp-call.md). Selection: [`references/task-and-pipeline-selection.md`](references/task-and-pipeline-selection.md).
|
|
18
|
+
MCP tool shapes: [`docs/mcp.md`](../../docs/mcp.md). CLI flags and exit codes: [`docs/cli-reference.md`](../../docs/cli-reference.md). Direct tool calls: [`references/mcp-call.md`](references/mcp-call.md). Selection: [`references/task-and-pipeline-selection.md`](references/task-and-pipeline-selection.md). Gate presentation: [`references/native-question-ui.md`](references/native-question-ui.md).
|
|
18
19
|
|
|
19
20
|
## Preconditions
|
|
20
21
|
|
|
@@ -38,15 +39,15 @@ node ../stageflow/scripts/detect-host.mjs --base-url http://127.0.0.1:3847
|
|
|
38
39
|
|
|
39
40
|
Read [`references/task-and-pipeline-selection.md`](references/task-and-pipeline-selection.md). Follow it until you have a pipeline filesystem path and a task (catalog path, MCP inline object, or CLI throwaway file).
|
|
40
41
|
|
|
41
|
-
If this chat already has a `runId` and the request is check, answer, or continue: skip start. Host up → [Wait](#wait). Host down
|
|
42
|
+
If this chat already has a `runId` and the request is check, answer, or continue: skip start. Host up → [Wait](#wait). Host down → [CLI wait](#cli-wait).
|
|
42
43
|
|
|
43
44
|
**Done when** the target is named, or a live `runId` is in hand.
|
|
44
45
|
|
|
45
46
|
## MCP tools
|
|
46
47
|
|
|
47
|
-
Prefer this harness's native Stageflow MCP tools when their names are already in the tool list. When they are not, call [`scripts/mcp-call.mjs`](scripts/mcp-call.mjs) — see [`references/mcp-call.md`](references/mcp-call.md).
|
|
48
|
+
Prefer this harness's native Stageflow MCP tools when their names are already in the tool list. When they are not, call [`scripts/mcp-call.mjs`](scripts/mcp-call.mjs) — see [`references/mcp-call.md`](references/mcp-call.md). Use `--stateless` only when the **user** started the host with `--mcp-stateless`. Do not start `sf mcp` from this skill.
|
|
48
49
|
|
|
49
|
-
Call only `list_pipelines`, `list_tasks`, `start_run`, `get_run`, `wait_run`, `list_waiting`, `answer_gate`, `get_health`.
|
|
50
|
+
Call only these Stageflow MCP tools: `list_pipelines`, `list_tasks`, `start_run`, `get_run`, `wait_run`, `list_waiting`, `answer_gate`, `get_health`. Host question tools already in this harness's tool list (`AskQuestion`, `AskUserQuestion`, `ask_user`) are for [Gate](#gate) presentation, not Stageflow MCP.
|
|
50
51
|
|
|
51
52
|
## MCP path
|
|
52
53
|
|
|
@@ -78,9 +79,14 @@ Call only `list_pipelines`, `list_tasks`, `start_run`, `get_run`, `wait_run`, `l
|
|
|
78
79
|
|
|
79
80
|
### Gate
|
|
80
81
|
|
|
82
|
+
Read [`references/native-question-ui.md`](references/native-question-ui.md) before presenting a pending prompt.
|
|
83
|
+
|
|
81
84
|
1. `list_waiting` with `{ "runId" }`.
|
|
82
85
|
2. Print the pending prompt text **verbatim** (and artifacts / sub-questions when present).
|
|
83
|
-
3.
|
|
86
|
+
3. Present the decision as that reference directs:
|
|
87
|
+
- If a host question tool is already in this harness's tool list and the gate is representable, invoke that picker. For `multi_question`, one picker call with one question per sub-item when **every** sub-question is representable, then one `answer_gate`; otherwise the **whole** gate in this chat.
|
|
88
|
+
- Otherwise collect the reply in this chat.
|
|
89
|
+
4. Map the reply to the prompt `kind` (picker Accept/Reject → `accept`/`reject`):
|
|
84
90
|
|
|
85
91
|
| kind | `answer` |
|
|
86
92
|
|---|---|
|
|
@@ -89,12 +95,12 @@ Call only `list_pipelines`, `list_tasks`, `start_run`, `get_run`, `wait_run`, `l
|
|
|
89
95
|
| `artifact_backed` | `{ "promptId", "kind": "artifact_backed", "decision": "accept" \| "reject" }` |
|
|
90
96
|
| `multi_question` | `{ "promptId", "kind": "multi_question", "answers": { "<id>": { "kind", "text" \| "decision" } } }` |
|
|
91
97
|
|
|
92
|
-
`promptId` is `pending_prompt.id` or `waiting_prompt_id`. Map yes/y/accept/approve to `accept`; no/n/reject/deny to `reject`.
|
|
98
|
+
`promptId` is `pending_prompt.id` or `waiting_prompt_id`. Map yes/y/accept/approve/Accept to `accept`; no/n/reject/deny/Reject to `reject`. Each `multi_question` sub-answer keeps that sub's `kind` (`confirm` → `decision`, `free_text` → `text`). Collect every sub-answer before `answer_gate`.
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
5. `answer_gate` with `{ "runId", "stageId", "answer" }`.
|
|
101
|
+
6. Return to [Wait](#wait).
|
|
96
102
|
|
|
97
|
-
**Done when** `answer_gate` returns `{ "ok": true }` and Wait is re-entered. On `isError` (400 / 404 / 409), print the payload and
|
|
103
|
+
**Done when** `answer_gate` returns `{ "ok": true }` and Wait is re-entered. On `isError` (400 / 404 / 409), print the payload and collect a corrected reply the same way (picker if still representable; otherwise this chat).
|
|
98
104
|
|
|
99
105
|
## CLI path
|
|
100
106
|
|
|
@@ -113,24 +119,49 @@ Parse the single JSON document.
|
|
|
113
119
|
| `0` | `succeeded` | [Report](#report) |
|
|
114
120
|
| `1` | `failed` | [Report](#report) `reason` verbatim |
|
|
115
121
|
| `1` | `busy` | [Report](#report) `busy_capacity` / `busy_checkout` and the included fields. Stop. |
|
|
116
|
-
| `2` | `waiting` | [
|
|
122
|
+
| `2` | `waiting` | [CLI wait](#cli-wait) with this `runId` / `runDir` |
|
|
123
|
+
|
|
124
|
+
**Done when** the outcome is reported, or a waiting exit has handed `runId` to CLI wait.
|
|
125
|
+
|
|
126
|
+
## CLI wait
|
|
127
|
+
|
|
128
|
+
Host down after `sf run` exit `2`, or when this chat already has a `runId` and the host is down. Do not start `sf mcp`. Presentation follows [`references/native-question-ui.md`](references/native-question-ui.md). Submit with `sf runs answer --json`.
|
|
129
|
+
|
|
130
|
+
1. Probe again with [`../stageflow/scripts/detect-host.mjs`](../stageflow/scripts/detect-host.mjs) (the host may have appeared). Probe before each `sf runs answer`.
|
|
131
|
+
2. **Up:** use that host — native tools if present, otherwise `mcp-call.mjs` (omit `--stateless` unless the user started the host with `--mcp-stateless`). Continue at [Gate](#gate).
|
|
132
|
+
3. **Down:**
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
```
|
|
135
|
+
sf runs waiting --run <runId> --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Print the pending prompt **verbatim**. Present it as native-question-ui directs (picker or chat). Map the reply with the [Gate](#gate) kind table.
|
|
119
139
|
|
|
120
|
-
|
|
140
|
+
```
|
|
141
|
+
sf runs answer --run <runId> --stage <stageId> --answer '<json>' --json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Success is `{ "ok": true }` exit `0` even if the run parks again. Do not treat answer as terminal.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
sf runs wait --run <runId> --json --until any
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A shorter `--timeout-ms` is fine when the harness timeout is tight. Default `60000`, max `240000`. Branch on JSON `reason`, not wait exit `0`:
|
|
151
|
+
|
|
152
|
+
| `reason` | next |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `waiting` or `already` with a waiting snapshot | `sf runs waiting` then answer |
|
|
155
|
+
| `terminal` | [Report](#report) |
|
|
156
|
+
| `timeout` | call `sf runs wait` again |
|
|
121
157
|
|
|
122
|
-
|
|
158
|
+
Wait abort (exit `130`, `{ "error", "code": "aborted" }`) → [Report](#report): the run continues, resumable later. Stop. Do not reuse `sf run` exit `2` for wait.
|
|
123
159
|
|
|
124
|
-
|
|
125
|
-
2. **Up:** use that host — native tools if present, otherwise `mcp-call.mjs` (omit `--stateless`). Continue at [Gate](#gate).
|
|
126
|
-
3. **Down:** start `sf mcp --mcp-stateless` as a background process on that same project root. This is a disposable HITL bridge, not setup auto-start.
|
|
127
|
-
4. Probe until stdout is `up <baseUrl>`, at most 8 times. **Done when** the host is up. If it stays down, [Report](#report) the parked `runId` / `runDir` and the bind/stderr text. Stop.
|
|
128
|
-
5. Drive the rest with `mcp-call.mjs --stateless` only: [Gate](#gate) → [Wait](#wait) until terminal. A second wait on the same run reuses this process.
|
|
129
|
-
6. When the run is terminal, stop the bridge process.
|
|
160
|
+
4. If `sf runs answer` refuses because the host came up, continue that gate via MCP [Gate](#gate). Do not dual-write.
|
|
130
161
|
|
|
131
162
|
If this chat ends before terminal, say the run stays in `.stageflow/` and to continue by re-invoking this skill (or starting a host and using MCP).
|
|
132
163
|
|
|
133
|
-
**Done when** the run is terminal
|
|
164
|
+
**Done when** the run is terminal, aborted, or a hard error is printed.
|
|
134
165
|
|
|
135
166
|
## Report
|
|
136
167
|
|
|
@@ -142,10 +173,10 @@ Print one shape on every path:
|
|
|
142
173
|
4. Run id.
|
|
143
174
|
5. Run folder (`runDir` / `.stageflow/`).
|
|
144
175
|
|
|
145
|
-
MCP `get_run` / `wait_run` carry live stage state. `sf run --json` reports start and finish only — say that in chat when the CLI path ran without a
|
|
176
|
+
MCP `get_run` / `wait_run` and `sf runs wait` / `sf runs show` `--json` carry live stage state. `sf run --json` reports start and finish only — say that in chat when the CLI path ran without a later wait/show. A succeeded report names id and folder and does not keep waiting language. A failure names `reason` in this same shape.
|
|
146
177
|
|
|
147
178
|
**Done when** that five-part report is printed.
|
|
148
179
|
|
|
149
180
|
## Non-goals
|
|
150
181
|
|
|
151
|
-
This job starts, watches, and answers runs. It does not require `sf ui`. The operator console is not the answer path. It does not register or invent an MCP tool.
|
|
182
|
+
This job starts, watches, and answers runs. It does not require `sf ui`. The operator console is not the answer path. It does not register or invent an MCP tool. It does not start `sf mcp`. It does not retry, abandon, or rerun stages.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mcp-call
|
|
2
2
|
|
|
3
|
-
Use [`../scripts/mcp-call.mjs`](../scripts/mcp-call.mjs) when this harness has no native Stageflow MCP tools
|
|
3
|
+
Use [`../scripts/mcp-call.mjs`](../scripts/mcp-call.mjs) when the Stageflow host is up and this harness has no native Stageflow MCP tools. Do not start `sf mcp` from this skill.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
node scripts/mcp-call.mjs --base-url <url> --tool <name> --args '<json>' [--stateless]
|
|
@@ -8,7 +8,7 @@ node scripts/mcp-call.mjs --base-url <url> --tool <name> --args '<json>' [--stat
|
|
|
8
8
|
|
|
9
9
|
Default `--base-url` is `http://127.0.0.1:3847`. `--args` defaults to `{}`.
|
|
10
10
|
|
|
11
|
-
`--stateless` sends one `tools/call` and no session header. Use it for hosts started with `sf mcp --mcp-stateless
|
|
11
|
+
`--stateless` sends one `tools/call` and no session header. Use it for hosts the **user** started with `sf mcp --mcp-stateless`, not a skill-started process.
|
|
12
12
|
|
|
13
13
|
Without `--stateless`: `initialize`, capture `Mcp-Session-Id`, reuse it for `tools/call`.
|
|
14
14
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Native question UI
|
|
2
|
+
|
|
3
|
+
Present a waiting HITL gate on the coding-agent host's structured question tool when that tool can represent the gate. Otherwise collect the reply in this chat. Submit with `answer_gate` when the Stageflow host is up, or `sf runs answer --json` when it is down. This file is presentation only — it does not change MCP tools, `detect-host.mjs`, or the operator console.
|
|
4
|
+
|
|
5
|
+
Read `pending_prompt` from `list_waiting` or `sf runs waiting --json` (not only `waiting_questions` strings).
|
|
6
|
+
|
|
7
|
+
## Detect a picker
|
|
8
|
+
|
|
9
|
+
Inspect **this harness's current tool list**. A picker is a tool that collects a structured choice (question text plus labeled options), not a free-form chat reply.
|
|
10
|
+
|
|
11
|
+
Do not run `detect-host.mjs` for this. That script is Stageflow host up/down (MCP vs CLI). It does not detect a question UI.
|
|
12
|
+
|
|
13
|
+
| If this name is in the tool list | Use it |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `AskQuestion` | Cursor question cards |
|
|
16
|
+
| `AskUserQuestion` | Claude Code questions |
|
|
17
|
+
| `ask_user` | Pi questions |
|
|
18
|
+
|
|
19
|
+
If none of those (or another options-based question tool already in the list) is present, collect in chat. Do not invent a tool name or call shape. Codex and OpenCode stay in chat until an options-based question tool appears in the list.
|
|
20
|
+
|
|
21
|
+
Pass the pending message as the question text and the mapped labels as that tool's options. One question per call except `multi_question`: one call with one question per representable sub-item when the tool accepts a questions array (Cursor `AskQuestion` does). Do not invent placeholder options such as Type in Other — host Other is the custom-text escape. If the picker returns a custom or Other value, map that text the same as a chat reply.
|
|
22
|
+
|
|
23
|
+
**Done when** you know picker or chat for this gate.
|
|
24
|
+
|
|
25
|
+
## Representable vs chat
|
|
26
|
+
|
|
27
|
+
| Gate | Picker | Chat |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `confirm` | Accept and Reject | Host has no picker |
|
|
30
|
+
| `artifact_backed` | Accept and Reject (decision only) | Host has no picker |
|
|
31
|
+
| `free_text` with a harvested closed set | Those names as options | Open-ended, or harvest is ambiguous |
|
|
32
|
+
| `free_text` with no closed set | — | Always |
|
|
33
|
+
| `multi_question` | One picker call with one question per sub-item when **every** sub-question is representable; then one submit | Any sub-question is open or unmappable — the **whole** gate in chat |
|
|
34
|
+
|
|
35
|
+
Map picker Accept / yes / approve → `accept`. Reject / no / deny → `reject`. Always include `promptId` (`pending_prompt.id` or `waiting_prompt_id`).
|
|
36
|
+
|
|
37
|
+
| Item | Submit |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Top-level or sub `confirm` | `{ "kind": "confirm", "decision": "accept" \| "reject" }` |
|
|
40
|
+
| Top-level `artifact_backed` | `{ "kind": "artifact_backed", "decision": "accept" \| "reject" }` plus optional `text` after a reject follow-up |
|
|
41
|
+
| Top-level or sub `free_text` (including harvested) | `{ "kind": "free_text", "text": "<selected label>" }` |
|
|
42
|
+
|
|
43
|
+
Never submit a `confirm` item as `free_text`. For `multi_question`, wrap sub-answers in the Gate `answers` object and submit once (`answer_gate` or `sf runs answer --json`).
|
|
44
|
+
|
|
45
|
+
When in doubt, chat.
|
|
46
|
+
|
|
47
|
+
**Done when** the path (picker or chat) is chosen from this table.
|
|
48
|
+
|
|
49
|
+
## Harvest
|
|
50
|
+
|
|
51
|
+
Harvest only from `pending_prompt.message` (or a `multi_question` item `message`) when it names a **hard closed set**:
|
|
52
|
+
|
|
53
|
+
- `reply with exactly one of:`
|
|
54
|
+
- `choose one of:`
|
|
55
|
+
- `exactly one of`
|
|
56
|
+
|
|
57
|
+
Split the names after that directive (commas, `or`, newlines). Those literal names are the picker options.
|
|
58
|
+
|
|
59
|
+
Do **not** harvest loose example lists (`you might try`, `e.g.`, `for example` without `exactly one of`). A listed example set that is not a hard closed directive stays in chat.
|
|
60
|
+
|
|
61
|
+
Submit the selected label as `text` for `free_text` items. If the prompt also lists aliases for the same choice (for example `minimal` and `prototype~1`), keep every listed name on the card; do not rewrite the label at submit — the stage maps synonyms.
|
|
62
|
+
|
|
63
|
+
**Done when** the option list is the prompt's literal closed set, or the gate stayed in chat.
|
|
64
|
+
|
|
65
|
+
## Artifact-backed
|
|
66
|
+
|
|
67
|
+
1. Print the pending message and artifact paths (from `list_waiting`) in this chat. Do not put artifact bodies on the card. Do not call `read_artifact` unless the human asked to open a file.
|
|
68
|
+
2. Open the picker with Accept and Reject.
|
|
69
|
+
3. Accept → submit immediately (`decision: "accept"`).
|
|
70
|
+
4. Reject → one chat follow-up for optional notes, then submit `decision: "reject"` and `text` when they typed notes.
|
|
71
|
+
|
|
72
|
+
**Done when** the operator has seen the paths in chat and the decision is on the picker (or chat if there is no picker).
|
|
73
|
+
|
|
74
|
+
## Errors
|
|
75
|
+
|
|
76
|
+
On MCP `answer_gate` `isError` (400 / 404 / 409) or CLI `sf runs answer` exit `1`, print the payload. Collect a corrected reply on the picker if the gate is still representable; otherwise chat. Then submit again.
|
|
77
|
+
|
|
78
|
+
**Done when** `{ "ok": true }` or the error payload has been printed and a corrected reply is in hand.
|