stageflow 0.9.0 → 0.13.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 (167) hide show
  1. package/README.md +6 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +331 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +12 -7
  15. package/dist/agent/piAdapter.js +194 -44
  16. package/dist/agent/piIsolatedMcp.d.ts +23 -0
  17. package/dist/agent/piIsolatedMcp.js +223 -0
  18. package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
  19. package/dist/agent/piIsolatedMcpProbe.js +292 -0
  20. package/dist/agent/port.d.ts +52 -1
  21. package/dist/agent/port.js +1 -1
  22. package/dist/agent/resolveAgentPort.d.ts +26 -0
  23. package/dist/agent/resolveAgentPort.js +39 -0
  24. package/dist/cli/runCommand.js +2 -2
  25. package/dist/cli/runsCommand.d.ts +1 -1
  26. package/dist/cli/runsCommand.js +184 -2
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +11 -5
  29. package/dist/config/browseCatalog.js +1 -1
  30. package/dist/config/createPipeline.d.ts +2 -1
  31. package/dist/config/createPipeline.js +34 -9
  32. package/dist/config/createStage.js +14 -9
  33. package/dist/config/loadPipeline.js +11 -1
  34. package/dist/config/loadStage.d.ts +1 -0
  35. package/dist/config/loadStage.js +207 -2
  36. package/dist/config/loadStageflowManifest.js +14 -1
  37. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  38. package/dist/config/mergePipelineIncludes.js +14 -1
  39. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  40. package/dist/config/normalizePipelineStageEntry.js +56 -7
  41. package/dist/config/parseCompletionContract.d.ts +8 -0
  42. package/dist/config/parseCompletionContract.js +218 -0
  43. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  44. package/dist/config/parsePreEmitChecks.js +65 -0
  45. package/dist/config/pipelineNeeds.d.ts +19 -0
  46. package/dist/config/pipelineNeeds.js +151 -0
  47. package/dist/config/pipelineStageKeys.js +17 -1
  48. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  49. package/dist/config/resolveForkEmitContext.js +9 -2
  50. package/dist/config/resolvePipelineDag.d.ts +2 -1
  51. package/dist/config/resolvePipelineDag.js +182 -39
  52. package/dist/config/resolveStageMcpServers.d.ts +35 -0
  53. package/dist/config/resolveStageMcpServers.js +207 -0
  54. package/dist/config/validateCatalog.d.ts +1 -1
  55. package/dist/config/validateCatalog.js +51 -0
  56. package/dist/config/validateCompletionContract.d.ts +4 -0
  57. package/dist/config/validateCompletionContract.js +52 -0
  58. package/dist/envelope/check.d.ts +1 -1
  59. package/dist/envelope/check.js +86 -13
  60. package/dist/envelope/cloneForks.d.ts +1 -1
  61. package/dist/envelope/cloneForks.js +24 -9
  62. package/dist/envelope/feedbackLoop.d.ts +4 -0
  63. package/dist/envelope/feedbackLoop.js +31 -0
  64. package/dist/envelope/payloadSchema.d.ts +7 -1
  65. package/dist/envelope/payloadSchema.js +170 -13
  66. package/dist/envelope/preEmitChecks.d.ts +29 -0
  67. package/dist/envelope/preEmitChecks.js +57 -0
  68. package/dist/hitl/qaTrail.d.ts +25 -0
  69. package/dist/hitl/qaTrail.js +51 -0
  70. package/dist/mcp/catalogTools.js +4 -2
  71. package/dist/mcp/controlTools.js +72 -0
  72. package/dist/mcp/readArtifact.d.ts +2 -0
  73. package/dist/mcp/readArtifact.js +25 -1
  74. package/dist/mcp/waitingGates.d.ts +2 -1
  75. package/dist/mcp/waitingGates.js +17 -1
  76. package/dist/package-meta.d.ts +1 -1
  77. package/dist/package-meta.js +1 -1
  78. package/dist/projection/projectRun.d.ts +4 -1
  79. package/dist/projection/projectRun.js +7 -0
  80. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  81. package/dist/prompt/feedbackLoopContext.js +17 -0
  82. package/dist/prompt/priorEnvelope.d.ts +2 -2
  83. package/dist/prompt/priorEnvelope.js +4 -1
  84. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  85. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  86. package/dist/runstore/port.d.ts +185 -3
  87. package/dist/runstore/port.js +42 -3
  88. package/dist/runstore/runProjection.d.ts +7 -3
  89. package/dist/runstore/runProjection.js +30 -15
  90. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  91. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  92. package/dist/runstore/sqlite/schema.d.ts +1 -1
  93. package/dist/runstore/sqlite/schema.js +99 -0
  94. package/dist/runstore/stageInstanceId.d.ts +1 -1
  95. package/dist/runstore/stageInstanceId.js +3 -2
  96. package/dist/runstore/trackProjection.js +59 -27
  97. package/dist/runstore/verificationHistory.d.ts +19 -0
  98. package/dist/runstore/verificationHistory.js +30 -0
  99. package/dist/runtime/cloneSchedule.d.ts +40 -4
  100. package/dist/runtime/cloneSchedule.js +212 -28
  101. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  102. package/dist/runtime/completionCheckRunner.js +365 -0
  103. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  104. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  105. package/dist/runtime/envelopeRouting.d.ts +10 -3
  106. package/dist/runtime/envelopeRouting.js +146 -5
  107. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  108. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  109. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  110. package/dist/runtime/feedbackLoopDecision.js +207 -0
  111. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  112. package/dist/runtime/feedbackLoopRoute.js +13 -0
  113. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  114. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  115. package/dist/runtime/forkGeneration.d.ts +72 -0
  116. package/dist/runtime/forkGeneration.js +295 -0
  117. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  118. package/dist/runtime/gitCheckoutCapability.js +97 -0
  119. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  120. package/dist/runtime/manualRecoveryState.js +73 -0
  121. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  122. package/dist/runtime/pipelineScheduler.js +328 -45
  123. package/dist/runtime/replayLifecycle.d.ts +107 -0
  124. package/dist/runtime/replayLifecycle.js +168 -0
  125. package/dist/runtime/runManager.d.ts +54 -1
  126. package/dist/runtime/runManager.js +471 -6
  127. package/dist/runtime/runRetryCoordinator.d.ts +6 -1
  128. package/dist/runtime/runRetryCoordinator.js +43 -9
  129. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  130. package/dist/runtime/stageAttemptBootstrap.js +117 -3
  131. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  132. package/dist/runtime/stageRecovery.js +1 -1
  133. package/dist/runtime/stageRunner.d.ts +4 -1
  134. package/dist/runtime/stageRunner.js +29 -3
  135. package/dist/runtime/stageWorker.js +69 -2
  136. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  137. package/dist/runtime/verifiedStageExecution.d.ts +31 -0
  138. package/dist/runtime/verifiedStageExecution.js +127 -0
  139. package/dist/server/bootstrap.js +1 -0
  140. package/dist/server/http.js +137 -4
  141. package/dist/server/operatorResults.js +2 -0
  142. package/dist/server/projectMcpRoutes.d.ts +6 -0
  143. package/dist/server/projectMcpRoutes.js +38 -0
  144. package/dist/tools/askOperator.d.ts +161 -5
  145. package/dist/tools/askOperator.js +58 -25
  146. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  147. package/dist/tools/emitStageEnvelope.js +112 -24
  148. package/dist/types/completion.d.ts +43 -0
  149. package/dist/types/completion.js +1 -0
  150. package/dist/types/envelope.d.ts +18 -0
  151. package/dist/types/forkChoice.d.ts +9 -4
  152. package/dist/types/forkChoice.js +1 -1
  153. package/dist/types/pipeline.d.ts +41 -2
  154. package/dist/types/preEmitCheck.d.ts +18 -0
  155. package/dist/types/preEmitCheck.js +1 -0
  156. package/dist/types/stage.d.ts +14 -0
  157. package/dist/types/stageflowManifest.d.ts +2 -0
  158. package/dist/ui/assets/index-Yzh42vhf.js +118 -0
  159. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  160. package/dist/ui/index.html +2 -2
  161. package/package.json +5 -1
  162. package/skills/stageflow-author/SKILL.md +20 -2
  163. package/skills/stageflow-author/references/catalog-mapping.md +88 -3
  164. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  165. package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
  166. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
  167. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -1,12 +1,14 @@
