stageflow 0.8.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +24 -14
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +301 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +6 -3
  15. package/dist/agent/piAdapter.js +129 -21
  16. package/dist/agent/port.d.ts +50 -1
  17. package/dist/agent/resolveAgentPort.d.ts +26 -0
  18. package/dist/agent/resolveAgentPort.js +39 -0
  19. package/dist/cli/runCommand.js +2 -2
  20. package/dist/cli/runsCommand.d.ts +22 -0
  21. package/dist/cli/runsCommand.js +773 -0
  22. package/dist/cli/validateOutput.d.ts +2 -2
  23. package/dist/cli/validateOutput.js +2 -2
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +43 -5
  26. package/dist/config/browseCatalog.js +1 -1
  27. package/dist/config/createPipeline.d.ts +2 -1
  28. package/dist/config/createPipeline.js +34 -9
  29. package/dist/config/createStage.js +14 -9
  30. package/dist/config/loadPipeline.js +10 -1
  31. package/dist/config/loadStage.js +138 -2
  32. package/dist/config/loadStageflowManifest.js +14 -1
  33. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  34. package/dist/config/mergePipelineIncludes.js +14 -1
  35. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  36. package/dist/config/normalizePipelineStageEntry.js +49 -6
  37. package/dist/config/parseCompletionContract.d.ts +8 -0
  38. package/dist/config/parseCompletionContract.js +218 -0
  39. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  40. package/dist/config/parsePreEmitChecks.js +65 -0
  41. package/dist/config/pipelineNeeds.d.ts +19 -0
  42. package/dist/config/pipelineNeeds.js +151 -0
  43. package/dist/config/pipelineStageKeys.js +16 -1
  44. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  45. package/dist/config/resolveForkEmitContext.js +9 -2
  46. package/dist/config/resolvePipelineDag.d.ts +2 -1
  47. package/dist/config/resolvePipelineDag.js +182 -39
  48. package/dist/config/validateCatalog.d.ts +1 -1
  49. package/dist/config/validateCompletionContract.d.ts +4 -0
  50. package/dist/config/validateCompletionContract.js +52 -0
  51. package/dist/envelope/check.d.ts +1 -1
  52. package/dist/envelope/check.js +86 -13
  53. package/dist/envelope/cloneForks.d.ts +1 -1
  54. package/dist/envelope/cloneForks.js +24 -9
  55. package/dist/envelope/feedbackLoop.d.ts +4 -0
  56. package/dist/envelope/feedbackLoop.js +31 -0
  57. package/dist/envelope/payloadSchema.d.ts +7 -1
  58. package/dist/envelope/payloadSchema.js +170 -13
  59. package/dist/envelope/preEmitChecks.d.ts +29 -0
  60. package/dist/envelope/preEmitChecks.js +57 -0
  61. package/dist/hitl/qaTrail.d.ts +25 -0
  62. package/dist/hitl/qaTrail.js +51 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/mcp/catalogTools.js +4 -2
  66. package/dist/mcp/controlTools.js +72 -0
  67. package/dist/mcp/waitingGates.d.ts +2 -1
  68. package/dist/mcp/waitingGates.js +17 -1
  69. package/dist/package-meta.d.ts +1 -0
  70. package/dist/package-meta.js +1 -0
  71. package/dist/projection/projectRun.d.ts +4 -1
  72. package/dist/projection/projectRun.js +7 -0
  73. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  74. package/dist/prompt/feedbackLoopContext.js +17 -0
  75. package/dist/prompt/priorEnvelope.d.ts +2 -2
  76. package/dist/prompt/priorEnvelope.js +4 -1
  77. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  78. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  79. package/dist/runstore/port.d.ts +185 -3
  80. package/dist/runstore/port.js +42 -3
  81. package/dist/runstore/runProjection.d.ts +7 -3
  82. package/dist/runstore/runProjection.js +30 -15
  83. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  84. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  85. package/dist/runstore/sqlite/schema.d.ts +1 -1
  86. package/dist/runstore/sqlite/schema.js +99 -0
  87. package/dist/runstore/stageInstanceId.d.ts +1 -1
  88. package/dist/runstore/stageInstanceId.js +3 -2
  89. package/dist/runstore/trackProjection.js +59 -27
  90. package/dist/runstore/verificationHistory.d.ts +19 -0
  91. package/dist/runstore/verificationHistory.js +30 -0
  92. package/dist/runtime/cloneSchedule.d.ts +40 -4
  93. package/dist/runtime/cloneSchedule.js +212 -28
  94. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  95. package/dist/runtime/completionCheckRunner.js +365 -0
  96. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  97. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  98. package/dist/runtime/envelopeRouting.d.ts +10 -3
  99. package/dist/runtime/envelopeRouting.js +146 -5
  100. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  101. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  102. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  103. package/dist/runtime/feedbackLoopDecision.js +207 -0
  104. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  105. package/dist/runtime/feedbackLoopRoute.js +13 -0
  106. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  107. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  108. package/dist/runtime/forkGeneration.d.ts +72 -0
  109. package/dist/runtime/forkGeneration.js +295 -0
  110. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  111. package/dist/runtime/gitCheckoutCapability.js +97 -0
  112. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  113. package/dist/runtime/manualRecoveryState.js +73 -0
  114. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  115. package/dist/runtime/pipelineScheduler.js +328 -45
  116. package/dist/runtime/replayLifecycle.d.ts +107 -0
  117. package/dist/runtime/replayLifecycle.js +168 -0
  118. package/dist/runtime/runManager.d.ts +61 -1
  119. package/dist/runtime/runManager.js +501 -7
  120. package/dist/runtime/runRetryCoordinator.d.ts +8 -1
  121. package/dist/runtime/runRetryCoordinator.js +47 -10
  122. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  123. package/dist/runtime/stageAttemptBootstrap.js +79 -1
  124. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  125. package/dist/runtime/stageRecovery.js +1 -1
  126. package/dist/runtime/stageRunner.d.ts +4 -1
  127. package/dist/runtime/stageRunner.js +29 -3
  128. package/dist/runtime/stageWorker.js +69 -2
  129. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  130. package/dist/runtime/verifiedStageExecution.d.ts +29 -0
  131. package/dist/runtime/verifiedStageExecution.js +119 -0
  132. package/dist/server/bootstrap.js +1 -0
  133. package/dist/server/http.js +114 -0
  134. package/dist/server/operatorResults.js +2 -0
  135. package/dist/tools/askOperator.d.ts +161 -5
  136. package/dist/tools/askOperator.js +58 -25
  137. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  138. package/dist/tools/emitStageEnvelope.js +112 -24
  139. package/dist/types/completion.d.ts +43 -0
  140. package/dist/types/completion.js +1 -0
  141. package/dist/types/envelope.d.ts +18 -0
  142. package/dist/types/forkChoice.d.ts +9 -4
  143. package/dist/types/forkChoice.js +1 -1
  144. package/dist/types/pipeline.d.ts +39 -2
  145. package/dist/types/preEmitCheck.d.ts +18 -0
  146. package/dist/types/preEmitCheck.js +1 -0
  147. package/dist/types/stage.d.ts +13 -0
  148. package/dist/types/stageflowManifest.d.ts +2 -0
  149. package/dist/ui/assets/index-CXGz4nL7.js +118 -0
  150. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  151. package/dist/ui/index.html +2 -2
  152. package/package.json +2 -1
  153. package/skills/stageflow/references/control-surface.md +4 -2
  154. package/skills/stageflow-author/references/catalog-mapping.md +26 -2
  155. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  156. package/skills/stageflow-run/SKILL.md +56 -25
  157. package/skills/stageflow-run/references/mcp-call.md +2 -2
  158. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  159. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -1,6 +1,8 @@
