@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138

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 (230) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
2
+ import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
3
3
  export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
4
4
  import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
5
5
  import { spawn, spawnSync } from "node:child_process";
@@ -15,6 +15,7 @@ import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDela
15
15
  import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
16
16
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
17
17
  import { getResourceDatabase } from "../storage/database.js";
18
+ import { readBoundedReleaseFile } from "./release-source.js";
18
19
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
19
20
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
20
21
  // auditable attempt. A second caller must not mistake that small handoff window
@@ -59,7 +60,11 @@ export function terminalCheckSettled(context, record) {
59
60
  resource.close?.();
60
61
  }
61
62
  }
62
- export function codeExecutionEnvironment(workspaceRoot) {
63
+ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
64
+ if (checkEnvironment !== undefined && (!checkEnvironment || typeof checkEnvironment !== "object" || Array.isArray(checkEnvironment)
65
+ || Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
66
+ || checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
67
+ throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
63
68
  const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
64
69
  // The managed CLI's Node must win over a provider app's PATH: a Rosetta
65
70
  // provider can otherwise run x64 checks against arm64-installed modules.
@@ -67,6 +72,8 @@ export function codeExecutionEnvironment(workspaceRoot) {
67
72
  delete environment.NODE_PATH;
68
73
  for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
69
74
  delete environment[key];
75
+ if (checkEnvironment)
76
+ environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
70
77
  return environment;
71
78
  }
72
79
  export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
@@ -172,6 +179,10 @@ export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
172
179
  }
173
180
  export function projectPolicyCheckDeclarations(workspaceRoot, gate) {
174
181
  const { profile } = readCodeCheckProfile(workspaceRoot);
182
+ return projectPolicyCheckDeclarationsFromProfile(profile, gate);
183
+ }
184
+ /** The caller may already have independently admitted exact profile bytes. */
185
+ export function projectPolicyCheckDeclarationsFromProfile(profile, gate) {
175
186
  return (profile?.mandatory_by_gate[gate] ?? []).map((alias) => { const ref = `POLICY/${gate}/${alias.slice(7).replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`; return { id: ref, canonical_ref: ref, source: "project_policy", command: alias, purpose: `Mandatory project ${gate} gate.`, run_at: gate, availability: "available", ...(profile?.ports_by_alias?.[alias] ? { ports: profile.ports_by_alias[alias] } : {}), ...(profile?.inputs_by_alias?.[alias] ? { inputs: profile.inputs_by_alias[alias] } : {}) }; });
176
187
  }
177
188
  export function effectiveCheckDeclarations(workspaceRoot, declared, gates) { return gates.flatMap((gate) => [...declared.filter((check) => check.run_at === gate), ...projectPolicyCheckDeclarations(workspaceRoot, gate)]); }
@@ -182,6 +193,8 @@ export function validateCheckPlacement(workspaceRoot, checks) { const { profile
182
193
  if (check.run_at === "work" && aggregateCommands.has(check.command))
183
194
  throw new AppError("aggregate_check_requires_code_gate", "A project aggregate check must run at CODE or readiness, not inside one scoped Work", 2, { check_id: check.id, command: check.command, run_at: check.run_at }); }
184
195
  export function validateCheckDeclaration(workspaceRoot, check) {
196
+ if ((check.required_artifacts ?? []).some(file => path.posix.normalize(file.replaceAll("\\", "/")).split("/")[0] === "engine-outcome.json"))
197
+ throw new AppError("invalid_code_check_profile", "engine-outcome.json is reserved for engine execution evidence, not a candidate artifact", 2, { check_id: check.id });
185
198
  if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
186
199
  throw new AppError("unsupported_check_gate", "Check requires an autonomous executable gate; replan the incompatible declaration", 2, { check_id: check.id, run_at: check.run_at });
187
200
  if (check.runner_evidence !== undefined && !runnerContractsValid({ "@check/declaration": check.runner_evidence }))
@@ -256,7 +269,216 @@ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
256
269
  if (probe.status !== 0)
257
270
  throw new AppError("check_command_unavailable", "Available CODE check names an executable that is unavailable in this environment", 2, { check_id: checkId, command, executable: program });
258
271
  }
272
+ // An owning handler supplies this identity; it is a reuse boundary, not an
273
+ // authority proof. Omitting it preserves the historical SDLC input hash.
274
+ function validateCheckEvidenceContext(value) {
275
+ if (value !== undefined && (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/.test(value)))
276
+ throw new AppError("check_evidence_context_invalid", "Check evidence context must be a bounded opaque identifier", 2, { effect: "no_effect" });
277
+ }
278
+ const evidenceDigest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
279
+ /** Prepares current physical evidence outside a writer and retains an exact
280
+ * SQL-only fence. Declarations must come from the owning handler/policy, never
281
+ * from the result whose acceptance is being decided. This does not supply
282
+ * semantic acceptance or choose a memory verification policy.
283
+ */
284
+ export class PreparedCheckEvidence {
285
+ #input;
286
+ #snapshot;
287
+ #generation;
288
+ #references;
289
+ #artifacts;
290
+ #completions = [];
291
+ #passed;
292
+ constructor(input, snapshot, generation, references, artifacts, passed) {
293
+ this.#input = structuredClone({ projectId: input.projectId, runId: input.runId, runHome: input.runHome, workspaceRoot: input.workspaceRoot, scope: input.scope, checks: input.checks,
294
+ ...(input.mode === undefined ? {} : { mode: input.mode }), ...(input.workId === undefined ? {} : { workId: input.workId }), ...(input.expectedProfileHash === undefined ? {} : { expectedProfileHash: input.expectedProfileHash }), ...(input.evidenceContext === undefined ? {} : { evidenceContext: input.evidenceContext }), ...(input.checkEnvironment === undefined ? {} : { checkEnvironment: input.checkEnvironment }) });
295
+ this.#snapshot = snapshot;
296
+ this.#generation = generation;
297
+ this.#references = references;
298
+ this.#artifacts = artifacts;
299
+ this.#passed = passed;
300
+ Object.freeze(this);
301
+ }
302
+ static prepare(context, input) {
303
+ validateCheckEvidenceContext(input.evidenceContext);
304
+ const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
305
+ if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
306
+ fail();
307
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
308
+ const snapshot = checkEvidenceSqlSnapshot(context, input);
309
+ const selected = JSON.parse(snapshot);
310
+ if (!selected.run || fs.realpathSync.native(selected.run.workspace_root) !== fs.realpathSync.native(input.workspaceRoot)
311
+ || fs.realpathSync.native(selected.run.run_root ?? selected.run.run_dir) !== fs.realpathSync.native(input.runHome))
312
+ fail();
313
+ const rows = selected.receipts;
314
+ const root = fs.realpathSync.native(input.runHome);
315
+ for (const row of rows) {
316
+ const relative = path.relative(path.resolve(input.runHome), row.receipt_path);
317
+ if (!path.isAbsolute(row.receipt_path) || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative))
318
+ fail();
319
+ if (fs.existsSync(row.receipt_path)) {
320
+ const physical = path.relative(root, fs.realpathSync.native(row.receipt_path));
321
+ if (physical.startsWith(`..${path.sep}`) || physical === ".." || path.isAbsolute(physical))
322
+ fail();
323
+ }
324
+ }
325
+ for (const check of input.checks)
326
+ validateCheckDeclaration(input.workspaceRoot, check);
327
+ const { profile } = readCodeCheckProfile(input.workspaceRoot), profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
328
+ if (input.expectedProfileHash !== undefined && profileHash !== input.expectedProfileHash)
329
+ fail();
330
+ const checks = input.checks.map(check => {
331
+ const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
332
+ return { ...check, ports: check.ports ?? profile?.ports_by_alias?.[check.command] ?? [], inputs: check.inputs ?? profile?.inputs_by_alias?.[check.command] ?? [],
333
+ ...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
334
+ });
335
+ const commands = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map(check => check.command));
336
+ assertConsistentDeclarations(checks, commands);
337
+ const state = workspaceState(input.workspaceRoot, [input.runHome]);
338
+ const receipts = rows.map(projectCheckReceiptRow);
339
+ const artifacts = [], completions = [];
340
+ let passed = true;
341
+ const references = deduplicate(checks, commands).map(group => {
342
+ const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
343
+ const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
344
+ if (!receipt || !checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
345
+ fail();
346
+ passed = passed && receipt.status === "passed";
347
+ for (const artifact of receipt.artifacts)
348
+ artifacts.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), "artifacts", artifact.path)).split(path.sep).join("/"), sha256: artifact.sha256 });
349
+ const bytes = fs.readFileSync(receipt.receipt_path);
350
+ completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(fs.readFileSync(receipt.completion_path)).digest("hex") });
351
+ return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
352
+ });
353
+ const prepared = new PreparedCheckEvidence(input, snapshot, generation, references, artifacts, passed);
354
+ prepared.#completions = completions;
355
+ prepared.assertCustody(context);
356
+ return prepared;
357
+ }
358
+ references() { return structuredClone(this.#references); }
359
+ artifactReferences() { return structuredClone(this.#artifacts); }
360
+ sourceReferences() { return [...this.artifactReferences(), ...structuredClone(this.#completions), ...this.resultReferences().map(reference => ({ root: "run", ...reference }))]; }
361
+ qualityPassed() { return this.#passed; }
362
+ resultReferences() {
363
+ return this.#references.map(reference => ({ path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256 }));
364
+ }
365
+ /** For an owning acceptance writer to retain independently of agent bytes.
366
+ * The row digest pins historical execution, not later workspace policy.
367
+ */
368
+ retainedReferences(context) {
369
+ return context.db.readSnapshot(() => {
370
+ this.assertCustody(context);
371
+ return this.#references.map(reference => ({ id: reference.id, path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256,
372
+ sql_sha256: evidenceDigest(context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [this.#input.projectId, this.#input.runId, reference.id])) }));
373
+ });
374
+ }
375
+ assertOwner(scope) {
376
+ if (scope.projectId !== this.#input.projectId || scope.runId !== this.#input.runId || scope.scope !== undefined && scope.scope !== this.#input.scope)
377
+ throw new AppError("check_receipt_changed", "Prepared checks belong to another RUN or scope", 2, { effect: "no_effect" });
378
+ }
379
+ assertCustody(context) {
380
+ assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
381
+ if (checkEvidenceSqlSnapshot(context, this.#input) !== this.#snapshot)
382
+ throw new AppError("check_receipt_changed", "Prepared check SQL custody changed", 2, { effect: "no_effect" });
383
+ }
384
+ revalidate(context) {
385
+ const current = PreparedCheckEvidence.prepare(context, this.#input);
386
+ if (!isDeepStrictEqual(current.sourceReferences(), this.sourceReferences()) || !isDeepStrictEqual(current.#references, this.#references) || current.#snapshot !== this.#snapshot)
387
+ throw new AppError("check_evidence_stale", "Prepared check evidence changed", 2, { effect: "no_effect" });
388
+ this.assertCustody(context);
389
+ }
390
+ }
391
+ /** Historical aggregate execution proof. Selectors must come from an owning
392
+ * committed acceptance receipt, not a proposed result. This reader does not
393
+ * select current declarations, inspect the current workspace or execute checks.
394
+ * Relocation of SQL path fields still requires explicit receipt projection.
395
+ */
396
+ export class PreparedRetainedCheckEvidence {
397
+ #input;
398
+ #snapshot;
399
+ #generation;
400
+ constructor(input, snapshot, generation) {
401
+ this.#input = structuredClone(input);
402
+ this.#snapshot = snapshot;
403
+ this.#generation = generation;
404
+ Object.freeze(this);
405
+ }
406
+ static prepare(context, input) {
407
+ const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted checks lack exact historical execution proof", 2, { effect: "no_effect" }); };
408
+ if (!Array.isArray(input.references) || !input.references.length || input.references.length > 4096
409
+ || new Set(input.references.map(reference => reference?.id)).size !== input.references.length
410
+ || input.references.some(reference => !reference || typeof reference.id !== "string" || !reference.id
411
+ || typeof reference.path !== "string" || !/^[a-f0-9]{64}$/.test(reference.sha256) || !/^[a-f0-9]{64}$/.test(reference.sql_sha256)))
412
+ fail();
413
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId), snapshot = retainedCheckSqlSnapshot(context, input);
414
+ const selected = JSON.parse(snapshot);
415
+ if (!selected.run || selected.receipts.some(row => !row))
416
+ fail();
417
+ const runRoot = selected.run.run_root ?? selected.run.run_dir;
418
+ if (!path.isAbsolute(runRoot) || fs.realpathSync.native(runRoot) !== path.resolve(runRoot))
419
+ fail();
420
+ for (let index = 0; index < input.references.length; index++) {
421
+ const reference = input.references[index], row = selected.receipts[index];
422
+ if (row.scope !== "aggregate" || row.work_id !== null || evidenceDigest(row) !== reference.sql_sha256
423
+ || row.receipt_path !== path.join(runRoot, reference.path))
424
+ fail();
425
+ // Admit contained regular receipt/completion files before any helper
426
+ // reads them; escaping paths and symlink parents are rejected.
427
+ const bytes = readRetainedCheckFile(runRoot, reference.path);
428
+ readRetainedCheckFile(runRoot, path.posix.join(path.posix.dirname(reference.path), "completion.json"));
429
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== reference.sha256 || !checkReceiptEvidenceValidForMode(projectCheckReceiptRow(row), input.mode))
430
+ fail();
431
+ }
432
+ const prepared = new PreparedRetainedCheckEvidence(input, snapshot, generation);
433
+ prepared.assertCustody(context);
434
+ return prepared;
435
+ }
436
+ assertCustody(context) {
437
+ assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
438
+ if (retainedCheckSqlSnapshot(context, this.#input) !== this.#snapshot)
439
+ throw new AppError("retained_check_evidence_invalid", "Accepted check SQL custody changed", 2, { effect: "no_effect" });
440
+ }
441
+ revalidate(context) {
442
+ const current = PreparedRetainedCheckEvidence.prepare(context, this.#input);
443
+ if (current.#snapshot !== this.#snapshot)
444
+ throw new AppError("retained_check_evidence_invalid", "Accepted check proof changed", 2, { effect: "no_effect" });
445
+ this.assertCustody(context);
446
+ }
447
+ }
448
+ function readRetainedCheckFile(root, relative) {
449
+ const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted check file is not a contained regular file", 2, { effect: "no_effect" }); };
450
+ if (!relative || path.posix.isAbsolute(relative) || path.win32.isAbsolute(relative) || relative.includes("\\") || relative.includes("\0")
451
+ || relative.split("/").some(segment => !segment || segment === "." || segment === ".."))
452
+ fail();
453
+ try {
454
+ let parent = root;
455
+ if (!fs.lstatSync(parent).isDirectory())
456
+ fail();
457
+ for (const segment of relative.split("/").slice(0, -1)) {
458
+ parent = path.join(parent, segment);
459
+ if (!fs.lstatSync(parent).isDirectory())
460
+ fail();
461
+ }
462
+ return readBoundedReleaseFile(path.join(root, relative), 16 * 1024 * 1024);
463
+ }
464
+ catch {
465
+ return fail();
466
+ }
467
+ }
468
+ function retainedCheckSqlSnapshot(context, input) {
469
+ return context.db.readSnapshot(() => JSON.stringify({
470
+ run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]) ?? null,
471
+ receipts: input.references.map(reference => context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [input.projectId, input.runId, reference.id]) ?? null)
472
+ }));
473
+ }
474
+ function checkEvidenceSqlSnapshot(context, input) {
475
+ return context.db.readSnapshot(() => JSON.stringify({
476
+ run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]),
477
+ receipts: context.db.all("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? ORDER BY reservation_sequence,rowid", [input.projectId, input.runId])
478
+ }));
479
+ }
259
480
  export async function runCodeChecks(context, input) {
481
+ validateCheckEvidenceContext(input.evidenceContext);
260
482
  const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
261
483
  let gate;
262
484
  for (;;) {
@@ -305,7 +527,8 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
305
527
  const predecessor = validateFailedCheckRetry(context, { projectId: input.projectId, runId: input.runId, receiptId, declarations: checks, reason: input.retryReason });
306
528
  const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
307
529
  const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
308
- && checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome])) === predecessor.input_hash);
530
+ && (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
531
+ || sourceRepairContractMatches(predecessor, candidate, profileHash)));
309
532
  if (!group || retries.has(group))
310
533
  throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
311
534
  // Preserve the identity of a pre-deduplication failed execution when an
@@ -318,7 +541,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
318
541
  const before = workspaceState(input.workspaceRoot, [input.runHome]);
319
542
  const startedAt = context.now();
320
543
  const inputFingerprint = workspaceInputFingerprint(input.workspaceRoot, before.files, group.inputs, [input.runHome]);
321
- const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint);
544
+ const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
322
545
  const epoch = crypto.createHash("sha256").update(`${group.gate}\0${before.fingerprint}`).digest("hex");
323
546
  const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, profileHash, inputHash, epoch, startedAt };
324
547
  delete reservationInput.retryReceiptId;
@@ -363,7 +586,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
363
586
  const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
364
587
  if (group.runnerEvidence)
365
588
  fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
366
- result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
589
+ result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
367
590
  confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
368
591
  assertRunMutationAllowed(context, input.projectId, input.runId, generation);
369
592
  if (budget)
@@ -388,7 +611,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
388
611
  }
389
612
  const after = workspaceState(input.workspaceRoot, [input.runHome]);
390
613
  const mutationPaths = changedPaths(before.files, after.files);
391
- const artifacts = collectCheckArtifacts(evidenceDir, group.requiredArtifacts);
614
+ let artifacts = collectCheckArtifacts(evidenceDir, group.requiredArtifacts);
392
615
  if (fs.existsSync(stderrPath)) {
393
616
  if (!artifacts.complete)
394
617
  fs.appendFileSync(stderrPath, `\nmissing required evidence artifacts: ${artifacts.missing.join(", ")}\n`);
@@ -398,14 +621,26 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
398
621
  fs.appendFileSync(stderrPath, `\ncheck mutated project workspace: ${mutationPaths.join(", ")}\n`);
399
622
  }
400
623
  const runnerEvidence = group.runnerEvidence ? readRunnerEvidence(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
624
+ const physicalFailures = executionFailures({ exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
625
+ const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
626
+ if (group.runnerEvidence)
627
+ retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch }, input.workspaceRoot, physicalFailures, candidateFiles);
628
+ const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
629
+ artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
401
630
  const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
402
631
  const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
632
+ receipt.required_artifacts = requiredArtifacts;
633
+ receipt.candidate_root = input.workspaceRoot;
634
+ receipt.candidate_files = candidateFiles;
635
+ receipt.execution_failures = physicalFailures;
403
636
  if (group.runnerEvidence) {
404
637
  receipt.runner_evidence_required = group.runnerEvidence;
405
638
  receipt.runner_evidence = runnerEvidence ?? null;
406
639
  }
407
640
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [receipt.status, receipt.exit_code, receipt.after_fingerprint, JSON.stringify(receipt.mutation_paths), JSON.stringify(receipt.artifacts), receipt.finished_at, id, input.projectId]);
408
641
  writeReceipt(receiptPath, receipt);
642
+ if (status === "failed")
643
+ receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
409
644
  receipts.push(receipt);
410
645
  input.progress?.(`check ${index + 1}/${groups.length} ${status}: ${group.command}`);
411
646
  }
@@ -413,7 +648,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
413
648
  const current = workspaceState(input.workspaceRoot, [input.runHome]), currentProfileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
414
649
  for (const receipt of receipts) {
415
650
  const group = groups.find(candidate => candidate.declarations.some(check => receipt.check_refs.includes(checkRef(check))));
416
- const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome])) : null;
651
+ const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) : null;
417
652
  if (!group || !checkReceiptEvidenceValid(receipt) || receipt.input_hash !== currentHash)
418
653
  throw new AppError("check_evidence_stale", "Check evidence changed before the gate could be accepted", 1, { check_receipt_id: receipt.id });
419
654
  }
@@ -456,18 +691,44 @@ export function reconcileUnfinishedChecks(context, input) {
456
691
  throw new AppError("check_in_progress", "A previous check invocation is still initializing", 1, { check_receipt_id: pendingReceipt.id, initializing: true });
457
692
  }
458
693
  const after = workspaceState(input.workspaceRoot, input.runHome ? [input.runHome] : []);
459
- const artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
694
+ let artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
460
695
  const workspaceUnchanged = receipt?.before_fingerprint === after.fingerprint;
461
696
  const runnerEvidence = receipt?.runner_evidence_required ? readRunnerEvidence(path.join(path.dirname(pendingReceipt.receipt_path), "artifacts"), receipt.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch }) : undefined;