1
1
  import { normalizeForkChoice } from "../envelope/forkChoice.js";
2
2
  import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
3
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";
4
+ import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, cloneFailureContinuesSchedule, cloneFanoutConflict, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, failureIsAccepted, isCloneInstance, protectedClonableChildIds, sequentialLaterCloneIds, skipRejectedNeedDependents, } from "./cloneSchedule.js";
6
5
  import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
6
+ import { activeCohortFromCloneIds, forkChoicePreservesReplaySource, forkParentForNeedsStage, mintCohortForFanout, } from "./forkGeneration.js";
7
+ import { activeReplayId, activeSourceStageId, applySendBack, cohortOverrideMap, createReplaySchedule, hydrateFromStore, isHeld, launchAttemptFor, launchFor, noteActiveCohort, onContinued, onRouteStageFailed, onRouteStageRunning, onRouteStageSucceeded, rebindRouteAttempts, resolveDecision, } from "./replayLifecycle.js";
7
8
  import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
8
9
  import { runStage, isRunStageWaiting } from "./stageRunner.js";
9
- import { attemptContext } from "./stageAttemptContext.js";
10
+ import { attemptContext, resumeSessionFilePath } from "./stageAttemptContext.js";
11
+ const EMPTY_RETRY_DOWNSTREAM = new Set();
10
12
  function asDagSnapshot(dag) {
11
13
  const snapshot = dag;
12
14
  if (Array.isArray(snapshot.stage_ids))
@@ -53,10 +55,16 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
53
55
  for (const snap of runDetail.stages) {
54
56
  const state = snapshotToScheduleState(snap, overrides, snap.stage_id, executionMode);
55
57
  states.set(snap.stage_id, state);
56
- if (state === "failed" &&
57
- !cloneFailureContinuesSchedule(resolvedDag, snap.stage_id, completedEnvelopes)) {
58
- schedulingHalted = true;
58
+ }
59
+ for (const [stageId, state] of states) {
60
+ if (state !== "failed")
61
+ continue;
62
+ if (cloneFailureContinuesSchedule(resolvedDag, stageId, completedEnvelopes)) {
63
+ continue;
59
64
  }
65
+ if (failureIsAccepted(resolvedDag, stageId, states))
66
+ continue;
67
+ schedulingHalted = true;
60
68
  }
61
69
  return {
62
70
  states,
@@ -136,19 +144,16 @@ export async function resumeRun(options) {
136
144
  return state !== undefined && terminalState(state);
137
145
  });
138
146
  if (allTerminal) {
139
- const allSucceeded = dag.nodes.every((node) => {
140
- const s = hydrated.states.get(node.id);
141
- return s === "succeeded" || s === "skipped";
142
- });
143
- await prepared.store.updateRunStatus(prepared.run.runId, allSucceeded ? "succeeded" : "failed");
147
+ const unhandledFailure = scheduleHasUnhandledFailure(dag, hydrated.states);
148
+ await prepared.store.updateRunStatus(prepared.run.runId, unhandledFailure ? "failed" : "succeeded");
144
149
  return {
145
- ok: allSucceeded,
146
- outcome: allSucceeded ? "succeeded" : "failed",
150
+ ok: !unhandledFailure,
151
+ outcome: unhandledFailure ? "failed" : "succeeded",
147
152
  runDir: prepared.run.workspaceDir,
148
153
  runId: prepared.run.runId,
149
- ...(allSucceeded
150
- ? {}
151
- : { reason: hydrated.firstFailureReason ?? "pipeline incomplete" }),
154
+ ...(unhandledFailure
155
+ ? { reason: hydrated.firstFailureReason ?? "pipeline incomplete" }
156
+ : {}),
152
157
  };
153
158
  }
154
159
  return runPipelineDag(options);
@@ -180,9 +185,7 @@ function applyForkChoiceToSchedule(dag, forkStageId, chosen, states, options) {
180
185
  continue;
181
186
  if (!chosen.has(childId)) {
182
187
  skipPending(childId);
183
- for (const desc of collectDownstreamStageIds(dag, childId)) {
184
- skipPending(desc);
185
- }
188
+ skipRejectedNeedDependents(dag, childId, states, "skipped", skipPending);
186
189
  }
187
190
  }
188
191
  }
@@ -197,7 +200,7 @@ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
197
200
  applyForkChoiceToSchedule(dag, node.id, chosen, states);
198
201
  }
