stageflow 0.19.0 → 0.25.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 (120) hide show
  1. package/README.md +16 -3
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +93 -0
  5. package/dist/a2a/contracts.js +163 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +44 -0
  9. package/dist/a2a/registry.js +159 -0
  10. package/dist/a2a/server.d.ts +29 -0
  11. package/dist/a2a/server.js +341 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +496 -0
  14. package/dist/a2a/store.d.ts +106 -0
  15. package/dist/a2a/store.js +245 -0
  16. package/dist/agent/activity.d.ts +5 -0
  17. package/dist/agent/activityObserver.d.ts +3 -0
  18. package/dist/agent/activityObserver.js +6 -1
  19. package/dist/agent/piAdapter.js +7 -3
  20. package/dist/agent/port.d.ts +2 -0
  21. package/dist/agent/providerInspect.d.ts +19 -0
  22. package/dist/agent/providerInspect.js +35 -0
  23. package/dist/agent/streamLogRedact.d.ts +2 -0
  24. package/dist/agent/streamLogRedact.js +13 -0
  25. package/dist/cli/a2aCommand.d.ts +8 -0
  26. package/dist/cli/a2aCommand.js +131 -0
  27. package/dist/cli/artifactCommand.js +2 -2
  28. package/dist/cli/envelopeCommand.js +2 -2
  29. package/dist/cli/exportRunCommand.js +2 -2
  30. package/dist/cli/hostClient.d.ts +43 -0
  31. package/dist/cli/hostClient.js +182 -0
  32. package/dist/cli/providersCommand.js +5 -3
  33. package/dist/cli/runCommand.d.ts +6 -5
  34. package/dist/cli/runCommand.js +23 -31
  35. package/dist/cli/runOutput.d.ts +1 -1
  36. package/dist/cli/runStageCommand.d.ts +33 -0
  37. package/dist/cli/runStageCommand.js +407 -0
  38. package/dist/cli/runsCommand.d.ts +3 -7
  39. package/dist/cli/runsCommand.js +33 -94
  40. package/dist/cli.js +20 -5
  41. package/dist/config/describePipeline.d.ts +26 -0
  42. package/dist/config/describePipeline.js +37 -0
  43. package/dist/config/loadPipeline.d.ts +13 -1
  44. package/dist/config/loadPipeline.js +34 -4
  45. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  46. package/dist/config/mergePipelineIncludes.js +75 -0
  47. package/dist/config/validateCatalog.d.ts +2 -2
  48. package/dist/config/validateCatalog.js +24 -17
  49. package/dist/mcp/catalogTools.js +285 -52
  50. package/dist/mcp/controlTools.js +77 -19
  51. package/dist/mcp/deps.d.ts +3 -0
  52. package/dist/mcp/projectMcpTools.d.ts +3 -0
  53. package/dist/mcp/projectMcpTools.js +25 -0
  54. package/dist/mcp/providerTools.d.ts +3 -0
  55. package/dist/mcp/providerTools.js +17 -0
  56. package/dist/mcp/readArtifact.d.ts +9 -0
  57. package/dist/mcp/readArtifact.js +11 -0
  58. package/dist/mcp/server.js +9 -1
  59. package/dist/mcp/tailStreamLog.d.ts +8 -0
  60. package/dist/mcp/tailStreamLog.js +28 -0
  61. package/dist/mcp/toolResults.d.ts +17 -0
  62. package/dist/mcp/toolResults.js +15 -0
  63. package/dist/mcp/tools.js +4 -0
  64. package/dist/mcp/waitRun.js +4 -4
  65. package/dist/mcp/waitingGates.d.ts +1 -0
  66. package/dist/mcp/waitingGates.js +3 -2
  67. package/dist/package-meta.d.ts +1 -1
  68. package/dist/package-meta.js +1 -1
  69. package/dist/projection/projectRun.d.ts +3 -0
  70. package/dist/projection/projectRun.js +7 -0
  71. package/dist/runstore/createStore.d.ts +10 -0
  72. package/dist/runstore/createStore.js +13 -4
  73. package/dist/runstore/port.d.ts +17 -2
  74. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  75. package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
  76. package/dist/runstore/sqlite/schema.d.ts +1 -1
  77. package/dist/runstore/sqlite/schema.js +6 -0
  78. package/dist/runstore/submission.d.ts +11 -0
  79. package/dist/runstore/submission.js +7 -0
  80. package/dist/runstore/workspaceLayout.d.ts +1 -0
  81. package/dist/runstore/workspaceLayout.js +3 -0
  82. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  83. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  84. package/dist/runtime/feedbackLoopDecision.js +33 -1
  85. package/dist/runtime/pipelineRunner.d.ts +5 -3
  86. package/dist/runtime/pipelineRunner.js +6 -1
  87. package/dist/runtime/pipelineScheduler.js +32 -15
  88. package/dist/runtime/reloadRunCatalog.js +7 -1
  89. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  90. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  91. package/dist/runtime/resumeReconstruct.js +2 -2
  92. package/dist/runtime/runManager.d.ts +24 -2
  93. package/dist/runtime/runManager.js +83 -19
  94. package/dist/runtime/settingsFile.d.ts +2 -0
  95. package/dist/runtime/settingsFile.js +19 -0
  96. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  97. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  98. package/dist/runtime/stageStreamLog.d.ts +17 -0
  99. package/dist/runtime/stageStreamLog.js +89 -0
  100. package/dist/runtime/stageWorker.js +2 -1
  101. package/dist/server/bootstrap.d.ts +2 -0
  102. package/dist/server/bootstrap.js +22 -4
  103. package/dist/server/createHttpHost.js +6 -0
  104. package/dist/server/ensureGlobalService.d.ts +35 -0
  105. package/dist/server/ensureGlobalService.js +138 -0
  106. package/dist/server/http.d.ts +20 -1
  107. package/dist/server/http.js +450 -402
  108. package/dist/server/mcpHost.js +7 -8
  109. package/dist/server/operatorResults.d.ts +10 -0
  110. package/dist/server/operatorResults.js +24 -0
  111. package/dist/server/providerRoutes.d.ts +0 -6
  112. package/dist/server/providerRoutes.js +10 -18
  113. package/dist/types/pipeline.d.ts +8 -0
  114. package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
  115. package/dist/ui/index.html +1 -1
  116. package/package.json +2 -1
  117. package/skills/stageflow/references/control-surface.md +2 -2
  118. package/skills/stageflow-run/SKILL.md +27 -9
  119. package/skills/stageflow-run/references/mcp-call.md +1 -1
  120. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