462
- const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
463
697
  const exitCode = completion?.exit_code ?? null;
464
698
  const finishedAt = completion?.finished_at ?? context.now();
699
+ const physicalFailures = executionFailures({ exitCode, missing: artifacts.missing.filter(file => file !== "engine-outcome.json"), mutations: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], invalidRunner: Boolean(receipt?.runner_evidence_required && !runnerEvidence) });
700
+ let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
701
+ if (receipt?.runner_evidence_required) {
702
+ const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
703
+ if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
704
+ // A completed live writer's bound proof is already authoritative. Never erase its blockers during reconciliation.
705
+ const retained = context.db.get("SELECT artifacts_json FROM check_receipts WHERE id = ?", [pendingReceipt.id]);
706
+ const proven = resolvedReceiptArtifacts(pendingReceipt.receipt_path, retained ? JSON.parse(retained.artifacts_json) : [], receipt);
707
+ if (proven.candidate_root) {
708
+ physicalFailures.push(...(proven.execution_failures ?? []));
709
+ candidateFiles = proven.candidate_files ?? candidateFiles;
710
+ }
711
+ else {
712
+ physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
713
+ fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
714
+ }
715
+ }
716
+ retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
717
+ receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), "engine-outcome.json"])];
718
+ artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
719
+ }
720
+ const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && physicalFailures.length === 0 && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
465
721
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [status, exitCode, after.fingerprint, JSON.stringify(workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"]), JSON.stringify(artifacts.items), finishedAt, pendingReceipt.id, input.projectId]);
466
722
  if (process)
467
723
  finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
468
724
  if (receipt) {
469
725
  const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
726
+ repaired.candidate_root = input.workspaceRoot;
727
+ repaired.candidate_files = candidateFiles;
728
+ repaired.execution_failures = physicalFailures;
470
729
  writeReceipt(pendingReceipt.receipt_path, repaired);
730
+ if (status === "failed")
731
+ repaired.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(repaired, "diagnostic");
471
732
  reconciled.push(repaired);
472
733
  }
473
734
  }
@@ -486,6 +747,9 @@ function reserveOrReuseReceipt(context, input) {
486
747
  const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
487
748
  const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
488
749
  const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
750
+ const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
751
+ const sourceCorrection = Boolean(retryPredecessor && sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash));
752
+ const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
489
753
  return context.db.writeTransaction(() => {
490
754
  assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
491
755
  if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
@@ -499,9 +763,9 @@ function reserveOrReuseReceipt(context, input) {
499
763
  if (!input.retryReason?.trim())
500
764
  throw new AppError("retry_check_invalid", "A check retry requires the restored-condition reason", 2, { check_receipt_id: retryId });
501
765
  const predecessor = receipts.find(receipt => receipt.id === retryId);
502
- if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
766
+ if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash && !sourceCorrection || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
503
767
  throw new AppError("retry_check_invalid", "Retry predecessor does not match this exact check contract", 2, { check_receipt_id: retryId });
504
- if (prior?.id !== (successor?.id ?? predecessor.id))
768
+ if ((sourceCorrection ? latestContract?.id : prior?.id) !== (successor?.id ?? predecessor.id))
505
769
  throw new AppError("retry_check_invalid", "This retry was superseded by a newer check outcome; inspect the latest receipt", 2, { check_receipt_id: retryId, latest_check_receipt_id: prior?.id ?? null, latest_status: prior?.status ?? null });
506
770
  if (successor?.status === "running")
507
771
  throw new AppError("check_in_progress", "The requested retry is already running", 1, { check_receipt_id: successor.id, retry_of_receipt_id: retryId });
@@ -530,6 +794,10 @@ function reserveOrReuseReceipt(context, input) {
530
794
  return { reserved };
531
795
  }, "checks.reserveOrReuseReceipt");
532
796
  }
797
+ function sourceRepairContractMatches(receipt, group, profileHash) {
798
+ return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
799
+ && isDeepStrictEqual([...group.requiredArtifacts].sort(), receipt.required_artifacts.filter(file => file !== "engine-outcome.json").sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
800
+ }
533
801
  function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
534
802
  function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
535
803
  function readReceipt(file) { try {
@@ -561,17 +829,25 @@ export function validateFailedCheckRetry(context, input) {
561
829
  export function checkReceipts(context, input) {
562
830
  const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
563
831
  const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
564
- return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json), row) }));
832
+ return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map(projectCheckReceiptRow);
833
+ }
834
+ function projectCheckReceiptRow(row) {
835
+ const receipt = { ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json), row) };
836
+ if (receipt.status === "failed")
837
+ receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
838
+ return receipt;
565
839
  }
