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,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 {
@@ -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,9 @@
1
1
  import { resolveSkillByName } from "../config/listSkills.js";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { MCP_CATALOG_FILENAME, STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV, StageMcpError, resolveStageMcpServers, stampStagePromptArtifactsDir, } from "../config/resolveStageMcpServers.js";
4
+ import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
2
5
  import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
6
+ import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
3
7
  import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
4
8
  import { resumeSessionFilePath } from "./stageAttemptContext.js";
5
9
  import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
@@ -19,16 +23,49 @@ async function resolveStageSkillForRun(stage, catalog) {
19
23
  }
20
24
  return { ok: true, skillFilePath: resolved.filePath };
21
25
  }
22
- async function openStageWithOperatorCatalog(agent, input, catalog) {
26
+ async function resolveAttemptMcpServers(allowlist, factoryCwd, artifactsDir) {
27
+ const names = allowlist ?? [];
28
+ if (names.length === 0)
29
+ return undefined;
30
+ if (factoryCwd === undefined) {
31
+ throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" is missing`, "missing_catalog");
32
+ }
33
+ await mkdir(artifactsDir, { recursive: true });
34
+ const resolved = await resolveStageMcpServers({
35
+ projectRoot: factoryCwd,
36
+ allowlist: names,
37
+ env: {
38
+ ...process.env,
39
+ [STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV]: artifactsDir,
40
+ },
41
+ });
42
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
43
+ }
44
+ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, artifactsDir) {
23
45
  const skill = await resolveStageSkillForRun(input.stage, catalog);
24
46
  if (!skill.ok)
25
47
  return skill;
48
+ let resolvedMcpServers;
49
+ try {
50
+ resolvedMcpServers = await resolveAttemptMcpServers(input.stage.mcp, factoryCwd, artifactsDir);
51
+ }
52
+ catch (err) {
53
+ if (err instanceof StageMcpError) {
54
+ return { ok: false, reason: err.message };
55
+ }
56
+ throw err;
57
+ }
26
58
  try {
27
59
  const handle = agent.openStage({
28
60
  ...input,
61
+ stage: {
62
+ ...input.stage,
63
+ system_prompt: stampStagePromptArtifactsDir(input.stage.system_prompt, artifactsDir),
64
+ },
29
65
  ...(skill.skillFilePath !== undefined
30
66
  ? { skillFilePath: skill.skillFilePath }
31
67
  : {}),
68
+ ...(resolvedMcpServers !== undefined ? { resolvedMcpServers } : {}),
32
69
  });
33
70
  return { ok: true, handle };
34
71
  }
@@ -48,6 +85,52 @@ function resolveAttemptRoots(input, stageId) {
48
85
  ? withResolvedAuthPath(baseRoots, input.factoryCwd)
49
86
  : baseRoots;
50
87
  }
88
+ function evidencePreview(evidence) {
89
+ if (evidence === undefined)
90
+ return undefined;
91
+ try {
92
+ const text = JSON.stringify(evidence);
93
+ return text.length <= 4_000 ? text : `${text.slice(0, 4_000)}…`;
94
+ }
95
+ catch {
96
+ return "Verification evidence could not be serialized.";
97
+ }
98
+ }
99
+ async function repairContextForAttempt(input, stageId, attempt) {
100
+ if (attempt <= 1)
101
+ return undefined;
102
+ const recovery = input.dag.nodes.find((node) => node.id === stageId)?.recovery;
103
+ if (recovery?.mode !== "repair" && recovery?.mode !== "manual") {
104
+ return undefined;
105
+ }
106
+ const events = await input.store.listStageEvents(input.runId, stageId, attempt);
107
+ const guidance = events
108
+ .slice()
109
+ .reverse()
110
+ .find((event) => event.event === "manual_recovery_requested")?.guidance;
111
+ if (recovery.mode === "manual" && typeof guidance !== "string") {
112
+ return undefined;
113
+ }
114
+ const failed = (await input.store.listVerificationCheckResults(input.runId, stageId, attempt - 1)).filter((check) => check.status === "failed");
115
+ return {
116
+ prior_attempt: attempt - 1,
117
+ ...(typeof guidance === "string" && guidance.trim() !== ""
118
+ ? { operator_guidance: guidance.trim() }
119
+ : {}),
120
+ ...(recovery.include_failed_checks !== false
121
+ ? {
122
+ failed_checks: failed.map((check) => {
123
+ const preview = evidencePreview(check.evidence);
124
+ return {
125
+ id: check.check_id,
126
+ type: check.check_type,
127
+ ...(preview !== undefined ? { evidence_preview: preview } : {}),
128
+ };
129
+ }),
130
+ }
131
+ : {}),
132
+ };
133
+ }
51
134
  export async function openStageAttempt(input) {
52
135
  const stageId = input.stageId ?? input.stage.id;
53
136
  const definitionId = input.stage.id;
@@ -67,7 +150,24 @@ export async function openStageAttempt(input) {
67
150
  const resumeToken = input.resumeToken ??
68
151
  resumeSessionFilePath(input.workspaceDir, stageId, attempt);
69
152
  const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
70
- const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId);
153
+ const snapshot = input.dag;
154
+ const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId, {
155
+ ...(input.stage.clone_actions !== undefined
156
+ ? { allowedActions: input.stage.clone_actions }
157
+ : {}),
158
+ ...(snapshot.clone_input_schema !== undefined
159
+ ? { successorCloneInputSchemas: snapshot.clone_input_schema }
160
+ : {}),
161
+ });
162
+ const completionContract = input.dag.nodes.find((node) => node.id === stageId)?.completion;
163
+ const runtimeNode = input.dag.nodes.find((node) => node.id === stageId);
164
+ const isDynamicCloneInstance = runtimeNode?.definition_id !== undefined &&
165
+ runtimeNode.definition_id !== runtimeNode.id;
166
+ const feedbackLoopEmitContext = isDynamicCloneInstance || runtimeNode?.clonable === true
167
+ ? undefined
168
+ : runtimeNode?.feedback_loop;
169
+ const repairContext = await repairContextForAttempt(input, stageId, attempt);
170
+ const readQaTrail = createAttemptQaTrailReader(input.store, input.runId, stageId, attempt);
71
171
  const opened = await openStageWithOperatorCatalog(input.agent, {
72
172
  roots,
73
173
  stage: input.stage,
@@ -77,11 +177,25 @@ export async function openStageAttempt(input) {
77
177
  ...(priorResult.joinPriors !== undefined
78
178
  ? { priorEnvelopes: priorResult.joinPriors }
79
179
  : {}),
180
+ ...(priorResult.priorEnvelopesByStage !== undefined
181
+ ? { priorEnvelopesByStage: priorResult.priorEnvelopesByStage }
182
+ : {}),
80
183
  resumeToken,
184
+ ...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
81
185
  onActivity: input.onActivity,
82
186
  forkEmitContext,
83
187
  cloneEmitContext,
84
- }, input.operatorCatalog);
188
+ ...(feedbackLoopEmitContext !== undefined ? { feedbackLoopEmitContext } : {}),
189
+ ...(input.feedbackLoopContext !== undefined
190
+ ? { feedbackLoopContext: input.feedbackLoopContext }
191
+ : {}),
192
+ ...(completionContract !== undefined ? { completionContract } : {}),
193
+ ...(repairContext !== undefined ? { repairContext } : {}),
194
+ readQaTrail,
195
+ ...(input.stage.timeout_ms !== undefined
196
+ ? { timeoutMs: input.stage.timeout_ms }
197
+ : {}),
198
+ }, input.operatorCatalog, input.factoryCwd, attemptArtifactsDir(input.workspaceDir, stageId, attempt));
85
199
  if (!opened.ok)