@@ -0,0 +1,7 @@
1
+ export class RunSubmissionExistsError extends Error {
2
+ submission;
3
+ constructor(submission) {
4
+ super("Run submission already exists");
5
+ this.submission = submission;
6
+ }
7
+ }
@@ -2,6 +2,7 @@ export { stageDir } from "./paths.js";
2
2
  export declare function guardStageId(stageId: string): void;
3
3
  export declare function attemptWorkspaceDir(workspaceDir: string, stageId: string, attempt: number): string;
4
4
  export declare function attemptLogPath(workspaceDir: string, stageId: string, attempt: number): string;
5
+ export declare function attemptStreamLogPath(workspaceDir: string, stageId: string, attempt: number): string;
5
6
  export declare function attemptEnvelopePath(workspaceDir: string, stageId: string, attempt: number): string;
6
7
  export declare function attemptArtifactsDir(workspaceDir: string, stageId: string, attempt: number): string;
7
8
  export declare function attemptAgentDir(workspaceDir: string, stageId: string, attempt: number): string;
@@ -21,6 +21,9 @@ export function attemptWorkspaceDir(workspaceDir, stageId, attempt) {
21
21
  export function attemptLogPath(workspaceDir, stageId, attempt) {
22
22
  return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "log.jsonl");
23
23
  }
24
+ export function attemptStreamLogPath(workspaceDir, stageId, attempt) {
25
+ return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "stream.log");
26
+ }
24
27
  export function attemptEnvelopePath(workspaceDir, stageId, attempt) {
25
28
  return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "envelope.json");
26
29
  }
@@ -1,5 +1,5 @@
1
1
  import type { FeedbackLoopContext, StageSessionMode } from "../agent/port.js";
2
- import type { DeferredFeedbackSendBack, FeedbackLoopRecord, FeedbackReplayRecord, RunStore } from "../runstore/port.js";
2
+ import type { DeferredFeedbackSendBack, FeedbackLoopRecord, FeedbackReplayRecord, FeedbackReplayStagePassStatus, RunStore } from "../runstore/port.js";
3
3
  import type { StageEnvelope } from "../types/envelope.js";
4
4
  import type { ResolvedPipelineDag, ResolvedPipelineStageNode } from "../types/pipeline.js";
5
5
  import type { FeedbackScheduleState } from "./feedbackLoopSchedule.js";
