stageflow 0.18.0 → 0.23.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 (116) hide show
  1. package/README.md +2 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/activityObserver.d.ts +3 -0
  4. package/dist/agent/activityObserver.js +6 -1
  5. package/dist/agent/claudeAdapter.js +38 -0
  6. package/dist/agent/fakeAgent.js +3 -1
  7. package/dist/agent/piAdapter.js +40 -4
  8. package/dist/agent/port.d.ts +3 -1
  9. package/dist/agent/providerInspect.d.ts +19 -0
  10. package/dist/agent/providerInspect.js +35 -0
  11. package/dist/agent/stageTimeout.d.ts +9 -0
  12. package/dist/agent/stageTimeout.js +24 -0
  13. package/dist/agent/streamLogRedact.d.ts +2 -0
  14. package/dist/agent/streamLogRedact.js +13 -0
  15. package/dist/cli/artifactCommand.js +2 -2
  16. package/dist/cli/envelopeCommand.js +2 -2
  17. package/dist/cli/exportRunCommand.js +2 -2
  18. package/dist/cli/graphCommand.d.ts +15 -0
  19. package/dist/cli/graphCommand.js +79 -0
  20. package/dist/cli/graphRender.d.ts +2 -0
  21. package/dist/cli/graphRender.js +85 -0
  22. package/dist/cli/hostClient.d.ts +43 -0
  23. package/dist/cli/hostClient.js +182 -0
  24. package/dist/cli/providersCommand.js +5 -3
  25. package/dist/cli/runCommand.d.ts +6 -5
  26. package/dist/cli/runCommand.js +23 -31
  27. package/dist/cli/runOutput.d.ts +1 -1
  28. package/dist/cli/runsCommand.d.ts +4 -8
  29. package/dist/cli/runsCommand.js +65 -89
  30. package/dist/cli.js +13 -5
  31. package/dist/config/describePipeline.d.ts +26 -0
  32. package/dist/config/describePipeline.js +37 -0
  33. package/dist/config/loadPipeline.d.ts +13 -1
  34. package/dist/config/loadPipeline.js +34 -4
  35. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  36. package/dist/config/mergePipelineIncludes.js +75 -0
  37. package/dist/config/validateCatalog.d.ts +2 -2
  38. package/dist/config/validateCatalog.js +24 -17
  39. package/dist/mcp/catalogTools.js +146 -52
  40. package/dist/mcp/controlTools.js +94 -19
  41. package/dist/mcp/deps.d.ts +3 -0
  42. package/dist/mcp/projectMcpTools.d.ts +3 -0
  43. package/dist/mcp/projectMcpTools.js +25 -0
  44. package/dist/mcp/providerTools.d.ts +3 -0
  45. package/dist/mcp/providerTools.js +17 -0
  46. package/dist/mcp/readArtifact.d.ts +9 -0
  47. package/dist/mcp/readArtifact.js +11 -0
  48. package/dist/mcp/server.js +9 -1
  49. package/dist/mcp/tailStreamLog.d.ts +8 -0
  50. package/dist/mcp/tailStreamLog.js +28 -0
  51. package/dist/mcp/toolResults.d.ts +17 -0
  52. package/dist/mcp/toolResults.js +15 -0
  53. package/dist/mcp/tools.js +4 -0
  54. package/dist/mcp/waitRun.js +4 -4
  55. package/dist/mcp/waitingGates.d.ts +1 -0
  56. package/dist/mcp/waitingGates.js +3 -2
  57. package/dist/package-meta.d.ts +1 -1
  58. package/dist/package-meta.js +1 -1
  59. package/dist/projection/projectRun.d.ts +3 -0
  60. package/dist/projection/projectRun.js +7 -0
  61. package/dist/runstore/port.d.ts +3 -1
  62. package/dist/runstore/port.js +2 -0
  63. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
  64. package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
  65. package/dist/runstore/workspaceLayout.d.ts +1 -0
  66. package/dist/runstore/workspaceLayout.js +3 -0
  67. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  68. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  69. package/dist/runtime/feedbackLoopDecision.js +33 -1
  70. package/dist/runtime/forkGeneration.d.ts +4 -0
  71. package/dist/runtime/forkGeneration.js +16 -0
  72. package/dist/runtime/pipelineRunner.d.ts +3 -3
  73. package/dist/runtime/pipelineRunner.js +3 -1
  74. package/dist/runtime/pipelineScheduler.js +23 -1
  75. package/dist/runtime/reopenSkipped.d.ts +13 -0
  76. package/dist/runtime/reopenSkipped.js +56 -0
  77. package/dist/runtime/resumeReconstruct.js +2 -2
  78. package/dist/runtime/resumeTimedOut.d.ts +33 -0
  79. package/dist/runtime/resumeTimedOut.js +165 -0
  80. package/dist/runtime/runManager.d.ts +3 -1
  81. package/dist/runtime/runManager.js +110 -14
  82. package/dist/runtime/settingsFile.d.ts +2 -0
  83. package/dist/runtime/settingsFile.js +19 -0
  84. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  85. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  86. package/dist/runtime/stageRecovery.js +2 -2
  87. package/dist/runtime/stageRunner.js +6 -2
  88. package/dist/runtime/stageStreamLog.d.ts +17 -0
  89. package/dist/runtime/stageStreamLog.js +89 -0
  90. package/dist/runtime/stageWorker.js +2 -1
  91. package/dist/server/bootstrap.js +9 -2
  92. package/dist/server/ensureGlobalService.d.ts +35 -0
  93. package/dist/server/ensureGlobalService.js +138 -0
  94. package/dist/server/http.d.ts +20 -1
  95. package/dist/server/http.js +452 -386
  96. package/dist/server/mcpHost.js +7 -8
  97. package/dist/server/operatorResults.d.ts +10 -0
  98. package/dist/server/operatorResults.js +24 -0
  99. package/dist/server/providerRoutes.d.ts +0 -6
  100. package/dist/server/providerRoutes.js +10 -18
  101. package/dist/tools/askOperator.d.ts +1 -0
  102. package/dist/tools/askOperator.js +14 -11
  103. package/dist/tools/emitStageEnvelope.d.ts +1 -0
  104. package/dist/tools/emitStageEnvelope.js +3 -1
  105. package/dist/tools/prepareToolArguments.d.ts +4 -0
  106. package/dist/tools/prepareToolArguments.js +79 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
  109. package/dist/ui/assets/index-Da5EHU6M.js +119 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +1 -1
  112. package/skills/stageflow/references/control-surface.md +2 -2
  113. package/skills/stageflow-run/SKILL.md +27 -9
  114. package/skills/stageflow-run/references/mcp-call.md +1 -1
  115. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
  116. package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
