stageflow 0.18.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +2 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/activityObserver.d.ts +3 -0
  4. package/dist/agent/activityObserver.js +6 -1
  5. package/dist/agent/claudeAdapter.js +38 -0
  6. package/dist/agent/fakeAgent.js +3 -1
  7. package/dist/agent/piAdapter.js +40 -4
  8. package/dist/agent/port.d.ts +3 -1
  9. package/dist/agent/providerInspect.d.ts +19 -0
  10. package/dist/agent/providerInspect.js +35 -0
  11. package/dist/agent/stageTimeout.d.ts +9 -0
  12. package/dist/agent/stageTimeout.js +24 -0
  13. package/dist/agent/streamLogRedact.d.ts +2 -0
  14. package/dist/agent/streamLogRedact.js +13 -0
  15. package/dist/cli/artifactCommand.js +2 -2
  16. package/dist/cli/envelopeCommand.js +2 -2
  17. package/dist/cli/exportRunCommand.js +2 -2
  18. package/dist/cli/graphCommand.d.ts +15 -0
  19. package/dist/cli/graphCommand.js +79 -0
  20. package/dist/cli/graphRender.d.ts +2 -0
  21. package/dist/cli/graphRender.js +85 -0
  22. package/dist/cli/hostClient.d.ts +43 -0
  23. package/dist/cli/hostClient.js +182 -0
  24. package/dist/cli/providersCommand.js +5 -3
  25. package/dist/cli/runCommand.d.ts +6 -5
  26. package/dist/cli/runCommand.js +23 -31
  27. package/dist/cli/runOutput.d.ts +1 -1
  28. package/dist/cli/runsCommand.d.ts +4 -8
  29. package/dist/cli/runsCommand.js +65 -89
  30. package/dist/cli.js +13 -5
  31. package/dist/config/describePipeline.d.ts +26 -0
  32. package/dist/config/describePipeline.js +37 -0
  33. package/dist/config/loadPipeline.d.ts +13 -1
  34. package/dist/config/loadPipeline.js +34 -4
  35. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  36. package/dist/config/mergePipelineIncludes.js +75 -0
  37. package/dist/config/validateCatalog.d.ts +2 -2
  38. package/dist/config/validateCatalog.js +24 -17
  39. package/dist/mcp/catalogTools.js +146 -52
  40. package/dist/mcp/controlTools.js +94 -19
  41. package/dist/mcp/deps.d.ts +3 -0
  42. package/dist/mcp/projectMcpTools.d.ts +3 -0
  43. package/dist/mcp/projectMcpTools.js +25 -0
  44. package/dist/mcp/providerTools.d.ts +3 -0
  45. package/dist/mcp/providerTools.js +17 -0
  46. package/dist/mcp/readArtifact.d.ts +9 -0
  47. package/dist/mcp/readArtifact.js +11 -0
  48. package/dist/mcp/server.js +9 -1
  49. package/dist/mcp/tailStreamLog.d.ts +8 -0
  50. package/dist/mcp/tailStreamLog.js +28 -0
  51. package/dist/mcp/toolResults.d.ts +17 -0
  52. package/dist/mcp/toolResults.js +15 -0
  53. package/dist/mcp/tools.js +4 -0
  54. package/dist/mcp/waitRun.js +4 -4
  55. package/dist/mcp/waitingGates.d.ts +1 -0
  56. package/dist/mcp/waitingGates.js +3 -2
  57. package/dist/package-meta.d.ts +1 -1
  58. package/dist/package-meta.js +1 -1
  59. package/dist/projection/projectRun.d.ts +3 -0
  60. package/dist/projection/projectRun.js +7 -0
  61. package/dist/runstore/port.d.ts +3 -1
  62. package/dist/runstore/port.js +2 -0
  63. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
  64. package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
  65. package/dist/runstore/workspaceLayout.d.ts +1 -0
  66. package/dist/runstore/workspaceLayout.js +3 -0
  67. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  68. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  69. package/dist/runtime/feedbackLoopDecision.js +33 -1
  70. package/dist/runtime/forkGeneration.d.ts +4 -0
  71. package/dist/runtime/forkGeneration.js +16 -0
  72. package/dist/runtime/pipelineRunner.d.ts +3 -3
  73. package/dist/runtime/pipelineRunner.js +3 -1
  74. package/dist/runtime/pipelineScheduler.js +23 -1
  75. package/dist/runtime/reopenSkipped.d.ts +13 -0
  76. package/dist/runtime/reopenSkipped.js +56 -0
  77. package/dist/runtime/resumeReconstruct.js +2 -2
  78. package/dist/runtime/resumeTimedOut.d.ts +33 -0
  79. package/dist/runtime/resumeTimedOut.js +165 -0
  80. package/dist/runtime/runManager.d.ts +3 -1
  81. package/dist/runtime/runManager.js +110 -14
  82. package/dist/runtime/settingsFile.d.ts +2 -0
  83. package/dist/runtime/settingsFile.js +19 -0
  84. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  85. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  86. package/dist/runtime/stageRecovery.js +2 -2
  87. package/dist/runtime/stageRunner.js +6 -2
  88. package/dist/runtime/stageStreamLog.d.ts +17 -0
  89. package/dist/runtime/stageStreamLog.js +89 -0
  90. package/dist/runtime/stageWorker.js +2 -1
  91. package/dist/server/bootstrap.js +9 -2
  92. package/dist/server/ensureGlobalService.d.ts +35 -0
  93. package/dist/server/ensureGlobalService.js +138 -0
  94. package/dist/server/http.d.ts +20 -1
  95. package/dist/server/http.js +452 -386
  96. package/dist/server/mcpHost.js +7 -8
  97. package/dist/server/operatorResults.d.ts +10 -0
  98. package/dist/server/operatorResults.js +24 -0
  99. package/dist/server/providerRoutes.d.ts +0 -6
  100. package/dist/server/providerRoutes.js +10 -18
  101. package/dist/tools/askOperator.d.ts +1 -0
  102. package/dist/tools/askOperator.js +14 -11
  103. package/dist/tools/emitStageEnvelope.d.ts +1 -0
  104. package/dist/tools/emitStageEnvelope.js +3 -1
  105. package/dist/tools/prepareToolArguments.d.ts +4 -0
  106. package/dist/tools/prepareToolArguments.js +79 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
  109. package/dist/ui/assets/index-Da5EHU6M.js +119 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +1 -1
  112. package/skills/stageflow/references/control-surface.md +2 -2
  113. package/skills/stageflow-run/SKILL.md +27 -9
  114. package/skills/stageflow-run/references/mcp-call.md +1 -1
  115. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
  116. package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseStreamLogHeader } from "../runtime/stageStreamLog.js";
