stageflow 0.1.0 → 0.3.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 (132) hide show
  1. package/README.md +190 -13
  2. package/dist/agent/activity.d.ts +10 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/fakeAgent.js +4 -0
  7. package/dist/agent/piAdapter.d.ts +16 -0
  8. package/dist/agent/piAdapter.js +88 -21
  9. package/dist/agent/port.d.ts +2 -0
  10. package/dist/agent/providerAuth.js +12 -7
  11. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  12. package/dist/catalog/displayCatalogPath.js +28 -0
  13. package/dist/cli/ciIdentity.d.ts +14 -0
  14. package/dist/cli/ciIdentity.js +52 -0
  15. package/dist/cli/initCommand.d.ts +9 -0
  16. package/dist/cli/initCommand.js +71 -0
  17. package/dist/cli/initTemplates.d.ts +3 -0
  18. package/dist/cli/initTemplates.js +19 -0
  19. package/dist/cli/operatorCatalog.d.ts +10 -0
  20. package/dist/cli/operatorCatalog.js +16 -0
  21. package/dist/cli/runCommand.d.ts +26 -0
  22. package/dist/cli/runCommand.js +209 -0
  23. package/dist/cli/runOutput.d.ts +19 -0
  24. package/dist/cli/runOutput.js +125 -0
  25. package/dist/cli/validateCommand.d.ts +1 -1
  26. package/dist/cli/validateCommand.js +20 -3
  27. package/dist/cli.d.ts +12 -1
  28. package/dist/cli.js +82 -66
  29. package/dist/config/browseCatalog.d.ts +45 -0
  30. package/dist/config/browseCatalog.js +170 -0
  31. package/dist/config/createPipeline.d.ts +12 -3
  32. package/dist/config/createPipeline.js +292 -100
  33. package/dist/config/createStage.d.ts +10 -4
  34. package/dist/config/createStage.js +44 -14
  35. package/dist/config/listConfig.d.ts +14 -19
  36. package/dist/config/listConfig.js +26 -191
  37. package/dist/config/listModelsFromManifest.d.ts +2 -0
  38. package/dist/config/listModelsFromManifest.js +4 -0
  39. package/dist/config/loadOutcome.d.ts +4 -0
  40. package/dist/config/loadPipeline.d.ts +1 -15
  41. package/dist/config/loadPipeline.js +84 -142
  42. package/dist/config/loadStage.d.ts +4 -0
  43. package/dist/config/loadStage.js +60 -32
  44. package/dist/config/loadStageflowManifest.d.ts +5 -0
  45. package/dist/config/loadStageflowManifest.js +157 -0
  46. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  47. package/dist/config/mergePipelineIncludes.js +141 -0
  48. package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
  49. package/dist/config/normalizePipelineStageEntry.js +186 -0
  50. package/dist/config/pipelineStageKeys.d.ts +4 -0
  51. package/dist/config/pipelineStageKeys.js +14 -0
  52. package/dist/config/resolveCatalogContext.d.ts +13 -0
  53. package/dist/config/resolveCatalogContext.js +13 -0
  54. package/dist/config/resolveForkEmitContext.d.ts +3 -0
  55. package/dist/config/resolveForkEmitContext.js +12 -0
  56. package/dist/config/resolvePipelineDag.d.ts +5 -1
  57. package/dist/config/resolvePipelineDag.js +54 -36
  58. package/dist/config/scanCatalogPaths.d.ts +4 -0
  59. package/dist/config/scanCatalogPaths.js +83 -0
  60. package/dist/config/validateCatalog.d.ts +22 -4
  61. package/dist/config/validateCatalog.js +169 -132
  62. package/dist/envelope/check.js +7 -0
  63. package/dist/envelope/forkChoice.d.ts +3 -0
  64. package/dist/envelope/forkChoice.js +47 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +4 -0
  67. package/dist/mcp/projectRun.d.ts +3 -0
  68. package/dist/mcp/projectRun.js +7 -0
  69. package/dist/mcp/server.js +1 -1
  70. package/dist/mcp/tools.js +28 -11
  71. package/dist/project/findProjectRoot.d.ts +2 -0
  72. package/dist/project/findProjectRoot.js +51 -0
  73. package/dist/project/globalHome.d.ts +2 -0
  74. package/dist/project/globalHome.js +19 -0
  75. package/dist/project/resolveProjectContext.d.ts +9 -0
  76. package/dist/project/resolveProjectContext.js +38 -0
  77. package/dist/project/resolveStageflowContext.d.ts +11 -0
  78. package/dist/project/resolveStageflowContext.js +66 -0
  79. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  80. package/dist/runstore/normalizeCatalogPath.js +4 -0
  81. package/dist/runstore/port.d.ts +15 -0
  82. package/dist/runstore/runProjection.js +11 -1
  83. package/dist/runstore/sqlite/SqliteRunStore.js +46 -4
  84. package/dist/runstore/sqlite/schema.d.ts +1 -1
  85. package/dist/runstore/sqlite/schema.js +7 -1
  86. package/dist/runtime/credentialBinding.d.ts +8 -6
  87. package/dist/runtime/credentialBinding.js +18 -24
  88. package/dist/runtime/pipelineRunner.d.ts +11 -0
  89. package/dist/runtime/pipelineRunner.js +25 -1
  90. package/dist/runtime/pipelineScheduler.d.ts +3 -0
  91. package/dist/runtime/pipelineScheduler.js +70 -14
  92. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  93. package/dist/runtime/reloadRunCatalog.js +44 -0
  94. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  95. package/dist/runtime/resumeReconstruct.js +27 -10
  96. package/dist/runtime/runManager.d.ts +8 -0
  97. package/dist/runtime/runManager.js +49 -18
  98. package/dist/runtime/settingsFile.d.ts +12 -0
  99. package/dist/runtime/settingsFile.js +87 -6
  100. package/dist/runtime/stageAttemptBootstrap.js +3 -0
  101. package/dist/runtime/stageProcessLauncher.d.ts +1 -0
  102. package/dist/runtime/stageProcessLauncher.js +6 -0
  103. package/dist/runtime/stageRecovery.js +3 -1
  104. package/dist/runtime/stageRunner.d.ts +2 -0
  105. package/dist/runtime/stageRunner.js +9 -2
  106. package/dist/runtime/stageWorker.js +4 -6
  107. package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
  108. package/dist/server/http.js +33 -9
  109. package/dist/tools/emitStageEnvelope.d.ts +3 -1
  110. package/dist/tools/emitStageEnvelope.js +11 -1
  111. package/dist/types/envelope.d.ts +1 -0
  112. package/dist/types/forkChoice.d.ts +8 -0
  113. package/dist/types/forkChoice.js +1 -0
  114. package/dist/types/pipeline.d.ts +51 -2
  115. package/dist/types/stageflowManifest.d.ts +23 -0
  116. package/dist/types/stageflowManifest.js +1 -0
  117. package/dist/ui/assets/index-CFSzDZje.js +118 -0
  118. package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
  119. package/dist/ui/index.html +3 -2
  120. package/dist/ui/stageflow-icon.svg +12 -0
  121. package/package.json +7 -5
  122. package/dist/agent/cursorExtension.d.ts +0 -12
  123. package/dist/agent/cursorExtension.js +0 -88
  124. package/dist/runstore/catalog.d.ts +0 -8
  125. package/dist/runstore/catalog.js +0 -16
  126. package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
  127. package/dist/runstore/disk/DiskRunStore.js +0 -238
  128. package/dist/runstore/layout.d.ts +0 -22
  129. package/dist/runstore/layout.js +0 -58
  130. package/dist/runtime/hitlSeams.d.ts +0 -38
  131. package/dist/runtime/hitlSeams.js +0 -2
  132. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { validateCatalog as defaultValidateCatalog, type ValidationResult } from "../config/validateCatalog.js";
