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
package/dist/mcp/tools.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { registerCatalogTools } from "./catalogTools.js";
3
3
  import { registerControlTools } from "./controlTools.js";
4
+ import { registerProjectMcpTools } from "./projectMcpTools.js";
5
+ import { registerProviderTools } from "./providerTools.js";
4
6
  import { textResult } from "./toolResults.js";
5
7
  import { DEFAULT_TIMEOUT_MS, waitRun } from "./waitRun.js";
6
8
  export function registerMcpTools(server, deps) {
7
9
  const { store } = deps;
8
10
  registerCatalogTools(server, deps);
9
11
  registerControlTools(server, deps);
12
+ registerProviderTools(server, deps);
13
+ registerProjectMcpTools(server, deps);
10
14
  server.registerTool("wait_run", {
11
15
  description: "Long-poll until a run is waiting for HITL input and/or terminal (succeeded/failed), or until timeout_ms. Default until=any wakes on waiting OR terminal. Abort cancels only this wait (not the run). Optional notifications/progress when the client supplies _meta.progressToken. Compose: wait_run → answer_gate → wait_run.",
12
16
  inputSchema: z.object({
@@ -1,3 +1,4 @@
1
+ import { mapStoreLookupError } from "../server/operatorResults.js";
1
2
  import { projectRunForMcp } from "./projectRun.js";
2
3
  export const DEFAULT_TIMEOUT_MS = 60_000;
3
4
  export const MAX_TIMEOUT_MS = 240_000;
@@ -90,12 +91,11 @@ export async function waitRun(opts) {
90
91
  detail = await opts.store.readRun(opts.runId);
91
92
  }
92
93
  catch (err) {
93
- const message = err instanceof Error ? err.message : String(err);
94
- const notFound = /not found|no such|unknown run/i.test(message);
94
+ const mapped = mapStoreLookupError(err, { policy: "run" });
95
95
  return {
96
96
  ok: false,
97
- error: message,
98
- status: notFound ? 404 : 500,
97
+ error: mapped.error,
98
+ status: mapped.status,
99
99
  };
100
100
  }
101
101
  const run = projectRunForMcp(detail);
@@ -9,5 +9,6 @@ export declare function projectWaitingGate(opts: {
9
9
  }): Record<string, unknown>;
10
10
  export declare function projectWaitingGates(store: RunStore, opts?: {
11
11
  runId?: string;
12
+ projectRoot?: string;
12
13
  }): Promise<Array<Record<string, unknown>>>;
13
14
  export {};
@@ -83,8 +83,9 @@ export async function projectWaitingGates(store, opts) {
83
83
  return projectGatesForRun(detail, detail);
84
84
  }
85
85
  const summaries = await store.listRuns();
86
- const waiting = summaries.filter((r) => (r.waiting_stage_ids !== undefined && r.waiting_stage_ids.length > 0) ||
87
- r.waiting_stage_id !== undefined);
86
+ const waiting = summaries.filter((r) => (opts?.projectRoot === undefined || r.project_root === opts.projectRoot) &&
87
+ ((r.waiting_stage_ids !== undefined && r.waiting_stage_ids.length > 0) ||
88
+ r.waiting_stage_id !== undefined));
88
89
  const perRun = await Promise.all(waiting.map(async (summary) => {
89
90
  let detail;
90
91
  try {
@@ -1,2 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
- export declare const PACKAGE_VERSION = "0.19.0";
2
+ export declare const PACKAGE_VERSION = "0.23.0";
@@ -1,2 +1,2 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
- export const PACKAGE_VERSION = "0.19.0";
2
+ export const PACKAGE_VERSION = "0.23.0";
@@ -16,6 +16,8 @@ export type StageProjection = {
16
16
  artifacts: string[];
17
17
  last_at?: string;
18
18
  pending_prompt?: AskOperatorPrompt;
19
+ definition_id?: string;
20
+ cost_usd?: number;
19
21
  };
20
22
  export type RunProjection = {
21
23
  run_id: string;
@@ -40,6 +42,7 @@ export type RunProjection = {
40
42
  failed_reason?: RunSummary["failed_reason"];
41
43
  active_feedback_loop?: FeedbackLoopRecord;
42
44
  feedback_loops: FeedbackLoopHistory[];
45
+ total_cost_usd?: number;
43
46
  };
44
47
  export declare function projectRun(detail: RunDetail): RunProjection;
45
48
  export { projectRun as projectRunForMcp };
@@ -44,6 +44,9 @@ export function projectRun(detail) {
44
44
  ...(detail.active_feedback_loop !== undefined
45
45
  ? { active_feedback_loop: detail.active_feedback_loop }
46
46
  : {}),
47
+ ...(detail.total_cost_usd !== undefined
48
+ ? { total_cost_usd: detail.total_cost_usd }
49
+ : {}),
47
50
  feedback_loops: detail.feedback_loops ?? [],
48
51
  stages: detail.stages.map((stage) => ({
49
52
  stage_id: stage.stage_id,
@@ -69,6 +72,10 @@ export function projectRun(detail) {
69
72
  ...(stage.pending_prompt
70
73
  ? { pending_prompt: stage.pending_prompt }
71
74
  : {}),
75
+ ...(stage.definition_id !== undefined
76
+ ? { definition_id: stage.definition_id }
77
+ : {}),
78
+ ...(stage.cost_usd !== undefined ? { cost_usd: stage.cost_usd } : {}),
72
79
  })),
73
80
  };
74
81
  }
@@ -327,12 +327,14 @@ export interface RunStore {
327
327
  writeEnvelope(runId: string, stageId: string, envelope: StageEnvelope, options?: {
328
328
  attempt?: number;
329
329
  }): Promise<void>;
330
- readEnvelope(runId: string, stageId: string): Promise<StageEnvelope>;
330
+ readEnvelope(runId: string, stageId: string, attempt?: number): Promise<StageEnvelope>;
331
331
  appendStageEvent(runId: string, stageId: string, event: StageLogLine, options?: {
332
332
  attempt?: number;
333
333
  }): Promise<void>;
334
334
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
335
335
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
336
+ /** Every distinct non-empty project_root recorded across all runs. */
337
+ listProjectRoots(): Promise<string[]>;
336
338
  readRun(runId: string): Promise<RunDetail>;
337
339
  updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
338
340
  createFeedbackLoop(runId: string, input: CreateFeedbackLoopInput): Promise<FeedbackLoopRecord>;
@@ -29,7 +29,7 @@ export declare class SqliteRunStore implements RunStore {
29
29
  writeEnvelope(runId: string, stageId: string, envelope: StageEnvelope, options?: {
30
30
  attempt?: number;
31
31
  }): Promise<void>;
32
- readEnvelope(runId: string, stageId: string): Promise<StageEnvelope>;
32
+ readEnvelope(runId: string, stageId: string, attempt?: number): Promise<StageEnvelope>;
33
33
  private readEnvelopeFromDb;
34
34
  appendStageEvent(runId: string, stageId: string, event: StageLogLine, options?: {
35
35
  attempt?: number;
@@ -43,6 +43,7 @@ export declare class SqliteRunStore implements RunStore {
43
43
  private updateStageStatusDenorm;
44
44
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
45
45
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
46
+ listProjectRoots(): Promise<string[]>;
46
47
  readRun(runId: string): Promise<RunDetail>;
47
48
  createFeedbackLoop(runId: string, input: CreateFeedbackLoopInput): Promise<FeedbackLoopRecord>;
48
49
  getFeedbackLoop(runId: string, loopId: string): Promise<FeedbackLoopRecord>;
@@ -606,8 +606,15 @@ export class SqliteRunStore {
606
606
  });
607
607
  await this.updateStageExecution(runId, stageId, attempt, { envelope });
608
608
  }
609
- async readEnvelope(runId, stageId) {
609
+ async readEnvelope(runId, stageId, attempt) {
610
610
  await this.ready();
611
+ if (attempt !== undefined) {
612
+ const execution = await this.getStageExecution(runId, stageId, attempt);
613
+ if (execution.envelope != null) {
614
+ return execution.envelope;
615
+ }
616
+ throw new Error(`Envelope not found: ${runId}/${stageId}`);
617
+ }
611
618
  const execution = await this.getLatestStageExecution(runId, stageId);
612
619
  if (execution?.envelope != null) {
613
620
  return execution.envelope;
@@ -717,6 +724,13 @@ export class SqliteRunStore {
717
724
  }
718
725
  return summaries;
719
726
  }
727
+ async listProjectRoots() {
728
+ await this.ready();
729
+ const rows = this.db
730
+ .prepare(`SELECT DISTINCT project_root FROM runs WHERE project_root IS NOT NULL AND project_root != ''`)
731
+ .all();
732
+ return rows.map((row) => row.project_root);
733
+ }
720
734
  async readRun(runId) {
721
735
  await this.ready();
722
736
  const row = this.getRunRow(runId);
@@ -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 });
@@ -3,7 +3,7 @@ import { type ValidationFinding } from "../config/validateCatalog.js";
3
3
  import type { RunStore } from "../runstore/port.js";
4
4
  import type { StageEnvelope } from "../types/envelope.js";
5
5
  import type { StageHitlController } from "./stageHitl.js";
6
- import type { LoadedPipeline } from "../types/pipeline.js";
6
+ import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
7
7
  import type { TaskFile } from "../types/task.js";
8
8
  import { type StageExecutionMode } from "./stageConcurrency.js";
9
9
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
@@ -57,7 +57,7 @@ export declare function runPipeline(options: {
57
57
  store: RunStore;
58
58
  taskPath?: string;
59
59
  taskYaml?: string;
60
- pipeline: string;
60
+ pipeline: string | InlinePipelineDefinition;
61
61
  cwd?: string;
62
62
  projectRoot?: string;
63
63
  checkoutOverride?: string;
@@ -74,7 +74,7 @@ export declare function startPipeline(options: {
74
74
  store: RunStore;
75
75
  taskPath?: string;
76
76
  taskYaml?: string;
77
- pipeline: string;
77
+ pipeline: string | InlinePipelineDefinition;
78
78
  cwd?: string;
79
79
  projectRoot?: string;
80
80
  checkoutOverride?: string;
@@ -57,7 +57,9 @@ 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;
@@ -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,5 @@
1
1
  import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
2
+ import type { InlinePipelineDefinition } from "../types/pipeline.js";
2
3
  import { type RunStore } from "../runstore/port.js";
3
4
  import type { TaskFile } from "../types/task.js";
4
5
  import { type PipelineRunResult } from "./pipelineRunner.js";
@@ -125,7 +126,7 @@ export declare class RunManager {
125
126
  }>>;
126
127
  abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
127
128
  startRun(input: StartTaskInput & {
128
- pipeline: string;
129
+ pipeline: string | InlinePipelineDefinition;
129
130
  task?: string | TaskFile;
130
131
  checkoutOverride?: string;
131
132
  skipGates?: boolean;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { findProjectRoot } from "../project/findProjectRoot.js";
4
5
  import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
6
  import { loadRunContext } from "./resumeReconstruct.js";
6
7
  import { assertTimedOutStageEligible, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
@@ -12,7 +13,7 @@ import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
12
13
  import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
13
14
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
14
15
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
15
- import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, projectSettingsContext, readMaxConcurrentFromContext, writeMaxConcurrentToContext, } from "./settingsFile.js";
16
+ import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
16
17
  import { StageHitlController, waitKey, } from "./stageHitl.js";
17
18
  import { resolveAndValidateCheckout, } from "./stageRoots.js";
18
19
  import { orchestrateAnswerResume } from "./answerResume.js";
@@ -94,10 +95,9 @@ export class RunManager {
94
95
  this.cwd = options.cwd ?? process.cwd();
95
96
  this.projectRoot = options.projectRoot ?? this.cwd;
96
97
  this.isGitProject = options.isGitProject ?? false;
97
- const settingsCtx = projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject);
98
98
  this.maxConcurrent =
99
99
  options.maxConcurrent ??
100
- readMaxConcurrentFromContext(settingsCtx) ??
100
+ readMaxConcurrentFromGlobal() ??
101
101
  parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
102
102
  this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
103
103
  this.executionMode = readStageExecutionMode(process.env, options.executionMode);
@@ -119,7 +119,7 @@ export class RunManager {
119
119
  throw new Error(INVALID_SLOT_COUNT_MESSAGE);
120
120
  }
121
121
  this.maxConcurrent = parsed;
122
- writeMaxConcurrentToContext(projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject), parsed);
122
+ writeMaxConcurrentToGlobal(parsed);
123
123
  return this.getHealth();
124
124
  }
125
125
  getActiveCount() {
@@ -350,6 +350,11 @@ export class RunManager {
350
350
  }
351
351
  async startRun(input) {
352
352
  const cwd = this.options.cwd ?? process.cwd();
353
+ // An inline pipeline has no filesystem anchor to derive a project root from.
354
+ const derivedProjectRoot = typeof input.pipeline === "string"
355
+ ? (findProjectRoot(path.dirname(path.resolve(cwd, input.pipeline))) ??
356
+ this.projectRoot)
357
+ : this.projectRoot;
353
358
  let resolved;
354
359
  try {
355
360
  resolved = resolveStartTaskInput(input, cwd);
@@ -384,7 +389,7 @@ export class RunManager {
384
389
  gitSha: input.gitSha,
385
390
  ciPrUrl: input.ciPrUrl,
386
391
  ciJobUrl: input.ciJobUrl,
387
- }, undefined, resolved.kind === "path" ? resolved.taskPath : undefined);
392
+ }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
388
393
  }
389
394
  async rerun(runId) {
390
395
  const cwd = this.options.cwd ?? process.cwd();
@@ -456,6 +461,7 @@ export class RunManager {
456
461
  }
457
462
  const insertedForResume = !wasActive;
458
463
  try {
464
+ const runProjectRoot = detail.project_root ?? this.projectRoot;
459
465
  const done = reconstructTimedOutAndContinue({
460
466
  runId,
461
467
  stageId,
@@ -463,9 +469,9 @@ export class RunManager {
463
469
  store: this.options.store,
464
470
  hitl: this.hitl,
465
471
  executionMode: this.executionMode,
466
- cwd: this.cwd,
472
+ cwd: runProjectRoot,
467
473
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
468
- factoryCwd: this.projectRoot,
474
+ factoryCwd: runProjectRoot,
469
475
  ...(this.stageProcessLauncher !== undefined
470
476
  ? { stageProcessLauncher: this.stageProcessLauncher }
471
477
  : {}),
@@ -585,6 +591,7 @@ export class RunManager {
585
591
  };
586
592
  }
587
593
  try {
594
+ let meta;
588
595
  let orchestrationConflict = this.active.has(runId) &&
589
596
  !this.attachedWaiting.has(waitKey(runId, stageId));
590
597
  if (!this.retryCoordinator.isActive(runId)) {
@@ -607,7 +614,7 @@ export class RunManager {
607
614
  let detail;
608
615
  try {
609
616
  detail = await this.options.store.readRun(runId);
610
- await this.options.store.readRunMeta(runId);
617
+ meta = await this.options.store.readRunMeta(runId);
611
618
  }
612
619
  catch {
613
620
  this.retryInFlight.delete(retryKey);
@@ -626,12 +633,21 @@ export class RunManager {
626
633
  }
627
634
  }
628
635
  }
636
+ if (meta === undefined) {
637
+ try {
638
+ meta = await this.options.store.readRunMeta(runId);
639
+ }
640
+ catch {
641
+ this.retryInFlight.delete(retryKey);
642
+ return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
643
+ }
644
+ }
629
645
  const result = await this.retryCoordinator.retryStage({
630
646
  runId,
631
647
  stageId,
632
648
  store: this.options.store,
633
649
  agent: this.options.agent,
634
- cwd: this.cwd,
650
+ cwd: meta.project_root ?? this.projectRoot,
635
651
  operatorCatalog: this.options.operatorCatalog,
636
652
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
637
653
  executionMode: this.executionMode,
@@ -980,6 +996,7 @@ export class RunManager {
980
996
  };
981
997
  }
982
998
  const { meta, task, loaded } = await loadRunContext(this.options.store, runId, this.cwd);
999
+ const runProjectRoot = meta.project_root ?? this.projectRoot;
983
1000
  const done = runPipelineDag({
984
1001
  prepared: {
985
1002
  task,
@@ -990,8 +1007,8 @@ export class RunManager {
990
1007
  },
991
1008
  agent: this.options.agent,
992
1009
  store: this.options.store,
993
- cwd: this.cwd,
994
- projectRoot: this.projectRoot,
1010
+ cwd: runProjectRoot,
1011
+ projectRoot: runProjectRoot,
995
1012
  checkoutRoot: meta.checkout_root,
996
1013
  hitl: this.hitl,
997
1014
  operatorCatalog: this.options.operatorCatalog,
@@ -1041,10 +1058,12 @@ export class RunManager {
1041
1058
  const sessionFilePath = resumeSessionFilePath(workspaceDir, stageId, attempt);
1042
1059
  const eventOptions = { attempt };
1043
1060
  try {
1061
+ const runMeta = await store.readRunMeta(runId);
1062
+ const runProjectRoot = runMeta.project_root ?? this.projectRoot;
1044
1063
  const launchResult = await launcher.launch({
1045
1064
  runId,
1046
1065
  stageId,
1047
- rootDir: this.projectRoot,
1066
+ rootDir: runProjectRoot,
1048
1067
  mode: "resume",
1049
1068
  resumeAnswer: opaqueAnswer,
1050
1069
  attempt,
@@ -1079,8 +1098,8 @@ export class RunManager {
1079
1098
  run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
1080
1099
  agent: this.options.agent,
1081
1100
  store,
1082
- cwd: this.cwd,
1083
- projectRoot: this.projectRoot,
1101
+ cwd: meta.project_root ?? this.projectRoot,
1102
+ projectRoot: meta.project_root ?? this.projectRoot,
1084
1103
  checkoutRoot: meta.checkout_root,
1085
1104
  hitl: this.hitl,
1086
1105
  operatorCatalog: this.options.operatorCatalog,
@@ -1277,6 +1296,7 @@ export class RunManager {
1277
1296
  return false;
1278
1297
  }
1279
1298
  try {
1299
+ const runProjectRoot = meta.project_root ?? this.projectRoot;
1280
1300
  const { meta: loadedMeta, task, loaded, workspaceDir } = await loadRunContext(this.options.store, runId, this.cwd);
1281
1301
  const promise = runPipelineDag({
1282
1302
  prepared: {
@@ -1285,8 +1305,8 @@ export class RunManager {
1285
1305
  run: { runId, workspaceDir },
1286
1306
  agent: this.options.agent,
1287
1307
  store: this.options.store,
1288
- cwd: this.cwd,
1289
- projectRoot: this.projectRoot,
1308
+ cwd: runProjectRoot,
1309
+ projectRoot: runProjectRoot,
1290
1310
  checkoutRoot: loadedMeta.checkout_root,
1291
1311
  hitl: this.hitl,
1292
1312
  operatorCatalog: this.options.operatorCatalog,
@@ -23,6 +23,8 @@ export declare function readMaxConcurrentFromContext(ctx: ProjectContext): numbe
23
23
  export declare function readMaxConcurrentFromFile(cwd: string): number | undefined;
24
24
  export declare function writeMaxConcurrentToContext(ctx: ProjectContext, maxConcurrent: number): void;
25
25
  export declare function writeMaxConcurrentToFile(cwd: string, maxConcurrent: number): void;
26
+ export declare function readMaxConcurrentFromGlobal(): number | undefined;
27
+ export declare function writeMaxConcurrentToGlobal(maxConcurrent: number): void;
26
28
  export declare function readCredentialSourceFromFile(cwd: string): CredentialSource | undefined;
27
29
  export declare function writeCredentialSourceToContext(ctx: ProjectContext, credentialSource: CredentialSource): void;
28
30
  export declare function writeCredentialSourceToFile(cwd: string, credentialSource: CredentialSource): void;