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.
Files changed (38) hide show
  1. package/README.md +33 -0
  2. package/dist/cli/ciIdentity.d.ts +14 -0
  3. package/dist/cli/ciIdentity.js +52 -0
  4. package/dist/cli/runCommand.d.ts +24 -0
  5. package/dist/cli/runCommand.js +168 -0
  6. package/dist/cli/runOutput.d.ts +19 -0
  7. package/dist/cli/runOutput.js +125 -0
  8. package/dist/cli.d.ts +11 -1
  9. package/dist/cli.js +41 -58
  10. package/dist/mcp/server.js +1 -1
  11. package/dist/runstore/port.d.ts +6 -0
  12. package/dist/runstore/sqlite/SqliteRunStore.js +20 -4
  13. package/dist/runstore/sqlite/schema.d.ts +1 -1
  14. package/dist/runstore/sqlite/schema.js +4 -1
  15. package/dist/runtime/pipelineRunner.d.ts +8 -0
  16. package/dist/runtime/pipelineRunner.js +10 -0
  17. package/dist/runtime/pipelineScheduler.d.ts +1 -0
  18. package/dist/runtime/pipelineScheduler.js +19 -3
  19. package/dist/runtime/resumeReconstruct.js +4 -3
  20. package/dist/runtime/runManager.d.ts +4 -0
  21. package/dist/runtime/runManager.js +14 -3
  22. package/dist/runtime/stageProcessLauncher.d.ts +1 -0
  23. package/dist/runtime/stageProcessLauncher.js +3 -0
  24. package/dist/runtime/stageRunner.d.ts +2 -0
  25. package/dist/runtime/stageRunner.js +9 -2
  26. package/dist/runtime/stageWorker.js +2 -0
  27. package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/dist/agent/cursorExtension.d.ts +0 -12
  30. package/dist/agent/cursorExtension.js +0 -88
  31. package/dist/runstore/catalog.d.ts +0 -8
  32. package/dist/runstore/catalog.js +0 -16
  33. package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
  34. package/dist/runstore/disk/DiskRunStore.js +0 -238
  35. package/dist/runstore/layout.d.ts +0 -22
  36. package/dist/runstore/layout.js +0 -58
  37. package/dist/runtime/hitlSeams.d.ts +0 -38
  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
- export {};
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 { getAgentDir } from "@earendil-works/pi-coding-agent";
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" || command === "validate") {
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
- if (parsed.command !== "run") {
218
- console.error(`Unknown command: ${parsed.command}`);
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
- main(process.argv)
269
- .then((code) => process.exit(code))
270
- .catch((err) => {
271
- console.error(err instanceof Error ? err.message : String(err));
272
- process.exit(1);
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
+ }
@@ -4,7 +4,7 @@ import { registerMcpTools } from "./tools.js";
4
4
  export function createStageflowMcpServer(deps) {
5
5
  const server = new McpServer({
6
6
  name: "stageflow",
7
- version: "0.1.0",
7
+ version: "0.2.0",
8
8
  });
9
9
  registerMcpTools(server, deps);
10
10
  return server;
@@ -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
  /**