3
+ /** Read new content from a stage's stream.log since a caller's last cursor. */
4
+ export async function readStreamLogTail(streamLogPath, sinceOffset) {
5
+ let raw;
6
+ try {
7
+ raw = await readFile(streamLogPath);
8
+ }
9
+ catch {
10
+ return { text: "", nextOffset: 0 };
11
+ }
12
+ const { base, headerLength } = parseStreamLogHeader(raw);
13
+ const contentBytes = raw.subarray(headerLength);
14
+ const eof = base + contentBytes.length;
15
+ if (sinceOffset === undefined || sinceOffset < base) {
16
+ const truncated = sinceOffset !== undefined && sinceOffset < base;
17
+ return {
18
+ text: contentBytes.toString("utf8"),
19
+ nextOffset: eof,
20
+ ...(truncated ? { truncated: true, earliestOffset: base } : {}),
21
+ };
22
+ }
23
+ if (sinceOffset >= eof) {
24
+ return { text: "", nextOffset: eof };
25
+ }
26
+ const slice = contentBytes.subarray(sinceOffset - base);
27
+ return { text: slice.toString("utf8"), nextOffset: eof };
28
+ }
@@ -5,3 +5,20 @@ export declare function textResult(data: unknown, isError?: boolean): {
5
5
  }[];
6
6
  isError?: boolean | undefined;
7
7
  };
