stageflow 0.19.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 (90) hide show
  1. package/README.md +2 -1
  2. package/dist/agent/activity.d.ts +5 -0
  3. package/dist/agent/activityObserver.d.ts +3 -0
  4. package/dist/agent/activityObserver.js +6 -1
  5. package/dist/agent/piAdapter.js +7 -3
  6. package/dist/agent/port.d.ts +2 -0
  7. package/dist/agent/providerInspect.d.ts +19 -0
  8. package/dist/agent/providerInspect.js +35 -0
  9. package/dist/agent/streamLogRedact.d.ts +2 -0
  10. package/dist/agent/streamLogRedact.js +13 -0
  11. package/dist/cli/artifactCommand.js +2 -2
  12. package/dist/cli/envelopeCommand.js +2 -2
  13. package/dist/cli/exportRunCommand.js +2 -2
  14. package/dist/cli/hostClient.d.ts +43 -0
  15. package/dist/cli/hostClient.js +182 -0
  16. package/dist/cli/providersCommand.js +5 -3
  17. package/dist/cli/runCommand.d.ts +6 -5
  18. package/dist/cli/runCommand.js +23 -31
  19. package/dist/cli/runOutput.d.ts +1 -1
  20. package/dist/cli/runsCommand.d.ts +3 -7
  21. package/dist/cli/runsCommand.js +33 -94
  22. package/dist/cli.js +1 -5
  23. package/dist/config/describePipeline.d.ts +26 -0
  24. package/dist/config/describePipeline.js +37 -0
  25. package/dist/config/loadPipeline.d.ts +13 -1
  26. package/dist/config/loadPipeline.js +34 -4
  27. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  28. package/dist/config/mergePipelineIncludes.js +75 -0
  29. package/dist/config/validateCatalog.d.ts +2 -2
  30. package/dist/config/validateCatalog.js +24 -17
  31. package/dist/mcp/catalogTools.js +146 -52
  32. package/dist/mcp/controlTools.js +77 -19
  33. package/dist/mcp/deps.d.ts +3 -0
  34. package/dist/mcp/projectMcpTools.d.ts +3 -0
  35. package/dist/mcp/projectMcpTools.js +25 -0
  36. package/dist/mcp/providerTools.d.ts +3 -0
  37. package/dist/mcp/providerTools.js +17 -0
  38. package/dist/mcp/readArtifact.d.ts +9 -0
  39. package/dist/mcp/readArtifact.js +11 -0
  40. package/dist/mcp/server.js +9 -1
  41. package/dist/mcp/tailStreamLog.d.ts +8 -0
  42. package/dist/mcp/tailStreamLog.js +28 -0
  43. package/dist/mcp/toolResults.d.ts +17 -0
  44. package/dist/mcp/toolResults.js +15 -0
  45. package/dist/mcp/tools.js +4 -0
  46. package/dist/mcp/waitRun.js +4 -4
  47. package/dist/mcp/waitingGates.d.ts +1 -0
  48. package/dist/mcp/waitingGates.js +3 -2
  49. package/dist/package-meta.d.ts +1 -1
  50. package/dist/package-meta.js +1 -1
  51. package/dist/projection/projectRun.d.ts +3 -0
  52. package/dist/projection/projectRun.js +7 -0
  53. package/dist/runstore/port.d.ts +3 -1
  54. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
  55. package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
  56. package/dist/runstore/workspaceLayout.d.ts +1 -0
  57. package/dist/runstore/workspaceLayout.js +3 -0
  58. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  59. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  60. package/dist/runtime/feedbackLoopDecision.js +33 -1
  61. package/dist/runtime/pipelineRunner.d.ts +3 -3
  62. package/dist/runtime/pipelineRunner.js +3 -1
  63. package/dist/runtime/resumeReconstruct.js +2 -2
  64. package/dist/runtime/runManager.d.ts +2 -1
  65. package/dist/runtime/runManager.js +36 -16
  66. package/dist/runtime/settingsFile.d.ts +2 -0
  67. package/dist/runtime/settingsFile.js +19 -0
  68. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  69. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  70. package/dist/runtime/stageStreamLog.d.ts +17 -0
  71. package/dist/runtime/stageStreamLog.js +89 -0
  72. package/dist/runtime/stageWorker.js +2 -1
  73. package/dist/server/bootstrap.js +9 -2
  74. package/dist/server/ensureGlobalService.d.ts +35 -0
  75. package/dist/server/ensureGlobalService.js +138 -0
  76. package/dist/server/http.d.ts +20 -1
  77. package/dist/server/http.js +450 -402
  78. package/dist/server/mcpHost.js +7 -8
  79. package/dist/server/operatorResults.d.ts +10 -0
  80. package/dist/server/operatorResults.js +24 -0
  81. package/dist/server/providerRoutes.d.ts +0 -6
  82. package/dist/server/providerRoutes.js +10 -18
  83. package/dist/types/pipeline.d.ts +8 -0
  84. package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
  85. package/dist/ui/index.html +1 -1
  86. package/package.json +1 -1
  87. package/skills/stageflow/references/control-surface.md +2 -2
  88. package/skills/stageflow-run/SKILL.md +27 -9
  89. package/skills/stageflow-run/references/mcp-call.md +1 -1
  90. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
