stageflow 0.2.0 → 0.8.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 (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,8 +1,25 @@
1
- import { collectDownstreamClosure, collectDownstreamStageIds, } from "./dagTraversal.js";
1
+ import { normalizeForkChoice } from "../envelope/forkChoice.js";
2
+ import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
3
+ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
4
+ import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, cloneFailureContinuesSchedule, cloneFanoutConflict, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, isCloneInstance, protectedClonableChildIds, sequentialLaterCloneIds, } from "./cloneSchedule.js";
5
+ import { collectDownstreamStageIds } from "./dagTraversal.js";
2
6
  import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
3
7
  import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
4
8
  import { runStage, isRunStageWaiting } from "./stageRunner.js";
5
9
  import { attemptContext } from "./stageAttemptContext.js";
10
+ function asDagSnapshot(dag) {
11
+ const snapshot = dag;
12
+ if (Array.isArray(snapshot.stage_ids))
13
+ return snapshot;
14
+ return {
15
+ ...dag,
16
+ stage_ids: dag.nodes.map((n) => n.id),
17
+ };
18
+ }
19
+ export { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, };
20
+ function synthesizedFailureEnvelope(reason) {
21
+ return { status: "failure", summary: reason, artifacts: [] };
22
+ }
6
23
  export function snapshotToScheduleState(snap, overrides, stageId, executionMode) {
7
24
  const override = overrides.get(stageId);
8
25
  if (override !== undefined)
@@ -16,23 +33,28 @@ export function snapshotToScheduleState(snap, overrides, stageId, executionMode)
16
33
  return "active";
17
34
  case "waiting_for_input":
18
35
  return executionMode === "process" ? "waiting" : "active";
36
+ case "skipped":
37
+ return "skipped";
19
38
  default:
20
39
  return "pending";
21
40
  }
22
41
  }
23
42
  export async function hydrateScheduleFromStore(store, runId, dag, executionMode = "process") {
43
+ const runDetail = await store.readRun(runId);
44
+ const meta = await store.readRunMeta(runId);
45
+ const resolvedDag = meta.pipeline_dag ?? asDagSnapshot(dag);
24
46
  const states = new Map();
25
- for (const node of dag.nodes) {
47
+ for (const node of resolvedDag.nodes) {
26
48
  states.set(node.id, "pending");
27
49
  }
28
- const runDetail = await store.readRun(runId);
29
- const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
50
+ const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages, resolvedDag);
30
51
  let schedulingHalted = false;
31
52
  const overrides = new Map();
32
53
  for (const snap of runDetail.stages) {
33
54
  const state = snapshotToScheduleState(snap, overrides, snap.stage_id, executionMode);
34
55
  states.set(snap.stage_id, state);
35
- if (state === "failed") {
56
+ if (state === "failed" &&
57
+ !cloneFailureContinuesSchedule(resolvedDag, snap.stage_id, completedEnvelopes)) {
36
58
  schedulingHalted = true;
37
59
  }
38
60
  }
@@ -41,15 +63,23 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
41
63
  completedEnvelopes,
42
64
  schedulingHalted,
43
65
  firstFailureReason: undefined,
66
+ dag: resolvedDag,
44
67
  };
45
68
  }
46
69
  export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStageIds) {
47
70
  const runDetail = await store.readRun(runId);
48
- const downstream = collectDownstreamClosure(dag, retryStageIds);
71
+ const meta = await store.readRunMeta(runId);
72
+ const resolvedDag = meta.pipeline_dag ?? asDagSnapshot(dag);
73
+ const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages, resolvedDag);
74
+ const downstream = cloneRetryDownstream(resolvedDag, retryStageIds);
75
+ for (const rootId of retryStageIds) {
76
+ for (const id of sequentialLaterCloneIds(resolvedDag, rootId, completedEnvelopes)) {
77
+ downstream.add(id);
78
+ }
79
+ }
49
80
  const retryRootSet = new Set(retryStageIds);
50
- const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
51
81
  const states = new Map();
52
- for (const node of dag.nodes) {
82
+ for (const node of resolvedDag.nodes) {
53
83
  states.set(node.id, "pending");
54
84
  }
55
85
  const overrides = new Map();
@@ -72,6 +102,7 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
72
102
  completedEnvelopes,
73
103
  schedulingHalted: false,
74
104
  firstFailureReason: undefined,
105
+ dag: resolvedDag,
75
106
  };
