stageflow 0.8.0 → 0.12.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 (159) hide show
  1. package/README.md +24 -14
  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 +301 -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 +6 -3
  15. package/dist/agent/piAdapter.js +129 -21
  16. package/dist/agent/port.d.ts +50 -1
  17. package/dist/agent/resolveAgentPort.d.ts +26 -0
  18. package/dist/agent/resolveAgentPort.js +39 -0
  19. package/dist/cli/runCommand.js +2 -2
  20. package/dist/cli/runsCommand.d.ts +22 -0
  21. package/dist/cli/runsCommand.js +773 -0
  22. package/dist/cli/validateOutput.d.ts +2 -2
  23. package/dist/cli/validateOutput.js +2 -2
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +43 -5
  26. package/dist/config/browseCatalog.js +1 -1
  27. package/dist/config/createPipeline.d.ts +2 -1
  28. package/dist/config/createPipeline.js +34 -9
  29. package/dist/config/createStage.js +14 -9
  30. package/dist/config/loadPipeline.js +10 -1
  31. package/dist/config/loadStage.js +138 -2
  32. package/dist/config/loadStageflowManifest.js +14 -1
  33. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  34. package/dist/config/mergePipelineIncludes.js +14 -1
  35. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  36. package/dist/config/normalizePipelineStageEntry.js +49 -6
  37. package/dist/config/parseCompletionContract.d.ts +8 -0
  38. package/dist/config/parseCompletionContract.js +218 -0
  39. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  40. package/dist/config/parsePreEmitChecks.js +65 -0
  41. package/dist/config/pipelineNeeds.d.ts +19 -0
  42. package/dist/config/pipelineNeeds.js +151 -0
  43. package/dist/config/pipelineStageKeys.js +16 -1
  44. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  45. package/dist/config/resolveForkEmitContext.js +9 -2
  46. package/dist/config/resolvePipelineDag.d.ts +2 -1
  47. package/dist/config/resolvePipelineDag.js +182 -39
  48. package/dist/config/validateCatalog.d.ts +1 -1
  49. package/dist/config/validateCompletionContract.d.ts +4 -0
  50. package/dist/config/validateCompletionContract.js +52 -0
  51. package/dist/envelope/check.d.ts +1 -1
  52. package/dist/envelope/check.js +86 -13
  53. package/dist/envelope/cloneForks.d.ts +1 -1
  54. package/dist/envelope/cloneForks.js +24 -9
  55. package/dist/envelope/feedbackLoop.d.ts +4 -0
  56. package/dist/envelope/feedbackLoop.js +31 -0
  57. package/dist/envelope/payloadSchema.d.ts +7 -1
  58. package/dist/envelope/payloadSchema.js +170 -13
  59. package/dist/envelope/preEmitChecks.d.ts +29 -0
  60. package/dist/envelope/preEmitChecks.js +57 -0
  61. package/dist/hitl/qaTrail.d.ts +25 -0
  62. package/dist/hitl/qaTrail.js +51 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/mcp/catalogTools.js +4 -2
  66. package/dist/mcp/controlTools.js +72 -0
  67. package/dist/mcp/waitingGates.d.ts +2 -1
  68. package/dist/mcp/waitingGates.js +17 -1
  69. package/dist/package-meta.d.ts +1 -0
  70. package/dist/package-meta.js +1 -0
  71. package/dist/projection/projectRun.d.ts +4 -1
  72. package/dist/projection/projectRun.js +7 -0
  73. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  74. package/dist/prompt/feedbackLoopContext.js +17 -0
  75. package/dist/prompt/priorEnvelope.d.ts +2 -2
  76. package/dist/prompt/priorEnvelope.js +4 -1
  77. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  78. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  79. package/dist/runstore/port.d.ts +185 -3
  80. package/dist/runstore/port.js +42 -3
  81. package/dist/runstore/runProjection.d.ts +7 -3
  82. package/dist/runstore/runProjection.js +30 -15
  83. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  84. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  85. package/dist/runstore/sqlite/schema.d.ts +1 -1
  86. package/dist/runstore/sqlite/schema.js +99 -0
  87. package/dist/runstore/stageInstanceId.d.ts +1 -1
  88. package/dist/runstore/stageInstanceId.js +3 -2
  89. package/dist/runstore/trackProjection.js +59 -27
  90. package/dist/runstore/verificationHistory.d.ts +19 -0
  91. package/dist/runstore/verificationHistory.js +30 -0
  92. package/dist/runtime/cloneSchedule.d.ts +40 -4
  93. package/dist/runtime/cloneSchedule.js +212 -28
  94. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  95. package/dist/runtime/completionCheckRunner.js +365 -0
  96. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  97. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  98. package/dist/runtime/envelopeRouting.d.ts +10 -3
  99. package/dist/runtime/envelopeRouting.js +146 -5
  100. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  101. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  102. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  103. package/dist/runtime/feedbackLoopDecision.js +207 -0
  104. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  105. package/dist/runtime/feedbackLoopRoute.js +13 -0
  106. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  107. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  108. package/dist/runtime/forkGeneration.d.ts +72 -0
  109. package/dist/runtime/forkGeneration.js +295 -0
  110. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  111. package/dist/runtime/gitCheckoutCapability.js +97 -0
  112. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  113. package/dist/runtime/manualRecoveryState.js +73 -0
  114. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  115. package/dist/runtime/pipelineScheduler.js +328 -45
  116. package/dist/runtime/replayLifecycle.d.ts +107 -0
  117. package/dist/runtime/replayLifecycle.js +168 -0
  118. package/dist/runtime/runManager.d.ts +61 -1
  119. package/dist/runtime/runManager.js +501 -7
  120. package/dist/runtime/runRetryCoordinator.d.ts +8 -1
  121. package/dist/runtime/runRetryCoordinator.js +47 -10
  122. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  123. package/dist/runtime/stageAttemptBootstrap.js +79 -1
  124. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  125. package/dist/runtime/stageRecovery.js +1 -1
  126. package/dist/runtime/stageRunner.d.ts +4 -1
  127. package/dist/runtime/stageRunner.js +29 -3
  128. package/dist/runtime/stageWorker.js +69 -2
  129. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  130. package/dist/runtime/verifiedStageExecution.d.ts +29 -0
  131. package/dist/runtime/verifiedStageExecution.js +119 -0
  132. package/dist/server/bootstrap.js +1 -0
  133. package/dist/server/http.js +114 -0
  134. package/dist/server/operatorResults.js +2 -0
  135. package/dist/tools/askOperator.d.ts +161 -5
  136. package/dist/tools/askOperator.js +58 -25
  137. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  138. package/dist/tools/emitStageEnvelope.js +112 -24
  139. package/dist/types/completion.d.ts +43 -0
  140. package/dist/types/completion.js +1 -0
  141. package/dist/types/envelope.d.ts +18 -0
  142. package/dist/types/forkChoice.d.ts +9 -4
  143. package/dist/types/forkChoice.js +1 -1
  144. package/dist/types/pipeline.d.ts +39 -2
  145. package/dist/types/preEmitCheck.d.ts +18 -0
  146. package/dist/types/preEmitCheck.js +1 -0
  147. package/dist/types/stage.d.ts +13 -0
  148. package/dist/types/stageflowManifest.d.ts +2 -0
  149. package/dist/ui/assets/index-CXGz4nL7.js +118 -0
  150. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  151. package/dist/ui/index.html +2 -2
  152. package/package.json +2 -1
  153. package/skills/stageflow/references/control-surface.md +4 -2
  154. package/skills/stageflow-author/references/catalog-mapping.md +26 -2
  155. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  156. package/skills/stageflow-run/SKILL.md +56 -25
  157. package/skills/stageflow-run/references/mcp-call.md +2 -2
  158. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  159. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -1,3 +1,4 @@
