stageflow 0.2.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 (113) hide show
  1. package/README.md +158 -14
  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/initCommand.d.ts +9 -0
  14. package/dist/cli/initCommand.js +71 -0
  15. package/dist/cli/initTemplates.d.ts +3 -0
  16. package/dist/cli/initTemplates.js +19 -0
  17. package/dist/cli/operatorCatalog.d.ts +10 -0
  18. package/dist/cli/operatorCatalog.js +16 -0
  19. package/dist/cli/runCommand.d.ts +3 -1
  20. package/dist/cli/runCommand.js +48 -7
  21. package/dist/cli/validateCommand.d.ts +1 -1
  22. package/dist/cli/validateCommand.js +20 -3
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +52 -19
  25. package/dist/config/browseCatalog.d.ts +45 -0
  26. package/dist/config/browseCatalog.js +170 -0
  27. package/dist/config/createPipeline.d.ts +12 -3
  28. package/dist/config/createPipeline.js +292 -100
  29. package/dist/config/createStage.d.ts +10 -4
  30. package/dist/config/createStage.js +44 -14
  31. package/dist/config/listConfig.d.ts +14 -19
  32. package/dist/config/listConfig.js +26 -191
  33. package/dist/config/listModelsFromManifest.d.ts +2 -0
  34. package/dist/config/listModelsFromManifest.js +4 -0
  35. package/dist/config/loadOutcome.d.ts +4 -0
  36. package/dist/config/loadPipeline.d.ts +1 -15
  37. package/dist/config/loadPipeline.js +84 -142
  38. package/dist/config/loadStage.d.ts +4 -0
  39. package/dist/config/loadStage.js +60 -32
  40. package/dist/config/loadStageflowManifest.d.ts +5 -0
  41. package/dist/config/loadStageflowManifest.js +157 -0
  42. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  43. package/dist/config/mergePipelineIncludes.js +141 -0
  44. package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
  45. package/dist/config/normalizePipelineStageEntry.js +186 -0
  46. package/dist/config/pipelineStageKeys.d.ts +4 -0
  47. package/dist/config/pipelineStageKeys.js +14 -0
  48. package/dist/config/resolveCatalogContext.d.ts +13 -0
  49. package/dist/config/resolveCatalogContext.js +13 -0
  50. package/dist/config/resolveForkEmitContext.d.ts +3 -0
  51. package/dist/config/resolveForkEmitContext.js +12 -0
  52. package/dist/config/resolvePipelineDag.d.ts +5 -1
  53. package/dist/config/resolvePipelineDag.js +54 -36
  54. package/dist/config/scanCatalogPaths.d.ts +4 -0
  55. package/dist/config/scanCatalogPaths.js +83 -0
  56. package/dist/config/validateCatalog.d.ts +22 -4
  57. package/dist/config/validateCatalog.js +169 -132
  58. package/dist/envelope/check.js +7 -0
  59. package/dist/envelope/forkChoice.d.ts +3 -0
  60. package/dist/envelope/forkChoice.js +47 -0
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +4 -0
  63. package/dist/mcp/projectRun.d.ts +3 -0
  64. package/dist/mcp/projectRun.js +7 -0
  65. package/dist/mcp/tools.js +28 -11
  66. package/dist/project/findProjectRoot.d.ts +2 -0
  67. package/dist/project/findProjectRoot.js +51 -0
  68. package/dist/project/globalHome.d.ts +2 -0
  69. package/dist/project/globalHome.js +19 -0
  70. package/dist/project/resolveProjectContext.d.ts +9 -0
  71. package/dist/project/resolveProjectContext.js +38 -0
  72. package/dist/project/resolveStageflowContext.d.ts +11 -0
  73. package/dist/project/resolveStageflowContext.js +66 -0
  74. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  75. package/dist/runstore/normalizeCatalogPath.js +4 -0
  76. package/dist/runstore/port.d.ts +9 -0
  77. package/dist/runstore/runProjection.js +11 -1
  78. package/dist/runstore/sqlite/SqliteRunStore.js +30 -4
  79. package/dist/runstore/sqlite/schema.d.ts +1 -1
  80. package/dist/runstore/sqlite/schema.js +4 -1
  81. package/dist/runtime/credentialBinding.d.ts +8 -6
  82. package/dist/runtime/credentialBinding.js +18 -24
  83. package/dist/runtime/pipelineRunner.d.ts +3 -0
  84. package/dist/runtime/pipelineRunner.js +15 -1
  85. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  86. package/dist/runtime/pipelineScheduler.js +51 -11
  87. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  88. package/dist/runtime/reloadRunCatalog.js +44 -0
  89. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  90. package/dist/runtime/resumeReconstruct.js +23 -7
  91. package/dist/runtime/runManager.d.ts +4 -0
  92. package/dist/runtime/runManager.js +36 -16
  93. package/dist/runtime/settingsFile.d.ts +12 -0
  94. package/dist/runtime/settingsFile.js +87 -6
  95. package/dist/runtime/stageAttemptBootstrap.js +3 -0
  96. package/dist/runtime/stageProcessLauncher.js +3 -0
  97. package/dist/runtime/stageRecovery.js +3 -1
  98. package/dist/runtime/stageWorker.js +2 -6
  99. package/dist/server/http.js +33 -9
  100. package/dist/tools/emitStageEnvelope.d.ts +3 -1
  101. package/dist/tools/emitStageEnvelope.js +11 -1
  102. package/dist/types/envelope.d.ts +1 -0
  103. package/dist/types/forkChoice.d.ts +8 -0
  104. package/dist/types/forkChoice.js +1 -0
  105. package/dist/types/pipeline.d.ts +51 -2
  106. package/dist/types/stageflowManifest.d.ts +23 -0
  107. package/dist/types/stageflowManifest.js +1 -0
  108. package/dist/ui/assets/index-CFSzDZje.js +118 -0
  109. package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
  110. package/dist/ui/index.html +3 -2
  111. package/dist/ui/stageflow-icon.svg +12 -0
  112. package/package.json +7 -5
  113. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -41,8 +41,8 @@ import { createAskOperatorTool, } from "../tools/askOperator.js";