@@ -38,6 +38,14 @@ export type FeedbackSendBackRejected = {
38
38
  };
39
39
  export type FeedbackSendBackResult = FeedbackSendBackAccepted | FeedbackSendBackIdempotent | FeedbackSendBackWaitingForHuman | FeedbackSendBackRejected;
40
40
  export declare function terminalizeCurrentFeedbackReplay(store: RunStore, runId: string, loop: FeedbackLoopRecord, status: Extract<FeedbackReplayRecord["status"], "completed" | "failed" | "waiting_for_human">): Promise<void>;
41
+ export declare function updateFeedbackReplaySourcePass(options: {
42
+ store: RunStore;
43
+ runId: string;
44
+ loop: FeedbackLoopRecord;
45
+ sourceStageId: string;
46
+ status: Extract<FeedbackReplayStagePassStatus, "waiting" | "succeeded" | "failed">;
47
+ envelope?: StageEnvelope;
48
+ }): Promise<void>;
41
49
  export declare function acceptFeedbackSendBack(options: {
42
50
  store: RunStore;
43
51
  runId: string;
@@ -10,6 +10,33 @@ export async function terminalizeCurrentFeedbackReplay(store, runId, loop, statu
10
10
  return;
11
11
  await store.updateFeedbackReplay(runId, loop.current_replay_id, { status });
12
12
  }
13
+ export async function updateFeedbackReplaySourcePass(options) {
14
+ const { store, runId, loop, sourceStageId, status, envelope } = options;
15
+ if (loop.current_replay_id === undefined)
16
+ return;
17
+ const passes = await store.listFeedbackReplayStagePasses(runId, loop.current_replay_id);
18
+ const sourcePass = passes.find((pass) => pass.stage_id === sourceStageId);
19
+ if (sourcePass === undefined)
20
+ return;
21
+ if (sourcePass.status === "succeeded")
22
+ return;
23
+ if (status === "waiting") {
24
+ await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceStageId, { status: "waiting" });
25
+ return;
26
+ }
27
+ if (status === "succeeded") {
28
+ await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceStageId, {
29
+ status: "succeeded",
30
+ finished_at: new Date().toISOString(),
31
+ ...(envelope !== undefined ? { emitted_envelope: envelope } : {}),
32
+ });
33
+ return;
34
+ }
35
+ await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceStageId, {
36
+ status: "failed",
37
+ finished_at: new Date().toISOString(),
38
+ });
39
+ }
13
40
  async function resolveResumeSessionAttempt(store, runId, loopId, stageId, fallbackAttempt) {
14
41
  const replays = await store.listFeedbackReplays(runId, loopId);
15
42
  for (const replay of replays) {
@@ -182,6 +209,13 @@ export async function acceptFeedbackSendBack(options) {
182
209
  source_attempt: sourceAttempt,
183
210
  };
184
211
  await terminalizeCurrentFeedbackReplay(store, runId, loop, "waiting_for_human");
212
+ await updateFeedbackReplaySourcePass({
213
+ store,
214
+ runId,
215
+ loop,
216
+ sourceStageId: sourceNode.id,
217
+ status: "waiting",
218
+ });
185
219
  await store.updateFeedbackLoop(runId, loop.loop_id, {
186
220
  state: "waiting_for_human",
187
221
  deferred_send_back: deferred,
@@ -205,15 +239,14 @@ export async function acceptFeedbackSendBack(options) {
205
239
  };
206
240
  }
207
241
  if (loop.current_replay_id !== undefined) {
208
- const currentPasses = await store.listFeedbackReplayStagePasses(runId, loop.current_replay_id);
209
- const sourcePass = currentPasses.find((p) => p.stage_id === sourceNode.id);
210
- if (sourcePass !== undefined && sourcePass.status !== "succeeded") {
211
- await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceNode.id, {
212
- status: "succeeded",
213
- finished_at: new Date().toISOString(),
214
- emitted_envelope: envelope,
215
- });
216
- }
242
+ await updateFeedbackReplaySourcePass({
243
+ store,
244
+ runId,
245
+ loop,
246
+ sourceStageId: sourceNode.id,
247
+ status: "succeeded",
248
+ envelope,
249
+ });
217
250
  await store.updateFeedbackReplay(runId, loop.current_replay_id, {
218
251
  status: "completed",
219
252
  });
@@ -301,17 +334,14 @@ export async function markFeedbackLoopContinued(options) {
301
334
  (entry.state === "active" || entry.state === "waiting_for_human"));
302
335
  if (loop === undefined)
303
336
  return;
304
- if (loop.current_replay_id !== undefined) {
305
- const passes = await store.listFeedbackReplayStagePasses(runId, loop.current_replay_id);
306
- const sourcePass = passes.find((p) => p.stage_id === sourceStageId);
307
- if (sourcePass !== undefined && sourcePass.status !== "succeeded") {
308
- await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceStageId, {
309
- status: "succeeded",
310
- finished_at: new Date().toISOString(),
311
- emitted_envelope: envelope,
312
- });
313
- }
314
- }
337
+ await updateFeedbackReplaySourcePass({
338
+ store,
339
+ runId,
340
+ loop,
341
+ sourceStageId,
342
+ status: "succeeded",
343
+ envelope,
344
+ });
315
345
  await terminalizeCurrentFeedbackReplay(store, runId, loop, "completed");
316
346
  await store.updateFeedbackLoop(runId, loop.loop_id, {
317
347
  state: "continued",
@@ -1,7 +1,19 @@
1
- import { terminalizeCurrentFeedbackReplay } from "./feedbackLoopCoordinator.js";
1
+ import { terminalizeCurrentFeedbackReplay, updateFeedbackReplaySourcePass, } from "./feedbackLoopCoordinator.js";
2
2
  import { createFeedbackScheduleState } from "./feedbackLoopSchedule.js";
3
3
  import { applySendBack, releaseHold, } from "./replayLifecycle.js";
4
4
  const DECISION_CONFLICT = "feedback loop decision conflict: no longer waiting_for_human";
5
+ async function appendFeedbackLoopDecided(store, runId, loop, decision, reason) {
6
+ const latest = await store.getLatestStageExecution(runId, loop.source_stage_id);
7
+ const attempt = latest?.attempt;
8
+ const eventOptions = attempt !== undefined ? { attempt } : undefined;
9
+ const trimmed = reason?.trim();
10
+ await store.appendStageEvent(runId, loop.source_stage_id, {
11
+ event: "feedback_loop_decided",
12
+ decision,
13
+ loopId: loop.loop_id,
14
+ ...(trimmed ? { reason: trimmed } : {}),
15
+ }, eventOptions);
16
+ }
5
17
  async function clearSourceWaiting(store, runId, sourceStageId, nextStatus, reason) {
6
18
  const latest = await store.getLatestStageExecution(runId, sourceStageId);
7
19
  const attempt = latest?.attempt;
@@ -96,6 +108,7 @@ export async function resolveFeedbackLoopDecision(options) {
96
108
  reason: `${DECISION_CONFLICT} (loop=${loop.loop_id})`,
97
109
  };
98
110
  }
111
+ await appendFeedbackLoopDecided(store, runId, loop, "extend", options.reason);
99
112
  await clearSourceWaiting(store, runId, loop.source_stage_id, "succeeded");
100
113
  if (schedule === undefined) {
101
114
  const applied = await applySendBack({
@@ -168,8 +181,19 @@ export async function resolveFeedbackLoopDecision(options) {
168
181
  reason: `${DECISION_CONFLICT} (loop=${loop.loop_id})`,
169
182
  };
170
183
  }
184
+ await appendFeedbackLoopDecided(store, runId, loop, "continue", options.reason);
171
185
  await terminalizeCurrentFeedbackReplay(store, runId, loop, "completed");
172
186
  await clearSourceWaiting(store, runId, loop.source_stage_id, "succeeded");
187
+ await updateFeedbackReplaySourcePass({
188
+ store,
189
+ runId,
190
+ loop,
191
+ sourceStageId: loop.source_stage_id,
192
+ status: "succeeded",
193
+ ...(deferred !== undefined
194
+ ? { envelope: deferred.feedback_envelope }
195
+ : {}),
196
+ });
173
197
  if (schedule !== undefined) {
174
198
  schedule.states.set(loop.source_stage_id, "succeeded");
175
199
  if (schedule.completedEnvelopes.get(loop.source_stage_id) === undefined &&
@@ -190,8 +214,16 @@ export async function resolveFeedbackLoopDecision(options) {
190
214
  reason: `${DECISION_CONFLICT} (loop=${loop.loop_id})`,
191
215
  };
192
216
  }
217
+ await appendFeedbackLoopDecided(store, runId, loop, "abandon", abandonReason);
193
218
  await terminalizeCurrentFeedbackReplay(store, runId, loop, "failed");
194
219
  await clearSourceWaiting(store, runId, loop.source_stage_id, "failed", abandonReason);
220
+ await updateFeedbackReplaySourcePass({
221
+ store,
222
+ runId,
223
+ loop,
224
+ sourceStageId: loop.source_stage_id,
225
+ status: "failed",
226
+ });
195
227
  if (schedule !== undefined) {
196
228
  schedule.states.set(loop.source_stage_id, "failed");
197
229
  releaseHold({ feedback: schedule.feedback });
@@ -1,9 +1,10 @@
1
+ import type { RunSubmission } from "../runstore/submission.js";
1
2
  import type { AgentPort } from "../agent/port.js";
2
3
  import { type ValidationFinding } from "../config/validateCatalog.js";
3
4
  import type { RunStore } from "../runstore/port.js";
4
5
  import type { StageEnvelope } from "../types/envelope.js";
5
6
  import type { StageHitlController } from "./stageHitl.js";
6
- import type { LoadedPipeline } from "../types/pipeline.js";
7
+ import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
7
8
  import type { TaskFile } from "../types/task.js";
8
9
  import { type StageExecutionMode } from "./stageConcurrency.js";
9
10
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
@@ -57,7 +58,7 @@ export declare function runPipeline(options: {
57
58
  store: RunStore;
58
59
  taskPath?: string;
59
60
  taskYaml?: string;
60
- pipeline: string;
61
+ pipeline: string | InlinePipelineDefinition;
61
62
  cwd?: string;
62
63
  projectRoot?: string;
63
64
  checkoutOverride?: string;
@@ -70,11 +71,12 @@ export declare function runPipeline(options: {
70
71
  }): Promise<PipelineRunResult>;
71
72
  /** Create the run immediately, then execute stages in the returned promise. */
72
73
  export declare function startPipeline(options: {
74
+ submission?: RunSubmission;
73
75
  agent: AgentPort;
74
76
  store: RunStore;
75
77
  taskPath?: string;
76
78
  taskYaml?: string;
77
- pipeline: string;
79
+ pipeline: string | InlinePipelineDefinition;
78
80
  cwd?: string;
79
81
  projectRoot?: string;
80
82
  checkoutOverride?: string;
@@ -57,12 +57,15 @@ async function preparePipeline(options) {
57
57
  console.error(`${finding.code}: ${finding.message}`);
58
58
  }
59
59
  const checkoutRoot = await resolveAndValidateCheckout(task, options.checkoutOverride, options.cwd);
60
- const pipelinePath = normalizeCatalogPath(loaded.pipelinePath);
60
+ const pipelinePath = typeof options.pipeline === "string"
61
+ ? normalizeCatalogPath(loaded.pipelinePath)
62
+ : undefined;
61
63
  const taskPath = options.taskPath
62
64
  ? normalizeCatalogPath(path.resolve(options.cwd, options.taskPath))
63
65
  : undefined;
64
66
  const projectRoot = normalizeCatalogPath(options.projectRoot ?? options.cwd);
65
67
  const run = await options.store.createRun({
68
+ submission: options.submission,
66
69
  pipelineId: loaded.pipeline.id,
67
70
  taskYaml,
68
71
  taskId: task.id,
@@ -74,6 +77,7 @@ async function preparePipeline(options) {
74
77
  pipelinePath,
75
78
  taskPath,
76
79
  projectRoot,
80
+ inlinePipeline: typeof options.pipeline === "string" ? undefined : options.pipeline,
77
81
  });
78
82
  const executionMode = readStageExecutionMode(process.env, options.executionMode);
79
83
  const stageProcessLauncher = resolveStageProcessLauncher(executionMode, options.stageProcessLauncher);
@@ -143,6 +147,7 @@ export async function startPipeline(options) {
143
147
  const cwd = options.cwd ?? process.cwd();
144
148
  const projectRoot = options.projectRoot ?? cwd;
145
149
  const prepared = await preparePipeline({
150
+ submission: options.submission,
146
151
  agent: options.agent,
147
152
  store: options.store,
148
153
  taskPath: options.taskPath,
@@ -220,6 +220,25 @@ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
220
220
  applyForkChoiceToSchedule(dag, node.id, chosen, states);
221
221
  }
222
222
  }
223
+ async function applyEagerRouteIfSkipsFromEnvelopes(dag, states, completedEnvelopes, persistSkip) {
224
+ for (const [parentId, envelope] of completedEnvelopes) {
225
+ if (states.get(parentId) !== "succeeded")
226
+ continue;
227
+ for (const childId of dag.childrenOf[parentId] ?? []) {
228
+ const child = dag.nodes.find((n) => n.id === childId);
229
+ if (!child)
230
+ continue;
231
+ const result = classifyInboundAfterSuccess(child, parentId, envelope.payload);
232
+ if (result === "missing_field") {
233
+ return parentId;
234
+ }
235
+ if (isEagerSingleParentIfSkip(child, parentId, envelope.payload)) {
236
+ await persistSkip(childId);
237
+ }
238
+ }
239
+ }
240
+ return undefined;
241
+ }
223
242
  export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt, alreadyLaunchedIds) {
224
243
  retryRoots.set(stageId, attempt);
225
244
  const downstream = cloneRetryDownstream(dag, [stageId]);
@@ -472,6 +491,13 @@ export async function runPipelineDag(options) {
472
491
  await persistSkipPending(id);
473
492
  }
474
493
  }
494
+ const missingRouteIfParent = await applyEagerRouteIfSkipsFromEnvelopes(dag, states, completedEnvelopes, persistSkipPending);
495
+ if (missingRouteIfParent !== undefined) {
496
+ schedulingHalted = true;
497
+ if (firstFailureReason === undefined) {
498
+ firstFailureReason = `stage "${missingRouteIfParent}": route if field missing from payload`;
499
+ }
500
+ }
475
501
  const markSkippedPending = () => {
476
502
  for (const node of dag.nodes) {
477
503
  if (states.get(node.id) === "pending") {
@@ -756,22 +782,13 @@ export async function runPipelineDag(options) {
756
782
  await store.appendStageEvent(run.runId, id, { event: "skipped" });
757
783
  }
758
784
  }
759
- const childIds = dag.childrenOf[stageId] ?? [];
760
- for (const childId of childIds) {
761
- const child = dag.nodes.find((node) => node.id === childId);
762
- if (!child)
763
- continue;
764
- const result = classifyInboundAfterSuccess(child, stageId, envelope.payload);
765
- if (result === "missing_field") {
766
- schedulingHalted = true;
767
- if (firstFailureReason === undefined) {
768
- firstFailureReason = `stage "${stageId}": route if field missing from payload`;
769
- }
770
- return;
771
- }
772
- if (isEagerSingleParentIfSkip(child, stageId, envelope.payload)) {
773
- await persistSkipPending(childId);
785
+ const missingParent = await applyEagerRouteIfSkipsFromEnvelopes(dag, states, new Map([[stageId, envelope]]), persistSkipPending);
786
+ if (missingParent !== undefined) {
787
+ schedulingHalted = true;
788
+ if (firstFailureReason === undefined) {
789
+ firstFailureReason = `stage "${missingParent}": route if field missing from payload`;
774
790
  }
791
+ return;
775
792
  }
776
793
  await reopenRunnableSkippedStages(store, run.runId, dag, states, completedEnvelopes);
777
794
  };
@@ -1,5 +1,5 @@
1
1
  import { access } from "node:fs/promises";
2
- import { loadPipeline } from "../config/loadPipeline.js";
2
+ import { loadPipeline, loadPipelineFromObject } from "../config/loadPipeline.js";
3
3
  import { loadTask } from "../config/loadTask.js";
4
4
  import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
5
  function missingLocator(field, runId) {
@@ -33,6 +33,12 @@ async function assertCatalogFileExists(filePath, label) {
33
33
  }
34
34
  export async function reloadPipelineForRun(meta) {
35
35
  const projectRoot = requireProjectRoot(meta);
36
+ // An inline-pipeline run (no pipeline_path — nothing was ever written to a
37
+ // catalog file) carries its own pipeline body in meta.inline_pipeline
38
+ // instead, the same way a task without task_path reloads from task_yaml.
39
+ if (meta.inline_pipeline) {
40
+ return loadPipelineFromObject(meta.inline_pipeline, { projectRoot });
41
+ }
36
42
  const pipelinePath = requirePipelinePath(meta);
37
43
  await assertCatalogFileExists(pipelinePath, "Pipeline");
38
44
  return loadPipeline(pipelinePath, { cwd: projectRoot });
@@ -0,0 +1,30 @@
1
+ import { type StoreLookupKind } from "../server/operatorResults.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import type { TaskFile } from "../types/task.js";
4
+ export type EnvelopeRefInput = {
5
+ runId: string;
6
+ stageId: string;
7
+ attempt?: number;
8
+ };
9
+ type EnvelopeRefStore = Pick<RunStore, "readRunMeta" | "readRun" | "readEnvelope">;
10
+ /** Carries enough to map onto either MCP's textResult shape or an A2aApplicationError. */
11
+ export declare class EnvelopeRefError extends Error {
12
+ readonly status: number;
13
+ readonly kind: StoreLookupKind;
14
+ constructor(message: string, status: number, kind: StoreLookupKind);
15
+ }
16
+ /**
17
+ * Resolves one or more Envelope References (ADR-0002) into a TaskFile for
18
+ * the next standalone stage call — shared by the MCP run_stage tool and the
19
+ * A2A run_stage operation so this resolution logic exists in exactly one
20
+ * place (it was duplicated between them once already; see the reuse finding
21
+ * from this feature's code review).
22
+ *
23
+ * A single ref keeps today's exact shape: goal = its summary, input = its
24
+ * payload verbatim. Multiple refs namespace each resolved payload under its
25
+ * stageId (disambiguated by runId only if two refs share a stageId), so a
26
+ * stage author addresses each source by name in their own io.input.schema
27
+ * instead of one silently overwriting another on a key collision.
28
+ */
29
+ export declare function resolveEnvelopeRefsToTask(store: EnvelopeRefStore, refs: EnvelopeRefInput | EnvelopeRefInput[], checkout?: string): Promise<TaskFile>;
30
+ export {};
@@ -0,0 +1,79 @@
1
+ import { mapStoreLookupError } from "../server/operatorResults.js";
2
+ /** Carries enough to map onto either MCP's textResult shape or an A2aApplicationError. */
3
+ export class EnvelopeRefError extends Error {
4
+ status;
5
+ kind;
6
+ constructor(message, status, kind) {
7
+ super(message);
8
+ this.status = status;
9
+ this.kind = kind;
10
+ }
11
+ }
12
+ async function resolveOne(store, ref) {
13
+ try {
14
+ await store.readRunMeta(ref.runId);
15
+ }
16
+ catch (err) {
17
+ const mapped = mapStoreLookupError(err, { policy: "run" });
18
+ throw new EnvelopeRefError(mapped.error, mapped.status, mapped.kind);
19
+ }
20
+ try {
21
+ const detail = await store.readRun(ref.runId);
22
+ if (!detail.stages.some((s) => s.stage_id === ref.stageId)) {
23
+ throw new EnvelopeRefError(`Stage not found: ${ref.stageId}`, 404, "not_found");
24
+ }
25
+ return await store.readEnvelope(ref.runId, ref.stageId, ref.attempt);
26
+ }
27
+ catch (err) {
28
+ if (err instanceof EnvelopeRefError)
29
+ throw err;
30
+ const mapped = mapStoreLookupError(err, { policy: "envelope" });
31
+ throw new EnvelopeRefError(mapped.error, mapped.status, mapped.kind);
32
+ }
33
+ }
34
+ /**
35
+ * Resolves one or more Envelope References (ADR-0002) into a TaskFile for
36
+ * the next standalone stage call — shared by the MCP run_stage tool and the
37
+ * A2A run_stage operation so this resolution logic exists in exactly one
38
+ * place (it was duplicated between them once already; see the reuse finding
39
+ * from this feature's code review).
40
+ *
41
+ * A single ref keeps today's exact shape: goal = its summary, input = its
42
+ * payload verbatim. Multiple refs namespace each resolved payload under its
43
+ * stageId (disambiguated by runId only if two refs share a stageId), so a
44
+ * stage author addresses each source by name in their own io.input.schema
45
+ * instead of one silently overwriting another on a key collision.
46
+ */
47
+ export async function resolveEnvelopeRefsToTask(store, refs, checkout) {
48
+ const list = Array.isArray(refs) ? refs : [refs];
49
+ const resolved = [];
50
+ for (const ref of list) {
51
+ resolved.push({ ref, envelope: await resolveOne(store, ref) });
52
+ }
53
+ if (resolved.length === 1) {
54
+ const { ref, envelope } = resolved[0];
55
+ return {
56
+ id: `ref-${ref.runId}-${ref.stageId}`,
57
+ goal: envelope.summary,
58
+ input: envelope.payload ?? {},
59
+ ...(checkout ? { checkout } : {}),
60
+ };
61
+ }
62
+ const usedKeys = new Set();
63
+ const input = {};
64
+ const summaryLines = [];
65
+ for (const { ref, envelope } of resolved) {
66
+ let key = ref.stageId;
67
+ if (usedKeys.has(key))
68
+ key = `${ref.stageId}:${ref.runId}`;
69
+ usedKeys.add(key);
70
+ input[key] = envelope.payload ?? {};
71
+ summaryLines.push(`${key}: ${envelope.summary}`);
72
+ }
73
+ return {
74
+ id: `ref-${list.map((r) => r.stageId).join("+")}`,
75
+ goal: summaryLines.join("\n"),
76
+ input,
77
+ ...(checkout ? { checkout } : {}),
78
+ };
79
+ }
@@ -33,12 +33,12 @@ export async function loadRunContext(store, runId, cwd) {
33
33
  }
34
34
  export async function reconstructAndContinue(ctx) {
35
35
  const { runId, stageId, opaqueAnswer, agent, store, cwd } = ctx;
36
- const factoryCwd = ctx.factoryCwd ?? cwd;
37
36
  try {
38
37
  const latestExecution = await store.getLatestStageExecution(runId, stageId);
39
38
  const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
40
39
  const attemptOpt = attemptCtx.eventOptions();
41
40
  const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
41
+ const factoryCwd = meta.project_root ?? ctx.factoryCwd ?? cwd;
42
42
  const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
43
43
  const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
44
44
  if (stageIndex < 0) {
@@ -127,7 +127,7 @@ export async function reconstructAndContinue(ctx) {
127
127
  run: { runId, workspaceDir },
128
128
  agent,
129
129
  store,
130
- cwd,
130
+ cwd: factoryCwd,
131
131
  projectRoot: factoryCwd,
132
132
  checkoutRoot,
133
133
  hitl: ctx.hitl,
@@ -1,4 +1,6 @@
1
+ import { type RunSubmission } from "../runstore/submission.js";
1
2
  import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
3
+ import type { InlinePipelineDefinition } from "../types/pipeline.js";
2
4
  import { type RunStore } from "../runstore/port.js";
3
5
  import type { TaskFile } from "../types/task.js";
4
6
  import { type PipelineRunResult } from "./pipelineRunner.js";
@@ -34,6 +36,23 @@ export type CapacityHealth = {
34
36
  activeStageProcesses: number;
35
37
  maxActiveStageProcesses: number | null;
36
38
  };
39
+ export type StartRunOnceResult = {
40
+ ok: true;
41
+ runId: string;
42
+ /** False only for the caller whose submission durably created this run for the first time. */
43
+ reused: boolean;
44
+ /**
45
+ * Present when this call observed the run being launched in this process — either it launched
46
+ * the run itself, or it piggybacked on another in-flight call to the same submission key while
47
+ * that launch was still starting. Absent when the submission was already durably committed by
48
+ * an earlier, separate call: `reused: true` does not by itself imply `done` is absent, since a
49
+ * piggybacked call is also `reused: true`. Callers that need completion regardless of which case
50
+ * they hit must still fall back to polling run status by `runId`.
51
+ */
52
+ done?: Promise<PipelineRunResult>;
53
+ } | Extract<StartRunResult, {
54
+ ok: false;
55
+ }>;
37
56
  export type { DeliverAnswerResult };
38
57
  export type { RetryStageResult };
39
58
  export type AbandonStageResult = {
@@ -65,6 +84,7 @@ export type StopManualRecoveryResult = {
65
84
  };
66
85
  export declare class RunManager {
67
86
  private readonly options;
87
+ private readonly submissionsInFlight;
68
88
  private readonly active;
69
89
  private readonly checkoutLeases;
70
90
  private readonly provisionalIds;
@@ -124,15 +144,17 @@ export declare class RunManager {
124
144
  runId: string;
125
145
  }>>;
126
146
  abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
147
+ startRunOnce(input: Parameters<RunManager["startRun"]>[0], submission: RunSubmission): Promise<StartRunOnceResult>;
148
+ private startSubmittedRun;
127
149
  startRun(input: StartTaskInput & {
128
- pipeline: string;
150
+ pipeline: string | InlinePipelineDefinition;
129
151
  task?: string | TaskFile;
130
152
  checkoutOverride?: string;
131
153
  skipGates?: boolean;
132
154
  gitSha?: string;
133
155
  ciPrUrl?: string;
134
156
  ciJobUrl?: string;
135
- }): Promise<StartRunResult>;
157
+ }, submission?: RunSubmission): Promise<StartRunResult>;
136
158
  rerun(runId: string): Promise<StartRunResult>;
137
159
  retryStage(runId: string, stageId: string): Promise<RetryStageResult>;
138
160
  resumeTimedOutStage(runId: string, stageId: string): Promise<RetryStageResult>;