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
@@ -0,0 +1,295 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { instancesOfDefinition } from "../runstore/pipelineDagSnapshot.js";
3
+ import { collectDownstreamStageIds } from "./dagTraversal.js";
4
+ function asDagSnapshot(dag) {
5
+ const snapshot = dag;
6
+ if (Array.isArray(snapshot.stage_ids))
7
+ return snapshot;
8
+ return {
9
+ ...dag,
10
+ stage_ids: dag.nodes.map((n) => n.id),
11
+ };
12
+ }
13
+ /** Persistent route stages that fan out to clonable successors (or already have instances). */
14
+ export function routeRelatedForkParentIds(dag, routeStageIds) {
15
+ const routeSet = new Set(routeStageIds);
16
+ const parents = new Set();
17
+ const snapshot = asDagSnapshot(dag);
18
+ for (const stageId of routeStageIds) {
19
+ for (const childId of dag.childrenOf[stageId] ?? []) {
20
+ const child = dag.nodes.find((n) => n.id === childId);
21
+ if (child?.clonable === true) {
22
+ parents.add(stageId);
23
+ }
24
+ }
25
+ }
26
+ for (const stageId of routeStageIds) {
27
+ const node = dag.nodes.find((n) => n.id === stageId);
28
+ if (node?.clonable === true && node.needs !== null) {
29
+ parents.add(node.needs);
30
+ }
31
+ }
32
+ for (const node of dag.nodes) {
33
+ const defId = node.definition_id;
34
+ if (defId === undefined || defId === node.id)
35
+ continue;
36
+ if (node.needs !== null && routeSet.has(node.needs)) {
37
+ parents.add(node.needs);
38
+ }
39
+ }
40
+ for (const stageId of routeStageIds) {
41
+ const node = dag.nodes.find((n) => n.id === stageId);
42
+ if (node?.clonable !== true)
43
+ continue;
44
+ const instances = instancesOfDefinition(snapshot, stageId).filter((id) => id !== stageId);
45
+ if (instances.length > 0 && node.needs !== null) {
46
+ parents.add(node.needs);
47
+ }
48
+ }
49
+ return [...parents];
50
+ }
51
+ /** Clone instance ids currently under clonable successors of a fork parent. */
52
+ export function cloneInstanceIdsForForkParent(dag, forkParentStageId) {
53
+ const snapshot = asDagSnapshot(dag);
54
+ const ids = [];
55
+ const seen = new Set();
56
+ for (const childId of dag.childrenOf[forkParentStageId] ?? []) {
57
+ const child = dag.nodes.find((n) => n.id === childId);
58
+ if (child?.clonable === true) {
59
+ for (const id of instancesOfDefinition(snapshot, childId)) {
60
+ if (id === childId)
61
+ continue;
62
+ if (seen.has(id))
63
+ continue;
64
+ seen.add(id);
65
+ ids.push(id);
66
+ }
67
+ continue;
68
+ }
69
+ if (child?.definition_id !== undefined &&
70
+ child.definition_id !== child.id &&
71
+ !seen.has(childId)) {
72
+ seen.add(childId);
73
+ ids.push(childId);
74
+ }
75
+ }
76
+ for (const node of dag.nodes) {
77
+ if (node.needs !== forkParentStageId)
78
+ continue;
79
+ if (node.definition_id === undefined || node.definition_id === node.id) {
80
+ continue;
81
+ }
82
+ if (seen.has(node.id))
83
+ continue;
84
+ seen.add(node.id);
85
+ ids.push(node.id);
86
+ }
87
+ return ids;
88
+ }
89
+ export async function backfillActiveGenerationIfNeeded(store, runId, options) {
90
+ const { dag, forkParentStageId } = options;
91
+ const existing = await store.listForkGenerations(runId, {
92
+ forkParentStageId,
93
+ });
94
+ if (existing.some((g) => g.status === "active")) {
95
+ return existing.find((g) => g.status === "active");
96
+ }
97
+ const cloneStageIds = cloneInstanceIdsForForkParent(dag, forkParentStageId);
98
+ if (cloneStageIds.length === 0)
99
+ return undefined;
100
+ const maxNumber = existing.reduce((max, g) => Math.max(max, g.generation_number), 0);
101
+ return store.createForkGeneration(runId, {
102
+ generation_id: randomUUID(),
103
+ fork_parent_stage_id: forkParentStageId,
104
+ generation_number: maxNumber + 1,
105
+ clone_stage_ids: cloneStageIds,
106
+ status: "active",
107
+ });
108
+ }
109
+ export function activeCohortFromCloneIds(activeCloneIds) {
110
+ if (activeCloneIds === undefined || activeCloneIds === null) {
111
+ return { kind: "untracked" };
112
+ }
113
+ if (activeCloneIds.size === 0)
114
+ return { kind: "awaiting_mint" };
115
+ return { kind: "active", cloneIds: activeCloneIds };
116
+ }
117
+ export function cloneIdsFromActiveCohort(cohort) {
118
+ if (cohort.kind === "untracked")
119
+ return null;
120
+ if (cohort.kind === "awaiting_mint")
121
+ return new Set();
122
+ return new Set(cohort.cloneIds);
123
+ }
124
+ /** Resolve the fork parent that fans out into instances of `needsStageId`. */
125
+ export function forkParentForNeedsStage(dag, needsStageId) {
126
+ const snapshot = asDagSnapshot(dag);
127
+ const instances = Array.isArray(snapshot.stage_ids)
128
+ ? instancesOfDefinition(snapshot, needsStageId).filter((id) => id !== needsStageId)
129
+ : [];
130
+ if (instances.length === 0)
131
+ return null;
132
+ const sample = dag.nodes.find((n) => n.id === instances[0]);
133
+ const forkParentId = sample?.needs;
134
+ if (forkParentId === undefined || forkParentId === null)
135
+ return null;
136
+ return forkParentId;
137
+ }
138
+ export async function supersedeActiveGenerations(store, runId, options) {
139
+ const supersededCloneIds = new Set();
140
+ const supersededGenerations = [];
141
+ for (const forkParentStageId of options.forkParentStageIds) {
142
+ await backfillActiveGenerationIfNeeded(store, runId, {
143
+ dag: options.dag,
144
+ forkParentStageId,
145
+ });
146
+ const gens = await store.listForkGenerations(runId, { forkParentStageId });
147
+ for (const gen of gens) {
148
+ if (gen.status !== "active")
149
+ continue;
150
+ await store.updateForkGeneration(runId, gen.generation_id, {
151
+ status: "superseded",
152
+ });
153
+ supersededGenerations.push({ ...gen, status: "superseded" });
154
+ for (const id of gen.clone_stage_ids) {
155
+ supersededCloneIds.add(id);
156
+ }
157
+ }
158
+ }
159
+ return { supersededCloneIds, supersededGenerations };
160
+ }
161
+ export async function retireCohortsForRoute(options) {
162
+ const forkParentIds = routeRelatedForkParentIds(options.dag, options.routeStageIds);
163
+ const { supersededCloneIds } = await supersedeActiveGenerations(options.store, options.runId, {
164
+ dag: options.dag,
165
+ forkParentStageIds: forkParentIds,
166
+ });
167
+ return { supersededCloneIds, forkParentIds };
168
+ }
169
+ export async function collectSupersededCloneIdsForRoute(options) {
170
+ const forkParentIds = routeRelatedForkParentIds(options.dag, options.routeStageIds);
171
+ const supersededCloneIds = new Set();
172
+ for (const parentId of forkParentIds) {
173
+ const gens = await options.store.listForkGenerations(options.runId, {
174
+ forkParentStageId: parentId,
175
+ });
176
+ for (const gen of gens) {
177
+ if (gen.status !== "superseded")
178
+ continue;
179
+ for (const id of gen.clone_stage_ids) {
180
+ supersededCloneIds.add(id);
181
+ }
182
+ }
183
+ }
184
+ return supersededCloneIds;
185
+ }
186
+ export async function createGenerationForFanout(store, runId, options) {
187
+ const existing = await store.listForkGenerations(runId, {
188
+ forkParentStageId: options.forkParentStageId,
189
+ });
190
+ const maxNumber = existing.reduce((max, g) => Math.max(max, g.generation_number), 0);
191
+ return store.createForkGeneration(runId, {
192
+ generation_id: randomUUID(),
193
+ ...(options.replayId !== undefined ? { replay_id: options.replayId } : {}),
194
+ fork_parent_stage_id: options.forkParentStageId,
195
+ generation_number: maxNumber + 1,
196
+ clone_stage_ids: options.cloneStageIds,
197
+ status: "active",
198
+ });
199
+ }
200
+ export async function mintCohortForFanout(options) {
201
+ return createGenerationForFanout(options.store, options.runId, {
202
+ ...(options.replayId !== undefined ? { replayId: options.replayId } : {}),
203
+ forkParentStageId: options.forkParent,
204
+ cloneStageIds: options.cloneStageIds,
205
+ });
206
+ }
207
+ export async function activeCloneIdsForParent(store, runId, forkParentStageId) {
208
+ return cloneIdsFromActiveCohort(await activeCohortForParent(store, runId, forkParentStageId));
209
+ }
210
+ export async function activeCohortForParent(store, runId, forkParentStageId) {
211
+ const gens = await store.listForkGenerations(runId, { forkParentStageId });
212
+ if (gens.length === 0)
213
+ return { kind: "untracked" };
214
+ const active = gens.filter((g) => g.status === "active");
215
+ if (active.length === 0)
216
+ return { kind: "awaiting_mint" };
217
+ const ids = new Set();
218
+ for (const gen of active) {
219
+ for (const id of gen.clone_stage_ids) {
220
+ ids.add(id);
221
+ }
222
+ }
223
+ return { kind: "active", cloneIds: ids };
224
+ }
225
+ export async function selectActiveInput(options) {
226
+ const forkParentId = forkParentForNeedsStage(options.dag, options.needsStageId);
227
+ if (forkParentId === null)
228
+ return { kind: "untracked" };
229
+ const override = options.scheduleOverride;
230
+ if (override !== undefined) {
231
+ if (override.size === 0)
232
+ return { kind: "untracked" };
233
+ const tracked = override.get(forkParentId);
234
+ if (tracked === undefined)
235
+ return { kind: "untracked" };
236
+ return activeCohortFromCloneIds(tracked);
237
+ }
238
+ return activeCohortForParent(options.store, options.runId, forkParentId);
239
+ }
240
+ export function filterJoinInputs(allInstanceIds, cohort) {
241
+ if (cohort.kind === "untracked")
242
+ return [...allInstanceIds];
243
+ if (cohort.kind === "awaiting_mint")
244
+ return [];
245
+ return allInstanceIds.filter((id) => cohort.cloneIds.has(id));
246
+ }
247
+ export function filterJoinInstanceIds(allInstanceIds, activeCloneIds) {
248
+ return filterJoinInputs(allInstanceIds, activeCohortFromCloneIds(activeCloneIds));
249
+ }
250
+ /**
251
+ * During an active feedback replay, a fork_choice must keep at least one
252
+ * chosen branch that can still reach the replay source stage.
253
+ */
254
+ export function forkChoicePreservesReplaySource(dag, chosen, sourceStageId) {
255
+ const source = dag.nodes.find((n) => n.id === sourceStageId);
256
+ if (source === undefined)
257
+ return false;
258
+ for (const childId of chosen) {
259
+ if (childId === sourceStageId)
260
+ return true;
261
+ if (source.ancestors.includes(childId))
262
+ return true;
263
+ if (collectDownstreamStageIds(dag, childId).has(sourceStageId)) {
264
+ return true;
265
+ }
266
+ const child = dag.nodes.find((n) => n.id === childId);
267
+ if (child?.clonable === true) {
268
+ const snapshot = asDagSnapshot(dag);
269
+ for (const instanceId of instancesOfDefinition(snapshot, childId)) {
270
+ if (instanceId === childId)
271
+ continue;
272
+ if (instanceId === sourceStageId)
273
+ return true;
274
+ if (source.ancestors.includes(instanceId))
275
+ return true;
276
+ if (collectDownstreamStageIds(dag, instanceId).has(sourceStageId)) {
277
+ return true;
278
+ }
279
+ }
280
+ if (source.ancestors.includes(childId))
281
+ return true;
282
+ const joinDownstream = dag.nodes.filter((n) => n.needs === childId);
283
+ for (const join of joinDownstream) {
284
+ if (join.id === sourceStageId)
285
+ return true;
286
+ if (collectDownstreamStageIds(dag, join.id).has(sourceStageId)) {
287
+ return true;
288
+ }
289
+ if (source.ancestors.includes(join.id))
290
+ return true;
291
+ }
292
+ }
293
+ }
294
+ return false;
295
+ }
@@ -0,0 +1,7 @@
1
+ import type { CheckoutCapability } from "./completionCheckRunner.js";
2
+ /**
3
+ * Git-backed checkout observation. It fingerprints tracked and untracked
4
+ * files before the agent runs, so pre-existing changes are not attributed to
5
+ * the stage and content changes do not disappear behind a shared Git status.
6
+ */
7
+ export declare function createGitCheckoutCapability(root: string): CheckoutCapability;
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { lstat, readFile, readlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ function runGit(root, args) {
6
+ return new Promise((resolve, reject) => {
7
+ execFile("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
8
+ if (error) {
9
+ const detail = Buffer.isBuffer(stderr)
10
+ ? stderr.toString("utf8").trim()
11
+ : "";
12
+ reject(new Error(detail || error.message));
13
+ return;
14
+ }
15
+ resolve(stdout);
16
+ });
17
+ });
18
+ }
19
+ function nulPaths(output) {
20
+ return output.toString("utf8").split("\0").filter(Boolean);
21
+ }
22
+ async function fingerprintFile(root, relativePath) {
23
+ const target = path.join(root, relativePath);
24
+ try {
25
+ const stats = await lstat(target);
26
+ if (stats.isSymbolicLink()) {
27
+ const targetPath = await readlink(target);
28
+ return `link:${createHash("sha256").update(targetPath).digest("hex")}`;
29
+ }
30
+ if (!stats.isFile())
31
+ return `other:${stats.mode}:${stats.size}`;
32
+ const contents = await readFile(target);
33
+ return `file:${createHash("sha256").update(contents).digest("hex")}`;
34
+ }
35
+ catch (error) {
36
+ if (error.code === "ENOENT")
37
+ return "missing";
38
+ throw error;
39
+ }
40
+ }
41
+ async function filesAt(root) {
42
+ const [trackedOutput, untrackedOutput] = await Promise.all([
43
+ runGit(root, ["ls-files", "-z"]),
44
+ runGit(root, ["ls-files", "--others", "--exclude-standard", "-z"]),
45
+ ]);
46
+ const known = new Map();
47
+ for (const file of nulPaths(trackedOutput))
48
+ known.set(file, true);
49
+ for (const file of nulPaths(untrackedOutput))
50
+ known.set(file, false);
51
+ return [...known.entries()]
52
+ .map(([file, tracked]) => ({ path: file, tracked }))
53
+ .sort((a, b) => a.path.localeCompare(b.path));
54
+ }
55
+ async function capture(root) {
56
+ const files = await filesAt(root);
57
+ return {
58
+ entries: await Promise.all(files.map(async ({ path: file, tracked }) => ({
59
+ path: file,
60
+ fingerprint: `${tracked ? "tracked" : "untracked"}:${await fingerprintFile(root, file)}`,
61
+ }))),
62
+ };
63
+ }
64
+ function statusForAdded(fingerprint) {
65
+ return fingerprint.startsWith("untracked:") ? "untracked" : "added";
66
+ }
67
+ /**
68
+ * Git-backed checkout observation. It fingerprints tracked and untracked
69
+ * files before the agent runs, so pre-existing changes are not attributed to
70
+ * the stage and content changes do not disappear behind a shared Git status.
71
+ */
72
+ export function createGitCheckoutCapability(root) {
73
+ return {
74
+ capture: () => capture(root),
75
+ async changesSince(before) {
76
+ const after = await capture(root);
77
+ const earlier = new Map(before.entries.map((entry) => [entry.path, entry.fingerprint]));
78
+ const later = new Map(after.entries.map((entry) => [entry.path, entry.fingerprint]));
79
+ const paths = [...new Set([...earlier.keys(), ...later.keys()])].sort();
80
+ const changes = [];
81
+ for (const file of paths) {
82
+ const previous = earlier.get(file);
83
+ const current = later.get(file);
84
+ if (previous === undefined && current !== undefined) {
85
+ changes.push({ path: file, status: statusForAdded(current) });
86
+ }
87
+ else if (previous !== undefined && current === undefined) {
88
+ changes.push({ path: file, status: "deleted" });
89
+ }
90
+ else if (previous !== current) {
91
+ changes.push({ path: file, status: "modified" });
92
+ }
93
+ }
94
+ return changes;
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,40 @@
1
+ import type { RunStore, StageSnapshot } from "../runstore/port.js";
2
+ /**
3
+ * The derived recovery state of one stage. It deliberately separates the
4
+ * operator policy decision from the orchestration that starts an attempt.
5
+ */
6
+ export type ManualRecoveryState = {
7
+ kind: "not_manual";
8
+ } | {
9
+ kind: "stage_not_found";
10
+ } | {
11
+ kind: "not_failed";
12
+ status: StageSnapshot["status"];
13
+ } | {
14
+ kind: "not_verification_failure";
15
+ } | {
16
+ kind: "available";
17
+ failedAttempt: number;
18
+ } | {
19
+ kind: "stopped";
20
+ failedAttempt: number;
21
+ };
22
+ export type ManualRecoveryEligibility = {
23
+ ok: true;
24
+ failedAttempt: number;
25
+ } | {
26
+ ok: false;
27
+ reason: string;
28
+ status: number;
29
+ };
30
+ /**
31
+ * Interpret the persisted DAG and stage events for a manual-recovery stage.
32
+ * Callers decide what to do with that state; this module owns what it means.
33
+ */
34
+ export declare function readManualRecoveryState(store: RunStore, runId: string, stageId: string): Promise<ManualRecoveryState>;
35
+ export declare function manualRecoveryEligibility(state: ManualRecoveryState): ManualRecoveryEligibility;
36
+ export declare function blocksGenericRetry(state: ManualRecoveryState): boolean;
37
+ export declare function manualRecoveryHistory(state: ManualRecoveryState): {
38
+ status: "available" | "stopped";
39
+ failed_attempt: number;
40
+ } | undefined;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Interpret the persisted DAG and stage events for a manual-recovery stage.
3
+ * Callers decide what to do with that state; this module owns what it means.
4
+ */
5
+ export async function readManualRecoveryState(store, runId, stageId) {
6
+ const [detail, meta, latestExecution] = await Promise.all([
7
+ store.readRun(runId),
8
+ store.readRunMeta(runId),
9
+ store.getLatestStageExecution(runId, stageId),
10
+ ]);
11
+ const stage = detail.stages.find((item) => item.stage_id === stageId);
12
+ if (!stage)
13
+ return { kind: "stage_not_found" };
14
+ const recovery = meta.pipeline_dag?.nodes.find((node) => node.id === stageId)?.recovery;
15
+ if (recovery?.mode !== "manual")
16
+ return { kind: "not_manual" };
17
+ if (stage.status !== "failed") {
18
+ return { kind: "not_failed", status: stage.status };
19
+ }
20
+ if (latestExecution === null)
21
+ return { kind: "not_verification_failure" };
22
+ const events = await store.listStageEvents(runId, stageId, latestExecution.attempt);
23
+ if (latestExecution.verification_outcome !== "failed") {
24
+ return { kind: "not_verification_failure" };
25
+ }
26
+ if (events.some((event) => event.event === "manual_recovery_stopped")) {
27
+ return { kind: "stopped", failedAttempt: latestExecution.attempt };
28
+ }
29
+ return { kind: "available", failedAttempt: latestExecution.attempt };
30
+ }
31
+ export function manualRecoveryEligibility(state) {
32
+ switch (state.kind) {
33
+ case "available":
34
+ return { ok: true, failedAttempt: state.failedAttempt };
35
+ case "not_manual":
36
+ return {
37
+ ok: false,
38
+ reason: "Stage is not configured for manual recovery",
39
+ status: 409,
40
+ };
41
+ case "stage_not_found":
42
+ return { ok: false, reason: "Stage not found", status: 404 };
43
+ case "not_failed":
44
+ return {
45
+ ok: false,
46
+ reason: `Stage is not failed (status=${state.status})`,
47
+ status: 409,
48
+ };
49
+ case "not_verification_failure":
50
+ return {
51
+ ok: false,
52
+ reason: "Manual recovery is only available after a completion verification failure",
53
+ status: 409,
54
+ };
55
+ case "stopped":
56
+ return {
57
+ ok: false,
58
+ reason: "Manual recovery has been stopped for this stage",
59
+ status: 409,
60
+ };
61
+ }
62
+ }
63
+ export function blocksGenericRetry(state) {
64
+ return state.kind === "available" || state.kind === "stopped";
65
+ }
66
+ export function manualRecoveryHistory(state) {
67
+ if (state.kind !== "available" && state.kind !== "stopped")
68
+ return undefined;
69
+ return {
70
+ status: state.kind,
71
+ failed_attempt: state.failedAttempt,
72
+ };
73
+ }
@@ -4,6 +4,7 @@ import type { StageEnvelope } from "../types/envelope.js";
4
4
  import type { LoadedPipeline, ResolvedPipelineDag } from "../types/pipeline.js";
5
5
  import type { TaskFile } from "../types/task.js";
6
6
  import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule } from "./cloneSchedule.js";
7
+ import type { FeedbackLoopDecisionInput, ResolveFeedbackLoopDecisionResult } from "./feedbackLoopDecision.js";
7
8
  import type { PipelineRunResult } from "./pipelineRunner.js";
8
9
  import type { StageProcessLauncher } from "./stageProcessLauncher.js";
9
10
  import type { StageExecutionMode } from "./stageConcurrency.js";
@@ -79,6 +80,16 @@ export type RunPipelineDagOptions = {
79
80
  mutationQueue?: RetryMutationQueue;
80
81
  onLoopTick?: () => void | Promise<void>;
81
82
  onRetryRootTerminal?: OnRetryRootTerminal;
83
+ /**
84
+ * In-process park for feedback-loop exhausted-limit decisions. When omitted,
85
+ * the scheduler exits with outcome "waiting" and callers use
86
+ * resolveFeedbackLoopDecision + resume separately (Phase 6 surfaces).
87
+ */
88
+ onFeedbackLoopWaiting?: (ctx: {
89
+ loopId: string;
90
+ sourceStageId: string;
91
+ resolve: (decision: FeedbackLoopDecisionInput) => Promise<ResolveFeedbackLoopDecisionResult>;
92
+ }) => Promise<void>;
82
93
  };
83
94
  export type RetryRunOptions = {
84
95
  prepared: SchedulerPreparedPipeline;
@@ -92,6 +103,7 @@ export type RetryRunOptions = {
92
103
  };
93
104
  export declare function retryRun(options: RetryRunOptions): Promise<PipelineRunResult>;
94
105
  export declare function applyForkSkipsFromEnvelopes(dag: ResolvedPipelineDag, states: Map<string, StageScheduleState>, completedEnvelopes: Map<string, StageEnvelope>): void;
95
- export declare function applyRetryRootDelta(dag: ResolvedPipelineDag, states: Map<string, StageScheduleState>, completedEnvelopes: Map<string, StageEnvelope>, retryRoots: Map<string, number>, stageId: string, attempt: number, clearSchedulingHalt?: () => void): void;
106
+ export declare function applyRetryRootDelta(dag: ResolvedPipelineDag, states: Map<string, StageScheduleState>, completedEnvelopes: Map<string, StageEnvelope>, retryRoots: Map<string, number>, stageId: string, attempt: number, clearSchedulingHalt?: () => void, alreadyLaunchedIds?: ReadonlySet<string>): void;
96
107
  export declare function hasWorkOutsideBranch(dag: ResolvedPipelineDag, branchRootId: string, states: Map<string, StageScheduleState>, ctx?: HasWorkOutsideBranchContext): boolean;
108
+ export declare function hydratedScheduleHasRunnableWork(hydrated: HydratedSchedule): boolean;
97
109
  export declare function runPipelineDag(options: RunPipelineDagOptions): Promise<PipelineRunResult>;