41
41
  import { createWriteStageArtifactTool } from "../tools/writeStageArtifact.js";
42
42
  import "./cursorProvider.js";
43
43
  import { findProviderSupport } from "./providerSupport.js";
44
- import { mapSessionEventToActivity } from "./activity.js";
45
- import { createStageActivityObserver } from "./activityObserver.js";
44
+ import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
45
+ import { createStageActivityObserver, } from "./activityObserver.js";
46
46
  import { DEFAULT_STAGE_TIMEOUT_MS, runStageViaOpen } from "./port.js";
47
47
  /**
48
48
  * Stage tool allowlist for sealed Pi sessions.
@@ -260,31 +260,98 @@ export function createConnectedAskWaitStageHandle(options) {
260
260
  },
261
261
  };
262
262
  }
263
+ /**
264
+ * Extract display text from a Pi tool_execution_update partialResult.
265
+ * Prefers content[].text blocks; otherwise JSON-stringifies.
266
+ */
267
+ export function extractPartialResultText(partialResult) {
268
+ if (partialResult === undefined || partialResult === null) {
269
+ return "";
270
+ }
271
+ if (typeof partialResult === "string") {
272
+ return partialResult;
273
+ }
274
+ if (typeof partialResult === "object") {
275
+ const content = partialResult.content;
276
+ if (Array.isArray(content)) {
277
+ const parts = [];
278
+ for (const block of content) {
279
+ if (!block || typeof block !== "object")
280
+ continue;
281
+ const b = block;
282
+ if (b.type === "text" && typeof b.text === "string") {
283
+ parts.push(b.text);
284
+ }
285
+ }
286
+ if (parts.length > 0) {
287
+ return parts.join("");
288
+ }
289
+ }
290
+ try {
291
+ return JSON.stringify(partialResult);
292
+ }
293
+ catch {
294
+ return String(partialResult);
295
+ }
296
+ }
297
+ return String(partialResult);
298
+ }
299
+ /**
300
+ * Adapter-edge routing: stream deltas to the observer, map milestones to
301
+ * StageActivityEvent. Verbose thinking / live tool partials go to observer
302
+ * hooks (stderr streams + coalesced/throttled persist via onActivity).
303
+ */
304
+ export function routeSessionEventToProgress(event, options) {
305
+ const { observer, verbose } = options;
306
+ if (event.type === "message_update") {
307
+ const ame = event.assistantMessageEvent;
308
+ if (ame && typeof ame === "object") {
309
+ const update = ame;
310
+ if (update.type === "text_delta") {
311
+ observer.onAssistantTextDelta(update.delta ?? "");
312
+ return;
313
+ }
314
+ if (verbose && update.type === "thinking_delta") {
315
+ observer.onThinkingDelta(update.delta ?? "");
316
+ return;
317
+ }
318
+ }
319
+ return;
320
+ }
321
+ if (verbose &&
322
+ event.type === "tool_execution_update") {
323
+ const text = extractPartialResultText(event.partialResult);
324
+ if (text) {
325
+ observer.onToolPartialResult(text);
326
+ }
327
+ return;
328
+ }
329
+ if (event.type === "message_end" || event.type === "agent_end") {
330
+ observer.onStreamBoundary();
331
+ }
332
+ if (event.type === "agent_start" ||
333
+ event.type === "turn_start" ||
334
+ event.type === "tool_execution_start") {
335
+ observer.onStreamBoundary();
336
+ }
337
+ const activity = mapSessionEventToActivity(event);
338
+ if (!activity) {
339
+ return;
340
+ }
341
+ observer.onActivity(activity);
342
+ }
263
343
  /**
264
344
  * Map Pi session events to StageActivityEvent at the adapter edge, then
265
345
  * forward to the activity observer (stderr + onActivity).
266
346
  */