566
840
  /** File bytes remain immutable; only the reader projects their current location. */
567
841
  function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
568
842
  let original = {};
569
843
  try {
570
- original = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
844
+ const value = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
845
+ if (value && typeof value === "object" && !Array.isArray(value))
846
+ original = value;
571
847
  }
572
848
  catch { /* Historical missing receipts remain visibly incomplete. */ }
573
849
  const currentRoot = path.dirname(receiptPath);
574
- const oldRoot = original.receipt_path ? path.dirname(original.receipt_path) : currentRoot;
850
+ const oldRoot = typeof original.receipt_path === "string" ? path.dirname(original.receipt_path) : currentRoot;
575
851
  const relocate = (file) => {
576
852
  const relative = path.relative(oldRoot, file);
577
853
  return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
@@ -580,10 +856,44 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
580
856
  if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
581
857
  const root = path.join(currentRoot, "artifacts");
582
858
  const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
583
- if (retained.complete && JSON.stringify(retained.items) === JSON.stringify(artifacts))
859
+ if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256)))
584
860
  evidence = readRunnerEvidence(root, original.runner_evidence_required, { invocation_id: binding.id, check_id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch });
585
861
  }
586
- return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}) };
862
+ let outcome = {};
863
+ const outcomePath = path.join(currentRoot, "artifacts", "engine-outcome.json");
864
+ const retainedOutcome = collectCheckArtifacts(path.join(currentRoot, "artifacts"), ["engine-outcome.json"]);
865
+ if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
866
+ try {
867
+ const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
868
+ if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
869
+ outcome = value;
870
+ }
871
+ catch { /* Missing physical proof cannot grant attribution. */ }
872
+ }
873
+ return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
874
+ }
875
+ function executionFailures(input) {
876
+ const failures = [];
877
+ if (input.exitCode === null)
878
+ failures.push({ code: "completion_unknown", message: "Check completion is unknown" });
879
+ if (input.launcherError)
880
+ failures.push({ code: "launcher_error", message: input.launcherError });
881
+ if (input.cleanupUnconfirmed)
882
+ failures.push({ code: "cleanup_unconfirmed", message: "Check process cleanup is not confirmed" });
883
+ if (input.missing.length)
884
+ failures.push({ code: "missing_required_artifact", message: input.missing.join(", ") });
885
+ if (input.mutations.length)
886
+ failures.push({ code: "check_input_mutated", message: input.mutations.join(", ") });
887
+ if (input.invalidRunner)
888
+ failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
889
+ return failures;
890
+ }
891
+ function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
892
+ const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
893
+ if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
894
+ throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
895
+ fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
896
+ fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
587
897
  }
