stageflow 0.9.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +6 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +331 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +12 -7
  15. package/dist/agent/piAdapter.js +194 -44
  16. package/dist/agent/piIsolatedMcp.d.ts +23 -0
  17. package/dist/agent/piIsolatedMcp.js +223 -0
  18. package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
  19. package/dist/agent/piIsolatedMcpProbe.js +292 -0
  20. package/dist/agent/port.d.ts +52 -1
  21. package/dist/agent/port.js +1 -1
  22. package/dist/agent/resolveAgentPort.d.ts +26 -0
  23. package/dist/agent/resolveAgentPort.js +39 -0
  24. package/dist/cli/runCommand.js +2 -2
  25. package/dist/cli/runsCommand.d.ts +1 -1
  26. package/dist/cli/runsCommand.js +184 -2
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +11 -5
  29. package/dist/config/browseCatalog.js +1 -1
  30. package/dist/config/createPipeline.d.ts +2 -1
  31. package/dist/config/createPipeline.js +34 -9
  32. package/dist/config/createStage.js +14 -9
  33. package/dist/config/loadPipeline.js +11 -1
  34. package/dist/config/loadStage.d.ts +1 -0
  35. package/dist/config/loadStage.js +207 -2
  36. package/dist/config/loadStageflowManifest.js +14 -1
  37. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  38. package/dist/config/mergePipelineIncludes.js +14 -1
  39. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  40. package/dist/config/normalizePipelineStageEntry.js +56 -7
  41. package/dist/config/parseCompletionContract.d.ts +8 -0
  42. package/dist/config/parseCompletionContract.js +218 -0
  43. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  44. package/dist/config/parsePreEmitChecks.js +65 -0
  45. package/dist/config/pipelineNeeds.d.ts +19 -0
  46. package/dist/config/pipelineNeeds.js +151 -0
  47. package/dist/config/pipelineStageKeys.js +17 -1
  48. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  49. package/dist/config/resolveForkEmitContext.js +9 -2
  50. package/dist/config/resolvePipelineDag.d.ts +2 -1
  51. package/dist/config/resolvePipelineDag.js +182 -39
  52. package/dist/config/resolveStageMcpServers.d.ts +35 -0
  53. package/dist/config/resolveStageMcpServers.js +207 -0
  54. package/dist/config/validateCatalog.d.ts +1 -1
  55. package/dist/config/validateCatalog.js +51 -0
  56. package/dist/config/validateCompletionContract.d.ts +4 -0
  57. package/dist/config/validateCompletionContract.js +52 -0
  58. package/dist/envelope/check.d.ts +1 -1
  59. package/dist/envelope/check.js +86 -13
  60. package/dist/envelope/cloneForks.d.ts +1 -1
  61. package/dist/envelope/cloneForks.js +24 -9
  62. package/dist/envelope/feedbackLoop.d.ts +4 -0
  63. package/dist/envelope/feedbackLoop.js +31 -0
  64. package/dist/envelope/payloadSchema.d.ts +7 -1
  65. package/dist/envelope/payloadSchema.js +170 -13
  66. package/dist/envelope/preEmitChecks.d.ts +29 -0
  67. package/dist/envelope/preEmitChecks.js +57 -0
  68. package/dist/hitl/qaTrail.d.ts +25 -0
  69. package/dist/hitl/qaTrail.js +51 -0
  70. package/dist/mcp/catalogTools.js +4 -2
  71. package/dist/mcp/controlTools.js +72 -0
  72. package/dist/mcp/readArtifact.d.ts +2 -0
  73. package/dist/mcp/readArtifact.js +25 -1
  74. package/dist/mcp/waitingGates.d.ts +2 -1
  75. package/dist/mcp/waitingGates.js +17 -1
  76. package/dist/package-meta.d.ts +1 -1
  77. package/dist/package-meta.js +1 -1
  78. package/dist/projection/projectRun.d.ts +4 -1
  79. package/dist/projection/projectRun.js +7 -0
  80. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  81. package/dist/prompt/feedbackLoopContext.js +17 -0
  82. package/dist/prompt/priorEnvelope.d.ts +2 -2
  83. package/dist/prompt/priorEnvelope.js +4 -1
  84. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  85. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  86. package/dist/runstore/port.d.ts +185 -3
  87. package/dist/runstore/port.js +42 -3
  88. package/dist/runstore/runProjection.d.ts +7 -3
  89. package/dist/runstore/runProjection.js +30 -15
  90. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  91. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  92. package/dist/runstore/sqlite/schema.d.ts +1 -1
  93. package/dist/runstore/sqlite/schema.js +99 -0
  94. package/dist/runstore/stageInstanceId.d.ts +1 -1
  95. package/dist/runstore/stageInstanceId.js +3 -2
  96. package/dist/runstore/trackProjection.js +59 -27
  97. package/dist/runstore/verificationHistory.d.ts +19 -0
  98. package/dist/runstore/verificationHistory.js +30 -0
  99. package/dist/runtime/cloneSchedule.d.ts +40 -4
  100. package/dist/runtime/cloneSchedule.js +212 -28
  101. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  102. package/dist/runtime/completionCheckRunner.js +365 -0
  103. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  104. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  105. package/dist/runtime/envelopeRouting.d.ts +10 -3
  106. package/dist/runtime/envelopeRouting.js +146 -5
  107. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  108. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  109. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  110. package/dist/runtime/feedbackLoopDecision.js +207 -0
  111. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  112. package/dist/runtime/feedbackLoopRoute.js +13 -0
  113. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  114. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  115. package/dist/runtime/forkGeneration.d.ts +72 -0
  116. package/dist/runtime/forkGeneration.js +295 -0
  117. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  118. package/dist/runtime/gitCheckoutCapability.js +97 -0
  119. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  120. package/dist/runtime/manualRecoveryState.js +73 -0
  121. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  122. package/dist/runtime/pipelineScheduler.js +328 -45
  123. package/dist/runtime/replayLifecycle.d.ts +107 -0
  124. package/dist/runtime/replayLifecycle.js +168 -0
  125. package/dist/runtime/runManager.d.ts +54 -1
  126. package/dist/runtime/runManager.js +471 -6
  127. package/dist/runtime/runRetryCoordinator.d.ts +6 -1
  128. package/dist/runtime/runRetryCoordinator.js +43 -9
  129. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  130. package/dist/runtime/stageAttemptBootstrap.js +117 -3
  131. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  132. package/dist/runtime/stageRecovery.js +1 -1
  133. package/dist/runtime/stageRunner.d.ts +4 -1
  134. package/dist/runtime/stageRunner.js +29 -3
  135. package/dist/runtime/stageWorker.js +69 -2
  136. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  137. package/dist/runtime/verifiedStageExecution.d.ts +31 -0
  138. package/dist/runtime/verifiedStageExecution.js +127 -0
  139. package/dist/server/bootstrap.js +1 -0
  140. package/dist/server/http.js +137 -4
  141. package/dist/server/operatorResults.js +2 -0
  142. package/dist/server/projectMcpRoutes.d.ts +6 -0
  143. package/dist/server/projectMcpRoutes.js +38 -0
  144. package/dist/tools/askOperator.d.ts +161 -5
  145. package/dist/tools/askOperator.js +58 -25
  146. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  147. package/dist/tools/emitStageEnvelope.js +112 -24
  148. package/dist/types/completion.d.ts +43 -0
  149. package/dist/types/completion.js +1 -0
  150. package/dist/types/envelope.d.ts +18 -0
  151. package/dist/types/forkChoice.d.ts +9 -4
  152. package/dist/types/forkChoice.js +1 -1
  153. package/dist/types/pipeline.d.ts +41 -2
  154. package/dist/types/preEmitCheck.d.ts +18 -0
  155. package/dist/types/preEmitCheck.js +1 -0
  156. package/dist/types/stage.d.ts +14 -0
  157. package/dist/types/stageflowManifest.d.ts +2 -0
  158. package/dist/ui/assets/index-Yzh42vhf.js +118 -0
  159. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  160. package/dist/ui/index.html +2 -2
  161. package/package.json +5 -1
  162. package/skills/stageflow-author/SKILL.md +20 -2
  163. package/skills/stageflow-author/references/catalog-mapping.md +88 -3
  164. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  165. package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
  166. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
  167. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -0,0 +1,365 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { lstat, readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
6
+ const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
7
+ function captureOutput(chunks, chunk, currentSize) {
8
+ const remaining = MAX_COMMAND_OUTPUT_BYTES - currentSize;
9
+ if (remaining <= 0)
10
+ return currentSize;
11
+ const accepted = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
12
+ chunks.push(accepted);
13
+ return currentSize + accepted.byteLength;
14
+ }
15
+ /** Default Node implementation; callers may substitute a sandboxed executor. */
16
+ export const nodeCommandExecutor = {
17
+ async run(input) {
18
+ return new Promise((resolve) => {
19
+ let stdoutSize = 0;
20
+ let stderrSize = 0;
21
+ const stdout = [];
22
+ const stderr = [];
23
+ let timedOut = false;
24
+ let settled = false;
25
+ let timeout;
26
+ const finish = (result) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ if (timeout)
31
+ clearTimeout(timeout);
32
+ resolve(result);
33
+ };
34
+ let child;
35
+ try {
36
+ child = spawn(input.command, {
37
+ cwd: input.cwd,
38
+ shell: true,
39
+ stdio: ["ignore", "pipe", "pipe"],
40
+ });
41
+ }
42
+ catch (error) {
43
+ finish({
44
+ exit_code: null,
45
+ stdout: "",
46
+ stderr: "",
47
+ timed_out: false,
48
+ error: error instanceof Error ? error.message : String(error),
49
+ });
50
+ return;
51
+ }
52
+ child.stdout?.on("data", (data) => {
53
+ stdoutSize = captureOutput(stdout, Buffer.from(data), stdoutSize);
54
+ });
55
+ child.stderr?.on("data", (data) => {
56
+ stderrSize = captureOutput(stderr, Buffer.from(data), stderrSize);
57
+ });
58
+ child.once("error", (error) => {
59
+ finish({
60
+ exit_code: null,
61
+ stdout: Buffer.concat(stdout).toString("utf8"),
62
+ stderr: Buffer.concat(stderr).toString("utf8"),
63
+ timed_out: timedOut,
64
+ error: error.message,
65
+ });
66
+ });
67
+ child.once("close", (code) => {
68
+ finish({
69
+ exit_code: code,
70
+ stdout: Buffer.concat(stdout).toString("utf8"),
71
+ stderr: Buffer.concat(stderr).toString("utf8"),
72
+ timed_out: timedOut,
73
+ });
74
+ });
75
+ if (input.timeout_ms !== undefined) {
76
+ timeout = setTimeout(() => {
77
+ timedOut = true;
78
+ child.kill("SIGTERM");
79
+ }, input.timeout_ms);
80
+ }
81
+ });
82
+ },
83
+ };
84
+ function result(check, outcome, evidence, message) {
85
+ return {
86
+ check_id: check.id,
87
+ check_type: check.type,
88
+ outcome,
89
+ ...(message !== undefined ? { message } : {}),
90
+ evidence,
91
+ };
92
+ }
93
+ function resolveCommandCwd(check, base) {
94
+ return check.cwd === undefined ? base : path.resolve(base, check.cwd);
95
+ }
96
+ async function runCommandCheck(check, input) {
97
+ const base = input.commandWorkingDirectory;
98
+ if (base === undefined) {
99
+ return result(check, "error", { kind: "command", command: check.run, exit_code: null, stdout: "", stderr: "", timed_out: false }, "command check requires commandWorkingDirectory");
100
+ }
101
+ const cwd = resolveCommandCwd(check, base);
102
+ const executor = input.commandExecutor ?? nodeCommandExecutor;
103
+ try {
104
+ const execution = await executor.run({ command: check.run, cwd, timeout_ms: check.timeout_ms });
105
+ const evidence = {
106
+ kind: "command",
107
+ command: check.run,
108
+ cwd,
109
+ exit_code: execution.exit_code,
110
+ stdout: execution.stdout,
111
+ stderr: execution.stderr,
112
+ timed_out: execution.timed_out,
113
+ ...(execution.error !== undefined ? { error: execution.error } : {}),
114
+ };
115
+ if (execution.timed_out)
116
+ return result(check, "failed", evidence, "command timed out");
117
+ if (execution.error !== undefined)
118
+ return result(check, "error", evidence, execution.error);
119
+ if (execution.exit_code !== 0)
120
+ return result(check, "failed", evidence, `command exited with code ${execution.exit_code ?? "unknown"}`);
121
+ return result(check, "passed", evidence);
122
+ }
123
+ catch (error) {
124
+ return result(check, "error", { kind: "command", command: check.run, cwd, exit_code: null, stdout: "", stderr: "", timed_out: false }, error instanceof Error ? error.message : String(error));
125
+ }
126
+ }
127
+ async function inspectArtifact(artifactsDir, relativePath) {
128
+ const target = path.resolve(artifactsDir, relativePath);
129
+ const relative = path.relative(artifactsDir, target);
130
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
131
+ return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
132
+ }
133
+ try {
134
+ // lstat every component rather than just the target: lstat(target) follows
135
+ // a symlink in a parent directory, which could otherwise prove a file that
136
+ // lives outside the attempt artifact root.
137
+ const segments = relative.split(path.sep).filter((segment) => segment !== "");
138
+ let current = artifactsDir;
139
+ let stats;
140
+ for (const segment of segments) {
141
+ current = path.join(current, segment);
142
+ stats = await lstat(current);
143
+ if (stats.isSymbolicLink()) {
144
+ return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
145
+ }
146
+ if (current !== target && !stats.isDirectory()) {
147
+ return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
148
+ }
149
+ }
150
+ if (stats === undefined) {
151
+ return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
152
+ }
153
+ if (stats.isSymbolicLink()) {
154
+ return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
155
+ }
156
+ if (stats.isDirectory()) {
157
+ return { kind: "artifact", path: relativePath, exists: true, file_type: "directory" };
158
+ }
159
+ if (!stats.isFile()) {
160
+ return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
161
+ }
162
+ const content = await readFile(target);
163
+ return {
164
+ kind: "artifact",
165
+ path: relativePath,
166
+ exists: true,
167
+ file_type: "file",
168
+ size_bytes: stats.size,
169
+ sha256: createHash("sha256").update(content).digest("hex"),
170
+ };
171
+ }
172
+ catch (error) {
173
+ if (error.code === "ENOENT") {
174
+ return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+ async function runArtifactCheck(check, input) {
180
+ try {
181
+ const evidence = await inspectArtifact(input.artifactsDir, check.path);
182
+ if (evidence.file_type !== "file") {
183
+ return result(check, "failed", evidence, `artifact is ${evidence.file_type}`);
184
+ }
185
+ if (check.nonempty === true && evidence.size_bytes === 0) {
186
+ return result(check, "failed", evidence, "artifact is empty");
187
+ }
188
+ return result(check, "passed", evidence);
189
+ }
190
+ catch (error) {
191
+ return result(check, "error", { kind: "artifact", path: check.path, exists: false, file_type: "missing" }, error instanceof Error ? error.message : String(error));
192
+ }
193
+ }
194
+ function runPayloadSchemaCheck(check, input) {
195
+ const evidence = {
196
+ kind: "payload_schema",
197
+ schema_declared: input.payloadSchema !== undefined,
198
+ payload_present: input.envelope.payload !== undefined,
199
+ };
200
+ if (input.payloadSchema === undefined) {
201
+ evidence.validation_error = "stage does not declare payload_schema";
202
+ return result(check, "error", evidence, evidence.validation_error);
203
+ }
204
+ try {
205
+ assertEnvelopePayload(input.envelope, input.payloadSchema);
206
+ return result(check, "passed", evidence);
207
+ }
208
+ catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ evidence.validation_error = message;
211
+ return result(check, "failed", evidence, message);
212
+ }
213
+ }
214
+ async function runGateCheck(check, input) {
215
+ const baseEvidence = { kind: "gate", gate_kind: check.kind, decisions: [] };
216
+ if (!input.gates) {
217
+ return result(check, "error", baseEvidence, "gate check requires a gate decision provider");
218
+ }
219
+ try {
220
+ const decisions = [...(await input.gates.listDecisions(check.kind))]
221
+ .filter((decision) => decision.kind === check.kind)
222
+ .sort((a, b) => (a.prompt_id ?? "").localeCompare(b.prompt_id ?? "") || a.status.localeCompare(b.status));
223
+ const evidence = { ...baseEvidence, decisions };
224
+ const accepted = decisions.some((decision) => decision.status === "accepted");
225
+ const answered = decisions.some((decision) => decision.status === "answered");
226
+ const passes = check.kind === "confirm" ? accepted : accepted || answered;
227
+ return passes
228
+ ? result(check, "passed", evidence)
229
+ : result(check, "failed", evidence, `no accepted ${check.kind} gate decision`);
230
+ }
231
+ catch (error) {
232
+ return result(check, "error", baseEvidence, error instanceof Error ? error.message : String(error));
233
+ }
234
+ }
235
+ function normalizeCheckoutPath(value) {
236
+ const normalized = path.posix.normalize(value.replaceAll("\\", "/"));
237
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
238
+ return undefined;
239
+ }
240
+ return normalized;
241
+ }
242
+ function claimedPaths(payload, fields) {
243
+ if (fields === undefined)
244
+ return { paths: [], invalid: [] };
245
+ const paths = [];
246
+ const invalid = [];
247
+ for (const field of fields) {
248
+ const value = payload?.[field];
249
+ if (!Array.isArray(value)) {
250
+ invalid.push(field);
251
+ continue;
252
+ }
253
+ for (const candidate of value) {
254
+ if (typeof candidate !== "string") {
255
+ invalid.push(field);
256
+ continue;
257
+ }
258
+ const normalized = normalizeCheckoutPath(candidate);
259
+ if (normalized === undefined)
260
+ invalid.push(candidate);
261
+ else
262
+ paths.push(normalized);
263
+ }
264
+ }
265
+ return { paths: [...new Set(paths)].sort(), invalid: [...new Set(invalid)].sort() };
266
+ }
267
+ async function runCheckoutChangesCheck(check, input) {
268
+ const empty = { kind: "checkout_changes", changes: [] };
269
+ if (input.checkoutError !== undefined) {
270
+ return result(check, "error", empty, input.checkoutError);
271
+ }
272
+ if (!input.checkout || !input.checkoutBefore) {
273
+ return result(check, "error", empty, "checkout check requires a before snapshot and checkout capability");
274
+ }
275
+ try {
276
+ const rawChanges = await input.checkout.changesSince(input.checkoutBefore);
277
+ const changes = [...rawChanges]
278
+ .map((change) => ({ ...change, path: normalizeCheckoutPath(change.path) }))
279
+ .filter((change) => change.path !== undefined)
280
+ .sort((a, b) => a.path.localeCompare(b.path) || a.status.localeCompare(b.status));
281
+ const { paths, invalid } = claimedPaths(input.envelope.payload, check.path_fields);
282
+ const changedPaths = [...new Set(changes.map((change) => change.path))];
283
+ const evidence = {
284
+ kind: "checkout_changes",
285
+ changes,
286
+ ...(check.path_fields !== undefined ? { claimed_paths: paths } : {}),
287
+ };
288
+ if (changes.length === 0)
289
+ return result(check, "failed", evidence, "checkout has no changes from this attempt");
290
+ if (invalid.length > 0)
291
+ return result(check, "failed", evidence, `invalid claimed checkout paths: ${invalid.join(", ")}`);
292
+ if (check.path_fields !== undefined) {
293
+ const claimed = new Set(paths);
294
+ const actual = new Set(changedPaths);
295
+ const missing = paths.filter((item) => !actual.has(item));
296
+ const unclaimed = changedPaths.filter((item) => !claimed.has(item));
297
+ if (missing.length > 0)
298
+ evidence.missing_claimed_paths = missing;
299
+ if (unclaimed.length > 0)
300
+ evidence.unclaimed_changed_paths = unclaimed;
301
+ if (missing.length > 0 || unclaimed.length > 0) {
302
+ return result(check, "failed", evidence, "claimed checkout paths do not match observed changes");
303
+ }
304
+ }
305
+ return result(check, "passed", evidence);
306
+ }
307
+ catch (error) {
308
+ return result(check, "error", empty, error instanceof Error ? error.message : String(error));
309
+ }
310
+ }
311
+ function runChecklistCheck(check, input) {
312
+ const attestation = input.checklistAttestations?.find((item) => item.check_id === check.id);
313
+ const attested = [...new Set(attestation?.items ?? [])].sort();
314
+ const expected = [...check.items].sort();
315
+ const attestedSet = new Set(attested);
316
+ const expectedSet = new Set(expected);
317
+ const evidence = {
318
+ kind: "checklist",
319
+ expected_items: expected,
320
+ attested_items: attested,
321
+ missing_items: expected.filter((item) => !attestedSet.has(item)),
322
+ unexpected_items: attested.filter((item) => !expectedSet.has(item)),
323
+ };
324
+ if (evidence.missing_items.length > 0 || evidence.unexpected_items.length > 0) {
325
+ return result(check, "failed", evidence, "checklist attestation does not match required items");
326
+ }
327
+ return result(check, "passed", evidence);
328
+ }
329
+ async function runCheck(check, input) {
330
+ switch (check.type) {
331
+ case "command":
332
+ return runCommandCheck(check, input);
333
+ case "artifact":
334
+ return runArtifactCheck(check, input);
335
+ case "payload_schema":
336
+ return runPayloadSchemaCheck(check, input);
337
+ case "gate":
338
+ return runGateCheck(check, input);
339
+ case "checkout_changes":
340
+ return runCheckoutChangesCheck(check, input);
341
+ case "checklist":
342
+ return runChecklistCheck(check, input);
343
+ }
344
+ }
345
+ /**
346
+ * Run every completion check in declaration order. A failed check never throws
347
+ * or prevents later checks from producing their own repair evidence.
348
+ */
349
+ export async function runCompletionContract(input) {
350
+ const checks = [];
351
+ for (const check of input.contract.checks) {
352
+ await input.onCheckStart?.(check);
353
+ const completed = await runCheck(check, input);
354
+ await input.onCheckComplete?.(completed);
355
+ checks.push(completed);
356
+ }
357
+ const failedCheckIds = checks
358
+ .filter((check) => check.outcome !== "passed")
359
+ .map((check) => check.check_id);
360
+ return {
361
+ outcome: failedCheckIds.length === 0 ? "passed" : "failed",
362
+ checks,
363
+ failed_check_ids: failedCheckIds,
364
+ };
365
+ }
@@ -0,0 +1,3 @@
1
+ import type { CheckoutSnapshot } from "./completionCheckRunner.js";
2
+ export declare function readCompletionCheckoutBaseline(workspaceDir: string, stageId: string, attempt: number): Promise<CheckoutSnapshot | undefined>;
3
+ export declare function writeCompletionCheckoutBaseline(workspaceDir: string, stageId: string, attempt: number, snapshot: CheckoutSnapshot): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { attemptWorkspaceDir } from "../runstore/workspaceLayout.js";
4
+ function baselinePath(workspaceDir, stageId, attempt) {
5
+ return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "completion-checkout-before.json");
6
+ }
7
+ function parseSnapshot(value) {
8
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
9
+ return undefined;
10
+ }
11
+ const entries = value.entries;
12
+ if (!Array.isArray(entries))
13
+ return undefined;
14
+ if (!entries.every((entry) => entry !== null &&
15
+ typeof entry === "object" &&
16
+ typeof entry.path === "string" &&
17
+ typeof entry.fingerprint === "string")) {
18
+ return undefined;
19
+ }
20
+ return {
21
+ entries: entries.map((entry) => ({
22
+ path: entry.path,
23
+ fingerprint: entry.fingerprint,
24
+ })),
25
+ };
26
+ }
27
+ export async function readCompletionCheckoutBaseline(workspaceDir, stageId, attempt) {
28
+ try {
29
+ const raw = await readFile(baselinePath(workspaceDir, stageId, attempt), "utf8");
30
+ return parseSnapshot(JSON.parse(raw));
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return undefined;
35
+ throw new Error("completion checkout baseline is unreadable");
36
+ }
37
+ }
38
+ export async function writeCompletionCheckoutBaseline(workspaceDir, stageId, attempt, snapshot) {
39
+ const target = baselinePath(workspaceDir, stageId, attempt);
40
+ await mkdir(path.dirname(target), { recursive: true });
41
+ const temporary = `${target}.tmp`;
42
+ await writeFile(temporary, `${JSON.stringify(snapshot)}\n`, "utf8");
43
+ await rename(temporary, target);
44
+ }
@@ -1,5 +1,5 @@
1
1
  import type { RunStore, StageSnapshot } from "../runstore/port.js";