1
+ import { isNeedTerminalState, predecessorEdges } from "../config/pipelineNeeds.js";
1
2
  import { appendCloneInstances, instancesOfDefinition, } from "../runstore/pipelineDagSnapshot.js";
2
3
  import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
3
- import { collectDownstreamClosure, collectDownstreamStageIds } from "./dagTraversal.js";
4
+ import { collectDownstreamStageIds } from "./dagTraversal.js";
5
+ import { activeCohortFromCloneIds, filterJoinInputs, } from "./forkGeneration.js";
4
6
  function asDagSnapshot(dag) {
5
7
  const snapshot = dag;
6
8
  if (Array.isArray(snapshot.stage_ids))
@@ -86,13 +88,75 @@ export function sequentialLaterCloneIds(dag, stageId, completedEnvelopes) {
86
88
  return [];
87
89
  return instances.slice(i + 1);
88
90
  }
91
+ function parentNeedEdge(node, parentId, parentDefId) {
92
+ return predecessorEdges(node).find((edge) => edge.id === parentId || edge.id === parentDefId);
93
+ }
94
+ function edgeInstancesReady(dag, edge, states) {
95
+ const instances = definitionInstances(dag, edge.id);
96
+ if (instances.length === 0)
97
+ return false;
98
+ const hasSucceededSibling = instances.some((id) => states.get(id) === "succeeded");
99
+ return instances.every((id) => {
100
+ const state = states.get(id);
101
+ if (!isNeedTerminalState(state))
102
+ return false;
103
+ if (edge.on.includes(state))
104
+ return true;
105
+ return state === "skipped" && hasSucceededSibling;
106
+ });
107
+ }
108
+ function shouldSkipForObservedNeed(dag, node, parentId, parentDefId, observed) {
109
+ const edge = parentNeedEdge(node, parentId, parentDefId);
110
+ if (!edge)
111
+ return false;
112
+ if (edge.on.includes(observed))
113
+ return false;
114
+ if (observed === "skipped" &&
115
+ predecessorEdges(node).length === 1 &&
116
+ typeof node.needs === "string" &&
117
+ isCloneInstance(dag, parentId)) {
118
+ return false;
119
+ }
120
+ return true;
121
+ }
122
+ export function skipRejectedNeedDependents(dag, parentId, states, observed, onSkip) {
123
+ const parentDefId = definitionIdForInstance(asDagSnapshot(dag), parentId);
124
+ for (const node of dag.nodes) {
125
+ if (states.get(node.id) !== "pending")
126
+ continue;
127
+ if (!shouldSkipForObservedNeed(dag, node, parentId, parentDefId, observed)) {
128
+ continue;
129
+ }
130
+ onSkip(node.id);
131
+ skipRejectedNeedDependents(dag, node.id, states, "skipped", onSkip);
132
+ }
133
+ }
134
+ export function failureIsAccepted(dag, failedId, states) {
135
+ const defId = definitionIdForInstance(asDagSnapshot(dag), failedId);
136
+ for (const node of dag.nodes) {
137
+ const edge = parentNeedEdge(node, failedId, defId);
138
+ if (!edge?.on.includes("failed"))
139
+ continue;
140
+ const state = states.get(node.id);
141
+ if (state !== undefined && state !== "skipped")
142
+ return true;
143
+ }
144
+ return false;
145
+ }
89
146
  function joinSuccessorIds(dag, successorId) {
90
- return dag.nodes.filter((n) => n.needs === successorId).map((n) => n.id);
147
+ return dag.nodes
148
+ .filter((n) => predecessorEdges(n).some((edge) => edge.id === successorId))
149
+ .map((n) => n.id);
91
150
  }
92
151
  function joinAndDownstreamIds(dag, cloneStageId) {
93
152
  const defId = definitionIdForInstance(asDagSnapshot(dag), cloneStageId);
94
153
  const ids = [];
95
154
  for (const joinId of joinSuccessorIds(dag, defId)) {
155
+ const node = dag.nodes.find((n) => n.id === joinId);
156
+ if (node &&
157
+ !shouldSkipForObservedNeed(dag, node, cloneStageId, defId, "failed")) {
158
+ continue;
159
+ }
96
160
  ids.push(joinId);
97
161
  for (const desc of collectDownstreamStageIds(dag, joinId)) {
98
162
  ids.push(desc);
@@ -100,13 +164,33 @@ function joinAndDownstreamIds(dag, cloneStageId) {
100
164
  }
101
165
  return ids;
102
166
  }
103
- export function cloneFanoutConflict(dag, envelope) {
167
+ /**
168
+ * A fresh clone_forks fanout targeting a catalog id that already has
169
+ * instances is only safe when every existing instance was put back to
170
+ * `pending` by the current retry cascade (`retryDownstreamIds`) — that means
171
+ * an ancestor was legitimately retried and this parent re-emitted the same
172
+ * fanout for reactivation. Instances that are still active/terminal, or
173
+ * merely `pending` without having gone through a retry reset (e.g. an
174
+ * envelope processed twice in one live run), are rejected as before.
175
+ *
176
+ * Feedback-loop replay is different: the prior cohort was superseded
177
+ * (`skipped`) and a new generation must mint fresh `~N` ids, so
178
+ * `forceFreshCloneIds` bypasses the conflict.
179
+ */
180
+ export function cloneFanoutConflict(dag, envelope, states, retryDownstreamIds, options) {
181
+ if (options?.forceFreshCloneIds === true)
182
+ return undefined;
104
183
  const snapshot = asDagSnapshot(dag);
105
184
  for (const item of envelope.clone_forks ?? []) {
106
185
  if (item.action !== "fanout")
107
186
  continue;
108
- if (definitionInstances(dag, item.successor_id).some((id) => id !== item.successor_id)) {
109
- return `clone fan-out for "${item.successor_id}" is already instanced`;
187
+ const existing = definitionInstances(dag, item.successor_id).filter((id) => id !== item.successor_id);
188
+ if (existing.length > 0) {
189
+ const reactivatable = existing.every((id) => states.get(id) === "pending" && retryDownstreamIds.has(id));
190
+ if (!reactivatable) {
191
+ return `clone fan-out for "${item.successor_id}" is already instanced`;
192
+ }
193
+ continue;
110
194
  }
111
195
  if (!snapshot.stage_ids.includes(item.successor_id)) {
112
196
  return `clone fan-out for "${item.successor_id}" is already instanced`;
@@ -114,6 +198,18 @@ export function cloneFanoutConflict(dag, envelope) {
114
198
  }
115
199
  return undefined;
116
200
  }
201
+ export function nextFreeCloneSuffix(snapshot, catalogId) {
202
+ const prefix = `${catalogId}~`;
203
+ let max = 0;
204
+ for (const id of snapshot.stage_ids) {
205
+ if (!id.startsWith(prefix))
206
+ continue;
207
+ const n = Number(id.slice(prefix.length));
208
+ if (Number.isInteger(n) && n > max)
209
+ max = n;
210
+ }
211
+ return max + 1;
212
+ }
117
213
  export function protectedClonableChildIds(dag, predecessorId, envelope) {
118
214
  const ids = new Set();
119
215
  for (const childId of dag.childrenOf[predecessorId] ?? []) {
@@ -126,15 +222,35 @@ export function protectedClonableChildIds(dag, predecessorId, envelope) {
126
222
  }
127
223
  return ids;
128
224
  }
225
+ /**
226
+ * Downstream closure for a retry, resolved through clone-instance ids and
227
+ * static multi-parent edges. Catalog-level children (e.g. a join stage) are
228
+ * registered in `childrenOf` under the *definition* id, not per clone
229
+ * instance, so a plain childrenOf walk starting above a fanout dead-ends at
230
+ * the instance ids it discovers transitively. Resolving definition-id
231
+ * children and `predecessorEdges` join successors at every step means
232
+ * retrying an ancestor or a single clone instance still reaches the join
233
+ * and everything after it, without pulling in sibling terminals.
234
+ */
129
235
  export function cloneRetryDownstream(dag, stageIds) {
130
- const downstream = collectDownstreamClosure(dag, stageIds);
131
- for (const id of stageIds) {
132
- const defId = definitionIdForInstance(asDagSnapshot(dag), id);
133
- for (const child of dag.childrenOf[defId] ?? []) {
236
+ const snapshot = asDagSnapshot(dag);
237
+ const downstream = new Set();
238
+ const seeds = new Set(stageIds);
239
+ const queue = [...stageIds];
240
+ while (queue.length > 0) {
241
+ const id = queue.shift();
242
+ const defId = definitionIdForInstance(snapshot, id);
243
+ const children = new Set([
244
+ ...(dag.childrenOf[id] ?? []),
245
+ ...(defId !== id ? dag.childrenOf[defId] ?? [] : []),
246
+ ...joinSuccessorIds(dag, id),
247
+ ...(defId !== id ? joinSuccessorIds(dag, defId) : []),
248
+ ]);
249
+ for (const child of children) {
250
+ if (downstream.has(child) || seeds.has(child))
251
+ continue;
134
252
  downstream.add(child);
135
- for (const desc of collectDownstreamStageIds(dag, child)) {
136
- downstream.add(desc);
137
- }
253
+ queue.push(child);
138
254
  }
139
255
  }
140
256
  return downstream;
@@ -160,30 +276,53 @@ export function cloneFailFastSkipIds(dag, stageId, completedEnvelopes) {
160
276
  ids.push(...joinAndDownstreamIds(dag, stageId));
161
277
  return ids;
162
278
  }
163
- export function cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes) {
279
+ export function cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes, options) {
164
280
  const node = dag.nodes.find((n) => n.id === stageId);
165
281
  if (!node)
166
282
  return false;
167
283
  if (definitionInstances(dag, stageId).some((id) => id !== stageId)) {
168
284
  return false;
169
285
  }
170
- if (node.needs === null) {
286
+ const edges = predecessorEdges(node);
287
+ if (edges.length === 0) {
171
288
  return !sequentialPreviousUnsatisfied(dag, stageId, states, completedEnvelopes);
172
289
  }
173
- const instances = definitionInstances(dag, node.needs);
174
- if (instances.length <= 1) {
175
- const parentId = instances[0] ?? node.needs;
176
- if (states.get(parentId) !== "succeeded")
177
- return false;
290
+ if (edges.length >= 2) {
291
+ for (const edge of edges) {
292
+ if (!edgeInstancesReady(dag, edge, states))
293
+ return false;
294
+ }
178
295
  }
179
- else if (!instances.every((id) => states.get(id) === "succeeded")) {
180
- return false;
296
+ else {
297
+ const parentNeedId = typeof node.needs === "string" && node.needs ? node.needs : edges[0].id;
298
+ const allInstances = definitionInstances(dag, parentNeedId);
299
+ const cohort = options?.activeCohortForNeeds?.(parentNeedId) ??
300
+ activeCohortFromCloneIds(options?.activeCloneIdsForNeeds?.(parentNeedId));
301
+ if (cohort.kind === "awaiting_mint") {
302
+ return false;
303
+ }
304
+ const instances = filterJoinInputs(allInstances, cohort);
305
+ if (instances.length <= 1) {
306
+ const parentId = instances[0] ?? parentNeedId;
307
+ if (states.get(parentId) !== "succeeded")
308
+ return false;
309
+ }
310
+ else if (cohort.kind === "active") {
311
+ if (!instances.every((id) => states.get(id) === "succeeded")) {
312
+ return false;
313
+ }
314
+ }
315
+ else if (!instances.every((id) => states.get(id) === "succeeded" || states.get(id) === "skipped")) {
316
+ return false;
317
+ }
181
318
  }
182
319
  return !sequentialPreviousUnsatisfied(dag, stageId, states, completedEnvelopes);
183
320
  }
184
- export function applyCloneForksToSchedule(dag, predecessorId, envelope, states) {
321
+ export function applyCloneForksToSchedule(dag, predecessorId, envelope, states, options) {
185
322
  let current = asDagSnapshot(dag);
186
323
  const skippedIds = [];
324
+ const mintedBySuccessor = new Map();
325
+ const forceFresh = options?.forceFreshCloneIds === true;
187
326
  const skipPending = (stageId) => {
188
327
  if (states.get(stageId) === "pending") {
189
328
  states.set(stageId, "skipped");
@@ -193,15 +332,55 @@ export function applyCloneForksToSchedule(dag, predecessorId, envelope, states)
193
332
  for (const item of envelope.clone_forks ?? []) {
194
333
  if (item.action === "skip") {
195
334
  skipPending(item.successor_id);
196
- for (const desc of collectDownstreamStageIds(current, item.successor_id)) {
197
- skipPending(desc);
198
- }
335
+ skipRejectedNeedDependents(current, item.successor_id, states, "skipped", skipPending);
199
336
  continue;
200
337
  }
201
338
  if (item.action !== "fanout")
202
339
  continue;
203
- const existing = definitionInstances(current, item.successor_id);
204
- if (existing.some((id) => id !== item.successor_id)) {
340
+ const existing = definitionInstances(current, item.successor_id).filter((id) => id !== item.successor_id);
341
+ if (forceFresh) {
342
+ const { snapshot, instanceIds } = appendCloneInstances(current, {
343
+ catalogId: item.successor_id,
344
+ predecessorId,
345
+ count: item.clones.length,
346
+ startAt: nextFreeCloneSuffix(current, item.successor_id),
347
+ });
348
+ current = snapshot;
349
+ states.delete(item.successor_id);
350
+ for (const id of instanceIds) {
351
+ states.set(id, "pending");
352
+ }
353
+ mintedBySuccessor.set(item.successor_id, instanceIds);
354
+ continue;
355
+ }
356
+ if (existing.length > 0) {
357
+ // Reactivating instances a retry cascade reset to `pending` (see
358
+ // cloneFanoutConflict), which only allows this fanout through when
359
+ // *every* existing instance is pending. Anything short of that —
360
+ // a normal resume re-apply of an already-processed fanout, or only
361
+ // one sibling out of a cohort being retried directly — must stay a
362
+ // no-op here: growing/shrinking the cohort in those cases would mint
363
+ // or skip instances no retry actually asked for.
364
+ const allPending = existing.every((id) => states.get(id) === "pending");
365
+ if (!allPending)
366
+ continue;
367
+ const desired = item.clones.length;
368
+ for (const id of existing.slice(desired)) {
369
+ skipPending(id);
370
+ skipRejectedNeedDependents(current, id, states, "skipped", skipPending);
371
+ }
372
+ if (desired > existing.length) {
373
+ const { snapshot, instanceIds } = appendCloneInstances(current, {
374
+ catalogId: item.successor_id,
375
+ predecessorId,
376
+ count: desired - existing.length,
377
+ startAt: nextFreeCloneSuffix(current, item.successor_id),
378
+ });
379
+ current = snapshot;
380
+ for (const id of instanceIds) {
381
+ states.set(id, "pending");
382
+ }
383
+ }
205
384
  continue;
206
385
  }
207
386
  const { snapshot, instanceIds } = appendCloneInstances(current, {
@@ -214,8 +393,13 @@ export function applyCloneForksToSchedule(dag, predecessorId, envelope, states)
214
393
  for (const id of instanceIds) {
215
394
  states.set(id, "pending");
216
395
  }
396
+ mintedBySuccessor.set(item.successor_id, instanceIds);
217
397
  }
218
- return { dag: current, skippedIds };
398
+ return {
399
+ dag: current,
400
+ skippedIds,
401
+ ...(mintedBySuccessor.size > 0 ? { mintedBySuccessor } : {}),
402
+ };
219
403
  }
220
404
  export function applyCloneForksFromEnvelopes(dag, states, completedEnvelopes) {
221
405
  let current = asDagSnapshot(dag);
@@ -0,0 +1,137 @@
1
+ import type { CompletionCheck, CompletionContract } from "../types/completion.js";
2
+ import type { StageEnvelope } from "../types/envelope.js";
3
+ import type { StageGateKind } from "../types/stage.js";
4
+ /**
5
+ * The result of executing a pipeline-owned command. Command execution is a
6
+ * runtime capability so callers can supply a sandboxed implementation.
7
+ */
8
+ export type CommandExecution = {
9
+ exit_code: number | null;
10
+ stdout: string;
11
+ stderr: string;
12
+ timed_out: boolean;
13
+ error?: string;
14
+ };
15
+ export type CommandExecutionInput = {
16
+ command: string;
17
+ cwd: string;
18
+ timeout_ms?: number;
19
+ };
20
+ export type CommandExecutor = {
21
+ run(input: CommandExecutionInput): Promise<CommandExecution>;
22
+ };
23
+ /** A completed operator interaction, normalized outside the agent adapter. */
24
+ export type GateDecision = {
25
+ kind: StageGateKind;
26
+ status: "accepted" | "rejected" | "answered";
27
+ prompt_id?: string;
28
+ };
29
+ export type GateDecisionProvider = {
30
+ listDecisions(kind: StageGateKind): Promise<readonly GateDecision[]>;
31
+ };
32
+ /**
33
+ * A VCS-neutral checkout baseline. `fingerprint` is owned by the provider:
34
+ * for example, a blob hash, an mtime/content hash pair, or a Git index value.
35
+ */
36
+ export type CheckoutSnapshot = {
37
+ entries: readonly {
38
+ path: string;
39
+ fingerprint: string;
40
+ }[];
41
+ };
42
+ export type CheckoutChange = {
43
+ path: string;
44
+ status: "added" | "modified" | "deleted" | "renamed" | "untracked";
45
+ };
46
+ export type CheckoutCapability = {
47
+ capture(): Promise<CheckoutSnapshot>;
48
+ changesSince(before: CheckoutSnapshot): Promise<readonly CheckoutChange[]>;
49
+ };
50
+ /** The agent's explicit acknowledgement of every item in one checklist. */
51
+ export type ChecklistAttestation = {
52
+ check_id: string;
53
+ items: readonly string[];
54
+ };
55
+ export type CompletionCheckRunnerInput = {
56
+ contract: CompletionContract;
57
+ envelope: StageEnvelope;
58
+ /** The stage's declared payload_schema, when it has one. */
59
+ payloadSchema?: unknown;
60
+ /** Absolute attempt artifact directory. */
61
+ artifactsDir: string;
62
+ /** Base directory used for a command check without its own cwd. */
63
+ commandWorkingDirectory?: string;
64
+ commandExecutor?: CommandExecutor;
65
+ gates?: GateDecisionProvider;
66
+ /** Captured before the agent attempt began. */
67
+ checkoutBefore?: CheckoutSnapshot;
68
+ checkout?: CheckoutCapability;
69
+ /** Why the runtime could not establish the required pre-stage baseline. */
70
+ checkoutError?: string;
71
+ checklistAttestations?: readonly ChecklistAttestation[];
72
+ onCheckStart?: (check: CompletionCheck) => Promise<void> | void;
73
+ onCheckComplete?: (result: CompletionCheckResult) => Promise<void> | void;
74
+ };
75
+ export type CommandCheckEvidence = {
76
+ kind: "command";
77
+ command: string;
78
+ cwd?: string;
79
+ exit_code: number | null;
80
+ stdout: string;
81
+ stderr: string;
82
+ timed_out: boolean;
83
+ error?: string;
84
+ };
85
+ export type ArtifactCheckEvidence = {
86
+ kind: "artifact";
87
+ path: string;
88
+ exists: boolean;
89
+ file_type: "file" | "directory" | "symlink" | "other" | "missing";
90
+ size_bytes?: number;
91
+ sha256?: string;
92
+ };
93
+ export type PayloadSchemaCheckEvidence = {
94
+ kind: "payload_schema";
95
+ schema_declared: boolean;
96
+ payload_present: boolean;
97
+ validation_error?: string;
98
+ };
99
+ export type GateCheckEvidence = {
100
+ kind: "gate";
101
+ gate_kind: StageGateKind;
102
+ decisions: GateDecision[];
103
+ };
104
+ export type CheckoutChangesCheckEvidence = {
105
+ kind: "checkout_changes";
106
+ changes: CheckoutChange[];
107
+ claimed_paths?: string[];
108
+ missing_claimed_paths?: string[];
109
+ unclaimed_changed_paths?: string[];
110
+ };
111
+ export type ChecklistCheckEvidence = {
112
+ kind: "checklist";
113
+ expected_items: string[];
114
+ attested_items: string[];
115
+ missing_items: string[];
116
+ unexpected_items: string[];
117
+ };
118
+ export type CompletionCheckEvidence = CommandCheckEvidence | ArtifactCheckEvidence | PayloadSchemaCheckEvidence | GateCheckEvidence | CheckoutChangesCheckEvidence | ChecklistCheckEvidence;
119
+ export type CompletionCheckResult = {
120
+ check_id: string;
121
+ check_type: CompletionCheck["type"];
122
+ outcome: "passed" | "failed" | "error";
123
+ message?: string;
124
+ evidence: CompletionCheckEvidence;
125
+ };
126
+ export type CompletionVerificationResult = {
127
+ outcome: "passed" | "failed";
128
+ checks: CompletionCheckResult[];
129
+ failed_check_ids: string[];
130
+ };
131
+ /** Default Node implementation; callers may substitute a sandboxed executor. */
132
+ export declare const nodeCommandExecutor: CommandExecutor;
133
+ /**
134
+ * Run every completion check in declaration order. A failed check never throws
135
+ * or prevents later checks from producing their own repair evidence.
136
+ */
137
+ export declare function runCompletionContract(input: CompletionCheckRunnerInput): Promise<CompletionVerificationResult>;