@@ -1,18 +1,16 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { resolveGlobalOnlyAgentPort } from "../agent/resolveAgentPort.js";
4
3
  import { projectRun } from "../projection/projectRun.js";
5
4
  import { waitRun } from "../mcp/waitRun.js";
6
5
  import { projectWaitingGates } from "../mcp/waitingGates.js";
7
- import { resolveOperatorCatalog } from "./operatorCatalog.js";
8
6
  import { completeCliRun } from "./runCommand.js";
9
7
  import { reportCliRun } from "./runOutput.js";
8
+ import { globalStageflowHome } from "../project/globalHome.js";
10
9
  import { createRunStore } from "../runstore/createStore.js";
11
10
  import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
12
- import { RunManager } from "../runtime/runManager.js";
13
- import { readStageExecutionMode } from "../runtime/stageConcurrency.js";
14
- import { DEFAULT_PORT } from "../server/createHttpHost.js";
11
+ import { ensureGlobalService, hostBaseUrl, } from "../server/ensureGlobalService.js";
15
12
  import { mapRetryStageFailure } from "../server/operatorResults.js";
13
+ import { httpAbandonStage, httpDecideFeedbackLoop, httpDeliverAnswer, httpRecoverManualStageUntilStop, httpRerun, httpResumeTimedOutStage, httpRetryStageUntilStop, httpStopManualRecovery, } from "./hostClient.js";
16
14
  import { AskOperatorError, parseAskOperatorAnswer, } from "../tools/askOperator.js";
17
15
  export const RUNS_USAGE = `Usage:
18
16
  sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]
@@ -24,6 +22,7 @@ export const RUNS_USAGE = `Usage:
24
22
  sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
25
23
  sf runs feedback-decide --run <runId> --stage <sourceStageId> [--loop <loopId>] --decision extend|continue|abandon [--reason <text>] [--json]
26
24
  sf runs retry --run <runId> --stage <stageId> [--json]
25
+ sf runs resume --run <runId> --stage <stageId> [--json]
27
26
  sf runs abandon --run <runId> --stage <stageId> [--json]