588
898
  export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
589
899
  /** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
@@ -650,12 +960,13 @@ function deduplicate(checks, commands) {
650
960
  const reusable = check.reuse === "deterministic";
651
961
  // Stage labels identify obligations, not different processes in this gate.
652
962
  // Cross-invocation reuse still requires the explicit deterministic contract.
653
- const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, check.runner_evidence ?? null]);
963
+ const environmentInputs = normalizedEnvironmentInputs(check.environment_inputs);
964
+ const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, environmentInputs, check.runner_evidence ?? null]);
654
965
  const current = groups.get(key);
655
966
  if (current)
656
967
  current.declarations.push(check);
657
968
  else
658
- groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
969
+ groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(environmentInputs === undefined ? {} : { environmentInputs }), ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
659
970
  });
660
971
  return [...groups.values()].map(group => ({ ...group, gate: [...group.declarations].map(check => check.run_at).sort()[0], declarations: [...group.declarations].sort((left, right) => checkRef(left).localeCompare(checkRef(right))) }));
661
972
  }
@@ -703,16 +1014,34 @@ function checkRef(check) { return check.canonical_ref ?? check.id; }
703
1014
  function assertConsistentDeclarations(checks, commands) {
704
1015
  const seen = new Map();
705
1016
  for (const [index, check] of checks.entries()) {
706
- const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, check.runner_evidence ?? null]);
1017
+ const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, normalizedEnvironmentInputs(check.environment_inputs), check.runner_evidence ?? null]);
707
1018
  const ref = checkRef(check), prior = seen.get(ref);
708
1019
  if (prior && prior !== contract)
709
1020
  throw new AppError("check_declaration_conflict", "The same check reference has conflicting execution definitions", 2, { check_ref: ref });
710
1021
  seen.set(ref, contract);
711
1022
  }
712
1023
  }
713
- function environmentFingerprint(workspaceRoot) {
714
- const environment = codeExecutionEnvironment(workspaceRoot);
715
- return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment).filter(([, value]) => value !== undefined).sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
1024
+ function normalizedEnvironmentInputs(value) {
1025
+ if (value === undefined)
1026
+ return undefined;
1027
+ if (!Array.isArray(value) || !value.length || value.length > 64 || value.some(name => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*\*?$/.test(name)))
1028
+ throw new AppError("invalid_check_environment_inputs", "Internal check environment inputs must be bounded names or prefixes", 2, { effect: "no_effect" });
1029
+ return [...new Set(value)].sort();
1030
+ }
1031
+ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
1032
+ const environment = codeExecutionEnvironment(workspaceRoot, checkEnvironment);
1033
+ // Invocation authority is checked by its own owners, not an application check
1034
+ // input. Preserve arbitrary application variables (including DD_FLOW_PORT_*)
1035
+ // and tool selection; omit only known process/control bookkeeping.
1036
+ const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
1037
+ "DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
1038
+ "DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
1039
+ "DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
1040
+ "CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
1041
+ return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
1042
+ .filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
1043
+ && (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1044
+ .sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
716
1045
  }
717
1046
  function acquireWorkspaceGate(context, input) {
718
1047
  const operationId = workspaceOperationId(input.workspaceRoot);
@@ -806,19 +1135,43 @@ function explicitInputFiles(root, inputs, ignoredRoots) {
806
1135
  visit(path.resolve(root, input), input, new Set());
807
1136
  return [...result.entries()];
808
1137
  }
809
- function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot), input_fingerprint: inputFingerprint })).digest("hex"); }
810
- function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: group.requiredArtifacts, inputs: group.inputs }; }
811
- export function checkReceiptEvidenceValid(receipt) {
812
- if (receipt.status !== "passed" || receipt.exit_code !== 0 || receipt.before_fingerprint !== receipt.after_fingerprint || !fs.existsSync(receipt.completion_path) || readCompletion(receipt.completion_path)?.exit_code !== 0)
813
- return false;
814
- const stored = readReceipt(receipt.receipt_path);
815
- if (!stored || stored.status !== "passed" || stored.input_hash !== receipt.input_hash || stored.profile_hash !== receipt.profile_hash)
816
- return false;
817
- const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
818
- if (stored.runner_evidence_required && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
1138
+ function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
1139
+ function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: [...new Set([...group.requiredArtifacts, ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])], inputs: group.inputs }; }
1140
+ export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
1141
+ function checkReceiptEvidenceValidForMode(receipt, mode) {
1142
+ try {
1143
+ if (!(receipt.status === "passed" && receipt.exit_code === 0 || mode === "diagnostic" && receipt.status === "failed" && typeof receipt.exit_code === "number" && receipt.exit_code !== 0) || receipt.before_fingerprint !== receipt.after_fingerprint
1144
+ || receipt.mutation_paths.length !== 0 || receipt.completion_path !== path.join(path.dirname(receipt.receipt_path), "completion.json")
1145
+ || readCompletion(receipt.completion_path)?.exit_code !== receipt.exit_code)
1146
+ return false;
1147
+ const stored = readReceipt(receipt.receipt_path);
1148
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)
1149
+ || typeof stored.receipt_path !== "string" || !path.isAbsolute(stored.receipt_path)
1150
+ || path.basename(stored.receipt_path) !== "receipt.json"
1151
+ || stored.completion_path !== path.join(path.dirname(stored.receipt_path), "completion.json"))
1152
+ return false;
1153
+ // Gate/ref labels may be rebound for explicit deterministic reuse. Physical
1154
+ // execution identity and settled SQL evidence must never be rebound from a
1155
+ // mutable receipt file, including after recovery relocates the RUN home.
1156
+ const fields = ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code",
1157
+ "workspace_fingerprint", "before_fingerprint", "after_fingerprint", "profile_hash", "started_at", "finished_at"];
1158
+ if (fields.some(field => stored[field] !== receipt[field]) || !Array.isArray(stored.artifacts)
1159
+ || !Array.isArray(stored.mutation_paths) || stored.mutation_paths.length !== 0
1160
+ || JSON.stringify(stored.required_artifacts) !== JSON.stringify(receipt.required_artifacts)
1161
+ || JSON.stringify(stored.inputs) !== JSON.stringify(receipt.inputs))
1162
+ return false;
1163
+ const retained = resolvedReceiptArtifacts(receipt.receipt_path, stored.artifacts, receipt).artifacts;
1164
+ if (JSON.stringify(retained) !== JSON.stringify(receipt.artifacts))
1165
+ return false;
1166
+ const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
1167
+ if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
1168
+ return false;
1169
+ const artifacts = collectCheckArtifacts(root, receipt.required_artifacts);
1170
+ return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(retained);
1171
+ }
1172
+ catch {
819
1173
  return false;
820
- const artifacts = collectCheckArtifacts(root, stored.required_artifacts ?? []);
821
- return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(stored.artifacts ?? []);
1174
+ }
822
1175
  }
823
1176
  function parseArray(value, fallback) { try {
824
1177
  const parsed = JSON.parse(value);