2
- import type { StageEnvelope } from "../types/envelope.js";
2
+ import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
3
3
  import type { LoadedPipeline, ResolvedPipelineDag } from "../types/pipeline.js";
4
4
  import type { StageConfig } from "../types/stage.js";
5
5
  export declare function buildCompletedEnvelopesFromRun(store: RunStore, runId: string, stages?: StageSnapshot[], dag?: ResolvedPipelineDag): Promise<Map<string, StageEnvelope>>;
@@ -8,14 +8,21 @@ export type ResolvePriorEnvelopeResult = {
8
8
  ok: true;
9
9
  prior: StageEnvelope | null;
10
10
  joinPriors?: StageEnvelope[];
11
+ priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>;
11
12
  } | {
12
13
  ok: false;
13
14
  reason: string;
14
15
  };
15
- export declare function resolvePriorEnvelope(options: {
16
+ type ResolvePriorEnvelopeOptions = {
16
17
  dag: ResolvedPipelineDag;
17
18
  stageId: string;
18
19
  completedEnvelopes: Map<string, StageEnvelope>;
19
20
  store?: RunStore;
20
21
  runId?: string;
21
- }): Promise<ResolvePriorEnvelopeResult>;
22
+ stages?: StageSnapshot[];
23
+ /** When set (including null), skips store lookup for active fork generation. */
24
+ activeCloneIds?: Set<string> | null;
25
+ scheduleOverride?: ReadonlyMap<string, ReadonlySet<string>>;
26
+ };
27
+ export declare function resolvePriorEnvelope(options: ResolvePriorEnvelopeOptions): Promise<ResolvePriorEnvelopeResult>;
28
+ export {};
@@ -1,5 +1,7 @@
1
+ import { predecessorEdges } from "../config/pipelineNeeds.js";
1
2
  import { instancesOfDefinition, } from "../runstore/pipelineDagSnapshot.js";