2
- export declare const VALIDATE_USAGE = "Usage:\n sf validate [--pipeline <name-or-path>] [--strict] [--json]";
2
+ export declare const VALIDATE_USAGE = "Usage:\n sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]\n\n With no flags, validates all pipelines and tasks declared in stageflow.yaml (manifest-all).\n --pipeline validates one pipeline file (includes uses:/include: transitively).\n --task validates one task file.";
3
3
  export type ValidateCommandIo = {
4
4
  log: (line: string) => void;
5
5
  error: (line: string) => void;
@@ -1,7 +1,11 @@
1
1
  import { validateCatalog as defaultValidateCatalog, } from "../config/validateCatalog.js";
2
2
  import { exitCodeForValidation, formatValidationHuman, formatValidationJson, } from "./validateOutput.js";
3
3
  export const VALIDATE_USAGE = `Usage:
4
- sf validate [--pipeline <name-or-path>] [--strict] [--json]`;
4
+ sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
5
+
6
+ With no flags, validates all pipelines and tasks declared in stageflow.yaml (manifest-all).
7
+ --pipeline validates one pipeline file (includes uses:/include: transitively).
8
+ --task validates one task file.`;
5
9
  const defaultIo = {
6
10
  log: (line) => console.log(line),
7
11
  error: (line) => console.error(line),
@@ -14,6 +18,7 @@ function parseValidateArgs(args) {
14
18
  return { help: true, strict: false, json: false };
15
19
  }
16
20
  let pipeline;
21
+ let task;
17
22
  let strict = false;
18
23
  let json = false;
19
24
  let help = false;
@@ -29,6 +34,13 @@ function parseValidateArgs(args) {
29
34
  }
30
35
  pipeline = value;
31
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
+ }
32
44
  else if (arg === "--strict") {
33
45
  strict = true;
34
46
  }
@@ -42,7 +54,10 @@ function parseValidateArgs(args) {
42
54
  throw new Error(`Unexpected argument: ${arg}`);
43
55
  }
44
56
  }
45
- return { help, pipeline, strict, json };
57
+ if (pipeline !== undefined && task !== undefined) {
58
+ throw new Error("Use at most one of --pipeline or --task");
59
+ }
60
+ return { help, pipeline, task, strict, json };
46
61
  }
47
62
  export async function runValidateCommand(args, options = {}) {
48
63
  const cwd = options.cwd ?? process.cwd();
@@ -54,10 +69,12 @@ export async function runValidateCommand(args, options = {}) {
54
69
  out.error(VALIDATE_USAGE);
55
70
  return 0;
56
71
  }
72
+ const scope = parsed.pipeline ? "pipeline" : parsed.task ? "task" : "full";
57
73
  const result = await validateCatalogFn({
58
- scope: parsed.pipeline ? "pipeline" : "full",
74
+ scope,
59
75
  cwd,
60
76
  pipeline: parsed.pipeline,
77
+ task: parsed.task,
61
78
  strict: parsed.strict,
62
79
  });
63
80
  if (parsed.json) {
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,13 @@
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
+ };
13
+ export declare function sameCliEntry(a: string, b: string): boolean;
package/dist/cli.js CHANGED
@@ -1,20 +1,23 @@
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";
7
+ import { INIT_USAGE, runInitCommand } from "./cli/initCommand.js";
5
8
  import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
9
+ import { RUN_USAGE, runRunCommand } from "./cli/runCommand.js";
10
+ import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
6
11
  import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
7
- import { exitCodeForValidation, formatValidationHuman, } from "./cli/validateOutput.js";
8
12
  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";
13
+ import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
12
14
  import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
13
15
  import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
14
16
  import { DEFAULT_PORT, startUiServer } from "./server/http.js";
15
17
  const USAGE = `Usage:
16
- sf run --task <path> --pipeline <name-or-path> [--checkout <path>]
17
- sf validate [--pipeline <name-or-path>] [--strict] [--json]
18
+ sf init
19
+ sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
20
+ sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
18
21
  sf ui [--port ${DEFAULT_PORT}]
19
22
  sf providers list
20
23
  sf providers status [--provider <id>]
@@ -28,6 +31,10 @@ Stageflow (sf) runs YAML-defined automatic stage pipelines.
28
31
 
29
32
  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
33
 
34
+ ${INIT_USAGE}
35
+
36
+ ${RUN_USAGE}
37
+
31
38
  ${VALIDATE_USAGE}
32
39
 
33
40
  ${PROVIDERS_USAGE}`;
@@ -41,7 +48,10 @@ function parseArgs(argv) {
41
48
  return { help: true };
42
49
  }
43
50
  const command = args[0];
44
- if (command === "providers" || command === "validate") {
51
+ if (command === "providers" ||
52
+ command === "validate" ||
53
+ command === "run" ||
54
+ command === "init") {
45
55
  return { help: false, command };
46
56
  }
47
57
  if (args.includes("--help") || args.includes("-h")) {
@@ -75,14 +85,16 @@ function parseArgs(argv) {
75
85
  }
76
86
  return { help: false, command, task, pipeline, port, checkout };
77
87
  }
78
- function parseRunStageArgs(argv) {
88
+ export function parseRunStageArgs(argv) {
79
89
  let runId;
80
90
  let stageId;
81
91
  let mode;
82
92
  let resumeAnswer;
83
93
  let attempt;
84
94
  let sessionFilePath;
95
+ let operatorCwd;
85
96
  let operatorAgentDir;
97
+ let skipGates = false;
86
98
  for (let i = 0; i < argv.length; i++) {
87
99
  if (argv[i] === "--run-id") {
88
100
  runId = argv[++i];
@@ -126,16 +138,30 @@ function parseRunStageArgs(argv) {
126
138
  throw new Error("Missing value for --session-file");
127
139
  }
128
140
  }
141
+ else if (argv[i] === "--operator-cwd") {
142
+ operatorCwd = argv[++i];
143
+ if (operatorCwd === undefined) {
144
+ throw new Error("Missing value for --operator-cwd");
145
+ }
146
+ }
129
147
  else if (argv[i] === "--operator-agent-dir") {
130
148
  operatorAgentDir = argv[++i];
131
149
  if (operatorAgentDir === undefined) {
132
150
  throw new Error("Missing value for --operator-agent-dir");
133
151
  }
134
152
  }
153
+ else if (argv[i] === "--skip-gates") {
154
+ skipGates = true;
155
+ }
135
156
  }
136
157
  if (!runId || !stageId) {
137
158
  throw new Error("Missing --run-id and/or --stage-id");
138
159
  }
160
+ const operatorCatalog = resolveOperatorCatalog({
161
+ flags: { operatorCwd, operatorAgentDir },
162
+ env: process.env,
163
+ defaultCwd: process.cwd(),
164
+ });
139
165
  return {
140
166
  runId,
141
167
  stageId,
@@ -143,9 +169,8 @@ function parseRunStageArgs(argv) {
143
169
  resumeAnswer,
144
170
  attempt,
145
171
  sessionFilePath,
146
- ...(operatorAgentDir !== undefined
147
- ? { operatorCatalog: { cwd: process.cwd(), agentDir: operatorAgentDir } }
148
- : {}),
172
+ skipGates,
173
+ operatorCatalog,
149
174
  };
150
175
  }
151
176
  async function handleInternalRunStage(argv) {
@@ -163,6 +188,7 @@ async function handleInternalRunStage(argv) {
163
188
  attempt: parsed.attempt,
164
189
  sessionFilePath: parsed.sessionFilePath,
165
190
  operatorCatalog: parsed.operatorCatalog,
191
+ skipGates: parsed.skipGates,
166
192
  });
167
193
  exitForOutcome(outcome);
168
194
  }
@@ -185,18 +211,30 @@ async function main(argv) {
185
211
  console.log(USAGE);
186
212
  return argv.slice(2).length === 0 ? 1 : 0;
187
213
  }
188
- const rootDir = process.cwd();
214
+ const ctx = await resolveStageflowContext(process.cwd());
215
+ if (parsed.command === "init") {
216
+ return runInitCommand(argv.slice(3), { cwd: ctx.invocationCwd });
217
+ }
189
218
  if (parsed.command === "validate") {
190
- return runValidateCommand(argv.slice(3), { cwd: rootDir });
219
+ return runValidateCommand(argv.slice(3), { cwd: ctx.invocationCwd });
220
+ }
221
+ if (parsed.command === "run") {
222
+ return runRunCommand(argv.slice(3), {
223
+ cwd: ctx.invocationCwd,
224
+ projectRoot: ctx.projectRoot,
225
+ isGitProject: ctx.isGitProject,
226
+ });
191
227
  }
192
228
  if (parsed.command === "providers") {
193
- return runProvidersCommand(argv.slice(3), rootDir);
229
+ return runProvidersCommand(argv.slice(3), ctx.invocationCwd);
194
230
  }
195
- const store = createRunStore({ rootDir });
231
+ const store = createRunStore({ rootDir: ctx.projectRoot });
196
232
  if (parsed.command === "ui") {
197
233
  const { url, mcpUrl } = await startUiServer({
198
234
  agent: new PiAgentAdapter(),
199
235
  store,
236
+ cwd: ctx.invocationCwd,
237
+ rootDir: ctx.projectRoot,
200
238
  port: parsed.port,
201
239
  });
202
240
  console.log(`Operator console: ${url}`);
@@ -214,60 +252,38 @@ async function main(argv) {
214
252
  }
215
253
  return handleInternalRunStage(subArgs.slice(1));
216
254
  }
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}`);
255
+ console.error(`Unknown command: ${parsed.command}`);
256
+ console.error(USAGE);
257
257
  return 1;