199
202
  }
200
- export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt) {
203
+ export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt, alreadyLaunchedIds) {
201
204
  retryRoots.set(stageId, attempt);
202
205
  const downstream = cloneRetryDownstream(dag, [stageId]);
203
206
  const resetIds = new Set([stageId, ...downstream]);
@@ -206,7 +209,13 @@ export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots,
206
209
  }
207
210
  for (const id of resetIds) {
208
211
  const state = states.get(id);
209
- if (state === "active" || state === "succeeded") {
212
+ if (state === "active") {
213
+ completedEnvelopes.delete(id);
214
+ continue;
215
+ }
216
+ if (id === stageId &&
217
+ state === "succeeded" &&
218
+ alreadyLaunchedIds?.has(id)) {
210
219
  completedEnvelopes.delete(id);
211
220
  continue;
212
221
  }
@@ -218,7 +227,7 @@ export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots,
218
227
  export function hasWorkOutsideBranch(dag, branchRootId, states, ctx) {
219
228
  const branchIds = new Set([
220
229
  branchRootId,
221
- ...collectDownstreamStageIds(dag, branchRootId),
230
+ ...cloneRetryDownstream(dag, [branchRootId]),
222
231
  ]);
223
232
  for (const node of dag.nodes) {
224
233
  if (branchIds.has(node.id))
@@ -253,17 +262,56 @@ export function hasWorkOutsideBranch(dag, branchRootId, states, ctx) {
253
262
  function terminalState(status) {
254
263
  return status === "succeeded" || status === "failed" || status === "skipped";
255
264
  }
265
+ function scheduleHasUnhandledFailure(dag, states) {
266
+ for (const [stageId, state] of states) {
267
+ if (state !== "failed")
268
+ continue;
269
+ if (failureIsAccepted(dag, stageId, states))
270
+ continue;
271
+ return true;
272
+ }
273
+ return false;
274
+ }
256
275
  function sortRunnableIds(dag, ids) {
257
276
  const indexById = new Map(dag.nodes.map((node) => [node.id, node.stageIndex]));
258
277
  return ids.slice().sort((a, b) => (indexById.get(a) ?? 0) - (indexById.get(b) ?? 0));
259
278
  }
260
- function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes) {
279
+ function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes, feedback) {
261
280
  if (schedulingHalted)
262
281
  return false;
282
+ if (feedback !== undefined && isHeld(feedback, stageId))
283
+ return false;
263
284
  const state = states.get(stageId);
264
285
  if (state !== "pending")
265
286
  return false;
266
- return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes);
287
+ return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes, {
288
+ activeCohortForNeeds: (needsId) => {
289
+ const override = feedback !== undefined ? cohortOverrideMap(feedback) : undefined;
290
+ if (override === undefined || override.size === 0) {
291
+ return { kind: "untracked" };
292
+ }
293
+ const forkParentId = forkParentForNeedsStage(dag, needsId);
294
+ if (forkParentId === null)
295
+ return { kind: "untracked" };
296
+ const tracked = override.get(forkParentId);
297
+ if (tracked === undefined)
298
+ return { kind: "untracked" };
299
+ return activeCohortFromCloneIds(tracked);
300
+ },
301
+ });
302
+ }
303
+ export function hydratedScheduleHasRunnableWork(hydrated) {
304
+ if (hydrated.schedulingHalted)
305
+ return false;
306
+ const dag = hydrated.dag;
307
+ if (dag === undefined)
308
+ return false;
309
+ for (const node of dag.nodes) {
310
+ if (isRunnable(dag, node.id, hydrated.states, hydrated.schedulingHalted, hydrated.completedEnvelopes)) {
311
+ return true;
312
+ }
313
+ }
314
+ return false;
267
315
  }
268
316
  export async function runPipelineDag(options) {
269
317
  const { prepared, maxActiveStagesPerRun } = options;
@@ -280,7 +328,34 @@ export async function runPipelineDag(options) {
280
328
  }
281
329
  const stageById = buildStageConfigById(loaded);
282
330
  const retryContext = options.retryContext;
331
+ const feedbackSchedule = createReplaySchedule();
332
+ await hydrateFromStore(feedbackSchedule, store, run.runId, dag);
333
+ if (retryContext !== undefined &&
334
+ activeReplayId(feedbackSchedule) !== undefined) {
335
+ const routeIds = new Set(feedbackSchedule.feedback.contextsByStageId.keys());
336
+ const cascade = cloneRetryDownstream(dag, [
337
+ ...retryContext.retryRoots.keys(),
338
+ ]);
339
+ const overrides = new Map();
340
+ for (const [stageId, attempt] of retryContext.retryRoots) {
341
+ if (routeIds.has(stageId))
342
+ overrides.set(stageId, attempt);
343
+ }
344
+ for (const stageId of cascade) {
345
+ if (!routeIds.has(stageId))
346
+ continue;
347
+ if (overrides.has(stageId))
348
+ continue;
349
+ const exec = await store.createStageExecution(run.runId, stageId);
350
+ overrides.set(stageId, exec.attempt);
351
+ }
352
+ await rebindRouteAttempts(feedbackSchedule, store, run.runId, overrides);
353
+ }
283
354
  const resolveLaunchAttempt = async (stageId) => {
355
+ const feedbackAttempt = launchAttemptFor(feedbackSchedule, stageId);
356
+ if (feedbackAttempt !== undefined) {
357
+ return feedbackAttempt;
358
+ }
284
359
  if (retryContext) {
285
360
  const preCreatedAttempt = retryContext.retryRoots.get(stageId);
286
361
  if (preCreatedAttempt !== undefined) {
@@ -289,11 +364,10 @@ export async function runPipelineDag(options) {
289
364
  const execution = await store.createStageExecution(run.runId, stageId);
290
365
  return execution.attempt;
291
366
  }
292
- const attemptCount = await store.countStageAttempts(run.runId, stageId);
293
- if (attemptCount === 0) {
294
- await store.createStageExecution(run.runId, stageId);
295
- }
296
- return 1;
367
+ const latest = await store.getLatestStageExecution(run.runId, stageId);
368
+ if (latest !== null)
369
+ return latest.attempt;
370
+ return (await store.createStageExecution(run.runId, stageId)).attempt;
297
371
  };
298
372
  if (options.resumeFromStageId !== undefined) {
299
373
  const resumedId = options.resumeFromStageId;
@@ -338,6 +412,7 @@ export async function runPipelineDag(options) {
338
412
  }
339
413
  let activeCount = 0;
340
414
  const inFlight = new Set();
415
+ const launchedIds = new Set();
341
416
  const notifyRetryRootTerminal = (stageId, outcome) => {
342
417
  if (retryContext === undefined)
343
418
  return;
@@ -346,12 +421,26 @@ export async function runPipelineDag(options) {
346
421
  return;
347
422
  options.onRetryRootTerminal?.(stageId, attempt, outcome);
348
423
  };
424
+ const persistSkipRejected = async (parentId, observed) => {
425
+ const ids = [];
426
+ skipRejectedNeedDependents(dag, parentId, states, observed, (id) => {
427
+ if (states.get(id) !== "pending")
428
+ return;
429
+ states.set(id, "skipped");
430
+ ids.push(id);
431
+ });
432
+ for (const id of ids) {
433
+ notifyRetryRootTerminal(id, "skipped");
434
+ await store.appendStageEvent(run.runId, id, { event: "skipped" });
435
+ }
436
+ };
349
437
  const persistSkipPending = async (id) => {
350
438
  if (states.get(id) !== "pending")
351
439
  return;
352
440
  states.set(id, "skipped");
353
441
  notifyRetryRootTerminal(id, "skipped");
354
442
  await store.appendStageEvent(run.runId, id, { event: "skipped" });
443
+ await persistSkipRejected(id, "skipped");
355
444
  };
356
445
  for (const [stageId, state] of states) {
357
446
  if (state !== "failed")
@@ -369,7 +458,7 @@ export async function runPipelineDag(options) {
369
458
  }
370
459
  };
371
460
  const markBranchDownstreamSkipped = (stageId) => {
372
- for (const id of collectDownstreamStageIds(dag, stageId)) {
461
+ for (const id of cloneRetryDownstream(dag, [stageId])) {
373
462
  if (states.get(id) === "pending") {
374
463
  states.set(id, "skipped");
375
464
  notifyRetryRootTerminal(id, "skipped");
@@ -387,7 +476,7 @@ export async function runPipelineDag(options) {
387
476
  options.mutationQueue.drainPending((stageId, attempt) => {
388
477
  applyRetryRootDelta(dag, states, completedEnvelopes, retryContext.retryRoots, stageId, attempt, () => {
389
478
  schedulingHalted = false;
390
- });
479
+ }, launchedIds);
391
480
  });
392
481
  };
393
482
  const recordCloneFailureEnvelope = async (stageId, reason, envelope) => {
@@ -409,10 +498,42 @@ export async function runPipelineDag(options) {
409
498
  }
410
499
  }
411
500
  };
501
+ const scheduleAutomaticRepair = async (stageId) => {
502
+ const recovery = dag.nodes.find((node) => node.id === stageId)?.recovery;
503
+ if (recovery?.mode !== "repair")
504
+ return false;
505
+ const latestAttempt = await store.getLatestStageExecution(run.runId, stageId);
506
+ if (latestAttempt?.verification_outcome !== "failed")
507
+ return false;
508
+ const attempts = await store.countStageAttempts(run.runId, stageId);
509
+ if (attempts >= recovery.max_attempts)
510
+ return false;
511
+ const nextAttempt = await store.createStageExecution(run.runId, stageId);
512
+ if (retryContext !== undefined) {
513
+ retryContext.retryRoots.set(stageId, nextAttempt.attempt);
514
+ }
515
+ if (activeReplayId(feedbackSchedule) !== undefined) {
516
+ await rebindRouteAttempts(feedbackSchedule, store, run.runId, new Map([[stageId, nextAttempt.attempt]]));
517
+ }
518
+ states.set(stageId, "pending");
519
+ completedEnvelopes.delete(stageId);
520
+ return true;
521
+ };
412
522
  const onStageFailure = async (stageId, reason, envelope) => {
523
+ if (await scheduleAutomaticRepair(stageId))
524
+ return;
525
+ if (activeReplayId(feedbackSchedule) !== undefined) {
526
+ await onRouteStageFailed({
527
+ store,
528
+ runId: run.runId,
529
+ schedule: feedbackSchedule,
530
+ stageId,
531
+ });
532
+ }
413
533
  states.set(stageId, "failed");
414
534
  notifyRetryRootTerminal(stageId, "failed");
415
535
  await recordCloneFailureEnvelope(stageId, reason, envelope);
536
+ await persistSkipRejected(stageId, "failed");
416
537
  if (cloneFailureContinuesSchedule(dag, stageId, completedEnvelopes)) {
417
538
  if (firstFailureReason === undefined) {
418
539
  firstFailureReason = reason;
@@ -426,23 +547,33 @@ export async function runPipelineDag(options) {
426
547
  return;
427
548
  }
428
549
  if (retryContext) {
429
- markBranchDownstreamSkipped(stageId);
550
+ const accepted = failureIsAccepted(dag, stageId, states);
551
+ if (!accepted) {
552
+ markBranchDownstreamSkipped(stageId);
553
+ }
430
554
  drainRetryMutations();
431
- if (!hasWorkOutsideBranch(dag, stageId, states, outsideWorkContext())) {
555
+ if (!accepted &&
556
+ !hasWorkOutsideBranch(dag, stageId, states, outsideWorkContext())) {
432
557
  schedulingHalted = true;
433
558
  }
434
- if (firstFailureReason === undefined) {
559
+ if (!accepted && firstFailureReason === undefined) {
435
560
  firstFailureReason = reason;
436
561
  }
437
562
  return;
438
563
  }
439
- if (!schedulingHalted) {
440
- schedulingHalted = true;
441
- firstFailureReason = reason;
564
+ if (!failureIsAccepted(dag, stageId, states)) {
565
+ if (!schedulingHalted) {
566
+ schedulingHalted = true;
567
+ firstFailureReason = reason;
568
+ }
442
569
  }
443
570
  };
444
571
  const onStageSuccess = async (stageId, envelope) => {
445
- const conflict = cloneFanoutConflict(dag, envelope);
572
+ const retryDownstreamIds = retryContext
573
+ ? cloneRetryDownstream(dag, [...retryContext.retryRoots.keys()])
574
+ : EMPTY_RETRY_DOWNSTREAM;
575
+ const forceFreshCloneIds = activeReplayId(feedbackSchedule) !== undefined;
576
+ const conflict = cloneFanoutConflict(dag, envelope, states, retryDownstreamIds, { forceFreshCloneIds });
446
577
  if (conflict !== undefined) {
447
578
  await store.appendStageEvent(run.runId, stageId, {
448
579
  event: "failed",
@@ -454,10 +585,119 @@ export async function runPipelineDag(options) {
454
585
  states.set(stageId, "succeeded");
455
586
  completedEnvelopes.set(stageId, envelope);
456
587
  notifyRetryRootTerminal(stageId, "succeeded");
588
+ const feedbackAction = envelope.feedback_loop;
589
+ if (feedbackAction?.action === "send_back") {
590
+ const node = dag.nodes.find((n) => n.id === stageId);
591
+ if (node === undefined || node.feedback_loop === undefined) {
592
+ await handlePostSuccessError(stageId, `feedback_loop send_back from stage "${stageId}" without feedback_loop policy`);
593
+ return;
594
+ }
595
+ const latest = await store.getLatestStageExecution(run.runId, stageId);
596
+ const sourceAttempt = latest?.attempt ?? 1;
597
+ const applied = await applySendBack({
598
+ store,
599
+ runId: run.runId,
600
+ sourceNode: node,
601
+ sourceAttempt,
602
+ envelope,
603
+ schedule: feedbackSchedule,
604
+ projection: {
605
+ dag,
606
+ states,
607
+ completedEnvelopes,
608
+ },
609
+ });
610
+ if (applied.kind === "rejected") {
611
+ await handlePostSuccessError(stageId, applied.reason);
612
+ return;
613
+ }
614
+ if (applied.kind === "waiting_for_human") {
615
+ const waitAttempt = sourceAttempt;
616
+ await store.appendStageEvent(run.runId, stageId, { event: "waiting_for_input" }, { attempt: waitAttempt });
617
+ try {
618
+ await store.updateStageExecution(run.runId, stageId, waitAttempt, {
619
+ status: "waiting_for_input",
620
+ });
621
+ }
622
+ catch {
623
+ // execution row may be absent in unusual harnesses
624
+ }
625
+ states.set(stageId, "waiting");
626
+ if (options.onFeedbackLoopWaiting !== undefined) {
627
+ let decisionResult;
628
+ await options.onFeedbackLoopWaiting({
629
+ loopId: applied.loop.loop_id,
630
+ sourceStageId: stageId,
631
+ resolve: async (decision) => {
632
+ decisionResult = await resolveDecision({
633
+ store,
634
+ runId: run.runId,
635
+ decision: decision.decision,
636
+ loopId: applied.loop.loop_id,
637
+ reason: decision.reason,
638
+ schedule: feedbackSchedule,
639
+ projection: {
640
+ dag,
641
+ states,
642
+ completedEnvelopes,
643
+ },
644
+ sourceNode: node,
645
+ });
646
+ return decisionResult;
647
+ },
648
+ });
649
+ if (decisionResult === undefined) {
650
+ return;
651
+ }
652
+ if (!decisionResult.ok) {
653
+ await handlePostSuccessError(stageId, decisionResult.reason);
654
+ return;
655
+ }
656
+ if (decisionResult.effect === "abandoned") {
657
+ schedulingHalted = true;
658
+ if (firstFailureReason === undefined) {
659
+ firstFailureReason = decisionResult.reason;
660
+ }
661
+ return;
662
+ }
663
+ return;
664
+ }
665
+ return;
666
+ }
667
+ for (const id of applied.retiredSkippedStageIds) {
668
+ await store.appendStageEvent(run.runId, id, { event: "skipped" });
669
+ }
670
+ return;
671
+ }
672
+ if (feedbackAction?.action === "continue") {
673
+ await onContinued({
674
+ store,
675
+ runId: run.runId,
676
+ sourceStageId: stageId,
677
+ envelope,
678
+ schedule: feedbackSchedule,
679
+ });
680
+ }
681
+ else if (activeReplayId(feedbackSchedule) !== undefined) {
682
+ await onRouteStageSucceeded({
683
+ store,
684
+ runId: run.runId,
685
+ schedule: feedbackSchedule,
686
+ stageId,
687
+ envelope,
688
+ });
689
+ }
457
690
  const node = dag.nodes.find((n) => n.id === stageId);
458
691
  const protectedIds = protectedClonableChildIds(dag, stageId, envelope);
459
692
  if (node?.fork) {
460
693
  const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
694
+ const sourceId = activeSourceStageId(feedbackSchedule);
695
+ if (activeReplayId(feedbackSchedule) !== undefined &&
696
+ sourceId !== undefined &&
697
+ !forkChoicePreservesReplaySource(dag, new Set(chosen), sourceId)) {
698
+ await handlePostSuccessError(stageId, `fork_choice during feedback replay must preserve a path to source "${sourceId}"`);
699
+ return;
700
+ }
461
701
  const skippedIds = [];
462
702
  applyForkChoiceToSchedule(dag, stageId, chosen, states, {
463
703
  protectedIds,
@@ -470,7 +710,9 @@ export async function runPipelineDag(options) {
470
710
  await store.appendStageEvent(run.runId, id, { event: "skipped" });
471
711
  }
472
712
  }
473
- const applied = applyCloneForksToSchedule(dag, stageId, envelope, states);
713
+ const applied = applyCloneForksToSchedule(dag, stageId, envelope, states, {
714
+ forceFreshCloneIds,
715
+ });
474
716
  const dagChanged = applied.dag !== dag;
475
717
  dag = applied.dag;
476
718
  if (dagChanged) {
@@ -480,6 +722,21 @@ export async function runPipelineDag(options) {
480
722
  notifyRetryRootTerminal(id, "skipped");
481
723
  await store.appendStageEvent(run.runId, id, { event: "skipped" });
482
724
  }
725
+ if (applied.mintedBySuccessor !== undefined) {
726
+ for (const [, cloneStageIds] of applied.mintedBySuccessor) {
727
+ if (cloneStageIds.length === 0)
728
+ continue;
729
+ const replayId = activeReplayId(feedbackSchedule);
730
+ await mintCohortForFanout({
731
+ store,
732
+ runId: run.runId,
733
+ ...(replayId !== undefined ? { replayId } : {}),
734
+ forkParent: stageId,
735
+ cloneStageIds,
736
+ });
737
+ noteActiveCohort(feedbackSchedule, stageId, cloneStageIds);
738
+ }
739
+ }
483
740
  };
484
741
  const handlePostSuccessError = async (stageId, reason) => {
485
742
  if (states.get(stageId) === "succeeded") {
@@ -500,6 +757,22 @@ export async function runPipelineDag(options) {
500
757
  }
501
758
  const attempt = await resolveLaunchAttempt(stageId);
502
759
  const attemptCtx = attemptContext(attempt);
760
+ const prep = launchFor(feedbackSchedule, stageId);
761
+ const feedbackLoopContext = prep?.feedbackLoopContext;
762
+ const sessionMode = prep?.sessionMode;
763
+ const priorAttempt = prep?.priorAttempt;
764
+ const resumeToken = sessionMode === "feedback_resume" && priorAttempt !== undefined
765
+ ? resumeSessionFilePath(run.workspaceDir, stageId, priorAttempt)
766
+ : undefined;
767
+ if (activeReplayId(feedbackSchedule) !== undefined &&
768
+ feedbackLoopContext !== undefined) {
769
+ await onRouteStageRunning({
770
+ store,
771
+ runId: run.runId,
772
+ schedule: feedbackSchedule,
773
+ stageId,
774
+ });
775
+ }
503
776
  if (executionMode === "process") {
504
777
  const launcher = options.stageProcessLauncher;
505
778
  if (!launcher) {
@@ -511,6 +784,16 @@ export async function runPipelineDag(options) {
511
784
  stageId,
512
785
  rootDir: factoryCwd,
513
786
  attempt,
787
+ ...(sessionMode !== undefined
788
+ ? {
789
+ mode: sessionMode === "feedback_resume"
790
+ ? "feedback_resume"
791
+ : sessionMode === "new_session"
792
+ ? "new_session"
793
+ : "run",
794
+ }
795
+ : {}),
796
+ ...(resumeToken !== undefined ? { sessionFilePath: resumeToken } : {}),
514
797
  ...(prepared.operatorCatalog !== undefined
515
798
  ? { operatorCatalog: prepared.operatorCatalog }
516
799
  : {}),
@@ -550,6 +833,9 @@ export async function runPipelineDag(options) {
550
833
  operatorCatalog: prepared.operatorCatalog,
551
834
  completedEnvelopes,
552
835
  skipGates: prepared.skipGates,
836
+ ...(sessionMode !== undefined ? { sessionMode } : {}),
837
+ ...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
838
+ ...(resumeToken !== undefined ? { resumeToken } : {}),
553
839
  });
554
840
  if (isRunStageWaiting(result)) {
555
841
  await onStageFailure(stageId, WAIT_WITHOUT_WORKER_DISPATCH);
@@ -577,6 +863,7 @@ export async function runPipelineDag(options) {
577
863
  if (states.get(stageId) !== "pending")
578
864
  return;
579
865
  states.set(stageId, "active");
866
+ launchedIds.add(stageId);
580
867
  activeCount += 1;
581
868
  const taskPromise = launchStage(stageId).finally(() => {
582
869
  activeCount -= 1;
@@ -587,7 +874,7 @@ export async function runPipelineDag(options) {
587
874
  const pickRunnable = () => {
588
875
  const ids = [];
589
876
  for (const node of dag.nodes) {
590
- if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes)) {
877
+ if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes, feedbackSchedule)) {
591
878
  ids.push(node.id);
592
879
  }
593
880
  }
@@ -660,11 +947,7 @@ export async function runPipelineDag(options) {
660
947
  reason: firstFailureReason,
661
948
  };
662
949
  }
663
- const allSucceeded = dag.nodes.every((node) => {
664
- const s = states.get(node.id);
665
- return s === "succeeded" || s === "skipped";
666
- });
667
- if (!allSucceeded) {
950
+ if (scheduleHasUnhandledFailure(dag, states) || !allTerminal()) {
668
951
  if (retryContext === undefined) {
669
952
  await store.updateRunStatus(run.runId, "failed");
670
953
  }