76
107
  }
77
108
  export async function hydrateScheduleForRetry(store, runId, dag, retryStageId) {
@@ -79,7 +110,8 @@ export async function hydrateScheduleForRetry(store, runId, dag, retryStageId) {
79
110
  }
80
111
  export async function resumeRun(options) {
81
112
  const { prepared, resumeFromStageId, initialPrior } = options;
82
- const dag = prepared.loaded.dag;
113
+ const meta = await prepared.store.readRunMeta(prepared.run.runId);
114
+ const dag = meta.pipeline_dag ?? buildPipelineDagSnapshotFromLoaded(prepared.loaded);
83
115
  const downstream = dag.childrenOf[resumeFromStageId];
84
116
  if (downstream?.length) {
85
117
  return runPipelineDag(options);
@@ -104,7 +136,10 @@ export async function resumeRun(options) {
104
136
  return state !== undefined && terminalState(state);
105
137
  });
106
138
  if (allTerminal) {
107
- const allSucceeded = dag.nodes.every((node) => hydrated.states.get(node.id) === "succeeded");
139
+ const allSucceeded = dag.nodes.every((node) => {
140
+ const s = hydrated.states.get(node.id);
141
+ return s === "succeeded" || s === "skipped";
142
+ });
108
143
  await prepared.store.updateRunStatus(prepared.run.runId, allSucceeded ? "succeeded" : "failed");
109
144
  return {
110
145
  ok: allSucceeded,
@@ -120,7 +155,7 @@ export async function resumeRun(options) {
120
155
  }
121
156
  export async function retryRun(options) {
122
157
  const { prepared, retryRoots } = options;
123
- const hydrated = await hydrateScheduleForRetryRoots(prepared.store, prepared.run.runId, prepared.loaded.dag, [...retryRoots.keys()]);
158
+ const hydrated = await hydrateScheduleForRetryRoots(prepared.store, prepared.run.runId, buildPipelineDagSnapshotFromLoaded(prepared.loaded), [...retryRoots.keys()]);
124
159
  return runPipelineDag({
125
160
  prepared: options.prepared,
126
161
  maxActiveStagesPerRun: options.maxActiveStagesPerRun,
@@ -133,10 +168,42 @@ export async function retryRun(options) {
133
168
  onRetryRootTerminal: options.onRetryRootTerminal,
134
169
  });
135
170
  }
171
+ function applyForkChoiceToSchedule(dag, forkStageId, chosen, states, options) {
172
+ const skipPending = (stageId) => {
173
+ if (states.get(stageId) === "pending") {
174
+ states.set(stageId, "skipped");
175
+ options?.onSkip?.(stageId);
176
+ }
177
+ };
178
+ for (const childId of dag.childrenOf[forkStageId] ?? []) {
179
+ if (options?.protectedIds?.has(childId))
180
+ continue;
181
+ if (!chosen.has(childId)) {
182
+ skipPending(childId);
183
+ for (const desc of collectDownstreamStageIds(dag, childId)) {
184
+ skipPending(desc);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
190
+ for (const node of dag.nodes) {
191
+ if (!node.fork)
192
+ continue;
193
+ const envelope = completedEnvelopes.get(node.id);
194
+ if (!envelope)
195
+ continue;
196
+ const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
197
+ applyForkChoiceToSchedule(dag, node.id, chosen, states);
198
+ }
199
+ }
136
200
  export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt) {
137
201
  retryRoots.set(stageId, attempt);
138
- const downstream = collectDownstreamClosure(dag, [stageId]);
202
+ const downstream = cloneRetryDownstream(dag, [stageId]);
139
203
  const resetIds = new Set([stageId, ...downstream]);
204
+ for (const id of sequentialLaterCloneIds(dag, stageId, completedEnvelopes)) {
205
+ resetIds.add(id);
206
+ }
140
207
  for (const id of resetIds) {
141
208
  const state = states.get(id);
142
209
  if (state === "active" || state === "succeeded") {
@@ -190,27 +257,28 @@ function sortRunnableIds(dag, ids) {
190
257
  const indexById = new Map(dag.nodes.map((node) => [node.id, node.stageIndex]));
191
258
  return ids.slice().sort((a, b) => (indexById.get(a) ?? 0) - (indexById.get(b) ?? 0));
192
259
  }
193
- function isRunnable(dag, stageId, states, schedulingHalted) {
260
+ function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes) {
194
261
  if (schedulingHalted)
195
262
  return false;
196
263
  const state = states.get(stageId);
197
264
  if (state !== "pending")
198
265
  return false;
199
- const node = dag.nodes.find((n) => n.id === stageId);
200
- if (!node)
201
- return false;
202
- if (node.needs === null)
203
- return true;
204
- return states.get(node.needs) === "succeeded";
266
+ return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes);
205
267
  }
206
268
  export async function runPipelineDag(options) {
207
269
  const { prepared, maxActiveStagesPerRun } = options;
208
270
  const executionMode = options.executionMode ?? "process";
209
- const { agent, task, loaded, run, store, checkoutRoot, hitl, cwd, } = prepared;
210
- const dag = loaded.dag;
271
+ const { agent, task, loaded, run, store, checkoutRoot, hitl, cwd, projectRoot, } = prepared;
272
+ const factoryCwd = projectRoot ?? cwd ?? process.cwd();
273
+ const fallbackDag = buildPipelineDagSnapshotFromLoaded(loaded);
274
+ let { states, completedEnvelopes, schedulingHalted, firstFailureReason, dag: hydratedDag, } = options.initialSchedule ??
275
+ (await hydrateScheduleFromStore(store, run.runId, fallbackDag, executionMode));
276
+ let dag = hydratedDag ?? fallbackDag;
277
+ if (hydratedDag === undefined) {
278
+ const meta = await store.readRunMeta(run.runId);
279
+ dag = meta.pipeline_dag ?? fallbackDag;
280
+ }
211
281
  const stageById = buildStageConfigById(loaded);
212
- let { states, completedEnvelopes, schedulingHalted, firstFailureReason } = options.initialSchedule ??
213
- (await hydrateScheduleFromStore(store, run.runId, dag, executionMode));
214
282
  const retryContext = options.retryContext;
215
283
  const resolveLaunchAttempt = async (stageId) => {
216
284
  if (retryContext) {
@@ -260,6 +328,14 @@ export async function runPipelineDag(options) {
260
328
  }
261
329
  }
262
330
  }
331
+ applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes);
332
+ {
333
+ const applied = applyCloneForksFromEnvelopes(dag, states, completedEnvelopes);
334
+ if (applied.dag !== dag) {
335
+ await store.updatePipelineDag(run.runId, applied.dag);
336
+ }
337
+ dag = applied.dag;
338
+ }
263
339
  let activeCount = 0;
264
340
  const inFlight = new Set();
265
341
  const notifyRetryRootTerminal = (stageId, outcome) => {
@@ -270,6 +346,20 @@ export async function runPipelineDag(options) {
270
346
  return;
271
347
  options.onRetryRootTerminal?.(stageId, attempt, outcome);
272
348
  };
349
+ const persistSkipPending = async (id) => {
350
+ if (states.get(id) !== "pending")
351
+ return;
352
+ states.set(id, "skipped");
353
+ notifyRetryRootTerminal(id, "skipped");
354
+ await store.appendStageEvent(run.runId, id, { event: "skipped" });
355
+ };
356
+ for (const [stageId, state] of states) {
357
+ if (state !== "failed")
358
+ continue;
359
+ for (const id of cloneFailFastSkipIds(dag, stageId, completedEnvelopes)) {
360
+ await persistSkipPending(id);
361
+ }
362
+ }
273
363
  const markSkippedPending = () => {
274
364
  for (const node of dag.nodes) {
275
365
  if (states.get(node.id) === "pending") {
@@ -300,9 +390,41 @@ export async function runPipelineDag(options) {
300
390
  });
301
391
  });
302
392
  };
303
- const onStageFailure = async (stageId, reason) => {
393
+ const recordCloneFailureEnvelope = async (stageId, reason, envelope) => {
394
+ if (!isCloneInstance(dag, stageId)) {
395
+ if (envelope !== undefined) {
396
+ completedEnvelopes.set(stageId, envelope);
397
+ }
398
+ return;
399
+ }
400
+ const failureEnvelope = envelope ??
401
+ completedEnvelopes.get(stageId) ??
402
+ synthesizedFailureEnvelope(reason);
403
+ completedEnvelopes.set(stageId, failureEnvelope);
404
+ if (envelope === undefined) {
405
+ try {
406
+ await store.writeEnvelope(run.runId, stageId, failureEnvelope);
407
+ }
408
+ catch {
409
+ }
410
+ }
411
+ };
412
+ const onStageFailure = async (stageId, reason, envelope) => {
304
413
  states.set(stageId, "failed");
305
414
  notifyRetryRootTerminal(stageId, "failed");
415
+ await recordCloneFailureEnvelope(stageId, reason, envelope);
416
+ if (cloneFailureContinuesSchedule(dag, stageId, completedEnvelopes)) {
417
+ if (firstFailureReason === undefined) {
418
+ firstFailureReason = reason;
419
+ }
420
+ for (const id of cloneFailFastSkipIds(dag, stageId, completedEnvelopes)) {
421
+ await persistSkipPending(id);
422
+ }
423
+ if (retryContext) {
424
+ drainRetryMutations();
425
+ }
426
+ return;
427
+ }
306
428
  if (retryContext) {
307
429
  markBranchDownstreamSkipped(stageId);
308
430
  drainRetryMutations();
@@ -319,13 +441,59 @@ export async function runPipelineDag(options) {
319
441
  firstFailureReason = reason;
320
442
  }
321
443
  };
322
- const onStageSuccess = (stageId, envelope) => {
444
+ const onStageSuccess = async (stageId, envelope) => {
445
+ const conflict = cloneFanoutConflict(dag, envelope);
446
+ if (conflict !== undefined) {
447
+ await store.appendStageEvent(run.runId, stageId, {
448
+ event: "failed",
449
+ reason: conflict,
450
+ });
451
+ await onStageFailure(stageId, conflict);
452
+ return;
453
+ }
323
454
  states.set(stageId, "succeeded");
324
455
  completedEnvelopes.set(stageId, envelope);
325
456
  notifyRetryRootTerminal(stageId, "succeeded");
457
+ const node = dag.nodes.find((n) => n.id === stageId);
458
+ const protectedIds = protectedClonableChildIds(dag, stageId, envelope);
459
+ if (node?.fork) {
460
+ const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
461
+ const skippedIds = [];
462
+ applyForkChoiceToSchedule(dag, stageId, chosen, states, {
463
+ protectedIds,
464
+ onSkip: (id) => {
465
+ notifyRetryRootTerminal(id, "skipped");
466
+ skippedIds.push(id);
467
+ },
468
+ });
469
+ for (const id of skippedIds) {
470
+ await store.appendStageEvent(run.runId, id, { event: "skipped" });
471
+ }
472
+ }
473
+ const applied = applyCloneForksToSchedule(dag, stageId, envelope, states);
474
+ const dagChanged = applied.dag !== dag;
475
+ dag = applied.dag;
476
+ if (dagChanged) {
477
+ await store.updatePipelineDag(run.runId, dag);
478
+ }
479
+ for (const id of applied.skippedIds) {
480
+ notifyRetryRootTerminal(id, "skipped");
481
+ await store.appendStageEvent(run.runId, id, { event: "skipped" });
482
+ }
483
+ };
484
+ const handlePostSuccessError = async (stageId, reason) => {
485
+ if (states.get(stageId) === "succeeded") {
486
+ completedEnvelopes.delete(stageId);
487
+ await store.appendStageEvent(run.runId, stageId, {
488
+ event: "failed",
489
+ reason,
490
+ });
491
+ }
492
+ await onStageFailure(stageId, reason);
326
493
  };
327
494
  const launchStage = async (stageId) => {
328
- const stage = stageById.get(stageId);
495
+ const definitionId = definitionIdForInstance(dag, stageId);
496
+ const stage = stageById.get(definitionId);
329
497
  if (!stage) {
330
498
  await onStageFailure(stageId, `stage config missing for "${stageId}"`);
331
499
  return;
@@ -341,7 +509,7 @@ export async function runPipelineDag(options) {
341
509
  const launchResult = await launcher.launch({
342
510
  runId: run.runId,
343
511
  stageId,
344
- rootDir: cwd,
512
+ rootDir: factoryCwd,
345
513
  attempt,
346
514
  ...(prepared.operatorCatalog !== undefined
347
515
  ? { operatorCatalog: prepared.operatorCatalog }
@@ -351,11 +519,11 @@ export async function runPipelineDag(options) {
351
519
  if (launchResult.type === "succeeded") {
352
520
  try {
353
521
  const envelope = await store.readEnvelope(run.runId, stageId);
354
- onStageSuccess(stageId, envelope);
522
+ await onStageSuccess(stageId, envelope);
355
523
  }
356
- catch {
357
- states.set(stageId, "succeeded");
358
- notifyRetryRootTerminal(stageId, "succeeded");
524
+ catch (err) {
525
+ const reason = err instanceof Error ? err.message : String(err);
526
+ await handlePostSuccessError(stageId, reason);
359
527
  }
360
528
  return;
361
529
  }
@@ -371,13 +539,14 @@ export async function runPipelineDag(options) {
371
539
  store,
372
540
  runId: run.runId,
373
541
  stage,
542
+ stageId,
374
543
  task,
375
544
  dag,
376
545
  checkoutRoot,
377
546
  workspaceDir: run.workspaceDir,
378
547
  hitl,
379
548
  attemptCtx,
380
- factoryCwd: cwd,
549
+ factoryCwd,
381
550
  operatorCatalog: prepared.operatorCatalog,
382
551
  completedEnvelopes,
383
552
  skipGates: prepared.skipGates,
@@ -387,11 +556,17 @@ export async function runPipelineDag(options) {
387
556
  return;
388
557
  }
389
558
  if (!result.ok) {
390
- await onStageFailure(stageId, result.reason ?? "stage failed");
559
+ await onStageFailure(stageId, result.reason ?? "stage failed", result.envelope);
391
560
  return;
392
561
  }
393
562
  if (result.envelope) {
394
- onStageSuccess(stageId, result.envelope);
563
+ try {
564
+ await onStageSuccess(stageId, result.envelope);
565
+ }
566
+ catch (err) {
567
+ const reason = err instanceof Error ? err.message : String(err);
568
+ await handlePostSuccessError(stageId, reason);
569
+ }
395
570
  }
396
571
  else {
397
572
  states.set(stageId, "succeeded");
@@ -412,7 +587,7 @@ export async function runPipelineDag(options) {
412
587
  const pickRunnable = () => {
413
588
  const ids = [];
414
589
  for (const node of dag.nodes) {
415
- if (isRunnable(dag, node.id, states, schedulingHalted)) {
590
+ if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes)) {
416
591
  ids.push(node.id);
417
592
  }
418
593
  }
@@ -485,7 +660,10 @@ export async function runPipelineDag(options) {
485
660
  reason: firstFailureReason,
486
661
  };
487
662
  }
488
- const allSucceeded = dag.nodes.every((node) => states.get(node.id) === "succeeded");
663
+ const allSucceeded = dag.nodes.every((node) => {
664
+ const s = states.get(node.id);
665
+ return s === "succeeded" || s === "skipped";
666
+ });
489
667
  if (!allSucceeded) {
490
668
  if (retryContext === undefined) {
491
669
  await store.updateRunStatus(run.runId, "failed");
@@ -498,9 +676,7 @@ export async function runPipelineDag(options) {
498
676
  reason: firstFailureReason ?? "pipeline incomplete",
499
677
  };
500
678
  }
501
- if (retryContext === undefined) {
502
- await store.updateRunStatus(run.runId, "succeeded");
503
- }
679
+ await store.updateRunStatus(run.runId, "succeeded");
504
680
  return {
505
681
  ok: true,
506
682
  outcome: "succeeded",
@@ -0,0 +1,5 @@
1
+ import type { RunMeta } from "../runstore/port.js";
2
+ import type { LoadedPipeline } from "../types/pipeline.js";
3
+ import type { TaskFile } from "../types/task.js";
4
+ export declare function reloadPipelineForRun(meta: RunMeta): Promise<LoadedPipeline>;
5
+ export declare function reloadTaskForRun(meta: RunMeta): Promise<TaskFile>;
@@ -0,0 +1,44 @@
1
+ import { access } from "node:fs/promises";
2
+ import { loadPipeline } from "../config/loadPipeline.js";
3
+ import { loadTask } from "../config/loadTask.js";
4
+ import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
+ function missingLocator(field, runId) {
6
+ throw new Error(`Run ${runId} is missing ${field}; reload requires stored catalog locators.`);
7
+ }
8
+ function requireProjectRoot(meta) {
9
+ if (!meta.project_root) {
10
+ missingLocator("project_root", meta.run_id);
11
+ }
12
+ return normalizeCatalogPath(meta.project_root);
13
+ }
14
+ function requirePipelinePath(meta) {
15
+ if (!meta.pipeline_path) {
16
+ missingLocator("pipeline_path", meta.run_id);
17
+ }
18
+ return normalizeCatalogPath(meta.pipeline_path);
19
+ }
20
+ function requireTaskPath(meta) {
21
+ if (!meta.task_path) {
22
+ missingLocator("task_path", meta.run_id);
23
+ }
24
+ return normalizeCatalogPath(meta.task_path);
25
+ }
26
+ async function assertCatalogFileExists(filePath, label) {
27
+ try {
28
+ await access(filePath);
29
+ }
30
+ catch {
31
+ throw new Error(`${label} not found at stored path: ${filePath}`);
32
+ }
33
+ }
34
+ export async function reloadPipelineForRun(meta) {
35
+ const projectRoot = requireProjectRoot(meta);
36
+ const pipelinePath = requirePipelinePath(meta);
37
+ await assertCatalogFileExists(pipelinePath, "Pipeline");
38
+ return loadPipeline(pipelinePath, { cwd: projectRoot });
39
+ }
40
+ export async function reloadTaskForRun(meta) {
41
+ const taskPath = requireTaskPath(meta);
42
+ await assertCatalogFileExists(taskPath, "Task");
43
+ return loadTask(taskPath);
44
+ }
@@ -16,6 +16,7 @@ export type PreparedResumeContext = {
16
16
  executionMode: StageExecutionMode;
17
17
  stageProcessLauncher?: StageProcessLauncher;
18
18
  cwd: string;
19
+ factoryCwd?: string;
19
20
  maxActiveStagesPerRun: number;
20
21
  operatorCatalog?: OperatorCatalog;
21
22
  };
@@ -1,7 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
2
3
  import { fakeHitlResumePath } from "../agent/fakeAgent.js";
3
- import { loadPipeline } from "../config/loadPipeline.js";
4
4
  import { loadTaskFromYaml } from "../config/loadTask.js";
5
+ import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
6
+ import { reloadPipelineForRun, reloadTaskForRun, } from "./reloadRunCatalog.js";
7
+ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
5
8
  import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
6
9
  import { resumeRun } from "./pipelineScheduler.js";
7
10
  import { buildStageRoots, withResolvedAuthPath } from "./stageRoots.js";
@@ -10,20 +13,34 @@ import { attemptContext } from "./stageAttemptContext.js";
10
13
  import { openStageAttempt, } from "./stageAttemptBootstrap.js";
11
14
  export async function loadRunContext(store, runId, cwd) {
12
15
  const meta = await store.readRunMeta(runId);
13
- const taskYaml = await store.readTaskYaml(runId);
14
- const task = loadTaskFromYaml(taskYaml, `run ${runId} task`);
15
- const loaded = await loadPipeline(meta.pipeline_id, { cwd });
16
+ const reloadMeta = {
17
+ ...meta,
18
+ project_root: meta.project_root ?? normalizeCatalogPath(cwd),
19
+ };
20
+ const loaded = await reloadPipelineForRun(reloadMeta);
21
+ let taskYaml;
22
+ let task;
23
+ if (meta.task_path) {
24
+ task = await reloadTaskForRun(meta);
25
+ taskYaml = await readFile(normalizeCatalogPath(meta.task_path), "utf8");
26
+ }
27
+ else {
28
+ taskYaml = await store.readTaskYaml(runId);
29
+ task = loadTaskFromYaml(taskYaml, `run ${runId} task`);
30
+ }
16
31
  const workspaceDir = store.getWorkspaceDir(runId);
17
32
  return { meta, taskYaml, task, loaded, workspaceDir };
18
33
  }
19
34
  export async function reconstructAndContinue(ctx) {
20
35
  const { runId, stageId, opaqueAnswer, agent, store, cwd } = ctx;
36
+ const factoryCwd = ctx.factoryCwd ?? cwd;
21
37
  try {
22
38
  const latestExecution = await store.getLatestStageExecution(runId, stageId);
23
39
  const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
24
40
  const attemptOpt = attemptCtx.eventOptions();
25
41
  const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
26
- const stageIndex = loaded.stages.findIndex((s) => s.id === stageId);
42
+ const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
43
+ const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
27
44
  if (stageIndex < 0) {
28
45
  const reason = `Stage ${stageId} not in pipeline ${meta.pipeline_id}`;
29
46
  await store.appendStageEvent(runId, stageId, {
@@ -34,9 +51,10 @@ export async function reconstructAndContinue(ctx) {
34
51
  return { ok: false, reason };
35
52
  }
36
53
  const stage = loaded.stages[stageIndex];
54
+ const dag = meta.pipeline_dag ?? loaded.dag;
37
55
  const workspaceDir = store.getWorkspaceDir(runId);
38
56
  const checkoutRoot = meta.checkout_root;
39
- const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), cwd);
57
+ const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), factoryCwd);
40
58
  const fakeResume = fakeHitlResumePath(roots, stageId);
41
59
  const piSession = attemptCtx.sessionPath(workspaceDir, stageId);
42
60
  if (!existsSync(fakeResume) && !existsSync(piSession)) {
@@ -53,11 +71,12 @@ export async function reconstructAndContinue(ctx) {
53
71
  store,
54
72
  runId,
55
73
  stage,
74
+ stageId,
56
75
  task,
57
- dag: loaded.dag,
76
+ dag,
58
77
  checkoutRoot,
59
78
  workspaceDir,
60
- factoryCwd: cwd,
79
+ factoryCwd,
61
80
  attemptCtx,
62
81
  operatorCatalog: ctx.operatorCatalog,
63
82
  onActivity: (event) => {
@@ -78,15 +97,16 @@ export async function reconstructAndContinue(ctx) {
78
97
  store,
79
98
  runId,
80
99
  stage,
100
+ stageId,
81
101
  task,
82
- dag: loaded.dag,
102
+ dag,
83
103
  checkoutRoot,
84
104
  workspaceDir,
85
105
  hitl: ctx.hitl,
86
106
  skipStarted: true,
87
107
  existingHandle: opened.handle,
88
108
  attemptCtx,
89
- factoryCwd: cwd,
109
+ factoryCwd,
90
110
  operatorCatalog: ctx.operatorCatalog,
91
111
  });
92
112
  if (isRunStageWaiting(result)) {
@@ -108,6 +128,7 @@ export async function reconstructAndContinue(ctx) {
108
128
  agent,
109
129
  store,
110
130
  cwd,
131
+ projectRoot: factoryCwd,
111
132
  checkoutRoot,
112
133
  hitl: ctx.hitl,
113
134
  operatorCatalog: ctx.operatorCatalog,
@@ -0,0 +1,18 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ export type RunChangeKind = "created" | "status" | "waiting";
3
+ export type RunChangeEvent = {
4
+ runId: string;
5
+ kind: RunChangeKind;
6
+ };
7
+ type RunChangeListener = (event: RunChangeEvent) => void;
8
+ export declare class RunChangeBus {
9
+ private readonly listeners;
10
+ on(listener: RunChangeListener): () => void;
11
+ off(listener: RunChangeListener): void;
12
+ emit(event: RunChangeEvent): void;
13
+ }
14
+ export declare function createRunChangeBus(): RunChangeBus;
15
+ export declare function isRunStoreWrapped(store: RunStore): boolean;
16
+ export declare function getRunChangeBusFromWrappedStore(store: RunStore): RunChangeBus | undefined;
17
+ export declare function wrapRunStoreWithChangeBus(store: RunStore, bus: RunChangeBus): RunStore;
18
+ export {};