stageflow 0.1.0 → 0.2.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 +33 -0
- package/dist/cli/ciIdentity.d.ts +14 -0
- package/dist/cli/ciIdentity.js +52 -0
- package/dist/cli/runCommand.d.ts +24 -0
- package/dist/cli/runCommand.js +168 -0
- package/dist/cli/runOutput.d.ts +19 -0
- package/dist/cli/runOutput.js +125 -0
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +41 -58
- package/dist/mcp/server.js +1 -1
- package/dist/runstore/port.d.ts +6 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +20 -4
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +4 -1
- package/dist/runtime/pipelineRunner.d.ts +8 -0
- package/dist/runtime/pipelineRunner.js +10 -0
- package/dist/runtime/pipelineScheduler.d.ts +1 -0
- package/dist/runtime/pipelineScheduler.js +19 -3
- package/dist/runtime/resumeReconstruct.js +4 -3
- package/dist/runtime/runManager.d.ts +4 -0
- package/dist/runtime/runManager.js +14 -3
- package/dist/runtime/stageProcessLauncher.d.ts +1 -0
- package/dist/runtime/stageProcessLauncher.js +3 -0
- package/dist/runtime/stageRunner.d.ts +2 -0
- package/dist/runtime/stageRunner.js +9 -2
- package/dist/runtime/stageWorker.js +2 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
- package/package.json +1 -1
- package/dist/agent/cursorExtension.d.ts +0 -12
- package/dist/agent/cursorExtension.js +0 -88
- package/dist/runstore/catalog.d.ts +0 -8
- package/dist/runstore/catalog.js +0 -16
- package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
- package/dist/runstore/disk/DiskRunStore.js +0 -238
- package/dist/runstore/layout.d.ts +0 -22
- package/dist/runstore/layout.js +0 -58
- package/dist/runtime/hitlSeams.d.ts +0 -38
- package/dist/runtime/hitlSeams.js +0 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Requires **Node.js ≥ 20**.
|
|
|
12
12
|
npm i -g stageflow
|
|
13
13
|
# or
|
|
14
14
|
npx stageflow
|
|
15
|
+
# or, from a packed tarball
|
|
16
|
+
npm i -g ./stageflow-*.tgz
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
`better-sqlite3` ships prebuilds for common platforms. `--ignore-scripts` is fine when a prebuild exists. Benign `node-gyp` warnings during install can be ignored if `require("better-sqlite3")` works.
|
|
@@ -31,6 +33,37 @@ sf run --task tasks/foo.yaml --pipeline <pipeline-id>
|
|
|
31
33
|
|
|
32
34
|
Connect model providers in the console (Settings → Providers) or via `sf providers …`. Stageflow is a thin Pi shell: reuse an existing Pi login (`pi_home`) or store credentials in an SF-owned file (`sf_owned`). You do not need Pi CLI `/login` as a hard prerequisite.
|
|
33
35
|
|
|
36
|
+
## Headless / CI
|
|
37
|
+
|
|
38
|
+
The guest actor is the CLI (`sf` / `stageflow`). `sf ui` and MCP are not required in the job.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sf validate --strict --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Validate exits `0` or `1` only (no waiting / `2`). It checks pipeline and stage YAML only; it does not prove provider auth, Task, or checkout.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sf providers login <providerId> --api-key-env <VAR>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the provider also supports OAuth, pass `--type api_key`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
sf run --task tasks/foo.yaml --pipeline <pipeline-id> --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The process exits `0` when the Run succeeded, `1` when it failed (including a busy start), and `2` when waiting. `sf run --json` prints one stdout document. `ok` is true only for `succeeded`. Busy has no `runId`.
|
|
57
|
+
|
|
58
|
+
| outcome | ok | runId | exit |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| `succeeded` | true | present | `0` |
|
|
61
|
+
| `failed` | false | present after start; omit when start never created a run | `1` |
|
|
62
|
+
| `waiting` | false | present | `2` |
|
|
63
|
+
| `busy` | false | omit | `1` |
|
|
64
|
+
|
|
65
|
+
On a mixed Pipeline, default wait parks the Run (exit `2`). `--skip-gates` fails the Stage (exit `1`). A Pipeline with no HITL does not need the flag.
|
|
66
|
+
|
|
34
67
|
## State
|
|
35
68
|
|
|
36
69
|
Runtime state lives in **`.stageflow/`** (SQLite + per-run workspaces under `.stageflow/runs/`). If `.stageflow` is missing and `.software-factory` exists from an older install, the next store open renames it to `.stageflow` once.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CiIdentityFlags = {
|
|
2
|
+
gitSha?: string;
|
|
3
|
+
ciPrUrl?: string;
|
|
4
|
+
ciJobUrl?: string;
|
|
5
|
+
};
|
|
6
|
+
export type CiIdentity = {
|
|
7
|
+
gitSha?: string;
|
|
8
|
+
ciPrUrl?: string;
|
|
9
|
+
ciJobUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveCiIdentity(input: {
|
|
12
|
+
flags: CiIdentityFlags;
|
|
13
|
+
env: Record<string, string | undefined>;
|
|
14
|
+
}): CiIdentity;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function nonempty(value) {
|
|
2
|
+
if (value === undefined)
|
|
3
|
+
return undefined;
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
6
|
+
}
|
|
7
|
+
function prNumberFromEnv(env) {
|
|
8
|
+
const ref = nonempty(env.GITHUB_REF);
|
|
9
|
+
if (ref !== undefined) {
|
|
10
|
+
const match = /^refs\/pull\/(\d+)\//.exec(ref);
|
|
11
|
+
if (match?.[1] !== undefined)
|
|
12
|
+
return match[1];
|
|
13
|
+
}
|
|
14
|
+
const refName = nonempty(env.GITHUB_REF_NAME);
|
|
15
|
+
if (refName !== undefined) {
|
|
16
|
+
const match = /^(\d+)\/merge$/.exec(refName);
|
|
17
|
+
if (match?.[1] !== undefined)
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function resolveCiIdentity(input) {
|
|
23
|
+
const result = {};
|
|
24
|
+
const gitSha = nonempty(input.flags.gitSha) ?? nonempty(input.env.GITHUB_SHA);
|
|
25
|
+
if (gitSha !== undefined)
|
|
26
|
+
result.gitSha = gitSha;
|
|
27
|
+
const flagPrUrl = nonempty(input.flags.ciPrUrl);
|
|
28
|
+
if (flagPrUrl !== undefined) {
|
|
29
|
+
result.ciPrUrl = flagPrUrl;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const prNumber = prNumberFromEnv(input.env);
|
|
33
|
+
const server = nonempty(input.env.GITHUB_SERVER_URL);
|
|
34
|
+
const repo = nonempty(input.env.GITHUB_REPOSITORY);
|
|
35
|
+
if (prNumber !== undefined && server !== undefined && repo !== undefined) {
|
|
36
|
+
result.ciPrUrl = `${server}/${repo}/pull/${prNumber}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const flagJobUrl = nonempty(input.flags.ciJobUrl);
|
|
40
|
+
if (flagJobUrl !== undefined) {
|
|
41
|
+
result.ciJobUrl = flagJobUrl;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const server = nonempty(input.env.GITHUB_SERVER_URL);
|
|
45
|
+
const repo = nonempty(input.env.GITHUB_REPOSITORY);
|
|
46
|
+
const runId = nonempty(input.env.GITHUB_RUN_ID);
|
|
47
|
+
if (server !== undefined && repo !== undefined && runId !== undefined) {
|
|
48
|
+
result.ciJobUrl = `${server}/${repo}/actions/runs/${runId}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type StartRunResult } from "../runtime/runManager.js";
|
|
2
|
+
import { type CliRunReportIo } from "./runOutput.js";
|
|
3
|
+
export declare const RUN_USAGE = "Usage:\n sf run --task <path> --pipeline <name-or-path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>]";
|
|
4
|
+
export type RunCommandIo = CliRunReportIo;
|
|
5
|
+
export type StartRunFn = (input: {
|
|
6
|
+
task: string;
|
|
7
|
+
pipeline: string;
|
|
8
|
+
checkoutOverride?: string;
|
|
9
|
+
skipGates?: boolean;
|
|
10
|
+
gitSha?: string;
|
|
11
|
+
ciPrUrl?: string;
|
|
12
|
+
ciJobUrl?: string;
|
|
13
|
+
}) => Promise<StartRunResult>;
|
|
14
|
+
export declare function completeCliRun(started: Extract<StartRunResult, {
|
|
15
|
+
ok: true;
|
|
16
|
+
}>, io?: RunCommandIo, options?: {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
}): Promise<number>;
|
|
19
|
+
export declare function runRunCommand(args: string[], options?: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
io?: Partial<RunCommandIo>;
|
|
22
|
+
startRun?: StartRunFn;
|
|
23
|
+
env?: Record<string, string | undefined>;
|
|
24
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { PiAgentAdapter } from "../agent/piAdapter.js";
|
|
3
|
+
import { createRunStore } from "../runstore/createStore.js";
|
|
4
|
+
import { PipelineValidationError } from "../runtime/pipelineRunner.js";
|
|
5
|
+
import { RunManager } from "../runtime/runManager.js";
|
|
6
|
+
import { readStageExecutionMode } from "../runtime/stageConcurrency.js";
|
|
7
|
+
import { reportCliRun, } from "./runOutput.js";
|
|
8
|
+
import { resolveCiIdentity } from "./ciIdentity.js";
|
|
9
|
+
import { exitCodeForValidation, formatValidationHuman, formatValidationJson, } from "./validateOutput.js";
|
|
10
|
+
export const RUN_USAGE = `Usage:
|
|
11
|
+
sf run --task <path> --pipeline <name-or-path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>]`;
|
|
12
|
+
const defaultIo = {
|
|
13
|
+
log: (line) => console.log(line),
|
|
14
|
+
error: (line) => console.error(line),
|
|
15
|
+
};
|
|
16
|
+
function parseRunArgs(args) {
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
return { help: false, json: false, skipGates: false };
|
|
19
|
+
}
|
|
20
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
21
|
+
return { help: true, json: false, skipGates: false };
|
|
22
|
+
}
|
|
23
|
+
let task;
|
|
24
|
+
let pipeline;
|
|
25
|
+
let checkout;
|
|
26
|
+
let gitSha;
|
|
27
|
+
let ciPrUrl;
|
|
28
|
+
let ciJobUrl;
|
|
29
|
+
let json = false;
|
|
30
|
+
let skipGates = false;
|
|
31
|
+
let help = false;
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i];
|
|
34
|
+
if (arg === "--help" || arg === "-h") {
|
|
35
|
+
help = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "--task") {
|
|
38
|
+
const value = args[++i];
|
|
39
|
+
if (value === undefined || value.length === 0) {
|
|
40
|
+
throw new Error("Missing value for --task");
|
|
41
|
+
}
|
|
42
|
+
task = value;
|
|
43
|
+
}
|
|
44
|
+
else if (arg === "--pipeline") {
|
|
45
|
+
const value = args[++i];
|
|
46
|
+
if (value === undefined || value.length === 0) {
|
|
47
|
+
throw new Error("Missing value for --pipeline");
|
|
48
|
+
}
|
|
49
|
+
pipeline = value;
|
|
50
|
+
}
|
|
51
|
+
else if (arg === "--checkout") {
|
|
52
|
+
const value = args[++i];
|
|
53
|
+
if (value === undefined || value.length === 0) {
|
|
54
|
+
throw new Error("Missing value for --checkout");
|
|
55
|
+
}
|
|
56
|
+
checkout = value;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--git-sha") {
|
|
59
|
+
const value = args[++i];
|
|
60
|
+
if (value === undefined || value.length === 0) {
|
|
61
|
+
throw new Error("Missing value for --git-sha");
|
|
62
|
+
}
|
|
63
|
+
gitSha = value;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--ci-pr-url") {
|
|
66
|
+
const value = args[++i];
|
|
67
|
+
if (value === undefined || value.length === 0) {
|
|
68
|
+
throw new Error("Missing value for --ci-pr-url");
|
|
69
|
+
}
|
|
70
|
+
ciPrUrl = value;
|
|
71
|
+
}
|
|
72
|
+
else if (arg === "--ci-job-url") {
|
|
73
|
+
const value = args[++i];
|
|
74
|
+
if (value === undefined || value.length === 0) {
|
|
75
|
+
throw new Error("Missing value for --ci-job-url");
|
|
76
|
+
}
|
|
77
|
+
ciJobUrl = value;
|
|
78
|
+
}
|
|
79
|
+
else if (arg === "--json") {
|
|
80
|
+
json = true;
|
|
81
|
+
}
|
|
82
|
+
else if (arg === "--skip-gates") {
|
|
83
|
+
skipGates = true;
|
|
84
|
+
}
|
|
85
|
+
else if (arg.startsWith("-")) {
|
|
86
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { help, json, skipGates, task, pipeline, checkout, gitSha, ciPrUrl, ciJobUrl };
|
|
93
|
+
}
|
|
94
|
+
function defaultStartRun(cwd) {
|
|
95
|
+
return async (input) => {
|
|
96
|
+
const store = createRunStore({ rootDir: cwd });
|
|
97
|
+
const manager = new RunManager({
|
|
98
|
+
agent: new PiAgentAdapter(),
|
|
99
|
+
store,
|
|
100
|
+
cwd,
|
|
101
|
+
operatorCatalog: { cwd, agentDir: getAgentDir() },
|
|
102
|
+
executionMode: readStageExecutionMode(process.env, "process"),
|
|
103
|
+
});
|
|
104
|
+
return manager.startRun(input);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export async function completeCliRun(started, io = defaultIo, options = {}) {
|
|
108
|
+
const result = await started.done;
|
|
109
|
+
return reportCliRun({ kind: "completion", result }, { json: options.json, io });
|
|
110
|
+
}
|
|
111
|
+
export async function runRunCommand(args, options = {}) {
|
|
112
|
+
const cwd = options.cwd ?? process.cwd();
|
|
113
|
+
const out = { ...defaultIo, ...options.io };
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = parseRunArgs(args);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
out.error(message);
|
|
121
|
+
out.error(RUN_USAGE);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
if (parsed.help) {
|
|
125
|
+
out.error(RUN_USAGE);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (!parsed.task || !parsed.pipeline) {
|
|
129
|
+
out.error("Missing --task and/or --pipeline");
|
|
130
|
+
out.error(RUN_USAGE);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
const startRun = options.startRun ?? defaultStartRun(cwd);
|
|
134
|
+
const identity = resolveCiIdentity({
|
|
135
|
+
flags: {
|
|
136
|
+
gitSha: parsed.gitSha,
|
|
137
|
+
ciPrUrl: parsed.ciPrUrl,
|
|
138
|
+
ciJobUrl: parsed.ciJobUrl,
|
|
139
|
+
},
|
|
140
|
+
env: options.env ?? process.env,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
const started = await startRun({
|
|
144
|
+
task: parsed.task,
|
|
145
|
+
pipeline: parsed.pipeline,
|
|
146
|
+
checkoutOverride: parsed.checkout,
|
|
147
|
+
...(parsed.skipGates ? { skipGates: true } : {}),
|
|
148
|
+
...identity,
|
|
149
|
+
});
|
|
150
|
+
if (!started.ok) {
|
|
151
|
+
return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: out });
|
|
152
|
+
}
|
|
153
|
+
return completeCliRun(started, out, { json: parsed.json });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof PipelineValidationError) {
|
|
157
|
+
if (parsed.json) {
|
|
158
|
+
out.log(formatValidationJson(err.result));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
out.error(formatValidationHuman(err.result));
|
|
162
|
+
}
|
|
163
|
+
return exitCodeForValidation(err.result);
|
|
164
|
+
}
|
|
165
|
+
out.error(err instanceof Error ? err.message : String(err));
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PipelineRunResult } from "../runtime/pipelineRunner.js";
|
|
2
|
+
import type { StartRunResult } from "../runtime/runManager.js";
|
|
3
|
+
export type CliRunReportIo = {
|
|
4
|
+
log: (line: string) => void;
|
|
5
|
+
error: (line: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export type CliRunReportEvent = {
|
|
8
|
+
kind: "start-failure";
|
|
9
|
+
started: Extract<StartRunResult, {
|
|
10
|
+
ok: false;
|
|
11
|
+
}>;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "completion";
|
|
14
|
+
result: PipelineRunResult;
|
|
15
|
+
};
|
|
16
|
+
export declare function reportCliRun(event: CliRunReportEvent, options: {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
io: CliRunReportIo;
|
|
19
|
+
}): number;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function stringify(payload) {
|
|
2
|
+
return JSON.stringify(payload, null, 2);
|
|
3
|
+
}
|
|
4
|
+
function isBusyCode(code) {
|
|
5
|
+
return code === "busy_capacity" || code === "busy_checkout";
|
|
6
|
+
}
|
|
7
|
+
function formatRunBusyJson(started) {
|
|
8
|
+
const payload = {
|
|
9
|
+
ok: false,
|
|
10
|
+
outcome: "busy",
|
|
11
|
+
};
|
|
12
|
+
if (started.code !== undefined)
|
|
13
|
+
payload.code = started.code;
|
|
14
|
+
payload.reason = started.reason;
|
|
15
|
+
if (started.activeCount !== undefined)
|
|
16
|
+
payload.activeCount = started.activeCount;
|
|
17
|
+
if (started.maxConcurrent !== undefined) {
|
|
18
|
+
payload.maxConcurrent = started.maxConcurrent;
|
|
19
|
+
}
|
|
20
|
+
if (started.activeRunIds !== undefined) {
|
|
21
|
+
payload.activeRunIds = started.activeRunIds;
|
|
22
|
+
}
|
|
23
|
+
if (started.conflictingRunId !== undefined) {
|
|
24
|
+
payload.conflictingRunId = started.conflictingRunId;
|
|
25
|
+
}
|
|
26
|
+
if (started.conflictingCheckout !== undefined) {
|
|
27
|
+
payload.conflictingCheckout = started.conflictingCheckout;
|
|
28
|
+
}
|
|
29
|
+
return stringify(payload);
|
|
30
|
+
}
|
|
31
|
+
function formatRunStartFailedJson(started) {
|
|
32
|
+
const payload = {
|
|
33
|
+
ok: false,
|
|
34
|
+
outcome: "failed",
|
|
35
|
+
reason: started.reason,
|
|
36
|
+
};
|
|
37
|
+
if (started.code !== undefined)
|
|
38
|
+
payload.code = started.code;
|
|
39
|
+
return stringify(payload);
|
|
40
|
+
}
|
|
41
|
+
function formatStartFailureJson(started) {
|
|
42
|
+
if (isBusyCode(started.code)) {
|
|
43
|
+
return formatRunBusyJson(started);
|
|
44
|
+
}
|
|
45
|
+
return formatRunStartFailedJson(started);
|
|
46
|
+
}
|
|
47
|
+
function formatRunCompletionJson(result) {
|
|
48
|
+
if (result.outcome === "succeeded") {
|
|
49
|
+
return stringify({
|
|
50
|
+
ok: true,
|
|
51
|
+
outcome: "succeeded",
|
|
52
|
+
runId: result.runId,
|
|
53
|
+
runDir: result.runDir,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (result.outcome === "waiting") {
|
|
57
|
+
return stringify({
|
|
58
|
+
ok: false,
|
|
59
|
+
outcome: "waiting",
|
|
60
|
+
runId: result.runId,
|
|
61
|
+
runDir: result.runDir,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const payload = {
|
|
65
|
+
ok: false,
|
|
66
|
+
outcome: "failed",
|
|
67
|
+
runId: result.runId,
|
|
68
|
+
runDir: result.runDir,
|
|
69
|
+
};
|
|
70
|
+
if (result.reason !== undefined)
|
|
71
|
+
payload.reason = result.reason;
|
|
72
|
+
return stringify(payload);
|
|
73
|
+
}
|
|
74
|
+
function writeStartFailureHuman(started, io) {
|
|
75
|
+
const code = started.code ?? "error";
|
|
76
|
+
io.error(`${code}: ${started.reason}`);
|
|
77
|
+
if (started.conflictingRunId !== undefined) {
|
|
78
|
+
io.error(`conflictingRunId: ${started.conflictingRunId}`);
|
|
79
|
+
}
|
|
80
|
+
if (started.conflictingCheckout !== undefined) {
|
|
81
|
+
io.error(`conflictingCheckout: ${started.conflictingCheckout}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function writeCompletionHuman(result, io) {
|
|
85
|
+
switch (result.outcome) {
|
|
86
|
+
case "waiting":
|
|
87
|
+
io.error(`Pipeline waiting. Run folder: ${result.runDir} (${result.runId})`);
|
|
88
|
+
return;
|
|
89
|
+
case "failed":
|
|
90
|
+
io.error(`Pipeline failed: ${result.reason}`);
|
|
91
|
+
io.error(`Run folder: ${result.runDir}`);
|
|
92
|
+
return;
|
|
93
|
+
case "succeeded":
|
|
94
|
+
io.log(`Pipeline succeeded. Run folder: ${result.runDir}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function exitCodeForRunOutcome(outcome) {
|
|
99
|
+
switch (outcome) {
|
|
100
|
+
case "succeeded":
|
|
101
|
+
return 0;
|
|
102
|
+
case "waiting":
|
|
103
|
+
return 2;
|
|
104
|
+
case "failed":
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function reportCliRun(event, options) {
|
|
109
|
+
if (event.kind === "start-failure") {
|
|
110
|
+
if (options.json) {
|
|
111
|
+
options.io.log(formatStartFailureJson(event.started));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
writeStartFailureHuman(event.started, options.io);
|
|
115
|
+
}
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
if (options.json) {
|
|
119
|
+
options.io.log(formatRunCompletionJson(event.result));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
writeCompletionHuman(event.result, options.io);
|
|
123
|
+
}
|
|
124
|
+
return exitCodeForRunOutcome(event.result.outcome);
|
|
125
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import type { OperatorCatalog } from "./runtime/stageAttemptBootstrap.js";
|
|
3
|
+
export declare function parseRunStageArgs(argv: string[]): {
|
|
4
|
+
runId: string;
|
|
5
|
+
stageId: string;
|
|
6
|
+
mode?: "run" | "resume";
|
|
7
|
+
resumeAnswer?: unknown;
|
|
8
|
+
attempt?: number;
|
|
9
|
+
sessionFilePath?: string;
|
|
10
|
+
operatorCatalog?: OperatorCatalog;
|
|
11
|
+
skipGates?: boolean;
|
|
12
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { PiAgentAdapter } from "./agent/piAdapter.js";
|
|
5
7
|
import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
|
|
8
|
+
import { RUN_USAGE, runRunCommand } from "./cli/runCommand.js";
|
|
6
9
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
7
|
-
import { exitCodeForValidation, formatValidationHuman, } from "./cli/validateOutput.js";
|
|
8
10
|
import { createRunStore } from "./runstore/createStore.js";
|
|
9
|
-
import { PipelineValidationError } from "./runtime/pipelineRunner.js";
|
|
10
|
-
import { RunManager } from "./runtime/runManager.js";
|
|
11
|
-
import { readStageExecutionMode } from "./runtime/stageConcurrency.js";
|
|
12
11
|
import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
|
|
13
12
|
import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
|
|
14
13
|
import { DEFAULT_PORT, startUiServer } from "./server/http.js";
|
|
15
14
|
const USAGE = `Usage:
|
|
16
|
-
sf run --task <path> --pipeline <name-or-path> [--checkout <path>]
|
|
15
|
+
sf run --task <path> --pipeline <name-or-path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>]
|
|
17
16
|
sf validate [--pipeline <name-or-path>] [--strict] [--json]
|
|
18
17
|
sf ui [--port ${DEFAULT_PORT}]
|
|
19
18
|
sf providers list
|
|
@@ -28,6 +27,8 @@ Stageflow (sf) runs YAML-defined automatic stage pipelines.
|
|
|
28
27
|
|
|
29
28
|
Store backend: SF_STORE=sqlite only. SF_STORE=disk is rejected; disk-era .stageflow/runs trees import when the SQLite store is empty. Data under .stageflow/.
|
|
30
29
|
|
|
30
|
+
${RUN_USAGE}
|
|
31
|
+
|
|
31
32
|
${VALIDATE_USAGE}
|
|
32
33
|
|
|
33
34
|
${PROVIDERS_USAGE}`;
|
|
@@ -41,7 +42,9 @@ function parseArgs(argv) {
|
|
|
41
42
|
return { help: true };
|
|
42
43
|
}
|
|
43
44
|
const command = args[0];
|
|
44
|
-
if (command === "providers" ||
|
|
45
|
+
if (command === "providers" ||
|
|
46
|
+
command === "validate" ||
|
|
47
|
+
command === "run") {
|
|
45
48
|
return { help: false, command };
|
|
46
49
|
}
|
|
47
50
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -75,7 +78,7 @@ function parseArgs(argv) {
|
|
|
75
78
|
}
|
|
76
79
|
return { help: false, command, task, pipeline, port, checkout };
|
|
77
80
|
}
|
|
78
|
-
function parseRunStageArgs(argv) {
|
|
81
|
+
export function parseRunStageArgs(argv) {
|
|
79
82
|
let runId;
|
|
80
83
|
let stageId;
|
|
81
84
|
let mode;
|
|
@@ -83,6 +86,7 @@ function parseRunStageArgs(argv) {
|
|
|
83
86
|
let attempt;
|
|
84
87
|
let sessionFilePath;
|
|
85
88
|
let operatorAgentDir;
|
|
89
|
+
let skipGates = false;
|
|
86
90
|
for (let i = 0; i < argv.length; i++) {
|
|
87
91
|
if (argv[i] === "--run-id") {
|
|
88
92
|
runId = argv[++i];
|
|
@@ -132,6 +136,9 @@ function parseRunStageArgs(argv) {
|
|
|
132
136
|
throw new Error("Missing value for --operator-agent-dir");
|
|
133
137
|
}
|
|
134
138
|
}
|
|
139
|
+
else if (argv[i] === "--skip-gates") {
|
|
140
|
+
skipGates = true;
|
|
141
|
+
}
|
|
135
142
|
}
|
|
136
143
|
if (!runId || !stageId) {
|
|
137
144
|
throw new Error("Missing --run-id and/or --stage-id");
|
|
@@ -143,6 +150,7 @@ function parseRunStageArgs(argv) {
|
|
|
143
150
|
resumeAnswer,
|
|
144
151
|
attempt,
|
|
145
152
|
sessionFilePath,
|
|
153
|
+
skipGates,
|
|
146
154
|
...(operatorAgentDir !== undefined
|
|
147
155
|
? { operatorCatalog: { cwd: process.cwd(), agentDir: operatorAgentDir } }
|
|
148
156
|
: {}),
|
|
@@ -163,6 +171,7 @@ async function handleInternalRunStage(argv) {
|
|
|
163
171
|
attempt: parsed.attempt,
|
|
164
172
|
sessionFilePath: parsed.sessionFilePath,
|
|
165
173
|
operatorCatalog: parsed.operatorCatalog,
|
|
174
|
+
skipGates: parsed.skipGates,
|
|
166
175
|
});
|
|
167
176
|
exitForOutcome(outcome);
|
|
168
177
|
}
|
|
@@ -189,6 +198,9 @@ async function main(argv) {
|
|
|
189
198
|
if (parsed.command === "validate") {
|
|
190
199
|
return runValidateCommand(argv.slice(3), { cwd: rootDir });
|
|
191
200
|
}
|
|
201
|
+
if (parsed.command === "run") {
|
|
202
|
+
return runRunCommand(argv.slice(3), { cwd: rootDir });
|
|
203
|
+
}
|
|
192
204
|
if (parsed.command === "providers") {
|
|
193
205
|
return runProvidersCommand(argv.slice(3), rootDir);
|
|
194
206
|
}
|
|
@@ -214,60 +226,31 @@ async function main(argv) {
|
|
|
214
226
|
}
|
|
215
227
|
return handleInternalRunStage(subArgs.slice(1));
|
|
216
228
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
console.error(USAGE);
|
|
220
|
-
return 1;
|
|
221
|
-
}
|
|
222
|
-
if (!parsed.task || !parsed.pipeline) {
|
|
223
|
-
console.error("Missing --task and/or --pipeline");
|
|
224
|
-
console.error(USAGE);
|
|
225
|
-
return 1;
|
|
226
|
-
}
|
|
227
|
-
const manager = new RunManager({
|
|
228
|
-
agent: new PiAgentAdapter(),
|
|
229
|
-
store,
|
|
230
|
-
cwd: rootDir,
|
|
231
|
-
operatorCatalog: { cwd: rootDir, agentDir: getAgentDir() },
|
|
232
|
-
executionMode: readStageExecutionMode(process.env, "process"),
|
|
233
|
-
});
|
|
234
|
-
const started = await manager.startRun({
|
|
235
|
-
task: parsed.task,
|
|
236
|
-
pipeline: parsed.pipeline,
|
|
237
|
-
checkoutOverride: parsed.checkout,
|
|
238
|
-
});
|
|
239
|
-
if (!started.ok) {
|
|
240
|
-
const code = started.code ?? "error";
|
|
241
|
-
console.error(`${code}: ${started.reason}`);
|
|
242
|
-
if (started.conflictingRunId !== undefined) {
|
|
243
|
-
console.error(`conflictingRunId: ${started.conflictingRunId}`);
|
|
244
|
-
}
|
|
245
|
-
if (started.conflictingCheckout !== undefined) {
|
|
246
|
-
console.error(`conflictingCheckout: ${started.conflictingCheckout}`);
|
|
247
|
-
}
|
|
248
|
-
return started.status ?? 1;
|
|
249
|
-
}
|
|
250
|
-
const result = await started.done;
|
|
251
|
-
if (result.ok) {
|
|
252
|
-
console.log(`Pipeline succeeded. Run folder: ${result.runDir}`);
|
|
253
|
-
return 0;
|
|
254
|
-
}
|
|
255
|
-
console.error(`Pipeline failed: ${result.reason}`);
|
|
256
|
-
console.error(`Run folder: ${result.runDir}`);
|
|
229
|
+
console.error(`Unknown command: ${parsed.command}`);
|
|
230
|
+
console.error(USAGE);
|
|
257
231
|
return 1;
|
|
258
232
|
}
|
|
259
233
|
catch (err) {
|
|
260
|
-
if (err instanceof PipelineValidationError) {
|
|
261
|
-
console.error(formatValidationHuman(err.result));
|
|
262
|
-
return exitCodeForValidation(err.result);
|
|
263
|
-
}
|
|
264
234
|
console.error(err instanceof Error ? err.message : String(err));
|
|
265
235
|
return 1;
|
|
266
236
|
}
|
|
267
237
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
238
|
+
function isDirectCliInvocation() {
|
|
239
|
+
const self = fileURLToPath(import.meta.url);
|
|
240
|
+
return process.argv.slice(1).some((arg) => {
|
|
241
|
+
try {
|
|
242
|
+
return realpathSync(path.resolve(arg)) === realpathSync(self);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (isDirectCliInvocation()) {
|
|
250
|
+
main(process.argv)
|
|
251
|
+
.then((code) => process.exit(code))
|
|
252
|
+
.catch((err) => {
|
|
253
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|
|
256
|
+
}
|
package/dist/mcp/server.js
CHANGED
package/dist/runstore/port.d.ts
CHANGED
|
@@ -36,6 +36,9 @@ export type RunMeta = {
|
|
|
36
36
|
task_id?: string;
|
|
37
37
|
updated_at?: string;
|
|
38
38
|
checkout_root?: string;
|
|
39
|
+
git_sha?: string;
|
|
40
|
+
ci_pr_url?: string;
|
|
41
|
+
ci_job_url?: string;
|
|
39
42
|
pipeline_dag?: RunPipelineDagSnapshot;
|
|
40
43
|
};
|
|
41
44
|
export type CreatedRun = {
|
|
@@ -108,6 +111,9 @@ export type CreateRunInput = {
|
|
|
108
111
|
taskYaml: string;
|
|
109
112
|
taskId?: string;
|
|
110
113
|
checkoutRoot?: string;
|
|
114
|
+
gitSha?: string;
|
|
115
|
+
ciPrUrl?: string;
|
|
116
|
+
ciJobUrl?: string;
|
|
111
117
|
pipelineDag?: RunPipelineDagSnapshot;
|
|
112
118
|
};
|
|
113
119
|
/**
|