2
3
  import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
4
+ import { activeCohortFromCloneIds, filterJoinInputs, selectActiveInput, } from "./forkGeneration.js";
3
5
  function definitionInstances(dag, catalogId) {
4
6
  const snapshot = dag;
5
7
  if (Array.isArray(snapshot.stage_ids)) {
@@ -7,6 +9,20 @@ function definitionInstances(dag, catalogId) {
7
9
  }
8
10
  return dag.nodes.some((n) => n.id === catalogId) ? [catalogId] : [];
9
11
  }
12
+ async function resolveActiveCohortForNeeds(dag, needsId, store, runId, activeCloneIds, scheduleOverride) {
13
+ if (activeCloneIds !== undefined) {
14
+ return activeCohortFromCloneIds(activeCloneIds);
15
+ }
16
+ if (store === undefined || runId === undefined)
17
+ return { kind: "untracked" };
18
+ return selectActiveInput({
19
+ store,
20
+ runId,
21
+ dag,
22
+ needsStageId: needsId,
23
+ ...(scheduleOverride !== undefined ? { scheduleOverride } : {}),
24
+ });
25
+ }
10
26
  export async function buildCompletedEnvelopesFromRun(store, runId, stages, dag) {
11
27
  const completedEnvelopes = new Map();
12
28
  const snapshotStages = stages ?? (await store.readRun(runId)).stages;
@@ -43,6 +59,123 @@ export function buildStageConfigById(loaded) {
43
59
  function dagNode(dag, stageId) {
44
60
  return dag.nodes.find((node) => node.id === stageId);
45
61
  }
62
+ const SYNTHETIC_SKIPPED_TERMINAL = {
63
+ status: "skipped",
64
+ summary: "stage was skipped",
65
+ artifacts: [],
66
+ };
67
+ function mintedCloneInstances(dag, parentId) {
68
+ return definitionInstances(dag, parentId).filter((id) => id !== parentId);
69
+ }
70
+ function isClonableParent(dag, parentId) {
71
+ const node = dagNode(dag, parentId);
72
+ return node?.clonable === true || mintedCloneInstances(dag, parentId).length > 0;
73
+ }
74
+ function failureReasonFromSnapshot(snap) {
75
+ let reason = "stage failed";
76
+ if (snap === undefined)
77
+ return reason;
78
+ for (const ev of snap.events) {
79
+ if (ev.event === "failed" && typeof ev.reason === "string" && ev.reason) {
80
+ reason = ev.reason;
81
+ }
82
+ }
83
+ return reason;
84
+ }
85
+ async function loadStageSnapshots(options) {
86
+ if (options.stages !== undefined)
87
+ return options.stages;
88
+ if (options.store !== undefined && options.runId !== undefined) {
89
+ return (await options.store.readRun(options.runId)).stages;
90
+ }
91
+ return [];
92
+ }
93
+ async function readCachedEnvelope(options, snapshots, stageId) {
94
+ const fromMap = options.completedEnvelopes.get(stageId);
95
+ if (fromMap !== undefined)
96
+ return structuredClone(fromMap);
97
+ const snap = snapshots.get(stageId);
98
+ if (snap?.envelope)
99
+ return structuredClone(snap.envelope);
100
+ if (options.store !== undefined && options.runId !== undefined) {
101
+ try {
102
+ return structuredClone(await options.store.readEnvelope(options.runId, stageId));
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ return undefined;
109
+ }
110
+ async function terminalForPersistedStage(options, snapshots, stageId) {
111
+ const snap = snapshots.get(stageId);
112
+ if (snap?.status === "skipped") {
113
+ return { ok: true, value: { ...SYNTHETIC_SKIPPED_TERMINAL } };
114
+ }
115
+ if (snap?.status === "failed") {
116
+ const envelope = await readCachedEnvelope(options, snapshots, stageId);
117
+ if (envelope?.status === "failure") {
118
+ return { ok: true, value: envelope };
119
+ }
120
+ return {
121
+ ok: true,
122
+ value: {
123
+ status: "failure",
124
+ summary: failureReasonFromSnapshot(snap),
125
+ artifacts: [],
126
+ },
127
+ };
128
+ }
129
+ if (snap?.status === "succeeded") {
130
+ const envelope = await readCachedEnvelope(options, snapshots, stageId);
131
+ if (envelope === undefined) {
132
+ return {
133
+ ok: false,
134
+ reason: `missing envelope for upstream stage "${stageId}"`,
135
+ };
136
+ }
137
+ return { ok: true, value: envelope };
138
+ }
139
+ return {
140
+ ok: false,
141
+ reason: `missing envelope for upstream stage "${stageId}"`,
142
+ };
143
+ }
144
+ async function resolveGenericJoinPriors(options, node) {
145
+ const snapshots = new Map((await loadStageSnapshots(options)).map((snap) => [snap.stage_id, snap]));
146
+ const priorEnvelopesByStage = {};
147
+ for (const edge of predecessorEdges(node)) {
148
+ const parentId = edge.id;
149
+ if (isClonableParent(options.dag, parentId)) {
150
+ const minted = mintedCloneInstances(options.dag, parentId);
151
+ if (minted.length > 0) {
152
+ const list = [];
153
+ for (const id of minted) {
154
+ const terminal = await terminalForPersistedStage(options, snapshots, id);
155
+ if (!terminal.ok)
156
+ return terminal;
157
+ list.push(terminal.value);
158
+ }
159
+ priorEnvelopesByStage[parentId] = list;
160
+ continue;
161
+ }
162
+ if (snapshots.get(parentId)?.status === "skipped") {
163
+ priorEnvelopesByStage[parentId] = [];
164
+ continue;
165
+ }
166
+ const once = await terminalForPersistedStage(options, snapshots, parentId);
167
+ if (!once.ok)
168
+ return once;
169
+ priorEnvelopesByStage[parentId] = [once.value];
170
+ continue;
171
+ }
172
+ const terminal = await terminalForPersistedStage(options, snapshots, parentId);
173
+ if (!terminal.ok)
174
+ return terminal;
175
+ priorEnvelopesByStage[parentId] = terminal.value;
176
+ }
177
+ return { ok: true, prior: null, priorEnvelopesByStage };
178
+ }
46
179
  export async function resolvePriorEnvelope(options) {
47
180
  const node = dagNode(options.dag, options.stageId);
48
181
  if (!node) {
@@ -51,11 +184,18 @@ export async function resolvePriorEnvelope(options) {
51
184
  reason: `stage "${options.stageId}" not in pipeline DAG`,
52
185
  };
53
186
  }
54
- if (node.needs === null) {
187
+ const edges = predecessorEdges(node);
188
+ if (edges.length === 0) {
55
189
  return { ok: true, prior: null };
56
190
  }
57
- const joinInstances = definitionInstances(options.dag, node.needs);
58
- if (joinInstances.length > 1) {
191
+ if (edges.length >= 2) {
192
+ return resolveGenericJoinPriors(options, node);
193
+ }
194
+ const parentId = typeof node.needs === "string" && node.needs ? node.needs : edges[0].id;
195
+ const allJoinInstances = definitionInstances(options.dag, parentId);
196
+ if (allJoinInstances.length > 1) {
197
+ const cohort = await resolveActiveCohortForNeeds(options.dag, parentId, options.store, options.runId, options.activeCloneIds, options.scheduleOverride);
198
+ const joinInstances = filterJoinInputs(allJoinInstances, cohort);
59
199
  const joinPriors = [];
60
200
  for (const id of joinInstances) {
61
201
  const fromMap = options.completedEnvelopes.get(id);
@@ -83,7 +223,6 @@ export async function resolvePriorEnvelope(options) {
83
223
  }
84
224
  return { ok: true, prior: null, joinPriors };
85
225
  }
86
- const parentId = node.needs;
87
226
  const fromMap = options.completedEnvelopes.get(parentId);
88
227
  let parent = fromMap;
89
228
  if (parent === undefined && options.store !== undefined && options.runId !== undefined) {
@@ -108,7 +247,9 @@ export async function resolvePriorEnvelope(options) {
108
247
  return { ok: true, prior: structuredClone(item.envelope) };
109
248
  }
110
249
  if (item.action === "fanout") {
111
- const instanceIds = definitionInstances(options.dag, item.successor_id);
250
+ const allInstanceIds = definitionInstances(options.dag, item.successor_id);
251
+ const cohort = await resolveActiveCohortForNeeds(options.dag, item.successor_id, options.store, options.runId, options.activeCloneIds, options.scheduleOverride);
252
+ const instanceIds = filterJoinInputs(allInstanceIds, cohort);
112
253
  const index = instanceIds.indexOf(options.stageId);
113
254
  if (index >= 0) {
114
255
  const clone = item.clones[index];