267
347
  function attachStageProgress(session, onActivity) {
268
348
  const observer = createStageActivityObserver({ onActivity, writeStderr: true });
349
+ const verbose = readActivityVerbose();
269
350
  const unsubscribe = session.subscribe((event) => {
270
- if (event.type === "message_update" &&
271
- event.assistantMessageEvent.type === "text_delta") {
272
- observer.onAssistantTextDelta(event.assistantMessageEvent.delta ?? "");
273
- return;
274
- }
275
- if (event.type === "message_end" || event.type === "agent_end") {
276
- observer.onStreamBoundary();
277
- }
278
- if (event.type === "agent_start" ||
279
- event.type === "turn_start" ||
280
- event.type === "tool_execution_start") {
281
- observer.onStreamBoundary();
282
- }
283
- const activity = mapSessionEventToActivity(event);
284
- if (!activity) {
285
- return;
286
- }
287
- observer.onActivity(activity);
351
+ routeSessionEventToProgress(event, {
352
+ observer,
353
+ verbose,
354
+ });
288
355
  });
289
356
  return () => {
290
357
  unsubscribe();
@@ -616,7 +683,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
616
683
  const provider = findProviderSupport(input.stage.model);
617
684
  const capture = {};
618
685
  const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
619
- const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema);
686
+ const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext);
620
687
  const emitTool = defineTool(emitDef);
