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
@@ -3,6 +3,7 @@ import { mapRetryStageFailure, mapStartFailure, } from "../server/operatorResult
3
3
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
4
4
  import { textResult } from "./toolResults.js";
5
5
  import { projectWaitingGates } from "./waitingGates.js";
6
+ import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
6
7
  export function registerControlTools(server, deps) {
7
8
  const { manager, store } = deps;
8
9
  server.registerTool("list_waiting", {
@@ -36,6 +37,30 @@ export function registerControlTools(server, deps) {
36
37
  }
37
38
  return textResult({ ok: true });
38
39
  });
40
+ server.registerTool("decide_feedback_loop", {
41
+ description: "Resolve a feedback-loop wait_for_human decision (extend, continue, or abandon). Same semantics as POST /api/runs/:id/stages/:stageId/feedback-decision.",
42
+ inputSchema: z.object({
43
+ runId: z.string(),
44
+ stageId: z.string(),
45
+ decision: z.enum(["extend", "continue", "abandon"]),
46
+ loopId: z.string().optional(),
47
+ reason: z.string().optional(),
48
+ }),
49
+ }, async ({ runId, stageId, decision, loopId, reason }) => {
50
+ const result = await manager.decideFeedbackLoop(runId, stageId, {
51
+ decision,
52
+ ...(loopId !== undefined ? { loopId } : {}),
53
+ ...(reason !== undefined ? { reason } : {}),
54
+ });
55
+ if (!result.ok) {
56
+ return textResult({ error: result.reason, status: result.status }, true);
57
+ }
58
+ return textResult({
59
+ ok: true,
60
+ effect: result.effect,
61
+ loopId: result.loopId,
62
+ });
63
+ });
39
64
  server.registerTool("list_stage_events", {
40
65
  description: "List persisted stage log events for a run stage (optional attempt filter). Prefer over bloating get_run.",
41
66
  inputSchema: z.object({
@@ -66,6 +91,53 @@ export function registerControlTools(server, deps) {
66
91
  return textResult({ error: message, status: notFound ? 404 : 500 }, true);
67
92
  }
68
93
  });
94
+ server.registerTool("get_stage_verification", {
95
+ description: "Read every execution attempt for a stage, including durable completion-check results and their evidence. Use this to understand an automatic repair or a failed verification.",
96
+ inputSchema: z.object({
97
+ runId: z.string(),
98
+ stageId: z.string(),
99
+ }),
100
+ }, async ({ runId, stageId }) => {
101
+ try {
102
+ return textResult(await readStageVerificationHistory(store, runId, stageId));
103
+ }
104
+ catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err);
106
+ const notFound = /not found|no such/i.test(message);
107
+ return textResult({ error: message, status: notFound ? 404 : 500 }, true);
108
+ }
109
+ });
110
+ server.registerTool("recover_manual_stage", {
111
+ description: "Explicitly approve one fresh attempt for a manual-recovery stage after a completion verification failure. Optional guidance is recorded and supplied to the agent with the failed-check evidence.",
112
+ inputSchema: z.object({
113
+ runId: z.string(),
114
+ stageId: z.string(),
115
+ guidance: z.string().max(4_000).optional(),
116
+ }),
117
+ }, async ({ runId, stageId, guidance }) => {
118
+ const result = await manager.recoverManualStage(runId, stageId, guidance);
119
+ if (!result.ok) {
120
+ return textResult({ ...mapRetryStageFailure(result), status: result.status }, true);
121
+ }
122
+ return textResult({
123
+ runId: result.runId,
124
+ stageId: result.stageId,
125
+ attemptIndex: result.attemptIndex,
126
+ });
127
+ });
128
+ server.registerTool("stop_manual_recovery", {
129
+ description: "Record the operator's decision to leave a manual-recovery stage failed. This is terminal for that stage; start a fresh run to try again later.",
130
+ inputSchema: z.object({
131
+ runId: z.string(),
132
+ stageId: z.string(),
133
+ }),
134
+ }, async ({ runId, stageId }) => {
135
+ const result = await manager.stopManualRecovery(runId, stageId);
136
+ if (!result.ok) {
137
+ return textResult({ error: result.reason, status: result.status }, true);
138
+ }
139
+ return textResult(result);
140
+ });
69
141
  server.registerTool("get_envelope", {
70
142
  description: "Read the full StageEnvelope for a run stage (latest attempt). Returns 404 when absent.",
71
143
  inputSchema: z.object({
@@ -1,10 +1,11 @@
1
- import type { RunStore, RunSummary, StageSnapshot } from "../runstore/port.js";
1
+ import type { RunDetail, RunStore, RunSummary, StageSnapshot } from "../runstore/port.js";
2
2
  type WaitingSummarySource = Pick<RunSummary, "run_id" | "waiting_stage_id" | "waiting_stage_ids" | "waiting_kind" | "waiting_summary" | "waiting_prompt_id" | "waiting_artifacts" | "waiting_questions">;
3
3
  export declare function projectWaitingGate(opts: {
4
4
  runId: string;
5
5
  stageId: string;
6
6
  stage: StageSnapshot;
7
7
  summary: WaitingSummarySource;
8
+ activeFeedbackLoop?: RunDetail["active_feedback_loop"];
8
9
  }): Record<string, unknown>;
9
10
  export declare function projectWaitingGates(store: RunStore, opts?: {
10
11
  runId?: string;
@@ -1,5 +1,5 @@
1
1
  export function projectWaitingGate(opts) {
2
- const { runId, stageId, stage, summary } = opts;
2
+ const { runId, stageId, stage, summary, activeFeedbackLoop } = opts;
3
3
  const prompt = stage.pending_prompt;
4
4
  const item = {
5
5
  runId,
@@ -36,6 +36,21 @@ export function projectWaitingGate(opts) {
36
36
  item.waiting_questions = summary.waiting_questions;
37
37
  }
38
38
  }
39
+ if ((item.waiting_kind === "feedback_loop_decision" ||
40
+ activeFeedbackLoop?.state === "waiting_for_human") &&
41
+ activeFeedbackLoop !== undefined) {
42
+ item.feedback_loop_id = activeFeedbackLoop.loop_id;
43
+ if (activeFeedbackLoop.deferred_send_back?.target !== undefined) {
44
+ item.deferred_target = activeFeedbackLoop.deferred_send_back.target;
45
+ }
46
+ if (item.waiting_kind === undefined) {
47
+ item.waiting_kind = "feedback_loop_decision";
48
+ }
49
+ if (item.waiting_summary === undefined) {
50
+ item.waiting_summary =
51
+ "Feedback loop limit reached — extend, continue, or abandon";
52
+ }
53
+ }
39
54
  return item;
40
55
  }
41
56
  function projectGatesForRun(detail, summary) {
@@ -51,6 +66,7 @@ function projectGatesForRun(detail, summary) {
51
66
  stageId,
52
67
  stage,
53
68
  summary,
69
+ activeFeedbackLoop: detail.active_feedback_loop,
54
70
  }));
55
71
  }
56
72
  return items;
@@ -1 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
+ export declare const PACKAGE_VERSION = "0.12.0";
@@ -1 +1,2 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
+ export const PACKAGE_VERSION = "0.12.0";
@@ -1,4 +1,4 @@
1
- import type { RunDetail, StageSnapshot } from "../runstore/port.js";
1
+ import type { FeedbackLoopHistory, FeedbackLoopRecord, RunDetail, StageSnapshot } from "../runstore/port.js";
2
2
  import type { PipelineTrackProjection, RunSummary } from "../runstore/port.js";
3
3
  import type { AskOperatorPrompt } from "../tools/askOperator.js";
4
4
  import type { StageEnvelope } from "../types/envelope.js";
@@ -13,6 +13,7 @@ export type StageProjection = {
13
13
  payload?: Record<string, unknown>;
14
14
  fork_choice?: string[];
15
15
  clone_forks?: CloneForkItem[];
16
+ feedback_loop?: StageEnvelope["feedback_loop"];
16
17
  } | null;
17
18
  artifacts: string[];
18
19
  last_at?: string;
@@ -39,6 +40,8 @@ export type RunProjection = {
39
40
  waiting_questions?: RunSummary["waiting_questions"];
40
41
  failed_stage_id?: RunSummary["failed_stage_id"];
41
42
  failed_reason?: RunSummary["failed_reason"];
43
+ active_feedback_loop?: FeedbackLoopRecord;
44
+ feedback_loops: FeedbackLoopHistory[];
42
45
  };
43
46
  export declare function projectRun(detail: RunDetail): RunProjection;
44
47
  export { projectRun as projectRunForMcp };
@@ -41,6 +41,10 @@ export function projectRun(detail) {
41
41
  ...(detail.failed_reason !== undefined
42
42
  ? { failed_reason: detail.failed_reason }
43
43
  : {}),
44
+ ...(detail.active_feedback_loop !== undefined
45
+ ? { active_feedback_loop: detail.active_feedback_loop }
46
+ : {}),
47
+ feedback_loops: detail.feedback_loops ?? [],
44
48
  stages: detail.stages.map((stage) => ({
45
49
  stage_id: stage.stage_id,
46
50
  status: stage.status,
@@ -58,6 +62,9 @@ export function projectRun(detail) {
58
62
  ...(stage.envelope.clone_forks !== undefined
59
63
  ? { clone_forks: stage.envelope.clone_forks }
60
64
  : {}),
65
+ ...(stage.envelope.feedback_loop !== undefined
66
+ ? { feedback_loop: stage.envelope.feedback_loop }
67
+ : {}),
61
68
  }
62
69
  : null,
63
70
  artifacts: stage.artifacts,
@@ -0,0 +1,2 @@
1
+ import type { FeedbackLoopContext, StageSessionMode } from "../agent/port.js";
2
+ export declare function formatFeedbackLoopContext(ctx: FeedbackLoopContext, sessionMode?: StageSessionMode): string;
@@ -0,0 +1,17 @@
1
+ function resolveFeedbackSessionMode(ctx, sessionMode) {
2
+ if (sessionMode === "new_session" || sessionMode === "feedback_resume") {
3
+ return sessionMode;
4
+ }
5
+ return ctx.replay_session === "new_session" ? "new_session" : "feedback_resume";
6
+ }
7
+ export function formatFeedbackLoopContext(ctx, sessionMode) {
8
+ const mode = resolveFeedbackSessionMode(ctx, sessionMode);
9
+ const continuity = mode === "new_session"
10
+ ? "starting a fresh agent session"
11
+ : "continuing the prior agent session";
12
+ return [
13
+ "Feedback Loop Context:",
14
+ `Session mode: ${mode} (${continuity})`,
15
+ JSON.stringify(ctx, null, 2),
16
+ ].join("\n");
17
+ }
@@ -1,2 +1,2 @@
1
- import type { StageEnvelope } from "../types/envelope.js";
2
- export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null, priorEnvelopes?: StageEnvelope[]): string;
1
+ import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
2
+ export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null, priorEnvelopes?: StageEnvelope[], priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>): string;
@@ -1,4 +1,7 @@
1
- export function formatPriorEnvelope(priorEnvelope, priorEnvelopes) {
1
+ export function formatPriorEnvelope(priorEnvelope, priorEnvelopes, priorEnvelopesByStage) {
2
+ if (priorEnvelopesByStage !== undefined) {
3
+ return `Prior envelopes by stage (declaration order):\n${JSON.stringify(priorEnvelopesByStage, null, 2)}`;
4
+ }
2
5
  if (priorEnvelopes !== undefined && priorEnvelopes.length > 0) {
3
6
  return `Prior envelopes (${priorEnvelopes.length} clones, clone-list order):\n${JSON.stringify(priorEnvelopes, null, 2)}`;
4
7
  }
@@ -8,6 +8,7 @@ export declare function appendCloneInstances(snapshot: RunPipelineDagSnapshot, i
8
8
  catalogId: string;
9
9
  predecessorId: string;
10
10
  count: number;
11
+ startAt?: number;
11
12
  }): {
12
13
  snapshot: RunPipelineDagSnapshot;
13
14
  instanceIds: string[];
@@ -1,10 +1,15 @@
1
+ import { hydrateResolvedNeeds } from "../config/pipelineNeeds.js";
1
2
  import { mintCloneInstanceIds } from "./stageInstanceId.js";
2
3
  export function buildPipelineDagSnapshotFromLoaded(loaded) {
3
4
  const gate_kinds = {};
5
+ const clone_input_schema = {};
4
6
  for (const stage of loaded.stages) {
5
- if (stage.gate_kinds?.length) {
7
+ if (stage.gate_kinds !== undefined) {
6
8
  gate_kinds[stage.id] = stage.gate_kinds;
7
9
  }
10
+ if (stage.clone_input_schema !== undefined) {
11
+ clone_input_schema[stage.id] = stage.clone_input_schema;
12
+ }
8
13
  }
9
14
  return {
10
15
  stage_ids: [...loaded.pipeline.stages],
@@ -15,12 +20,16 @@ export function buildPipelineDagSnapshotFromLoaded(loaded) {
15
20
  roots: loaded.dag.roots,
16
21
  childrenOf: loaded.dag.childrenOf,
17
22
  ...(Object.keys(gate_kinds).length > 0 ? { gate_kinds } : {}),
23
+ ...(Object.keys(clone_input_schema).length > 0
24
+ ? { clone_input_schema }
25
+ : {}),
18
26
  };
19
27
  }
20
28
  export function linearCompatDagSnapshot(stageIds) {
21
29
  const nodes = stageIds.map((id, index) => ({
22
30
  id,
23
31
  needs: index === 0 ? null : stageIds[index - 1],
32
+ needsEdges: index === 0 ? [] : [{ id: stageIds[index - 1], on: ["succeeded"] }],
24
33
  ancestors: index === 0 ? [] : stageIds.slice(0, index),
25
34
  stageIndex: index,
26
35
  definition_id: id,
@@ -44,7 +53,14 @@ export function parsePipelineDagSnapshot(raw) {
44
53
  const value = raw;
45
54
  if (!Array.isArray(value.stage_ids) || !Array.isArray(value.nodes))
46
55
  return null;
47
- return raw;
56
+ const nodes = value.nodes.map((node) => {
57
+ if (!node || typeof node !== "object")
58
+ return node;
59
+ const record = node;
60
+ const hydrated = hydrateResolvedNeeds(record);
61
+ return { ...record, ...hydrated };
62
+ });
63
+ return { ...raw, nodes };
48
64
  }
49
65
  export function instancesOfDefinition(snapshot, catalogId) {
50
66
  const nodeById = new Map(snapshot.nodes.map((n) => [n.id, n]));
@@ -54,15 +70,24 @@ export function instancesOfDefinition(snapshot, catalogId) {
54
70
  });
55
71
  }
56
72
  export function appendCloneInstances(snapshot, input) {
57
- const { catalogId, predecessorId, count } = input;
58
- if (!snapshot.stage_ids.includes(catalogId)) {
73
+ const { catalogId, predecessorId, count, startAt } = input;
74
+ // A catalog id that's already been fanned out once is no longer present as
75
+ // its own stage_ids entry — it was replaced by its instances. Minting
76
+ // *more* instances for it (a retried fanout requesting a larger clone
77
+ // count) is still valid as long as those instances exist.
78
+ const existingInstanceIdxs = snapshot.stage_ids
79
+ .map((id, i) => [id, i])
80
+ .filter(([id]) => id !== catalogId && id.startsWith(`${catalogId}~`))
81
+ .map(([, i]) => i);
82
+ const catalogIdx = snapshot.stage_ids.indexOf(catalogId);
83
+ if (catalogIdx === -1 && existingInstanceIdxs.length === 0) {
59
84
  throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
60
85
  }
61
86
  const predNode = snapshot.nodes.find((n) => n.id === predecessorId);
62
87
  if (!predNode) {
63
88
  throw new Error(`predecessor "${predecessorId}" is not in the run DAG snapshot`);
64
89
  }
65
- const instanceIds = mintCloneInstanceIds(catalogId, count);
90
+ const instanceIds = mintCloneInstanceIds(catalogId, count, startAt);
66
91
  for (const id of instanceIds) {
67
92
  if (snapshot.stage_ids.includes(id) ||
68
93
  snapshot.nodes.some((n) => n.id === id)) {
@@ -70,22 +95,37 @@ export function appendCloneInstances(snapshot, input) {
70
95
  }
71
96
  }
72
97
  const catalogNode = snapshot.nodes.find((n) => n.id === catalogId);
73
- if (!catalogNode) {
98
+ const templateNode = catalogNode ?? snapshot.nodes.find((n) => n.definition_id === catalogId);
99
+ if (!templateNode) {
74
100
  throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
75
101
  }
76
- const catalogIdx = snapshot.stage_ids.indexOf(catalogId);
102
+ const insertAt = catalogIdx !== -1 ? catalogIdx : Math.max(...existingInstanceIdxs) + 1;
103
+ const removeAt = catalogIdx !== -1 ? catalogIdx + 1 : insertAt;
77
104
  const stage_ids = [
78
- ...snapshot.stage_ids.slice(0, catalogIdx),
105
+ ...snapshot.stage_ids.slice(0, insertAt),
79
106
  ...instanceIds,
80
- ...snapshot.stage_ids.slice(catalogIdx + 1),
107
+ ...snapshot.stage_ids.slice(removeAt),
81
108
  ];
82
109
  const instanceNodes = instanceIds.map((id) => ({
83
110
  id,
84
111
  definition_id: catalogId,
85
112
  needs: predecessorId,
113
+ needsEdges: [{ id: predecessorId, on: ["succeeded"] }],
86
114
  ancestors: [...predNode.ancestors, predecessorId],
87
115
  stageIndex: 0,
88
- ...(catalogNode.fork !== undefined ? { fork: catalogNode.fork } : {}),
116
+ ...(templateNode.fork !== undefined ? { fork: templateNode.fork } : {}),
117
+ ...(templateNode.completion !== undefined
118
+ ? { completion: templateNode.completion }
119
+ : {}),
120
+ ...(templateNode.recovery !== undefined
121
+ ? { recovery: templateNode.recovery }
122
+ : {}),
123
+ ...(templateNode.feedback_loop !== undefined
124
+ ? { feedback_loop: templateNode.feedback_loop }
125
+ : {}),
126
+ ...(templateNode.replay_safe !== undefined
127
+ ? { replay_safe: templateNode.replay_safe }
128
+ : {}),
89
129
  }));
90
130
  const remaining = new Map(snapshot.nodes
91
131
  .filter((n) => n.id !== catalogId)
@@ -112,7 +152,7 @@ export function appendCloneInstances(snapshot, input) {
112
152
  ...instanceIds,
113
153
  ...predChildren.slice(predIdx + 1),
114
154
  ];
115
- const definitionParent = catalogNode.needs;
155
+ const definitionParent = catalogNode?.needs;
116
156
  if (definitionParent && definitionParent !== predecessorId) {
117
157
  const parentChildren = childrenOf[definitionParent] ?? [];
118
158
  childrenOf[definitionParent] = parentChildren.filter((id) => id !== catalogId);
@@ -1,12 +1,14 @@
1
1
  import type { StageLogLine } from "../agent/activity.js";
2
2
  import type { AskOperatorPrompt } from "../tools/askOperator.js";
3
3
  import type { StageEnvelope } from "../types/envelope.js";
4
- import type { ResolvedPipelineDag } from "../types/pipeline.js";
4
+ import type { FeedbackLoopConfig, ResolvedPipelineDag } from "../types/pipeline.js";
5
+ import type { CompletionCheck } from "../types/completion.js";
5
6
  import type { StageGateKind } from "../types/stage.js";
6
7
  export type RunStatus = "created" | "running" | "succeeded" | "failed";
7
8
  export type RunPipelineDagSnapshot = ResolvedPipelineDag & {
8
9
  stage_ids: string[];
9
10
  gate_kinds?: Record<string, StageGateKind[]>;
11
+ clone_input_schema?: Record<string, unknown>;
10
12
  };
11
13
  export type StageReadiness = "blocked" | "ready" | "running" | "waiting" | "succeeded" | "failed" | "skipped";
12
14
  export type PipelineTrackNode = {
@@ -19,6 +21,9 @@ export type PipelineTrackNode = {
19
21
  gate_kinds?: StageGateKind[];
20
22
  attempt_count?: number;
21
23
  definition_id?: string;
24
+ feedback_loop?: {
25
+ target: string;
26
+ };
22
27
  };
23
28
  export type PipelineTrackEdge = {
24
29
  from: string;
@@ -70,16 +75,145 @@ export type StageExecution = {
70
75
  stage_id: string;
71
76
  attempt: number;
72
77
  status: StageSnapshot["status"];
78
+ verification_outcome: VerificationOutcome;
73
79
  started_at?: string;
74
80
  finished_at?: string;
75
81
  envelope: StageEnvelope | null;
76
82
  };
77
83
  export type StageExecutionPatch = {
78
84
  status?: StageSnapshot["status"];
85
+ verification_outcome?: VerificationOutcome;
79
86
  started_at?: string;
80
87
  finished_at?: string;
81
88
  envelope?: StageEnvelope | null;
82
89
  };
90
+ /** The durable disposition of completion verification for one stage attempt. */
91
+ export type VerificationOutcome = "not_run" | "passed" | "failed" | "error";
92
+ /** The lifecycle state of one independently-run completion check. */
93
+ export type VerificationCheckStatus = "pending" | "running" | "passed" | "failed" | "skipped";
94
+ /**
95
+ * Durable evidence for one check in one stage execution attempt.
96
+ *
97
+ * `evidence` is intentionally JSON-shaped: each checker owns its evidence
98
+ * schema (for example, command output metadata or an artifact digest), while
99
+ * the run store preserves it without privileging a particular checker.
100
+ */
101
+ export type VerificationCheckResult = {
102
+ run_id: string;
103
+ stage_id: string;
104
+ attempt: number;
105
+ check_id: string;
106
+ check_type: CompletionCheck["type"];
107
+ status: VerificationCheckStatus;
108
+ started_at?: string;
109
+ finished_at?: string;
110
+ evidence?: Record<string, unknown>;
111
+ };
112
+ /** Input for creating or updating a verification-check lifecycle record. */
113
+ export type VerificationCheckResultPatch = {
114
+ check_id: string;
115
+ check_type: CompletionCheck["type"];
116
+ status: VerificationCheckStatus;
117
+ started_at?: string;
118
+ finished_at?: string;
119
+ evidence?: Record<string, unknown>;
120
+ };
121
+ /** Durable lifecycle for one source-owned feedback-loop policy in a run. */
122
+ export type FeedbackLoopState = "active" | "waiting_for_human" | "continued" | "abandoned" | "completed";
123
+ /** Durable lifecycle for one accepted send-back through a feedback loop. */
124
+ export type FeedbackReplayStatus = "scheduled" | "active" | "waiting_for_human" | "completed" | "failed" | "superseded";
125
+ /** Lifecycle for one persistent stage's pass within a feedback replay. */
126
+ export type FeedbackReplayStagePassStatus = "pending" | "running" | "waiting" | "succeeded" | "failed" | "superseded";
127
+ /** Active/history state for a dynamic clone cohort created by a fork parent. */
128
+ export type ForkGenerationStatus = "active" | "completed" | "superseded";
129
+ /** Deferred over-limit send_back awaiting a human feedback-loop decision. */
130
+ export type DeferredFeedbackSendBack = {
131
+ target: string;
132
+ feedback_envelope: StageEnvelope;
133
+ source_attempt: number;
134
+ };
135
+ export type FeedbackLoopRecord = {
136
+ run_id: string;
137
+ loop_id: string;
138
+ source_stage_id: string;
139
+ source_attempt: number;
140
+ policy: FeedbackLoopConfig;
141
+ state: FeedbackLoopState;
142
+ current_replay_id?: string;
143
+ current_replay_number?: number;
144
+ deferred_send_back?: DeferredFeedbackSendBack;
145
+ created_at: string;
146
+ updated_at: string;
147
+ };
148
+ export type FeedbackReplayRecord = {
149
+ run_id: string;
150
+ replay_id: string;
151
+ loop_id: string;
152
+ source_stage_id: string;
153
+ source_attempt: number;
154
+ target_stage_id: string;
155
+ /** One-based accepted send-back count for this loop. */
156
+ replay_number: number;
157
+ max_replays: number;
158
+ replay_session: FeedbackLoopConfig["replay_session"];
159
+ route_stage_ids: string[];
160
+ feedback_envelope: StageEnvelope;
161
+ status: FeedbackReplayStatus;
162
+ created_at: string;
163
+ updated_at: string;
164
+ };
165
+ export type FeedbackReplayStagePassRecord = {
166
+ run_id: string;
167
+ replay_id: string;
168
+ stage_id: string;
169
+ /** Execution attempt whose session this pass resumes or newly creates. */
170
+ stage_attempt: number;
171
+ /** Immutable attempt whose Pi session resume targets; never rebound. */
172
+ session_origin_attempt?: number;
173
+ session_mode: FeedbackLoopConfig["replay_session"];
174
+ status: FeedbackReplayStagePassStatus;
175
+ started_at?: string;
176
+ finished_at?: string;
177
+ emitted_envelope?: StageEnvelope;
178
+ };
179
+ export type ForkGenerationRecord = {
180
+ run_id: string;
181
+ generation_id: string;
182
+ /** Undefined for an initial fan-out that is not part of a replay. */
183
+ replay_id?: string;
184
+ fork_parent_stage_id: string;
185
+ generation_number: number;
186
+ clone_stage_ids: string[];
187
+ status: ForkGenerationStatus;
188
+ created_at: string;
189
+ updated_at: string;
190
+ };
191
+ export type CreateFeedbackLoopInput = Omit<FeedbackLoopRecord, "run_id" | "state" | "current_replay_id" | "current_replay_number" | "created_at" | "updated_at"> & {
192
+ state?: FeedbackLoopState;
193
+ };
194
+ export type FeedbackLoopPatch = Partial<Pick<FeedbackLoopRecord, "state" | "current_replay_id" | "current_replay_number" | "policy">> & {
195
+ /** Set to clear a previously deferred send_back. */
196
+ deferred_send_back?: DeferredFeedbackSendBack | null;
197
+ };
198
+ export type CreateFeedbackReplayInput = Omit<FeedbackReplayRecord, "run_id" | "status" | "created_at" | "updated_at"> & {
199
+ status?: FeedbackReplayStatus;
200
+ };
201
+ export type FeedbackReplayPatch = Partial<Pick<FeedbackReplayRecord, "status">>;
202
+ export type CreateFeedbackReplayStagePassInput = Omit<FeedbackReplayStagePassRecord, "run_id" | "status" | "started_at" | "finished_at" | "emitted_envelope"> & {
203
+ status?: FeedbackReplayStagePassStatus;
204
+ started_at?: string;
205
+ finished_at?: string;
206
+ emitted_envelope?: StageEnvelope;
207
+ };
208
+ export type FeedbackReplayStagePassPatch = Partial<Pick<FeedbackReplayStagePassRecord, "status" | "stage_attempt">> & {
209
+ started_at?: string | null;
210
+ finished_at?: string | null;
211
+ emitted_envelope?: StageEnvelope | null;
212
+ };
213
+ export type CreateForkGenerationInput = Omit<ForkGenerationRecord, "run_id" | "status" | "created_at" | "updated_at"> & {
214
+ status?: ForkGenerationStatus;
215
+ };
216
+ export type ForkGenerationPatch = Partial<Pick<ForkGenerationRecord, "status">>;
83
217
  export type CompactStage = {
84
218
  id: string;
85
219
  status: StageSnapshot["status"];
@@ -103,17 +237,31 @@ export type RunSummary = {
103
237
  waiting_stage_ids?: string[];
104
238
  /** Short prompt text for Today triage; omitted when not waiting. */
105
239
  waiting_summary?: string;
106
- waiting_kind?: AskOperatorPrompt["kind"];
240
+ waiting_kind?: AskOperatorPrompt["kind"] | "feedback_loop_decision";
107
241
  waiting_prompt_id?: string;
108
242
  waiting_artifacts?: string[];
109
243
  waiting_questions?: string[];
110
244
  failed_stage_id?: string;
111
245
  failed_reason?: string;
246
+ /** The active loop, when a run is currently replaying or awaiting a decision. */
247
+ active_feedback_loop?: FeedbackLoopRecord;
112
248
  };
113
249
  export type RunDetail = Omit<RunSummary, "stages"> & {
114
250
  task_yaml: string;
115
251
  stages: StageSnapshot[];
116
252
  pipeline_track: PipelineTrackProjection;
253
+ /** Append-only feedback-loop and replay history, ordered by creation. */
254
+ feedback_loops: FeedbackLoopHistory[];
255
+ };
256
+ export type FeedbackLoopHistory = {
257
+ loop: FeedbackLoopRecord;
258
+ replays: Array<{
259
+ replay: FeedbackReplayRecord;
260
+ stage_passes: FeedbackReplayStagePassRecord[];
261
+ fork_generations: ForkGenerationRecord[];
262
+ }>;
263
+ /** Fork cohorts not associated with a replay, for example the first pass. */
264
+ fork_generations: ForkGenerationRecord[];
117
265
  };
118
266
  export type CreateRunInput = {
119
267
  pipelineId: string;
@@ -156,6 +304,15 @@ export interface RunStore {
156
304
  countStageAttempts(runId: string, stageId: string): Promise<number>;
157
305
  getStageExecution(runId: string, stageId: string, attempt: number): Promise<StageExecution>;
158
306
  updateStageExecution(runId: string, stageId: string, attempt: number, patch: StageExecutionPatch): Promise<void>;
307
+ /** Create or update evidence for one check in a stage execution attempt. */
308
+ upsertVerificationCheckResult(runId: string, stageId: string, result: VerificationCheckResultPatch, options?: {
309
+ attempt?: number;
310
+ }): Promise<void>;
311
+ /**
312
+ * List verification evidence in write order. Without an attempt, returns all
313
+ * attempts for the stage; callers may pass an attempt to inspect one retry.
314
+ */
315
+ listVerificationCheckResults(runId: string, stageId: string, attempt?: number): Promise<VerificationCheckResult[]>;
159
316
  writeEnvelope(runId: string, stageId: string, envelope: StageEnvelope, options?: {
160
317
  attempt?: number;
161
318
  }): Promise<void>;
@@ -167,6 +324,31 @@ export interface RunStore {
167
324
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
168
325
  readRun(runId: string): Promise<RunDetail>;
169
326
  updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
327
+ createFeedbackLoop(runId: string, input: CreateFeedbackLoopInput): Promise<FeedbackLoopRecord>;
328
+ getFeedbackLoop(runId: string, loopId: string): Promise<FeedbackLoopRecord>;
329
+ listFeedbackLoops(runId: string): Promise<FeedbackLoopRecord[]>;
330
+ /**
331
+ * Patch a feedback loop. When `expectedState` is set, the update is conditional
332
+ * (CAS): returns false if the row exists but state does not match.
333
+ * Throws if the loop is missing.
334
+ */
335
+ updateFeedbackLoop(runId: string, loopId: string, patch: FeedbackLoopPatch, options?: {
336
+ expectedState?: FeedbackLoopState;
337
+ }): Promise<boolean>;
338
+ createFeedbackReplay(runId: string, input: CreateFeedbackReplayInput): Promise<FeedbackReplayRecord>;
339
+ getFeedbackReplay(runId: string, replayId: string): Promise<FeedbackReplayRecord>;
340
+ listFeedbackReplays(runId: string, loopId: string): Promise<FeedbackReplayRecord[]>;
341
+ updateFeedbackReplay(runId: string, replayId: string, patch: FeedbackReplayPatch): Promise<void>;
342
+ createFeedbackReplayStagePass(runId: string, input: CreateFeedbackReplayStagePassInput): Promise<FeedbackReplayStagePassRecord>;
343
+ listFeedbackReplayStagePasses(runId: string, replayId: string): Promise<FeedbackReplayStagePassRecord[]>;
344
+ updateFeedbackReplayStagePass(runId: string, replayId: string, stageId: string, patch: FeedbackReplayStagePassPatch): Promise<void>;
345
+ createForkGeneration(runId: string, input: CreateForkGenerationInput): Promise<ForkGenerationRecord>;
346
+ listForkGenerations(runId: string, options?: {
347
+ replayId?: string;
348
+ forkParentStageId?: string;
349
+ }): Promise<ForkGenerationRecord[]>;
350
+ updateForkGeneration(runId: string, generationId: string, patch: ForkGenerationPatch): Promise<void>;
170
351
  }
171
352
  export declare function stageStatusFromEvents(events: StageLogEvent[]): StageSnapshot["status"];
172
- export declare function deriveStatusFromStages(stages: StageSnapshot[]): RunStatus;
353
+ export declare function findUnhandledFailedStage(stages: StageSnapshot[], dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null): StageSnapshot | undefined;
354
+ export declare function deriveStatusFromStages(stages: StageSnapshot[], dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null): RunStatus;