28
27
  sf runs rerun --run <runId> [--json]`;
29
28
  const defaultIo = {
@@ -122,6 +121,8 @@ function flagsFor(subcommand) {
122
121
  return FEEDBACK_DECIDE_FLAGS;
123
122
  case "retry":
124
123
  return RETRY_FLAGS;
124
+ case "resume":
125
+ return RETRY_FLAGS;
125
126
  case "abandon":
126
127
  return ABANDON_FLAGS;
127
128
  case "rerun":
@@ -252,28 +253,6 @@ function resolveRunIdFromFile(fromPath, cwd) {
252
253
  }
253
254
  return runId;
254
255
  }
255
- function hostBaseUrl() {
256
- return `http://127.0.0.1:${DEFAULT_PORT}`;
257
- }
258
- export async function defaultProbeHost() {
259
- const ac = new AbortController();
260
- const timer = setTimeout(() => ac.abort(), 1500);
261
- try {
262
- const res = await fetch(`${hostBaseUrl()}/api/health`, {
263
- signal: ac.signal,
264
- });
265
- if (res.status !== 200)
266
- return "down";
267
- JSON.parse(await res.text());
268
- return "up";
269
- }
270
- catch {
271
- return "down";
272
- }
273
- finally {
274
- clearTimeout(timer);
275
- }
276
- }
277
256
  function printJson(io, payload) {
278
257
  io.log(JSON.stringify(payload, null, 2));
279
258
  }
@@ -301,15 +280,22 @@ async function resolveShowWaitRunId(parsed, cwd, io) {
301
280
  }
302
281
  return parsed.runId;
303
282
  }