1
+ import { findUnhandledFailedStage, } from "../runstore/port.js";
1
2
  import { retryRun, } from "./pipelineScheduler.js";
2
3
  import { loadRunContext } from "./resumeReconstruct.js";
3
4
  import { syncRunStatusFromStages } from "./stageRecovery.js";
@@ -19,6 +20,20 @@ export class RetryRootWaitTimeoutError extends Error {
19
20
  this.name = "RetryRootWaitTimeoutError";
20
21
  }
21
22
  }
23
+ export function succeededStageRetryBlocker(stageSnap) {
24
+ if (stageSnap.definition_id !== undefined &&
25
+ stageSnap.definition_id !== stageSnap.stage_id) {
26
+ return `Cannot re-run a succeeded clone instance stage (stage=${stageSnap.stage_id}, definition=${stageSnap.definition_id}): clone instances are created by upstream fan-out and cannot be rebuilt in place. Use rerun to start a new run.`;
27
+ }
28
+ const forks = stageSnap.envelope?.clone_forks;
29
+ if (Array.isArray(forks) && forks.length > 0) {
30
+ return `Cannot re-run a succeeded clonable-fanout stage (stage=${stageSnap.stage_id}): its envelope declared clone_forks and existing clone instances cannot be pruned. Use rerun to start a new run.`;
31
+ }
32
+ if (stageSnap.envelope?.fork_choice !== undefined) {
33
+ return `Cannot re-run a succeeded fork stage (stage=${stageSnap.stage_id}): its envelope recorded a fork_choice and skipped branches cannot be restored. Use rerun to start a new run.`;
34
+ }
35
+ return undefined;
36
+ }
22
37
  export function assertStageRetryEligible(detail, stageId, opts) {
23
38
  const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
24
39
  if (!stageSnap) {
@@ -35,23 +50,34 @@ export function assertStageRetryEligible(detail, stageId, opts) {
35
50
  status: 409,
36
51
  };
37
52
  }
53
+ const runStatus = opts?.persistedStatus ?? detail.status;
38
54
  const recoveryActive = opts?.recoveryActive === true;
39
- if (detail.status !== "failed") {
40
- if (!(detail.status === "running" && recoveryActive)) {
55
+ if (runStatus !== "failed" && runStatus !== "succeeded") {
56
+ const acceptedPendingRetry = runStatus === "running" &&
57
+ stageSnap.status === "failed" &&
58
+ findUnhandledFailedStage(detail.stages, opts?.dag) === undefined &&
59
+ detail.stages.some((stage) => stage.status === "pending");
60
+ if (!(runStatus === "running" && recoveryActive) && !acceptedPendingRetry) {
41
61
  return {
42
62
  ok: false,
43
- reason: `Run is not failed (status=${detail.status})`,
63
+ reason: `Run is not failed or succeeded (status=${runStatus})`,
44
64
  status: 409,
45
65
  };
46
66
  }
47
67
  }
48
- if (stageSnap.status !== "failed") {
68
+ if (stageSnap.status !== "failed" && stageSnap.status !== "succeeded") {
49
69
  return {
50
70
  ok: false,
51
- reason: `Stage is not failed (status=${stageSnap.status})`,
71
+ reason: `Stage is not failed or succeeded (status=${stageSnap.status})`,
52
72
  status: 409,
53
73
  };
54
74
  }
75
+ if (stageSnap.status === "succeeded") {
76
+ const blocker = succeededStageRetryBlocker(stageSnap);
77
+ if (blocker !== undefined) {
78
+ return { ok: false, reason: blocker, status: 409 };
79
+ }
80
+ }
55
81
  return { ok: true };
56
82
  }
57
83
  function mutationKey(stageId, attempt) {
@@ -60,7 +86,7 @@ function mutationKey(stageId, attempt) {
60
86
  function rootWaitKey(runId, stageId, attempt) {
61
87
  return `${runId}\0${stageId}\0${attempt}`;
62
88
  }
63
- function readRetryRootWaitTimeoutMs() {
89
+ export function readRetryRootWaitTimeoutMs() {
64
90
  const raw = process.env.STAGEFLOW_RETRY_ROOT_WAIT_TIMEOUT_MS;
65
91
  if (raw !== undefined && raw !== "") {
66
92
  const parsed = Number(raw);
@@ -225,8 +251,13 @@ export class RunRetryCoordinator {
225
251
  async retryStage(req) {
226
252
  const { runId, stageId, store, tracking } = req;
227
253
  let detail;
254
+ let metaStatus;
255
+ let dag;
228
256
  try {
229
257
  detail = await store.readRun(runId);
258
+ const meta = await store.readRunMeta(runId);
259
+ metaStatus = meta.status ?? detail.status;
260
+ dag = meta.pipeline_dag;
230
261
  }
231
262
  catch {
232
263
  return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
@@ -234,6 +265,8 @@ export class RunRetryCoordinator {
234
265
  const recoveryActive = this.isActive(runId);
235
266
  const eligibility = assertStageRetryEligible(detail, stageId, {
236
267
  recoveryActive,
268
+ persistedStatus: metaStatus,
269
+ ...(dag !== undefined ? { dag } : {}),
237
270
  });
238
271
  if (!eligibility.ok) {
239
272
  return {
@@ -258,7 +291,7 @@ export class RunRetryCoordinator {
258
291
  status: 409,
259
292
  };
260
293
  }
261
- const priorRunStatus = detail.status;
294
+ const priorRunStatus = metaStatus;
262
295
  let insertedForResume = false;
263
296
  let bumpedRunning = false;
264
297
  try {
@@ -272,7 +305,7 @@ export class RunRetryCoordinator {
272
305
  };
273
306
  }
274
307
  insertedForResume = tracked.insertedForResume === true;
275
- if (priorRunStatus === "failed") {
308
+ if (priorRunStatus === "failed" || priorRunStatus === "succeeded") {
276
309
  await store.updateRunStatus(runId, "running");
277
310
  bumpedRunning = true;
278
311
  }
@@ -286,6 +319,7 @@ export class RunRetryCoordinator {
286
319
  }
287
320
  }
288
321
  const execution = await store.createStageExecution(runId, stageId);
322
+ await req.beforeAttemptStart?.(execution.attempt);
289
323
  const { meta, task, loaded, workspaceDir } = await loadRunContext(store, runId, req.cwd);
290
324
  const prepared = {
291
325
  task,
@@ -298,7 +332,7 @@ export class RunRetryCoordinator {
298
332
  hitl: req.hitl,
299
333
  operatorCatalog: req.operatorCatalog,
300
334
  };
301
- if (recoveryActive) {
335
+ if (this.isActive(runId)) {
302
336
  await this.addRoot(runId, stageId, execution.attempt);
303
337
  }
304
338
  else {
@@ -315,12 +349,15 @@ export class RunRetryCoordinator {
315
349
  tracking.onOrchestrationStarted(runId, orchestrationPromise);
316
350
  }
317
351
  }
318
- await this.waitForRoot(runId, stageId, store);
352
+ if (req.awaitRoot !== false) {
353
+ await this.waitForRoot(runId, stageId, store);
354
+ }
319
355
  return {
320
356
  ok: true,
321
357
  runId,
322
358
  stageId,
323
359
  attemptIndex: execution.attempt,
360
+ done: this.getOrchestrationPromise(runId),
324
361
  };
325
362
  }
326
363
  catch (err) {
@@ -1,4 +1,4 @@
1
- import type { AgentPort, StageHandle, StageRunInput } from "../agent/port.js";
1
+ import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunInput, StageSessionMode } from "../agent/port.js";
2
2
  import type { RunStore } from "../runstore/port.js";
3
3
  import type { StageEnvelope } from "../types/envelope.js";
4
4
  import type { ResolvedPipelineDag } from "../types/pipeline.js";
@@ -25,9 +25,11 @@ export type StageAttemptOpenInput = {
25
25
  onActivity?: StageRunInput["onActivity"];
26
26
  roots?: StageRoots;
27
27
  resumeToken?: string;
28
+ sessionMode?: StageSessionMode;
28
29
  workerRoots?: boolean;
29
30
  completedEnvelopes?: Map<string, StageEnvelope>;
30
31
  stageId?: string;
32
+ feedbackLoopContext?: FeedbackLoopContext;
31
33
  };
32
34
  export type StageAttemptOpenResult = {
33
35
  ok: true;
@@ -1,5 +1,6 @@
1
1
  import { resolveSkillByName } from "../config/listSkills.js";
2
2
  import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
3
+ import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
3
4
  import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
4
5
  import { resumeSessionFilePath } from "./stageAttemptContext.js";
5
6
  import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
@@ -48,6 +49,52 @@ function resolveAttemptRoots(input, stageId) {
48
49
  ? withResolvedAuthPath(baseRoots, input.factoryCwd)
49
50
  : baseRoots;
50
51
  }
52
+ function evidencePreview(evidence) {
53
+ if (evidence === undefined)
54
+ return undefined;
55
+ try {
56
+ const text = JSON.stringify(evidence);
57
+ return text.length <= 4_000 ? text : `${text.slice(0, 4_000)}…`;
58
+ }
59
+ catch {
60
+ return "Verification evidence could not be serialized.";
61
+ }
62
+ }
63
+ async function repairContextForAttempt(input, stageId, attempt) {
64
+ if (attempt <= 1)
65
+ return undefined;
66
+ const recovery = input.dag.nodes.find((node) => node.id === stageId)?.recovery;
67
+ if (recovery?.mode !== "repair" && recovery?.mode !== "manual") {
68
+ return undefined;
69
+ }
70
+ const events = await input.store.listStageEvents(input.runId, stageId, attempt);
71
+ const guidance = events
72
+ .slice()
73
+ .reverse()
74
+ .find((event) => event.event === "manual_recovery_requested")?.guidance;
75
+ if (recovery.mode === "manual" && typeof guidance !== "string") {
76
+ return undefined;
77
+ }
78
+ const failed = (await input.store.listVerificationCheckResults(input.runId, stageId, attempt - 1)).filter((check) => check.status === "failed");
79
+ return {
80
+ prior_attempt: attempt - 1,
81
+ ...(typeof guidance === "string" && guidance.trim() !== ""
82
+ ? { operator_guidance: guidance.trim() }
83
+ : {}),
84
+ ...(recovery.include_failed_checks !== false
85
+ ? {
86
+ failed_checks: failed.map((check) => {
87
+ const preview = evidencePreview(check.evidence);
88
+ return {
89
+ id: check.check_id,
90
+ type: check.check_type,
91
+ ...(preview !== undefined ? { evidence_preview: preview } : {}),
92
+ };
93
+ }),
94
+ }
95
+ : {}),
96
+ };
97
+ }
51
98
  export async function openStageAttempt(input) {
52
99
  const stageId = input.stageId ?? input.stage.id;
53
100
  const definitionId = input.stage.id;
@@ -67,7 +114,24 @@ export async function openStageAttempt(input) {
67
114
  const resumeToken = input.resumeToken ??
68
115
  resumeSessionFilePath(input.workspaceDir, stageId, attempt);
69
116
  const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
70
- const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId);
117
+ const snapshot = input.dag;
118
+ const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId, {
119
+ ...(input.stage.clone_actions !== undefined
120
+ ? { allowedActions: input.stage.clone_actions }
121
+ : {}),
122
+ ...(snapshot.clone_input_schema !== undefined
123
+ ? { successorCloneInputSchemas: snapshot.clone_input_schema }
124
+ : {}),
125
+ });
126
+ const completionContract = input.dag.nodes.find((node) => node.id === stageId)?.completion;
127
+ const runtimeNode = input.dag.nodes.find((node) => node.id === stageId);
128
+ const isDynamicCloneInstance = runtimeNode?.definition_id !== undefined &&
129
+ runtimeNode.definition_id !== runtimeNode.id;
130
+ const feedbackLoopEmitContext = isDynamicCloneInstance || runtimeNode?.clonable === true
131
+ ? undefined
132
+ : runtimeNode?.feedback_loop;
133
+ const repairContext = await repairContextForAttempt(input, stageId, attempt);
134
+ const readQaTrail = createAttemptQaTrailReader(input.store, input.runId, stageId, attempt);
71
135
  const opened = await openStageWithOperatorCatalog(input.agent, {
72
136
  roots,
73
137
  stage: input.stage,
@@ -77,10 +141,24 @@ export async function openStageAttempt(input) {
77
141
  ...(priorResult.joinPriors !== undefined
78
142
  ? { priorEnvelopes: priorResult.joinPriors }
79
143
  : {}),
144
+ ...(priorResult.priorEnvelopesByStage !== undefined
145
+ ? { priorEnvelopesByStage: priorResult.priorEnvelopesByStage }
146
+ : {}),
80
147
  resumeToken,
148
+ ...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
81
149
  onActivity: input.onActivity,
82
150
  forkEmitContext,
83
151
  cloneEmitContext,
152
+ ...(feedbackLoopEmitContext !== undefined ? { feedbackLoopEmitContext } : {}),
153
+ ...(input.feedbackLoopContext !== undefined
154
+ ? { feedbackLoopContext: input.feedbackLoopContext }
155
+ : {}),
156
+ ...(completionContract !== undefined ? { completionContract } : {}),
157
+ ...(repairContext !== undefined ? { repairContext } : {}),
158
+ readQaTrail,
159
+ ...(input.stage.timeout_ms !== undefined
160
+ ? { timeoutMs: input.stage.timeout_ms }
161
+ : {}),
84
162
  }, input.operatorCatalog);
85
163
  if (!opened.ok)
86
164
  return opened;
@@ -3,7 +3,7 @@ export type StageLaunchInput = {
3
3
  runId: string;
4
4
  stageId: string;
5
5
  rootDir: string;
6
- mode?: "run" | "resume";
6
+ mode?: "run" | "resume" | "feedback_resume" | "new_session";
7
7
  resumeAnswer?: unknown;
8
8
  attempt?: number;
9
9
  sessionFilePath?: string;
@@ -38,7 +38,7 @@ export async function syncRunStatusFromStages(store, runId) {
38
38
  if (meta.status === "succeeded")
39
39
  return;
40
40
  const run = await store.readRun(runId);
41
- const derived = deriveStatusFromStages(run.stages);
41
+ const derived = deriveStatusFromStages(run.stages, meta.pipeline_dag);
42
42
  if ((meta.status ?? "created") !== derived) {
43
43
  await store.updateRunStatus(runId, derived);
44
44
  }
@@ -1,4 +1,4 @@
1
- import type { AgentPort, StageHandle, StageRunResult } from "../agent/port.js";
1
+ import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunResult, StageSessionMode } from "../agent/port.js";
2
2
  import type { StageEnvelope } from "../types/envelope.js";
3
3
  import type { StageConfig } from "../types/stage.js";
4
4
  import type { TaskFile } from "../types/task.js";
@@ -45,6 +45,9 @@ export type RunStageOptions = {
45
45
  completedEnvelopes?: Map<string, StageEnvelope>;
46
46
  skipGates?: boolean;
47
47
  stageId?: string;
48
+ sessionMode?: StageSessionMode;
49
+ feedbackLoopContext?: FeedbackLoopContext;
50
+ resumeToken?: string;
48
51
  };
49
52
  /**
50
53
  * Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
@@ -2,6 +2,7 @@ import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
2
2
  import { buildStageRoots, withResolvedAuthPath, } from "./stageRoots.js";
3
3
  import { createDefaultHitlQaHooks, recordWaitEnter, } from "./stageHitl.js";
4
4
  import { openStageAttempt, } from "./stageAttemptBootstrap.js";
5
+ import { createVerifiedStageExecution } from "./verifiedStageExecution.js";
5
6
  export function isRunStageWaiting(outcome) {
6
7
  return "waiting" in outcome && outcome.waiting === true;
7
8
  }
@@ -113,7 +114,7 @@ export async function runStageYieldLoop(options) {
113
114
  }
114
115
  }
115
116
  export async function runStage(options) {
116
- const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, } = options;
117
+ const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, } = options;
117
118
  const stageId = options.stageId ?? stage.id;
118
119
  const attemptOpt = attemptCtx?.eventOptions();
119
120
  const baseRoots = rootsOverride ??
@@ -133,6 +134,16 @@ export async function runStage(options) {
133
134
  attemptCtx,
134
135
  });
135
136
  }
137
+ const verifiedExecution = createVerifiedStageExecution({
138
+ store,
139
+ runId,
140
+ stageId,
141
+ attempt,
142
+ stage,
143
+ dag,
144
+ roots,
145
+ });
146
+ await verifiedExecution.prepare();
136
147
  console.error(`Running stage ${stageId} (${stage.model})...`);
137
148
  let activityChain = Promise.resolve();
138
149
  const enqueueActivity = (event) => {
@@ -152,7 +163,7 @@ export async function runStage(options) {
152
163
  task,
153
164
  dag: dag ?? {
154
165
  nodes: [
155
- { id: stageId, needs: null, ancestors: [], stageIndex: 0 },
166
+ { id: stageId, needs: null, needsEdges: [], ancestors: [], stageIndex: 0 },
156
167
  ],
157
168
  roots: [stageId],
158
169
  childrenOf: {},
@@ -164,6 +175,9 @@ export async function runStage(options) {
164
175
  operatorCatalog,
165
176
  roots,
166
177
  completedEnvelopes,
178
+ ...(sessionMode !== undefined ? { sessionMode } : {}),
179
+ ...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
180
+ ...(resumeToken !== undefined ? { resumeToken } : {}),
167
181
  onActivity: (event) => {
168
182
  enqueueActivity(event);
169
183
  },
@@ -215,11 +229,23 @@ export async function runStage(options) {
215
229
  return { waiting: true };
216
230
  }
217
231
  try {
232
+ let result = outcome;
233
+ if (outcome.ok) {
234
+ try {
235
+ result = await verifiedExecution.verify(outcome);
236
+ }
237
+ catch (error) {
238
+ result = {
239
+ ok: false,
240
+ reason: `Completion verification errored: ${error instanceof Error ? error.message : String(error)}`,
241
+ };
242
+ }
243
+ }
218
244
  return await finalizeStageResult({
219
245
  store,
220
246
  runId,
221
247
  stageId,
222
- result: outcome,
248
+ result,
223
249
  activityChain,
224
250
  attemptCtx,
225
251
  });
@@ -1,4 +1,6 @@
1
- import { PiAgentAdapter } from "../agent/piAdapter.js";
1
+ import { resolveAgentPort } from "../agent/resolveAgentPort.js";
2
+ import { asAgentBackendId } from "../agent/agentBackend.js";
3
+ import { loadStageflowManifestOutcome } from "../config/loadStageflowManifest.js";
2
4
  import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
3
5
  import { createRunStore } from "../runstore/createStore.js";
4
6
  import { loadRunContext } from "./resumeReconstruct.js";
@@ -6,6 +8,7 @@ import { bindPiAgentDirEnv, rootsForStageWorker, withResolvedAuthPath, } from ".
6
8
  import { runStage, isRunStageWaiting, } from "./stageRunner.js";
7
9
  import { openStageAttempt } from "./stageAttemptBootstrap.js";
8
10
  import { attemptContext } from "./stageAttemptContext.js";
11
+ import { loadActiveFeedbackLoopContext } from "./feedbackLoopCoordinator.js";
9
12
  import { outcomeToWorkerResult, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
10
13
  export async function runStageWorker(input) {
11
14
  const store = createRunStore({ rootDir: input.rootDir });
@@ -19,7 +22,14 @@ export async function runStageWorker(input) {
19
22
  };
20
23
  }
21
24
  const dag = meta.pipeline_dag ?? loaded.dag;
22
- const agent = new PiAgentAdapter();
25
+ const manifestOutcome = await loadStageflowManifestOutcome(input.rootDir);
26
+ const agent = resolveAgentPort({
27
+ global: manifestOutcome.ok
28
+ ? asAgentBackendId(manifestOutcome.value.manifest.agent)
29
+ : undefined,
30
+ pipeline: asAgentBackendId(loaded.pipeline.agent),
31
+ stage: asAgentBackendId(stage.agent),
32
+ });
23
33
  const workspaceDir = store.getWorkspaceDir(input.runId);
24
34
  const checkoutRoot = meta.checkout_root;
25
35
  const mode = input.mode ?? "run";
@@ -45,6 +55,7 @@ export async function runStageWorker(input) {
45
55
  operatorCatalog: input.operatorCatalog,
46
56
  roots,
47
57
  resumeToken: input.sessionFilePath,
58
+ sessionMode: "waiting_resume",
48
59
  onActivity: (event) => {
49
60
  void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
50
61
  },
@@ -74,6 +85,62 @@ export async function runStageWorker(input) {
74
85
  });
75
86
  return outcome;
76
87
  }
88
+ if (mode === "feedback_resume" || mode === "new_session") {
89
+ const feedbackLoopContext = await loadActiveFeedbackLoopContext(store, input.runId, input.stageId);
90
+ if (mode === "feedback_resume" && feedbackLoopContext === undefined) {
91
+ return {
92
+ ok: false,
93
+ reason: "feedback_resume requires an active feedback-loop context",
94
+ };
95
+ }
96
+ const priorAttempt = feedbackLoopContext?.prior_stage_attempt;
97
+ const resumeToken = input.sessionFilePath ??
98
+ (mode === "feedback_resume" && priorAttempt !== undefined
99
+ ? attemptContext(priorAttempt).sessionPath(workspaceDir, input.stageId)
100
+ : undefined);
101
+ const opened = await openStageAttempt({
102
+ agent,
103
+ store,
104
+ runId: input.runId,
105
+ stage,
106
+ stageId: input.stageId,
107
+ task,
108
+ dag,
109
+ checkoutRoot,
110
+ workspaceDir,
111
+ factoryCwd: input.rootDir,
112
+ attemptCtx,
113
+ operatorCatalog: input.operatorCatalog,
114
+ roots,
115
+ ...(resumeToken !== undefined ? { resumeToken } : {}),
116
+ sessionMode: mode,
117
+ ...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
118
+ onActivity: (event) => {
119
+ void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
120
+ },
121
+ });
122
+ if (!opened.ok) {
123
+ return { ok: false, reason: opened.reason };
124
+ }
125
+ return runStage({
126
+ agent,
127
+ store,
128
+ runId: input.runId,
129
+ stage,
130
+ stageId: input.stageId,
131
+ task,
132
+ dag,
133
+ checkoutRoot,
134
+ workspaceDir,
135
+ existingHandle: opened.handle,
136
+ workerMode: true,
137
+ roots,
138
+ attemptCtx,
139
+ factoryCwd: input.rootDir,
140
+ operatorCatalog: input.operatorCatalog,
141
+ skipGates: input.skipGates,
142
+ });
143
+ }
77
144
  return runStage({
78
145
  agent,
79
146
  store,
@@ -10,7 +10,7 @@ export type StageWorkerInput = {
10
10
  runId: string;
11
11
  stageId: string;
12
12
  rootDir: string;
13
- mode?: "run" | "resume";
13
+ mode?: "run" | "resume" | "feedback_resume" | "new_session";
14
14
  resumeAnswer?: unknown;
15
15
  attempt?: number;
16
16
  sessionFilePath?: string;
@@ -0,0 +1,29 @@
1
+ import type { StageRunResult } from "../agent/port.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import type { ResolvedPipelineDag } from "../types/pipeline.js";
4
+ import type { StageConfig } from "../types/stage.js";
5
+ import type { StageRoots } from "./stageRoots.js";
6
+ export type VerifiedStageExecution = {
7
+ /** Establish durable observation before agent work begins. */
8
+ prepare(): Promise<void>;
9
+ /** Accept a successful agent result only when its contract passes. */
10
+ verify(candidate: Extract<StageRunResult, {
11
+ ok: true;
12
+ }>): Promise<StageRunResult>;
13
+ };
14
+ export declare function completionVerificationFailureReason(failedCheckIds: readonly string[]): string;
15
+ /**
16
+ * Owns the verification lifecycle for one immutable stage attempt: establish
17
+ * observation before agent work, then persist evidence before accepting a
18
+ * candidate handoff. Agent lifecycle and final stage status stay outside this
19
+ * module in stageRunner.
20
+ */
21
+ export declare function createVerifiedStageExecution(options: {
22
+ store: RunStore;
23
+ runId: string;
24
+ stageId: string;
25
+ attempt: number;
26
+ stage: StageConfig;
27
+ dag?: ResolvedPipelineDag;
28
+ roots: StageRoots;
29
+ }): VerifiedStageExecution;
@@ -0,0 +1,119 @@
1
+ import { listQaExchanges } from "../hitl/qaTrail.js";
2
+ import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
3
+ import { COMPLETION_VERIFICATION_FAILURE_PREFIX, } from "../types/completion.js";
4
+ import { runCompletionContract, } from "./completionCheckRunner.js";
5
+ import { readCompletionCheckoutBaseline, writeCompletionCheckoutBaseline, } from "./completionCheckoutBaseline.js";
6
+ import { createGitCheckoutCapability } from "./gitCheckoutCapability.js";
7
+ export function completionVerificationFailureReason(failedCheckIds) {
8
+ return `${COMPLETION_VERIFICATION_FAILURE_PREFIX} ${failedCheckIds.join(", ")}`;
9
+ }
10
+ function gateDecisionsForAttempt(options) {
11
+ const { store, runId, stageId, attempt } = options;
12
+ return {
13
+ async listDecisions(kind) {
14
+ const events = await store.listStageEvents(runId, stageId, attempt);
15
+ return listQaExchanges(events)
16
+ .filter((exchange) => exchange.prompt.kind === kind && exchange.answer !== null)
17
+ .map((exchange) => {
18
+ const answer = exchange.answer;
19
+ if (answer.kind === "confirm" || answer.kind === "artifact_backed") {
20
+ return {
21
+ kind,
22
+ prompt_id: exchange.prompt.id,
23
+ status: answer.decision === "accept" ? "accepted" : "rejected",
24
+ };
25
+ }
26
+ return { kind, prompt_id: exchange.prompt.id, status: "answered" };
27
+ });
28
+ },
29
+ };
30
+ }
31
+ /**
32
+ * Owns the verification lifecycle for one immutable stage attempt: establish
33
+ * observation before agent work, then persist evidence before accepting a
34
+ * candidate handoff. Agent lifecycle and final stage status stay outside this
35
+ * module in stageRunner.
36
+ */
37
+ export function createVerifiedStageExecution(options) {
38
+ const { store, runId, stageId, attempt, stage, dag, roots } = options;
39
+ const contract = dag?.nodes.find((node) => node.id === stageId)?.completion;
40
+ let checkout;
41
+ let checkoutBefore;
42
+ let checkoutError;
43
+ return {
44
+ async prepare() {
45
+ if (!contract?.checks.some((check) => check.type === "checkout_changes")) {
46
+ return;
47
+ }
48
+ if (roots.checkoutRoot === undefined) {
49
+ checkoutError = "checkout check requires a bound checkout";
50
+ return;
51
+ }
52
+ checkout = createGitCheckoutCapability(roots.checkoutRoot);
53
+ try {
54
+ checkoutBefore = await readCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt);
55
+ if (checkoutBefore === undefined) {
56
+ checkoutBefore = await checkout.capture();
57
+ await writeCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt, checkoutBefore);
58
+ }
59
+ }
60
+ catch (error) {
61
+ checkoutError = `could not capture checkout baseline: ${error instanceof Error ? error.message : String(error)}`;
62
+ }
63
+ },
64
+ async verify(candidate) {
65
+ if (contract === undefined)
66
+ return candidate;
67
+ try {
68
+ const verification = await runCompletionContract({
69
+ contract,
70
+ envelope: candidate.envelope,
71
+ payloadSchema: stage.payload_schema,
72
+ artifactsDir: attemptArtifactsDir(roots.runWorkspaceDir, stageId, attempt),
73
+ commandWorkingDirectory: roots.cwd,
74
+ gates: gateDecisionsForAttempt({ store, runId, stageId, attempt }),
75
+ checkout,
76
+ checkoutBefore,
77
+ checkoutError,
78
+ checklistAttestations: candidate.envelope.checklist_attestations,
79
+ onCheckStart: async (check) => {
80
+ await store.upsertVerificationCheckResult(runId, stageId, {
81
+ check_id: check.id,
82
+ check_type: check.type,
83
+ status: "running",
84
+ started_at: new Date().toISOString(),
85
+ }, { attempt });
86
+ },
87
+ onCheckComplete: async (check) => {
88
+ await store.upsertVerificationCheckResult(runId, stageId, {
89
+ check_id: check.check_id,
90
+ check_type: check.check_type,
91
+ status: check.outcome === "passed" ? "passed" : "failed",
92
+ finished_at: new Date().toISOString(),
93
+ evidence: {
94
+ ...check.evidence,
95
+ ...(check.message !== undefined ? { message: check.message } : {}),
96
+ ...(check.outcome === "error" ? { runner_outcome: "error" } : {}),
97
+ },
98
+ }, { attempt });
99
+ },
100
+ });
101
+ await store.updateStageExecution(runId, stageId, attempt, {
102
+ verification_outcome: verification.outcome,
103
+ });
104
+ if (verification.outcome === "passed")
105
+ return candidate;
106
+ return {
107
+ ok: false,
108
+ reason: completionVerificationFailureReason(verification.failed_check_ids),
109
+ };
110
+ }
111
+ catch (error) {
112
+ await store.updateStageExecution(runId, stageId, attempt, {
113
+ verification_outcome: "error",
114
+ });
115
+ throw error;
116
+ }
117
+ },
118
+ };
119
+ }
@@ -37,6 +37,7 @@ export async function bootstrapStageflowHost(options) {
37
37
  });
38
38
  await manager.attachWaitingStages();
39
39
  await manager.reconcileOrphanedStages();
40
+ await manager.resumeStalledSchedules();
40
41
  const mcpStateless = resolveMcpStateless({
41
42
  mcpStateless: options.mcpStateless,
42
43
  });