86
200
  return opened;
87
201
  return {
@@ -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,31 @@
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
+ /** Sidecar written before completion command checks so they can inspect fork_choice. */
7
+ export declare const COMPLETION_CANDIDATE_ENVELOPE_FILENAME = "completion-candidate-envelope.json";
8
+ export type VerifiedStageExecution = {
9
+ /** Establish durable observation before agent work begins. */
10
+ prepare(): Promise<void>;
11
+ /** Accept a successful agent result only when its contract passes. */
12
+ verify(candidate: Extract<StageRunResult, {
13
+ ok: true;
14
+ }>): Promise<StageRunResult>;
15
+ };
16
+ export declare function completionVerificationFailureReason(failedCheckIds: readonly string[]): string;
17
+ /**
18
+ * Owns the verification lifecycle for one immutable stage attempt: establish
19
+ * observation before agent work, then persist evidence before accepting a
20
+ * candidate handoff. Agent lifecycle and final stage status stay outside this
21
+ * module in stageRunner.
22
+ */
23
+ export declare function createVerifiedStageExecution(options: {
24
+ store: RunStore;
25
+ runId: string;
26
+ stageId: string;
27
+ attempt: number;
28
+ stage: StageConfig;
29
+ dag?: ResolvedPipelineDag;
30
+ roots: StageRoots;
31
+ }): VerifiedStageExecution;