@@ -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]
@@ -255,28 +253,6 @@ function resolveRunIdFromFile(fromPath, cwd) {
255
253
  }
256
254
  return runId;
257
255
  }
258
- function hostBaseUrl() {
259
- return `http://127.0.0.1:${DEFAULT_PORT}`;
260
- }
261
- export async function defaultProbeHost() {
262
- const ac = new AbortController();
263
- const timer = setTimeout(() => ac.abort(), 1500);
264
- try {
265
- const res = await fetch(`${hostBaseUrl()}/api/health`, {
266
- signal: ac.signal,
267
- });
268
- if (res.status !== 200)
269
- return "down";
270
- JSON.parse(await res.text());
271
- return "up";
272
- }
273
- catch {
274
- return "down";
275
- }
276
- finally {
277
- clearTimeout(timer);
278
- }
279
- }
280
256
  function printJson(io, payload) {
281
257
  io.log(JSON.stringify(payload, null, 2));
282
258
  }
@@ -304,15 +280,22 @@ async function resolveShowWaitRunId(parsed, cwd, io) {
304
280
  }
305
281
  return parsed.runId;
306
282
  }
283
+ const MUTATING_SUBCOMMANDS = new Set([
284
+ "answer",
285
+ "feedback-decide",
286
+ "retry",
287
+ "resume",
288
+ "recover",
289
+ "abandon",
290
+ "rerun",
291
+ ]);
307
292
  export async function runRunsCommand(args, options = {}) {
308
293
  const cwd = options.cwd ?? process.cwd();
309
- const projectRoot = options.projectRoot ?? cwd;
310
- const isGitProject = options.isGitProject ?? false;
311
294
  const out = { ...defaultIo, ...options.io };
312
- const env = options.env ?? process.env;
313
295
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
314
296
  const readStdin = options.readStdin ?? (() => readFileSync(0, "utf8"));
315
- const probeHost = options.probeHost ?? defaultProbeHost;
297
+ const base = options.hostBaseUrl ?? hostBaseUrl();
298
+ const ensureService = options.ensureService ?? (() => ensureGlobalService());
316
299
  let parsed;
317
300
  try {
318
301
  parsed = parseRunsArgs(args);
@@ -331,39 +314,23 @@ export async function runRunsCommand(args, options = {}) {
331
314
  let resolvedStore = options.store;
332
315
  const getStore = () => {
333
316
  if (resolvedStore === undefined) {
334
- resolvedStore = createRunStore({ rootDir: projectRoot });
317
+ resolvedStore = createRunStore({ rootDir: globalStageflowHome() });
335
318
  }
336
319
  return resolvedStore;
337
320
  };
338
321
  const mutatingIo = out;
339
- const agent = await resolveGlobalOnlyAgentPort(projectRoot);
340
- const buildManager = () => {
341
- const store = getStore();
342
- if (options.createManager)
343
- return options.createManager(store);
344
- const operatorCatalog = resolveOperatorCatalog({
345
- flags: {},
346
- env,
347
- defaultCwd: cwd,
348
- });
349
- return new RunManager({
350
- agent,
351
- store,
352
- cwd,
353
- projectRoot,
354
- isGitProject,
355
- operatorCatalog,
356
- executionMode: readStageExecutionMode(env, "process"),
357
- });
358
- };
359
- const guardHost = async () => {
360
- const status = await probeHost();
361
- if (status === "up") {
362
- 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
+ }
363
331
  return 1;
364
332
  }
365
- return undefined;
366
- };
333
+ }
367
334
  switch (parsed.subcommand) {
368
335
  case "list": {
369
336
  if (parsed.status === "waiting") {
@@ -527,9 +494,6 @@ export async function runRunsCommand(args, options = {}) {
527
494
  }
528
495
  }
529
496
  case "answer": {
530
- const blocked = await guardHost();
531
- if (blocked !== undefined)
532
- return blocked;
533
497
  if (!parsed.runId) {
534
498
  return usageError(out, "Missing --run");
535
499
  }
@@ -578,8 +542,7 @@ export async function runRunsCommand(args, options = {}) {
578
542
  out.error(message);
579
543
  return 1;
580
544
  }
581
- const manager = buildManager();
582
- const result = await manager.deliverAnswer(parsed.runId, parsed.stageId, answer);
545
+ const result = await httpDeliverAnswer(base, parsed.runId, parsed.stageId, parsedJson);
583
546
  if (!result.ok) {
584
547
  const payload = { error: result.reason };
585
548
  if (result.status !== undefined)
@@ -601,9 +564,6 @@ export async function runRunsCommand(args, options = {}) {
601
564
  return 0;
602
565
  }
603
566
  case "feedback-decide": {
604
- const blocked = await guardHost();
605
- if (blocked !== undefined)
606
- return blocked;
607
567
  if (!parsed.runId) {
608
568
  return usageError(out, "Missing --run");
609
569
  }
@@ -625,8 +585,7 @@ export async function runRunsCommand(args, options = {}) {
625
585
  }
626
586
  return 1;
627
587
  }
628
- const manager = buildManager();
629
- const result = await manager.decideFeedbackLoop(parsed.runId, parsed.stageId, {
588
+ const result = await httpDecideFeedbackLoop(base, parsed.runId, parsed.stageId, {
630
589
  decision: parsed.decision,
631
590
  ...(parsed.loopId !== undefined ? { loopId: parsed.loopId } : {}),
632
591
  ...(parsed.reason !== undefined ? { reason: parsed.reason } : {}),
@@ -656,14 +615,10 @@ export async function runRunsCommand(args, options = {}) {
656
615
  return 0;
657
616
  }
658
617
  case "retry": {
659
- const blocked = await guardHost();
660
- if (blocked !== undefined)
661
- return blocked;
662
618
  if (!parsed.runId || !parsed.stageId) {
663
619
  return usageError(out, "Missing --run and/or --stage");
664
620
  }
665
- const manager = buildManager();
666
- const result = await manager.retryStageUntilStop(parsed.runId, parsed.stageId);
621
+ const result = await httpRetryStageUntilStop(base, parsed.runId, parsed.stageId);
667
622
  if (!result.ok) {
668
623
  if (parsed.json) {
669
624
  printJson(out, {
@@ -679,14 +634,10 @@ export async function runRunsCommand(args, options = {}) {
679
634
  return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
680
635
  }
681
636
  case "resume": {
682
- const blocked = await guardHost();
683
- if (blocked !== undefined)
684
- return blocked;
685
637
  if (!parsed.runId || !parsed.stageId) {
686
638
  return usageError(out, "Missing --run and/or --stage");
687
639
  }
688
- const manager = buildManager();
689
- const result = await manager.resumeTimedOutStage(parsed.runId, parsed.stageId);
640
+ const result = await httpResumeTimedOutStage(base, parsed.runId, parsed.stageId);
690
641
  if (!result.ok) {
691
642
  if (parsed.json) {
692
643
  printJson(out, {
@@ -713,18 +664,14 @@ export async function runRunsCommand(args, options = {}) {
713
664
  return 0;
714
665
  }
715
666
  case "recover": {
716
- const blocked = await guardHost();
717
- if (blocked !== undefined)
718
- return blocked;
719
667
  if (!parsed.runId || !parsed.stageId) {
720
668
  return usageError(out, "Missing --run and/or --stage");
721
669
  }
722
- const manager = buildManager();
723
670
  if (parsed.stop) {
724
671
  if (parsed.guidance !== undefined) {
725
672
  return usageError(out, "--guidance cannot be used with --stop");
726
673
  }
727
- const result = await manager.stopManualRecovery(parsed.runId, parsed.stageId);
674
+ const result = await httpStopManualRecovery(base, parsed.runId, parsed.stageId);
728
675
  if (!result.ok) {
729
676
  if (parsed.json) {
730
677
  printJson(out, { error: result.reason, status: result.status });
@@ -742,7 +689,7 @@ export async function runRunsCommand(args, options = {}) {
742
689
  }
743
690
  return 0;
744
691
  }
745
- const result = await manager.recoverManualStageUntilStop(parsed.runId, parsed.stageId, parsed.guidance);
692
+ const result = await httpRecoverManualStageUntilStop(base, parsed.runId, parsed.stageId, parsed.guidance);
746
693
  if (!result.ok) {
747
694
  if (parsed.json) {
748
695
  printJson(out, { ...mapRetryStageFailure(result), status: result.status });
@@ -755,14 +702,10 @@ export async function runRunsCommand(args, options = {}) {
755
702
  return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
756
703
  }
757
704
  case "abandon": {
758
- const blocked = await guardHost();
759
- if (blocked !== undefined)
760
- return blocked;
761
705
  if (!parsed.runId || !parsed.stageId) {
762
706
  return usageError(out, "Missing --run and/or --stage");
763
707
  }
764
- const manager = buildManager();
765
- const result = await manager.abandonStage(parsed.runId, parsed.stageId);
708
+ const result = await httpAbandonStage(base, parsed.runId, parsed.stageId);
766
709
  if (!result.ok) {
767
710
  const payload = { error: result.reason };
768
711
  if (result.status !== undefined)
@@ -788,14 +731,10 @@ export async function runRunsCommand(args, options = {}) {
788
731
  return 0;
789
732
  }
790
733
  case "rerun": {
791
- const blocked = await guardHost();
792
- if (blocked !== undefined)
793
- return blocked;
794
734
  if (!parsed.runId) {
795
735
  return usageError(out, "Missing --run");
796
736
  }
797
- const manager = buildManager();
798
- const started = await manager.rerun(parsed.runId);
737
+ const started = await httpRerun(base, parsed.runId);
799
738
  if (!started.ok) {
800
739
  return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: mutatingIo });
801
740
  }
package/dist/cli.js CHANGED
@@ -306,8 +306,6 @@ async function main(argv) {
306
306
  if (parsed.command === "run") {
307
307
  return runRunCommand(argv.slice(3), {
308
308
  cwd: ctx.invocationCwd,
309
- projectRoot: ctx.projectRoot,
310
- isGitProject: ctx.isGitProject,
311
309
  });
312
310
  }
313
311
  if (parsed.command === "artifact") {
@@ -331,8 +329,6 @@ async function main(argv) {
331
329
  if (parsed.command === "runs") {
332
330
  return runRunsCommand(argv.slice(3), {
333
331
  cwd: ctx.invocationCwd,
334
- projectRoot: ctx.projectRoot,
335
- isGitProject: ctx.isGitProject,
336
332
  });
337
333
  }
338
334
  if (parsed.command === "providers") {
@@ -344,7 +340,7 @@ async function main(argv) {
344
340
  projectRoot: ctx.projectRoot,
345
341
  });
346
342
  }
347
- const store = createRunStore({ rootDir: ctx.projectRoot });
343
+ const store = createRunStore({ rootDir: ctx.globalHome });
348
344
  const globalAgent = globalAgentBackendFromManifest(ctx.manifest);
349
345
  if (parsed.command === "ui") {
350
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;
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { loadPipelineOutcome, resolvePipelinePath } from "./loadPipeline.js";
2
+ import { INLINE_PIPELINE_PATH, loadPipelineFromObjectOutcome, loadPipelineOutcome, resolvePipelinePath, } from "./loadPipeline.js";
3
3
  import { loadStageOutcome } from "./loadStage.js";
4
4
  import { loadTaskOutcome } from "./loadTask.js";
5
5
  import { readYamlObject } from "./readYamlObject.js";
@@ -241,23 +241,30 @@ async function runPipelineValidation(nameOrPath, options) {
241
241
  const { cwd, validateStages } = options;
242
242
  const projectRoot = options.projectRoot ?? cwd;
243
243
  let pipelinePath;
244
- try {
245
- pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
244
+ let outcome;
245
+ if (typeof nameOrPath === "string") {
246
+ try {
247
+ pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ return {
252
+ ok: false,
253
+ findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
254
+ {
255
+ code: "pipeline.load_error",
256
+ message,
257
+ category: "pipeline",
258
+ },
259
+ ]),
260
+ };
261
+ }
262
+ outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
263
+ }
264
+ else {
265
+ pipelinePath = path.resolve(cwd, INLINE_PIPELINE_PATH);
266
+ outcome = await loadPipelineFromObjectOutcome(nameOrPath, { cwd, projectRoot });
246
267
  }
247
- catch (err) {
248
- const message = err instanceof Error ? err.message : String(err);
249
- return {
250
- ok: false,
251
- findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
252
- {
253
- code: "pipeline.load_error",
254
- message,
255
- category: "pipeline",
256
- },
257
- ]),
258
- };
259
- }
260
- const outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
261
268
  const findings = [];
262
269
  if (!outcome.ok) {
263
270
  findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));