8
+ export declare function imageResult(mimeType: string, bytes: Buffer, identity: {
9
+ runId: string;
10
+ path: string;
11
+ mimeType: string;
12
+ }): {
13
+ content: ({
14
+ type: "image";
15
+ mimeType: string;
16
+ data: string;
17
+ text?: undefined;
18
+ } | {
19
+ mimeType?: undefined;
20
+ data?: undefined;
21
+ type: "text";
22
+ text: string;
23
+ })[];
24
+ };
@@ -4,3 +4,18 @@ export function textResult(data, isError = false) {
4
4
  ...(isError ? { isError: true } : {}),
5
5
  };
6
6
  }
7
+ export function imageResult(mimeType, bytes, identity) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "image",
12
+ mimeType,
13
+ data: bytes.toString("base64"),
14
+ },
15
+ {
16
+ type: "text",
17
+ text: JSON.stringify(identity, null, 2),
18
+ },
19
+ ],
20
+ };
21
+ }
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.18.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.18.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>;
@@ -12,6 +12,8 @@ export function stageStatusFromEvents(events) {
12
12
  status = "failed";
13
13
  if (ev.event === "skipped")
14
14
  status = "skipped";
15
+ if (ev.event === "reopened")
16
+ status = "pending";
15
17
  }
16
18
  return status;
17
19
  }
@@ -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 });
@@ -63,6 +63,10 @@ export declare function selectActiveInput(options: {
63
63
  needsStageId: string;
64
64
  scheduleOverride?: ReadonlyMap<string, ReadonlySet<string>>;
65
65
  }): Promise<ActiveCohort>;
66
+ export declare function forkCohortMapsFromStore(store: Pick<RunStore, "listForkGenerations">, runId: string): Promise<{
67
+ supersededCloneIds: Set<string>;
68
+ cohortByForkParent: Map<string, Set<string>>;
69
+ }>;
66
70
  export declare function filterJoinInputs(allInstanceIds: readonly string[], cohort: ActiveCohort): string[];
67
71
  export declare function filterJoinInstanceIds(allInstanceIds: readonly string[], activeCloneIds: Set<string> | null): string[];