258
258
  }
259
259
  catch (err) {
260
- if (err instanceof PipelineValidationError) {
261
- console.error(formatValidationHuman(err.result));
262
- return exitCodeForValidation(err.result);
263
- }
264
260
  console.error(err instanceof Error ? err.message : String(err));
265
261
  return 1;
266
262
  }
267
263
  }
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
- });
264
+ function normalizeCliEntry(filePath) {
265
+ const resolved = path.resolve(filePath);
266
+ let canonical;
267
+ try {
268
+ canonical = realpathSync(resolved);
269
+ }
270
+ catch {
271
+ canonical = resolved;
272
+ }
273
+ return canonical.replace(/\.(?:[cm]?[jt]s)$/i, "");
274
+ }
275
+ export function sameCliEntry(a, b) {
276
+ return normalizeCliEntry(a) === normalizeCliEntry(b);
277
+ }
278
+ function isDirectCliInvocation() {
279
+ const self = fileURLToPath(import.meta.url);
280
+ return process.argv.slice(1).some((arg) => sameCliEntry(arg, self));
281
+ }
282
+ if (isDirectCliInvocation()) {
283
+ main(process.argv)
284
+ .then((code) => process.exit(code))
285
+ .catch((err) => {
286
+ console.error(err instanceof Error ? err.message : String(err));
287
+ process.exit(1);
288
+ });
289
+ }
@@ -0,0 +1,45 @@
1
+ import type { StageGateKind } from "../types/stage.js";
2
+ import type { LoadedManifest } from "../types/stageflowManifest.js";
3
+ import type { LoadIssue } from "./loadOutcome.js";
4
+ import type { CatalogContext } from "./resolveCatalogContext.js";
5
+ import { type CatalogManifestStatus, type StageflowContext } from "../project/resolveStageflowContext.js";
6
+ export type TaskListing = {
7
+ path: string;
8
+ id: string;
9
+ goal: string;
10
+ };
11
+ export type PipelineStageListing = {
12
+ id: string;
13
+ gate_kinds?: StageGateKind[];
14
+ uses_path?: string;
15
+ inline?: boolean;
16
+ };
17
+ export type PipelineListing = {
18
+ path: string;
19
+ id: string;
20
+ stages: PipelineStageListing[];
21
+ };
22
+ export type CatalogBrowseWarning = {
23
+ path: string;
24
+ kind: "pipeline" | "task";
25
+ error: string;
26
+ };
27
+ export type CatalogBrowseResult = {
28
+ projectRoot: string | null;
29
+ manifest: LoadedManifest | null;
30
+ manifestStatus: CatalogManifestStatus;
31
+ issues: LoadIssue[];
32
+ pipelines: PipelineListing[];
33
+ tasks: TaskListing[];
34
+ models: string[];
35
+ warnings: CatalogBrowseWarning[];
36
+ };
37
+ export type CatalogScanPaths = {
38
+ pipelinePaths: string[];
39
+ taskPaths: string[];
40
+ };
41
+ export declare function getCatalogScanPaths(ctx: CatalogContext): Promise<CatalogScanPaths | null>;
42
+ export declare function browseCatalog(invocationCwd: string | StageflowContext): Promise<CatalogBrowseResult>;
43
+ export declare function listPipelinesForContext(ctx: CatalogContext): Promise<PipelineListing[]>;
44
+ export declare function listTasksForContext(ctx: CatalogContext): Promise<TaskListing[]>;
45
+ export declare function listModelsForContext(ctx: CatalogContext): Promise<string[]>;
@@ -0,0 +1,170 @@
1
+ import path from "node:path";
2
+ import { loadPipeline } from "./loadPipeline.js";
3
+ import { loadTask } from "./loadTask.js";
4
+ import { resolveStageflowContext, } from "../project/resolveStageflowContext.js";
5
+ import { scanCatalogPaths } from "./scanCatalogPaths.js";
6
+ const BAKED_MODEL_IDS = [
7
+ "anthropic/claude-sonnet-4-5",
8
+ "cursor/auto",
9
+ "cursor/composer-2-5",
10
+ ];
11
+ function repoRelPath(projectRoot, absPath) {
12
+ return path.relative(projectRoot, absPath).replace(/\\/g, "/");
13
+ }
14
+ function mapStageListing(stage, source, projectRoot) {
15
+ const listing = {
16
+ id: stage.id,
17
+ ...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
18
+ };
19
+ if (source?.kind === "inline") {
20
+ listing.inline = true;
21
+ }
22
+ else if (source?.kind === "file") {
23
+ listing.uses_path = repoRelPath(projectRoot, source.path);
24
+ }
25
+ return listing;
26
+ }
27
+ function bakedModels() {
28
+ return [...BAKED_MODEL_IDS].sort((a, b) => a.localeCompare(b));
29
+ }
30
+ function catalogReadyFromStageflow(ctx) {
31
+ return ctx.isGitProject && ctx.manifestStatus === "ok" && ctx.manifest !== null;
32
+ }
33
+ function catalogReady(ctx) {
34
+ return ctx.manifestStatus === "ok" && ctx.projectRoot !== null && ctx.manifest !== null;
35
+ }
36
+ export async function getCatalogScanPaths(ctx) {
37
+ if (!catalogReady(ctx)) {
38
+ return null;
39
+ }
40
+ const [pipelinePaths, taskPaths] = await Promise.all([
41
+ scanCatalogPaths(ctx.manifest, "pipeline"),
42
+ scanCatalogPaths(ctx.manifest, "task"),
43
+ ]);
44
+ return { pipelinePaths, taskPaths };
45
+ }
46
+ async function listTasksFromManifest(projectRoot, manifest, warnings) {
47
+ const filePaths = await scanCatalogPaths(manifest, "task");
48
+ const listings = [];
49
+ for (const filePath of filePaths) {
50
+ try {
51
+ const task = await loadTask(filePath);
52
+ listings.push({
53
+ path: repoRelPath(projectRoot, filePath),
54
+ id: task.id,
55
+ goal: task.goal,
56
+ });
57
+ }
58
+ catch (err) {
59
+ warnings.push({
60
+ path: repoRelPath(projectRoot, filePath),
61
+ kind: "task",
62
+ error: err instanceof Error ? err.message : String(err),
63
+ });
64
+ }
65
+ }
66
+ listings.sort((a, b) => {
67
+ const pathCmp = a.path.localeCompare(b.path);
68
+ if (pathCmp !== 0)
69
+ return pathCmp;
70
+ return a.id.localeCompare(b.id);
71
+ });
72
+ return listings;
73
+ }
74
+ async function listPipelinesFromManifest(projectRoot, manifest, warnings) {
75
+ const filePaths = await scanCatalogPaths(manifest, "pipeline");
76
+ const listings = [];
77
+ for (const filePath of filePaths) {
78
+ try {
79
+ const loaded = await loadPipeline(filePath, { cwd: projectRoot });
80
+ listings.push({
81
+ path: repoRelPath(projectRoot, filePath),
82
+ id: loaded.pipeline.id,
83
+ stages: loaded.stages.map((stage) => mapStageListing(stage, loaded.stageSources?.[stage.id], projectRoot)),
84
+ });
85
+ }
86
+ catch (err) {
87
+ warnings.push({
88
+ path: repoRelPath(projectRoot, filePath),
89
+ kind: "pipeline",
90
+ error: err instanceof Error ? err.message : String(err),
91
+ });
92
+ }
93
+ }
94
+ listings.sort((a, b) => {
95
+ const pathCmp = a.path.localeCompare(b.path);
96
+ if (pathCmp !== 0)
97
+ return pathCmp;
98
+ return a.id.localeCompare(b.id);
99
+ });
100
+ return listings;
101
+ }
102
+ async function listModelsFromManifest(projectRoot, manifest) {
103
+ const models = new Set(BAKED_MODEL_IDS);
104
+ const filePaths = await scanCatalogPaths(manifest, "pipeline");
105
+ for (const filePath of filePaths) {
106
+ try {
107
+ const loaded = await loadPipeline(filePath, { cwd: projectRoot });
108
+ for (const stage of loaded.stages) {
109
+ if (stage.model.trim()) {
110
+ models.add(stage.model);
111
+ }
112
+ }
113
+ }
114
+ catch {
115
+ // skip invalid pipelines
116
+ }
117
+ }
118
+ return [...models].sort((a, b) => a.localeCompare(b));
119
+ }
120
+ export async function browseCatalog(invocationCwd) {
121
+ const ctx = typeof invocationCwd === "string"
122
+ ? await resolveStageflowContext(invocationCwd)
123
+ : invocationCwd;
124
+ const base = {
125
+ projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
126
+ manifest: ctx.manifest,
127
+ manifestStatus: ctx.manifestStatus,
128
+ issues: ctx.manifestIssues,
129
+ };
130
+ if (!catalogReadyFromStageflow(ctx)) {
131
+ return {
132
+ ...base,
133
+ pipelines: [],
134
+ tasks: [],
135
+ models: bakedModels(),
136
+ warnings: [],
137
+ };
138
+ }
139
+ const warnings = [];
140
+ const [pipelines, tasks, models] = await Promise.all([
141
+ listPipelinesFromManifest(ctx.projectRoot, ctx.manifest, warnings),
142
+ listTasksFromManifest(ctx.projectRoot, ctx.manifest, warnings),
143
+ listModelsFromManifest(ctx.projectRoot, ctx.manifest),
144
+ ]);
145
+ return {
146
+ ...base,
147
+ pipelines,
148
+ tasks,
149
+ models,
150
+ warnings,
151
+ };
152
+ }
153
+ export async function listPipelinesForContext(ctx) {
154
+ if (!catalogReady(ctx)) {
155
+ return [];
156
+ }
157
+ return listPipelinesFromManifest(ctx.projectRoot, ctx.manifest, []);
158
+ }
159
+ export async function listTasksForContext(ctx) {
160
+ if (!catalogReady(ctx)) {
161
+ return [];
162
+ }
163
+ return listTasksFromManifest(ctx.projectRoot, ctx.manifest, []);
164
+ }
165
+ export async function listModelsForContext(ctx) {
166
+ if (!catalogReady(ctx)) {
167
+ return bakedModels();
168
+ }
169
+ return listModelsFromManifest(ctx.projectRoot, ctx.manifest);
170
+ }