283
+ const MUTATING_SUBCOMMANDS = new Set([
284
+ "answer",
285
+ "feedback-decide",
286
+ "retry",
287
+ "resume",
288
+ "recover",
289
+ "abandon",
290
+ "rerun",
291
+ ]);
304
292
  export async function runRunsCommand(args, options = {}) {
305
293
  const cwd = options.cwd ?? process.cwd();
306
- const projectRoot = options.projectRoot ?? cwd;
307
- const isGitProject = options.isGitProject ?? false;
308
294
  const out = { ...defaultIo, ...options.io };
309
- const env = options.env ?? process.env;
310
295
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
311
296
  const readStdin = options.readStdin ?? (() => readFileSync(0, "utf8"));
312
- const probeHost = options.probeHost ?? defaultProbeHost;
297
+ const base = options.hostBaseUrl ?? hostBaseUrl();
298
+ const ensureService = options.ensureService ?? (() => ensureGlobalService());
313
299
  let parsed;
314
300
  try {
315
301
  parsed = parseRunsArgs(args);
@@ -328,39 +314,23 @@ export async function runRunsCommand(args, options = {}) {
328
314
  let resolvedStore = options.store;
329
315
  const getStore = () => {
330
316
  if (resolvedStore === undefined) {
331
- resolvedStore = createRunStore({ rootDir: projectRoot });
317
+ resolvedStore = createRunStore({ rootDir: globalStageflowHome() });
332
318
  }
333
319
  return resolvedStore;
334
320
  };
335
321
  const mutatingIo = out;
336
- const agent = await resolveGlobalOnlyAgentPort(projectRoot);
337
- const buildManager = () => {
338
- const store = getStore();
339
- if (options.createManager)
340
- return options.createManager(store);
341
- const operatorCatalog = resolveOperatorCatalog({
342
- flags: {},
343
- env,
344
- defaultCwd: cwd,
345
- });
346
- return new RunManager({
347
- agent,
348
- store,
349
- cwd,
350
- projectRoot,
351
- isGitProject,
352
- operatorCatalog,
353
- executionMode: readStageExecutionMode(env, "process"),
354
- });
355
- };
356
- const guardHost = async () => {
357
- const status = await probeHost();
358
- if (status === "up") {
359
- out.error(`A Stageflow host is already running at ${hostBaseUrl()}. Use the operator console or MCP instead of mutating the store from the CLI.`);
322
+ if (MUTATING_SUBCOMMANDS.has(parsed.subcommand)) {
323
+ const ensured = await ensureService();
324
+ if (!ensured.ok) {
325
+ if (parsed.json) {
326
+ printJson(out, { error: ensured.message, reason: ensured.reason });
327
+ }
328
+ else {
329
+ out.error(ensured.message);
330
+ }
360
331
  return 1;
361
332
  }
362
- return undefined;
363
- };
333
+ }
364
334
  switch (parsed.subcommand) {
365
335
  case "list": {
366
336
  if (parsed.status === "waiting") {
@@ -524,9 +494,6 @@ export async function runRunsCommand(args, options = {}) {
524
494
  }
525
495
  }
526
496
  case "answer": {
527
- const blocked = await guardHost();
528
- if (blocked !== undefined)
529
- return blocked;
530
497
  if (!parsed.runId) {
531
498
  return usageError(out, "Missing --run");
532
499
  }
@@ -575,8 +542,7 @@ export async function runRunsCommand(args, options = {}) {
575
542
  out.error(message);
576
543
  return 1;
577
544
  }
578
- const manager = buildManager();
579
- const result = await manager.deliverAnswer(parsed.runId, parsed.stageId, answer);
545
+ const result = await httpDeliverAnswer(base, parsed.runId, parsed.stageId, parsedJson);
580
546
  if (!result.ok) {
581
547
  const payload = { error: result.reason };
582
548
  if (result.status !== undefined)
@@ -598,9 +564,6 @@ export async function runRunsCommand(args, options = {}) {
598
564
  return 0;
599
565
  }
600
566
  case "feedback-decide": {
601
- const blocked = await guardHost();
602
- if (blocked !== undefined)
603
- return blocked;
604
567
  if (!parsed.runId) {
605
568
  return usageError(out, "Missing --run");
606
569
  }
@@ -622,8 +585,7 @@ export async function runRunsCommand(args, options = {}) {
622
585
  }
623
586
  return 1;
624
587
  }
625
- const manager = buildManager();
626
- const result = await manager.decideFeedbackLoop(parsed.runId, parsed.stageId, {
588
+ const result = await httpDecideFeedbackLoop(base, parsed.runId, parsed.stageId, {
627
589
  decision: parsed.decision,
628
590
  ...(parsed.loopId !== undefined ? { loopId: parsed.loopId } : {}),
629
591
  ...(parsed.reason !== undefined ? { reason: parsed.reason } : {}),
@@ -653,14 +615,10 @@ export async function runRunsCommand(args, options = {}) {
653
615
  return 0;
654
616
  }
655
617
  case "retry": {
656
- const blocked = await guardHost();
657
- if (blocked !== undefined)
658
- return blocked;
659
618
  if (!parsed.runId || !parsed.stageId) {
660
619
  return usageError(out, "Missing --run and/or --stage");
661
620
  }
662
- const manager = buildManager();
663
- const result = await manager.retryStageUntilStop(parsed.runId, parsed.stageId);
621
+ const result = await httpRetryStageUntilStop(base, parsed.runId, parsed.stageId);
664
622
  if (!result.ok) {
665
623
  if (parsed.json) {
666
624
  printJson(out, {
@@ -675,19 +633,45 @@ export async function runRunsCommand(args, options = {}) {
675
633
  }
676
634
  return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
677
635
  }
636
+ case "resume": {
637
+ if (!parsed.runId || !parsed.stageId) {
638
+ return usageError(out, "Missing --run and/or --stage");
639
+ }
640
+ const result = await httpResumeTimedOutStage(base, parsed.runId, parsed.stageId);
641
+ if (!result.ok) {
642
+ if (parsed.json) {
643
+ printJson(out, {
644
+ ...mapRetryStageFailure(result),
645
+ status: result.status,
646
+ });
647
+ }
648
+ else {
649
+ out.error(result.reason);
650
+ }
651
+ return 1;
652
+ }
653
+ if (parsed.json) {
654
+ printJson(out, {
655
+ ok: true,
656
+ runId: result.runId,
657
+ stageId: result.stageId,
658
+ attemptIndex: result.attemptIndex,
659
+ });
660
+ }
661
+ else {
662
+ out.log(`ok\t${result.runId}\t${result.stageId}\t${result.attemptIndex}`);
663
+ }
664
+ return 0;
665
+ }
678
666
  case "recover": {
679
- const blocked = await guardHost();
680
- if (blocked !== undefined)
681
- return blocked;
682
667
  if (!parsed.runId || !parsed.stageId) {
683
668
  return usageError(out, "Missing --run and/or --stage");
684
669
  }
685
- const manager = buildManager();
686
670
  if (parsed.stop) {
687
671
  if (parsed.guidance !== undefined) {
688
672
  return usageError(out, "--guidance cannot be used with --stop");
689
673
  }
690
- const result = await manager.stopManualRecovery(parsed.runId, parsed.stageId);
674
+ const result = await httpStopManualRecovery(base, parsed.runId, parsed.stageId);
691
675
  if (!result.ok) {
692
676
  if (parsed.json) {
693
677
  printJson(out, { error: result.reason, status: result.status });
@@ -705,7 +689,7 @@ export async function runRunsCommand(args, options = {}) {
705
689
  }
706
690
  return 0;
707
691
  }
708
- const result = await manager.recoverManualStageUntilStop(parsed.runId, parsed.stageId, parsed.guidance);
692
+ const result = await httpRecoverManualStageUntilStop(base, parsed.runId, parsed.stageId, parsed.guidance);
709
693
  if (!result.ok) {
710
694
  if (parsed.json) {
711
695
  printJson(out, { ...mapRetryStageFailure(result), status: result.status });
@@ -718,14 +702,10 @@ export async function runRunsCommand(args, options = {}) {
718
702
  return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
719
703
  }
720
704
  case "abandon": {
721
- const blocked = await guardHost();
722
- if (blocked !== undefined)
723
- return blocked;
724
705
  if (!parsed.runId || !parsed.stageId) {
725
706
  return usageError(out, "Missing --run and/or --stage");
726
707
  }
727
- const manager = buildManager();
728
- const result = await manager.abandonStage(parsed.runId, parsed.stageId);
708
+ const result = await httpAbandonStage(base, parsed.runId, parsed.stageId);
729
709
  if (!result.ok) {
730
710
  const payload = { error: result.reason };
731
711
  if (result.status !== undefined)
@@ -751,14 +731,10 @@ export async function runRunsCommand(args, options = {}) {
751
731
  return 0;
752
732
  }
753
733
  case "rerun": {
754
- const blocked = await guardHost();
755
- if (blocked !== undefined)
756
- return blocked;
757
734
  if (!parsed.runId) {
758
735
  return usageError(out, "Missing --run");
759
736
  }
760
- const manager = buildManager();
761
- const started = await manager.rerun(parsed.runId);
737
+ const started = await httpRerun(base, parsed.runId);
762
738
  if (!started.ok) {
763
739
  return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: mutatingIo });
764
740
  }
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
14
14
  import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
15
15
  import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
16
16
  import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
17
+ import { GRAPH_USAGE, runGraphCommand } from "./cli/graphCommand.js";
17
18
  import { MIGRATE_YAML_USAGE, runMigrateYamlCommand } from "./cli/migrateYamlCommand.js";
18
19
  import { createRunStore } from "./runstore/createStore.js";
19
20
  import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
@@ -27,6 +28,7 @@ const USAGE = `Usage:
27
28
  sf init
28
29
  sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
29
30
  sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
31
+ sf graph --pipeline <path> [--json]
30
32
  sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
31
33
  sf artifact read --run <runId> --path <relPath> [--out <file>]
32
34
  sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
@@ -39,6 +41,7 @@ const USAGE = `Usage:
39
41
  sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]
40
42
  sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
41
43
  sf runs retry --run <runId> --stage <stageId> [--json]
44
+ sf runs resume --run <runId> --stage <stageId> [--json]
42
45
  sf runs abandon --run <runId> --stage <stageId> [--json]
43
46
  sf runs rerun --run <runId> [--json]
44
47
  sf ui [--port ${DEFAULT_PORT}] [--mcp-stateless]
@@ -66,6 +69,8 @@ ${RUN_USAGE}
66
69
 
67
70
  ${VALIDATE_USAGE}
68
71
 
72
+ ${GRAPH_USAGE}
73
+
69
74
  ${MIGRATE_YAML_USAGE}
70
75
 
71
76
  ${ARTIFACT_USAGE}
@@ -97,6 +102,7 @@ function parseArgs(argv) {
97
102
  const command = args[0];
98
103
  if (command === "providers" ||
99
104
  command === "validate" ||
105
+ command === "graph" ||
100
106
  command === "migrate-yaml" ||
101
107
  command === "run" ||
102
108
  command === "init" ||
@@ -285,6 +291,12 @@ async function main(argv) {
285
291
  projectRoot: ctx.projectRoot,
286
292
  });
287
293
  }
294
+ if (parsed.command === "graph") {
295
+ return runGraphCommand(argv.slice(3), {
296
+ cwd: ctx.invocationCwd,
297
+ projectRoot: ctx.projectRoot,
298
+ });
299
+ }
288
300
  if (parsed.command === "migrate-yaml") {
289
301
  return runMigrateYamlCommand(argv.slice(3), {
290
302
  cwd: ctx.invocationCwd,
@@ -294,8 +306,6 @@ async function main(argv) {
294
306
  if (parsed.command === "run") {
295
307
  return runRunCommand(argv.slice(3), {
296
308
  cwd: ctx.invocationCwd,
297
- projectRoot: ctx.projectRoot,
298
- isGitProject: ctx.isGitProject,
299
309
  });
300
310
  }
301
311
  if (parsed.command === "artifact") {
@@ -319,8 +329,6 @@ async function main(argv) {
319
329
  if (parsed.command === "runs") {
320
330
  return runRunsCommand(argv.slice(3), {
321
331
  cwd: ctx.invocationCwd,
322
- projectRoot: ctx.projectRoot,
323
- isGitProject: ctx.isGitProject,
324
332
  });
325
333
  }
326
334
  if (parsed.command === "providers") {
@@ -332,7 +340,7 @@ async function main(argv) {
332
340
  projectRoot: ctx.projectRoot,
333
341
  });
334
342
  }
335
- const store = createRunStore({ rootDir: ctx.projectRoot });
343
+ const store = createRunStore({ rootDir: ctx.globalHome });
336
344
  const globalAgent = globalAgentBackendFromManifest(ctx.manifest);
337
345
  if (parsed.command === "ui") {
338
346
  const mcpStateless = resolveMcpStateless({
@@ -0,0 +1,26 @@
1
+ import type { CloneMode, FeedbackLoopConfig, LoadedPipeline, PipelineForkConfig, PipelineNeedEdge, ResolvedPipelineStageNode, RouteIfPredicate } from "../types/pipeline.js";
2
+ import type { StageGateKind } from "../types/stage.js";
3
+ export type PipelineDescribeNeedEdge = {
4
+ id: string;
5
+ on: PipelineNeedEdge["on"];
6
+ if?: RouteIfPredicate;
7
+ };
8
+ export type PipelineDescribeNeeds = string | null | PipelineDescribeNeedEdge[];
9
+ export type PipelineDescribeStage = {
10
+ id: string;
11
+ needs: PipelineDescribeNeeds;
12
+ fork?: PipelineForkConfig;
13
+ gate_kinds?: StageGateKind[];
14
+ clone_cap?: number;
15
+ clone_mode?: CloneMode;
16
+ feedback_loop?: FeedbackLoopConfig;
17
+ entry?: boolean;
18
+ replay_safe?: boolean;
19
+ };
20
+ export type PipelineDescribe = {
21
+ id: string;
22
+ path: string;
23
+ stages: PipelineDescribeStage[];
24
+ };
25
+ export declare function describePipelineNeeds(node: ResolvedPipelineStageNode): PipelineDescribeNeeds;
26
+ export declare function describePipeline(loaded: LoadedPipeline): PipelineDescribe;
@@ -0,0 +1,37 @@
1
+ import { toPipelineNeeds } from "./pipelineNeeds.js";
2
+ export function describePipelineNeeds(node) {
3
+ if (node.needsEdges.length === 0)
4
+ return node.needs;
5
+ const collapsed = toPipelineNeeds(node.needsEdges);
6
+ if (typeof collapsed === "string")
7
+ return collapsed;
8
+ return collapsed.map((edge) => ({
9
+ id: edge.id,
10
+ on: [...edge.on],
11
+ ...(edge.if !== undefined ? { if: edge.if } : {}),
12
+ }));
13
+ }
14
+ export function describePipeline(loaded) {
15
+ const gateById = new Map(loaded.stages.map((stage) => [stage.id, stage.gate_kinds]));
16
+ const stages = loaded.dag.nodes.map((node) => {
17
+ const gateKinds = gateById.get(node.id);
18
+ return {
19
+ id: node.id,
20
+ needs: describePipelineNeeds(node),
21
+ ...(node.fork !== undefined ? { fork: { ...node.fork } } : {}),
22
+ ...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
23
+ ...(node.clone_mode !== undefined ? { clone_mode: node.clone_mode } : {}),
24
+ ...(node.feedback_loop !== undefined
25
+ ? { feedback_loop: { ...node.feedback_loop } }
26
+ : {}),
27
+ ...(node.entry !== undefined ? { entry: node.entry } : {}),
28
+ ...(node.replay_safe !== undefined ? { replay_safe: node.replay_safe } : {}),
29
+ ...(gateKinds !== undefined ? { gate_kinds: [...gateKinds] } : {}),
30
+ };
31
+ });
32
+ return {
33
+ id: loaded.pipeline.id,
34
+ path: loaded.pipelinePath,
35
+ stages,
36
+ };
37
+ }
@@ -1,8 +1,20 @@
1
- import type { LoadedPipeline } from "../types/pipeline.js";
1
+ import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
2
2
  import { type LoadOutcome } from "./loadOutcome.js";
3
3
  export type { LoadedPipeline } from "../types/pipeline.js";
4
4
  export type { LoadIssue, LoadOutcome } from "./loadOutcome.js";
5
5
  export declare function resolvePipelinePath(pipelinePath: string, cwd: string): Promise<string>;
6
+ /** Synthetic `pipelinePath` for a `LoadedPipeline` built from an inline object with no backing file. */
7
+ export declare const INLINE_PIPELINE_PATH = "<inline>";
8
+ export declare function loadPipelineFromObjectOutcome(raw: InlinePipelineDefinition, options?: {
9
+ cwd?: string;
10
+ projectRoot?: string;
11
+ requireIo?: boolean;
12
+ }): Promise<LoadOutcome<LoadedPipeline>>;
13
+ export declare function loadPipelineFromObject(raw: InlinePipelineDefinition, options?: {
14
+ cwd?: string;
15
+ projectRoot?: string;
16
+ requireIo?: boolean;
17
+ }): Promise<LoadedPipeline>;
6
18
  export declare function loadPipelineOutcome(nameOrPath: string, options?: {
7
19
  cwd?: string;
8
20
  projectRoot?: string;
@@ -2,7 +2,7 @@ import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { compilePayloadSchema, expandPayloadSchemaRefs, isPayloadSchemaSubset, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
4
4
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
5
- import { mergePipelineStages } from "./mergePipelineIncludes.js";
5
+ import { mergeInlinePipelineStages, mergePipelineStages, } from "./mergePipelineIncludes.js";
6
6
  import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
7
7
  import { loadStageFromObjectOutcome, loadStageOutcome, afterCompletionForStage } from "./loadStage.js";
8
8
  import { materializeStageModels } from "./materializeStageModels.js";
@@ -92,14 +92,44 @@ export async function resolvePipelinePath(pipelinePath, cwd) {
92
92
  throw new Error(`Pipeline file not found: ${resolved}. Pass a filesystem path to --pipeline (e.g. pipelines/hello.pipeline.yaml), not a bare pipeline id.`);
93
93
  }
94
94
  }
95
+ /** Synthetic `pipelinePath` for a `LoadedPipeline` built from an inline object with no backing file. */
96
+ export const INLINE_PIPELINE_PATH = "<inline>";
95
97
  async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd, requireIo = true) {
96
98
  const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
97
99
  const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
98
100
  if (!mergeOutcome.ok) {
99
101
  return loadFailure(mergeOutcome.issues);
100
102
  }
101
- const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, warnings: mergeWarnings, } = mergeOutcome.value;
102
- const ctx = { pipelineId, path: normalizedPipelinePath };
103
+ return buildLoadedPipelineFromMerge(mergeOutcome.value, {
104
+ projectRoot,
105
+ requireIo,
106
+ pipelinePath: normalizedPipelinePath,
107
+ });
108
+ }
109
+ export async function loadPipelineFromObjectOutcome(raw, options = {}) {
110
+ const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
111
+ const requireIo = options.requireIo !== false;
112
+ const mergeOutcome = await mergeInlinePipelineStages(raw);
113
+ if (!mergeOutcome.ok) {
114
+ return loadFailure(mergeOutcome.issues);
115
+ }
116
+ return buildLoadedPipelineFromMerge(mergeOutcome.value, {
117
+ projectRoot,
118
+ requireIo,
119
+ pipelinePath: INLINE_PIPELINE_PATH,
120
+ });
121
+ }
122
+ export async function loadPipelineFromObject(raw, options = {}) {
123
+ const outcome = await loadPipelineFromObjectOutcome(raw, options);
124
+ if (!outcome.ok) {
125
+ throw new Error(outcome.issues[0].message);
126
+ }
127
+ return outcome.value;
128
+ }
129
+ async function buildLoadedPipelineFromMerge(merge, options) {
130
+ const { projectRoot, requireIo, pipelinePath } = options;
131
+ const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, warnings: mergeWarnings, } = merge;
132
+ const ctx = { pipelineId, path: pipelinePath };
103
133
  const warnings = [...mergeWarnings];
104
134
  const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
105
135
  if (!normalizeOutcome.ok) {
@@ -274,7 +304,7 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd, requir
274
304
  pipeline,
275
305
  stages: loadedStages,
276
306
  dag,
277
- pipelinePath: normalizedPipelinePath,
307
+ pipelinePath,
278
308
  stageSources,
279
309
  }, warnings);
280
310
  }
@@ -1,4 +1,5 @@
1
1
  import type { PayloadSchemaMap } from "../envelope/payloadSchema.js";
2
+ import type { InlinePipelineDefinition } from "../types/pipeline.js";
2
3
  import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
3
4
  export type RawMergedEntry = {
4
5
  raw: unknown;
@@ -12,3 +13,13 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
12
13
  schemas?: PayloadSchemaMap;
13
14
  warnings: LoadIssue[];
14
15
  }>>;
16
+ /** Synthetic `declaringPath` for stage entries with no backing file. */
17
+ export declare const INLINE_DECLARING_PATH = "<inline pipeline>";
18
+ export declare function mergeInlinePipelineStages(raw: InlinePipelineDefinition): Promise<LoadOutcome<{
19
+ entries: RawMergedEntry[];
20
+ pipelineId: string;
21
+ agent?: string;
22
+ model?: string;
23
+ schemas?: PayloadSchemaMap;
24
+ warnings: LoadIssue[];
25
+ }>>;
@@ -201,6 +201,81 @@ export async function mergePipelineStages(rootPath) {
201
201
  warnings,
202
202
  });
203
203
  }
204
+ /** Synthetic `declaringPath` for stage entries with no backing file. */
205
+ export const INLINE_DECLARING_PATH = "<inline pipeline>";
206
+ export async function mergeInlinePipelineStages(raw) {
207
+ if (typeof raw.id !== "string" || !raw.id) {
208
+ return loadFailure([
209
+ {
210
+ code: "pipeline.invalid_shape",
211
+ message: `Invalid inline pipeline: id is required`,
212
+ category: "pipeline",
213
+ },
214
+ ]);
215
+ }
216
+ if (!Array.isArray(raw.stages) || raw.stages.length === 0) {
217
+ return loadFailure([
218
+ {
219
+ code: "pipeline.invalid_shape",
220
+ message: `Invalid inline pipeline ${raw.id}: stages must be non-empty`,
221
+ category: "pipeline",
222
+ pipelineId: raw.id,
223
+ },
224
+ ]);
225
+ }
226
+ const pipelineId = raw.id;
227
+ const agentField = parseAgentField(raw.agent);
228
+ if (!agentField.ok) {
229
+ return loadFailure([
230
+ {
231
+ code: "pipeline.invalid_agent",
232
+ message: `Invalid inline pipeline ${pipelineId}: ${agentField.message}`,
233
+ category: "pipeline",
234
+ pipelineId,
235
+ },
236
+ ]);
237
+ }
238
+ const agent = agentField.value;
239
+ const modelField = parseModelField(raw.model);
240
+ if (!modelField.ok) {
241
+ return loadFailure([
242
+ {
243
+ code: "pipeline.invalid_model",
244
+ message: `Invalid inline pipeline ${pipelineId}: ${modelField.message}`,
245
+ category: "pipeline",
246
+ pipelineId,
247
+ },
248
+ ]);
249
+ }
250
+ const model = modelField.value;
251
+ const schemasOutcome = parsePipelineSchemas(raw.schemas, "<inline pipeline>", pipelineId);
252
+ if (!schemasOutcome.ok)
253
+ return schemasOutcome;
254
+ const schemas = schemasOutcome.value;
255
+ const entries = [];
256
+ for (let index = 0; index < raw.stages.length; index++) {
257
+ const entry = raw.stages[index];
258
+ if (isPlainObject(entry) && typeof entry.uses === "string") {
259
+ return loadFailure([
260
+ {
261
+ code: "pipeline.invalid_shape",
262
+ message: `Invalid inline pipeline ${pipelineId}: stage entry at index ${index} has "uses" — inline pipelines cannot reference external stage files, all stage bodies must be inline`,
263
+ category: "pipeline",
264
+ pipelineId,
265
+ },
266
+ ]);
267
+ }
268
+ entries.push({ raw: entry, declaringPath: INLINE_DECLARING_PATH });
269
+ }
270
+ return loadSuccess({
271
+ entries,
272
+ pipelineId,
273
+ ...(agent !== undefined ? { agent } : {}),
274
+ ...(model !== undefined ? { model } : {}),
275
+ ...(schemas !== undefined ? { schemas } : {}),
276
+ warnings: [],
277
+ });
278
+ }
204
279
  function parsePipelineSchemas(raw, absPath, pipelineId) {
205
280
  if (raw === undefined)
206
281
  return loadSuccess(undefined);
@@ -1,4 +1,4 @@
1
- import type { LoadedPipeline } from "../types/pipeline.js";
1
+ import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
2
2
  import type { LoadIssue } from "./loadOutcome.js";
3
3
  export type ValidationSeverity = "error" | "warning";
4
4
  export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
@@ -50,7 +50,7 @@ export type LoadPipelineValidatedResult = {
50
50
  ok: false;
51
51
  findings: ValidationFinding[];
52
52
  };
53
- export declare function loadPipelineValidated(nameOrPath: string, options?: {
53
+ export declare function loadPipelineValidated(nameOrPath: string | InlinePipelineDefinition, options?: {
54
54
  cwd?: string;
55
55
  projectRoot?: string;
56
56
  validateStages?: boolean;