621
688
  const askDef = createAskOperatorTool({
622
689
  requestWait: (prompt) => askWaitChannel.requestWait(prompt),
@@ -1,4 +1,5 @@
1
1
  import type { StageEnvelope } from "../types/envelope.js";
2
+ import type { ForkEmitContext } from "../types/forkChoice.js";
2
3
  import type { StageConfig } from "../types/stage.js";
3
4
  import type { TaskFile } from "../types/task.js";
4
5
  import type { StageActivityEvent } from "./activity.js";
@@ -15,6 +16,7 @@ export type StageRunInput = {
15
16
  /** Optional observe hook; HITL wait/answer uses openStage beside this. */
16
17
  onActivity?: (event: StageActivityEvent) => void;
17
18
  skillFilePath?: string;
19
+ forkEmitContext?: ForkEmitContext;
18
20
  };
19
21
  export type StageRunResult = {
20
22
  ok: true;
@@ -1,5 +1,7 @@
1
1
  import { CredentialSynchronizationError, ModelRuntime, } from "@earendil-works/pi-coding-agent";
2
- import { isUsableAuthFile, parseCredentialSource, piHomeAuthPath, readCredentialSourceFromFile, resolveCredentialBinding, writeCredentialSourceToFile, } from "../runtime/credentialBinding.js";
2
+ import { isUsableAuthFile, parseCredentialSource, piHomeAuthPath, resolveCredentialBinding, writeCredentialSourceToContext, } from "../runtime/credentialBinding.js";
3
+ import { readPersistedCredentialSourceFromContext, } from "../runtime/settingsFile.js";
4
+ import { resolveProjectContext } from "../project/resolveProjectContext.js";
3
5
  export function makeMutationLock() {
4
6
  let tail = Promise.resolve();
5
7
  return {
@@ -245,8 +247,9 @@ export async function logoutProvider(cwd, providerId, ctx = defaultContext) {
245
247
  });
246
248
  }
247
249
  export function detectPiHome(cwd) {
248
- const binding = resolveCredentialBinding(cwd);
249
- const persisted = readCredentialSourceFromFile(cwd);
250
+ const projectCtx = resolveProjectContext(cwd);
251
+ const binding = resolveCredentialBinding(projectCtx);
252
+ const persisted = readPersistedCredentialSourceFromContext(projectCtx);
250
253
  return {
251
254
  piHomeUsable: isUsableAuthFile(piHomeAuthPath()),
252
255
  ...(persisted !== undefined ? { credentialSource: persisted } : {}),
@@ -255,8 +258,9 @@ export function detectPiHome(cwd) {
255
258
  };
256
259
  }
257
260
  export function getCredentialSourceSettings(cwd) {
258
- const persisted = readCredentialSourceFromFile(cwd);
259
- const binding = resolveCredentialBinding(cwd);
261
+ const projectCtx = resolveProjectContext(cwd);
262
+ const persisted = readPersistedCredentialSourceFromContext(projectCtx);
263
+ const binding = resolveCredentialBinding(projectCtx);
260
264
  return {
261
265
  ...(persisted !== undefined ? { credentialSource: persisted } : {}),
262
266
  binding: {
@@ -270,8 +274,9 @@ export function setCredentialSource(cwd, credentialSource) {
270
274
  if (parsed === undefined) {
271
275
  throw new ProviderAuthError('credentialSource must be "pi_home" or "sf_owned"', 400);
272
276
  }
273
- writeCredentialSourceToFile(cwd, parsed);
274
- const binding = resolveCredentialBinding(cwd);
277
+ const projectCtx = resolveProjectContext(cwd);
278
+ writeCredentialSourceToContext(projectCtx, parsed);
279
+ const binding = resolveCredentialBinding(projectCtx);
275
280
  return {
276
281
  credentialSource: parsed,
277
282
  binding: {
@@ -0,0 +1,12 @@
1
+ export declare function displayCatalogPath(absPath: string, projectRoot?: string): string;
2
+ export declare function normalizeCatalogSlashes(pathValue: string): string;
3
+ export type PipelinePathMatchInput = {
4
+ pipeline_id: string;
5
+ pipeline_path?: string;
6
+ project_root?: string;
7
+ };
8
+ export type PipelineListingPath = {
9
+ id: string;
10
+ path: string;
11
+ };
12
+ export declare function matchPipelineRun(run: PipelinePathMatchInput, pipeline: PipelineListingPath): boolean;
@@ -0,0 +1,28 @@
1
+ export function displayCatalogPath(absPath, projectRoot) {
2
+ if (projectRoot) {
3
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/$/, "");
4
+ const normalized = absPath.replace(/\\/g, "/");
5
+ if (normalized === root)
6
+ return ".";
7
+ const prefix = `${root}/`;
8
+ if (normalized.startsWith(prefix)) {
9
+ return normalized.slice(prefix.length);
10
+ }
11
+ }
12
+ const parts = absPath.replace(/\\/g, "/").split("/");
13
+ return parts[parts.length - 1] ?? absPath;
14
+ }
15
+ export function normalizeCatalogSlashes(pathValue) {
16
+ return pathValue.replace(/\\/g, "/");
17
+ }
18
+ export function matchPipelineRun(run, pipeline) {
19
+ if (run.pipeline_id === pipeline.id)
20
+ return true;
21
+ if (run.pipeline_path && run.project_root) {
22
+ const rel = displayCatalogPath(run.pipeline_path, run.project_root);
23
+ if (normalizeCatalogSlashes(rel) === normalizeCatalogSlashes(pipeline.path)) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ }
@@ -0,0 +1,9 @@
1
+ export declare const INIT_USAGE = "Usage:\n sf init";
2
+ export type InitCommandIo = {
3
+ log: (line: string) => void;
4
+ error: (line: string) => void;
5
+ };
6
+ export declare function runInitCommand(args: string[], options?: {
7
+ cwd?: string;
8
+ io?: Partial<InitCommandIo>;
9
+ }): Promise<number>;
@@ -0,0 +1,71 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { findProjectRoot } from "../project/findProjectRoot.js";
4
+ import { ensureGlobalHome } from "../project/globalHome.js";
5
+ import { HELLO_PIPELINE_YAML, HELLO_TASK_YAML, STAGEFLOW_YAML, } from "./initTemplates.js";
6
+ export const INIT_USAGE = `Usage:
7
+ sf init`;
8
+ const defaultIo = {
9
+ log: (line) => console.log(line),
10
+ error: (line) => console.error(line),
11
+ };
12
+ const INIT_TARGETS = [
13
+ { relPath: "stageflow.yaml", content: STAGEFLOW_YAML },
14
+ { relPath: "pipelines/hello.pipeline.yaml", content: HELLO_PIPELINE_YAML },
15
+ { relPath: "tasks/hello.task.yaml", content: HELLO_TASK_YAML },
16
+ ];
17
+ async function fileExists(absPath) {
18
+ try {
19
+ await access(absPath);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function parseInitArgs(args) {
27
+ if (args.length === 0) {
28
+ return { help: false };
29
+ }
30
+ if (args[0] === "--help" || args[0] === "-h") {
31
+ return { help: true };
32
+ }
33
+ if (args[0].startsWith("-")) {
34
+ throw new Error(`Unknown flag: ${args[0]}`);
35
+ }
36
+ throw new Error(`Unexpected argument: ${args[0]}`);
37
+ }
38
+ export async function runInitCommand(args, options = {}) {
39
+ const cwd = options.cwd ?? process.cwd();
40
+ const out = { ...defaultIo, ...options.io };
41
+ try {
42
+ const parsed = parseInitArgs(args);
43
+ if (parsed.help) {
44
+ out.error(INIT_USAGE);
45
+ return 0;
46
+ }
47
+ const initRoot = findProjectRoot(cwd) ?? path.resolve(cwd);
48
+ ensureGlobalHome();
49
+ for (const target of INIT_TARGETS) {
50
+ const absPath = path.join(initRoot, target.relPath);
51
+ if (await fileExists(absPath)) {
52
+ out.log(`Skipped ${target.relPath} (exists)`);
53
+ continue;
54
+ }
55
+ await mkdir(path.dirname(absPath), { recursive: true });
56
+ await writeFile(absPath, target.content, "utf8");
57
+ out.log(`Created ${target.relPath}`);
58
+ }
59
+ return 0;
60
+ }
61
+ catch (err) {
62
+ const message = err instanceof Error
63
+ ? err.message
64
+ : typeof err === "string"
65
+ ? err
66
+ : "Init command failed";
67
+ out.error(message);
68
+ out.error(INIT_USAGE);
69
+ return 1;
70
+ }
71
+ }
@@ -0,0 +1,3 @@
1
+ export declare const STAGEFLOW_YAML = "version: 1\ncatalog:\n pipelines:\n - pipelines\n tasks:\n - tasks\n patterns:\n pipeline: \"*.pipeline.yaml\"\n task: \"*.task.yaml\"\n";
2
+ export declare const HELLO_PIPELINE_YAML = "id: hello\nstages:\n - id: hello\n system_prompt: Say hello and emit a success envelope.\n model: anthropic/claude-sonnet-4-5\n";
3
+ export declare const HELLO_TASK_YAML = "id: hello\ngoal: Run the hello pipeline scaffold.\n";
@@ -0,0 +1,19 @@
1
+ export const STAGEFLOW_YAML = `version: 1
2
+ catalog:
3
+ pipelines:
4
+ - pipelines
5
+ tasks:
6
+ - tasks
7
+ patterns:
8
+ pipeline: "*.pipeline.yaml"
9
+ task: "*.task.yaml"
10
+ `;
11
+ export const HELLO_PIPELINE_YAML = `id: hello
12
+ stages:
13
+ - id: hello
14
+ system_prompt: Say hello and emit a success envelope.
15
+ model: anthropic/claude-sonnet-4-5
16
+ `;
17
+ export const HELLO_TASK_YAML = `id: hello
18
+ goal: Run the hello pipeline scaffold.
19
+ `;
@@ -0,0 +1,10 @@
1
+ import type { OperatorCatalog } from "../runtime/stageAttemptBootstrap.js";
2
+ export type OperatorCatalogFlags = {
3
+ operatorCwd?: string;
4
+ operatorAgentDir?: string;
5
+ };
6
+ export declare function resolveOperatorCatalog(input: {
7
+ flags: OperatorCatalogFlags;
8
+ env: Record<string, string | undefined>;
9
+ defaultCwd: string;
10
+ }): OperatorCatalog;
@@ -0,0 +1,16 @@
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
+ function nonempty(value) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ const trimmed = value.trim();
6
+ return trimmed.length === 0 ? undefined : trimmed;
7
+ }
8
+ export function resolveOperatorCatalog(input) {
9
+ const cwd = nonempty(input.flags.operatorCwd) ??
10
+ nonempty(input.env.STAGEFLOW_OPERATOR_CWD) ??
11
+ input.defaultCwd;
12
+ const agentDir = nonempty(input.flags.operatorAgentDir) ??
13
+ nonempty(input.env.STAGEFLOW_OPERATOR_AGENT_DIR) ??
14
+ getAgentDir();
15
+ return { cwd, agentDir };
16
+ }
@@ -1,6 +1,6 @@
1
1
  import { type StartRunResult } from "../runtime/runManager.js";
2
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>]";
3
+ export declare const RUN_USAGE = "Usage:\n 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>]";
4
4
  export type RunCommandIo = CliRunReportIo;
5
5
  export type StartRunFn = (input: {
6
6
  task: string;
@@ -18,6 +18,8 @@ export declare function completeCliRun(started: Extract<StartRunResult, {
18
18
  }): Promise<number>;
19
19
  export declare function runRunCommand(args: string[], options?: {
20
20
  cwd?: string;
21
+ projectRoot?: string;
22
+ isGitProject?: boolean;
21
23
  io?: Partial<RunCommandIo>;
22
24
  startRun?: StartRunFn;
23
25
  env?: Record<string, string | undefined>;
@@ -1,4 +1,3 @@
1
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
1
  import { PiAgentAdapter } from "../agent/piAdapter.js";
3
2
  import { createRunStore } from "../runstore/createStore.js";
4
3
  import { PipelineValidationError } from "../runtime/pipelineRunner.js";
@@ -6,9 +5,10 @@ import { RunManager } from "../runtime/runManager.js";
6
5
  import { readStageExecutionMode } from "../runtime/stageConcurrency.js";
7
6
  import { reportCliRun, } from "./runOutput.js";
8
7
  import { resolveCiIdentity } from "./ciIdentity.js";
8
+ import { resolveOperatorCatalog } from "./operatorCatalog.js";
9
9
  import { exitCodeForValidation, formatValidationHuman, formatValidationJson, } from "./validateOutput.js";
10
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>]`;
11
+ 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>]`;
12
12
  const defaultIo = {
13
13
  log: (line) => console.log(line),
14
14
  error: (line) => console.error(line),
@@ -26,6 +26,8 @@ function parseRunArgs(args) {
26
26
  let gitSha;
27
27
  let ciPrUrl;
28
28
  let ciJobUrl;
29
+ let operatorCwd;
30
+ let operatorAgentDir;
29
31
  let json = false;
30
32
  let skipGates = false;
31
33
  let help = false;
@@ -76,6 +78,20 @@ function parseRunArgs(args) {
76
78
  }
77
79
  ciJobUrl = value;
78
80
  }
81
+ else if (arg === "--operator-cwd") {
82
+ const value = args[++i];
83
+ if (value === undefined || value.length === 0) {
84
+ throw new Error("Missing value for --operator-cwd");
85
+ }
86
+ operatorCwd = value;
87
+ }
88
+ else if (arg === "--operator-agent-dir") {
89
+ const value = args[++i];
90
+ if (value === undefined || value.length === 0) {
91
+ throw new Error("Missing value for --operator-agent-dir");
92
+ }
93
+ operatorAgentDir = value;
94
+ }
79
95
  else if (arg === "--json") {
80
96
  json = true;
81
97
  }
@@ -89,16 +105,30 @@ function parseRunArgs(args) {
89
105
  throw new Error(`Unexpected argument: ${arg}`);
90
106
  }
91
107
  }
92
- return { help, json, skipGates, task, pipeline, checkout, gitSha, ciPrUrl, ciJobUrl };
108
+ return {
109
+ help,
110
+ json,
111
+ skipGates,
112
+ task,
113
+ pipeline,
114
+ checkout,
115
+ gitSha,
116
+ ciPrUrl,
117
+ ciJobUrl,
118
+ operatorCwd,
119
+ operatorAgentDir,
120
+ };
93
121
  }
94
- function defaultStartRun(cwd) {
122
+ function defaultStartRun(cwd, projectRoot, isGitProject, operatorCatalog) {
95
123
  return async (input) => {
96
- const store = createRunStore({ rootDir: cwd });
124
+ const store = createRunStore({ rootDir: projectRoot });
97
125
  const manager = new RunManager({
98
126
  agent: new PiAgentAdapter(),
99
127
  store,
100
128
  cwd,
101
- operatorCatalog: { cwd, agentDir: getAgentDir() },
129
+ projectRoot,
130
+ isGitProject,
131
+ operatorCatalog,
102
132
  executionMode: readStageExecutionMode(process.env, "process"),
103
133
  });
104
134
  return manager.startRun(input);
@@ -110,6 +140,8 @@ export async function completeCliRun(started, io = defaultIo, options = {}) {
110
140
  }
111
141
  export async function runRunCommand(args, options = {}) {
112
142
  const cwd = options.cwd ?? process.cwd();
143
+ const projectRoot = options.projectRoot ?? cwd;
144
+ const isGitProject = options.isGitProject ?? false;
113
145
  const out = { ...defaultIo, ...options.io };
114
146
  let parsed;
115
147
  try {
@@ -130,7 +162,16 @@ export async function runRunCommand(args, options = {}) {
130
162
  out.error(RUN_USAGE);
131
163
  return 1;
132
164
  }
133
- const startRun = options.startRun ?? defaultStartRun(cwd);
165
+ const operatorCatalog = resolveOperatorCatalog({
166
+ flags: {
167
+ operatorCwd: parsed.operatorCwd,
168
+ operatorAgentDir: parsed.operatorAgentDir,
169
+ },
170
+ env: options.env ?? process.env,
171
+ defaultCwd: cwd,
172
+ });
173
+ const startRun = options.startRun ??
174
+ defaultStartRun(cwd, projectRoot, isGitProject, operatorCatalog);
134
175
  const identity = resolveCiIdentity({
135
176
  flags: {
136
177
  gitSha: parsed.gitSha,
@@ -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
@@ -10,3 +10,4 @@ export declare function parseRunStageArgs(argv: string[]): {
10
10
  operatorCatalog?: OperatorCatalog;
11
11
  skipGates?: boolean;
12
12
  };
13
+ export declare function sameCliEntry(a: string, b: string): boolean;