68
72
  /**
@@ -251,6 +251,22 @@ export async function selectActiveInput(options) {
251
251
  }
252
252
  return activeCohortForParent(options.store, options.runId, forkParentId);
253
253
  }
254
+ export async function forkCohortMapsFromStore(store, runId) {
255
+ const gens = await store.listForkGenerations(runId);
256
+ const supersededCloneIds = new Set();
257
+ const cohortByForkParent = new Map();
258
+ for (const gen of gens) {
259
+ if (gen.status === "superseded") {
260
+ for (const id of gen.clone_stage_ids) {
261
+ supersededCloneIds.add(id);
262
+ }
263
+ }
264
+ else if (gen.status === "active") {
265
+ cohortByForkParent.set(gen.fork_parent_stage_id, new Set(gen.clone_stage_ids));
266
+ }
267
+ }
268
+ return { supersededCloneIds, cohortByForkParent };
269
+ }
254
270
  export function filterJoinInputs(allInstanceIds, cohort) {
255
271
  if (cohort.kind === "untracked")
256
272
  return [...allInstanceIds];
@@ -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;
@@ -4,7 +4,8 @@ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
4
4
  import { cloneFailureContinuesSchedule, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, failureIsAccepted, isCloneInstance, mint, protectedClonableChildIds, sequentialLaterCloneIds, skipRejectedNeedDependents, } from "./cloneSchedule.js";
5
5
  import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
6
6
  import { classifyInboundAfterSuccess, isEagerSingleParentIfSkip, joinAllowsRun, pickStalledJoinSkips, } from "./joinReadiness.js";
7
- import { forkChoicePreservesReplaySource, mintCohortForFanout, } from "./forkGeneration.js";
7
+ import { reopenRunnableSkippedStages, persistReopenedStages } from "./reopenSkipped.js";
8
+ import { forkChoicePreservesReplaySource, forkCohortMapsFromStore, mintCohortForFanout, } from "./forkGeneration.js";
8
9
  import { activeReplayId, activeSourceStageId, applySendBack, createReplaySchedule, hydrateFromStore, isHeld, launchAttemptFor, launchFor, noteActiveCohort, onContinued, onRouteStageFailed, onRouteStageRunning, onRouteStageSucceeded, rebindRouteAttempts, resolveDecision, } from "./replayLifecycle.js";
9
10
  import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
10
11
  import { runStage, isRunStageWaiting } from "./stageRunner.js";
@@ -65,6 +66,7 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
65
66
  continue;
66
67
  schedulingHalted = true;
67
68
  }
69
+ await reopenRunnableSkippedStages(store, runId, resolvedDag, states, completedEnvelopes);
68
70
  return {
69
71
  states,
70
72
  completedEnvelopes,
@@ -85,15 +87,20 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
85
87
  }
86
88
  }
87
89
  const retryRootSet = new Set(retryStageIds);
90
+ const { supersededCloneIds } = await forkCohortMapsFromStore(store, runId);
88
91
  const states = new Map();
89
92
  for (const node of resolvedDag.nodes) {
90
93
  states.set(node.id, "pending");
91
94
  }
92
95
  const overrides = new Map();
93
96
  for (const rootId of retryStageIds) {
97
+ if (supersededCloneIds.has(rootId))
98
+ continue;
94
99
  overrides.set(rootId, "pending");
95
100
  }
96
101
  for (const id of downstream) {
102
+ if (supersededCloneIds.has(id))
103
+ continue;
97
104
  overrides.set(id, "pending");
98
105
  }
99
106
  for (const snap of runDetail.stages) {
@@ -104,6 +111,20 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
104
111
  completedEnvelopes.delete(id);
105
112
  }
106
113
  }
114
+ const skippedToReopen = [];
115
+ const skipStates = new Map();
116
+ for (const snap of runDetail.stages) {
117
+ const id = snap.stage_id;
118
+ if (supersededCloneIds.has(id))
119
+ continue;
120
+ if (snap.status !== "skipped")
121
+ continue;
122
+ if (!retryRootSet.has(id) && !downstream.has(id))
123
+ continue;
124
+ skippedToReopen.push(id);
125
+ skipStates.set(id, "skipped");
126
+ }
127
+ await persistReopenedStages(store, runId, skippedToReopen, skipStates);
107
128
  return {
108
129
  states,
109
130
  completedEnvelopes,
@@ -752,6 +773,7 @@ export async function runPipelineDag(options) {
752
773
  await persistSkipPending(childId);
753
774
  }
754
775
  }
776
+ await reopenRunnableSkippedStages(store, run.runId, dag, states, completedEnvelopes);
755
777
  };
756
778
  const handlePostSuccessError = async (stageId, reason) => {
757
779
  if (states.get(stageId) === "succeeded") {
@@ -0,0 +1,13 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { StageEnvelope } from "../types/envelope.js";
3
+ import type { ResolvedPipelineDag } from "../types/pipeline.js";
4
+ import type { StageScheduleState } from "./pipelineScheduler.js";
5
+ export declare function isUnchosenForkChild(dag: ResolvedPipelineDag, stageId: string, envelopes: Map<string, StageEnvelope>): boolean;
6
+ export declare function eventsShowStarted(events: ReadonlyArray<{
7
+ event: string;
8
+ }>): boolean;
9
+ export declare function shouldReopenSkippedStage(dag: ResolvedPipelineDag, stageId: string, states: Map<string, StageScheduleState>, envelopes: Map<string, StageEnvelope>, hasStarted: boolean, options?: {
10
+ supersededCloneIds?: ReadonlySet<string>;
11
+ }): boolean;
12
+ export declare function persistReopenedStages(store: Pick<RunStore, "appendStageEvent">, runId: string, stageIds: Iterable<string>, states: Map<string, StageScheduleState>): Promise<void>;
13
+ export declare function reopenRunnableSkippedStages(store: Pick<RunStore, "appendStageEvent" | "listStageEvents" | "listForkGenerations">, runId: string, dag: ResolvedPipelineDag, states: Map<string, StageScheduleState>, envelopes: Map<string, StageEnvelope>): Promise<void>;