@deksden-com/dd-flow-cli 0.9.0-beta.145 → 0.9.0-beta.149

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 (101) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/build-info.json +7 -7
  3. package/dist/cli/command-inputs.js +6 -2
  4. package/dist/cli/command-metadata.js +18 -2
  5. package/dist/cli/diagnostic-invocation.js +46 -0
  6. package/dist/cli/diagnostic.js +120 -0
  7. package/dist/cli/help.js +35 -0
  8. package/dist/cli/memory-flow-admission.js +4 -2
  9. package/dist/cli/memory-init.js +5 -2
  10. package/dist/cli/run-cli.js +16 -15
  11. package/dist/cli.js +6 -0
  12. package/dist/domain/diagnostic-aspects.js +103 -0
  13. package/dist/domain/diagnostic-flows.js +8 -0
  14. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  15. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  16. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
  19. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  20. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  21. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  22. package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
  23. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
  24. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  25. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  26. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  27. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  28. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  29. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
  30. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  31. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  32. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  33. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  34. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  35. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
  36. package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
  37. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  38. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  39. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  40. package/dist/schemas/execution-bundle.schema.json +7 -1
  41. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  42. package/dist/services/check-environment.js +21 -0
  43. package/dist/services/check-runner-evidence.js +267 -15
  44. package/dist/services/code-checks.js +23 -15
  45. package/dist/services/controller-execution.js +10 -4
  46. package/dist/services/controller-fanout.js +13 -6
  47. package/dist/services/controller-memory-worker.js +3 -2
  48. package/dist/services/dashboard.js +2 -2
  49. package/dist/services/descriptor-run-settlement.js +5 -3
  50. package/dist/services/descriptor-run-start.js +22 -3
  51. package/dist/services/descriptor-stage-coordinator.js +37 -4
  52. package/dist/services/descriptor-stage-settlement.js +2 -1
  53. package/dist/services/diagnostic-evidence.js +73 -0
  54. package/dist/services/diagnostic-report-publication.js +72 -0
  55. package/dist/services/diagnostic-report.js +67 -0
  56. package/dist/services/diagnostic-run-bundle.js +74 -0
  57. package/dist/services/diagnostic-snapshot.js +159 -0
  58. package/dist/services/diagnostic-stage-handlers.js +108 -0
  59. package/dist/services/diagnostic-stage-results.js +132 -0
  60. package/dist/services/diagnostic-start.js +84 -0
  61. package/dist/services/diagnostic-work-batch.js +34 -0
  62. package/dist/services/diagnostic-work-collection.js +97 -0
  63. package/dist/services/diagnostic-work-registration.js +101 -0
  64. package/dist/services/diagnostic-work-results.js +93 -0
  65. package/dist/services/eval-snapshots.js +85 -29
  66. package/dist/services/execution-bundles.js +19 -7
  67. package/dist/services/flow-catalog.js +6 -1
  68. package/dist/services/frozen-engine.js +37 -2
  69. package/dist/services/hybrid-contracts.js +2 -0
  70. package/dist/services/installed-flow-source.js +25 -3
  71. package/dist/services/lifecycle-invocations.js +147 -34
  72. package/dist/services/memory-init-contexts.js +16 -0
  73. package/dist/services/memory-init-controller-start.js +12 -4
  74. package/dist/services/memory-init-lint-capability.js +5 -5
  75. package/dist/services/memory-init-native-capacity.js +4 -4
  76. package/dist/services/memory-init-preflight.js +10 -16
  77. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  78. package/dist/services/memory-layer.js +4 -3
  79. package/dist/services/memory-tool-probe.js +25 -0
  80. package/dist/services/memory-verification-checks.js +15 -4
  81. package/dist/services/native-daemon-history.js +2 -1
  82. package/dist/services/provider-capacity.js +43 -38
  83. package/dist/services/release-cache.js +8 -0
  84. package/dist/services/run-control.js +2 -1
  85. package/dist/services/run-controller-adapter.js +2 -1
  86. package/dist/services/run-controller-process.js +2 -1
  87. package/dist/services/run-controller-recovery.js +2 -1
  88. package/dist/services/run-controller.js +85 -15
  89. package/dist/services/run-fork.js +1 -1
  90. package/dist/services/runs.js +6 -1
  91. package/dist/services/runtime-scope-resume.js +12 -6
  92. package/dist/services/runtime-scope-stop.js +3 -2
  93. package/dist/services/runtime-service.js +2 -1
  94. package/dist/services/stage-handlers.js +16 -3
  95. package/dist/services/stage-pause.js +4 -1
  96. package/dist/services/version-status.js +1 -1
  97. package/dist/services/vnext-code.js +2 -2
  98. package/dist/services/vnext-fanout.js +31 -1
  99. package/dist/services/work-registry.js +37 -6
  100. package/dist/services/workspace-bootstrap.js +2 -1
  101. package/package.json +6 -3
@@ -5,14 +5,24 @@ import path from "node:path";
5
5
  import { parse } from "shell-quote";
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import { fileURLToPath } from "node:url";
8
10
  function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
9
11
  function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
10
12
  const validatedBrowserProofs = new WeakSet();
13
+ const unsupportedObservationContracts = new WeakSet();
14
+ const validatedNativeProofs = new WeakSet();
15
+ const validatedChildren = new WeakMap();
16
+ const validatedReports = new WeakMap();
17
+ const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
18
+ const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
19
+ export function candidateAbsencesValid(value) { return Array.isArray(value) && value.length <= 64 && value.every(item => object(item) && absoluteFile(item.file) && item.path_state === "missing" && object(item.declaration) && absoluteFile(item.declaration.file) && hash(item.declaration.sha256)); }
11
20
  function relativeProofPath(value) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= 4096 && !path.isAbsolute(value) && !value.includes("\\") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."); }
12
21
  function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
13
22
  function browserProofShape(proof) {
14
23
  const attempt = proof.attempt, step = proof.step;
15
- return proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
24
+ return (proof.observation_contract === undefined || proof.observation_contract === "dd-tasks/browser-observation@2" && object(proof.target) && [proof.target.page_id, proof.target.context_id].every(value => boundedString(value, 256) && Boolean(value)) && Number.isSafeInteger(proof.window_epoch) && Number(proof.window_epoch) > 0)
25
+ && proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
16
26
  && [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
17
27
  && object(step) && boundedString(step.id, 4096) && Boolean(step.id) && ["pw:api", "expect"].includes(String(step.category)) && step.file === proof.file && Number.isSafeInteger(step.line) && step.line > 0
18
28
  && proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
@@ -29,6 +39,21 @@ export function runnerFailureValid(value) {
29
39
  return false;
30
40
  if (value.repair_evidence !== undefined) {
31
41
  const proof = value.repair_evidence;
42
+ if (object(proof) && proof.rule === "candidate_test_runtime_error")
43
+ return ["TypeError", "ReferenceError"].includes(String(proof.exception_type))
44
+ && absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
45
+ && [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
46
+ if (object(proof) && proof.rule === "candidate_migration_sql")
47
+ return proof.native_code === "42601" && absoluteFile(proof.file) && hash(proof.file_sha256)
48
+ && boundedString(proof.operation_id, 4096) && Boolean(proof.operation_id) && proofArtifact(proof.native_artifact) && object(proof.world)
49
+ && boundedString(proof.world.invocation_id, 4096) && typeof proof.world.world_id === "string" && /^[a-f0-9]{32}$/.test(proof.world.world_id)
50
+ && boundedString(proof.world.role, 128) && Boolean(proof.world.role) && hash(proof.world.migrations_sha256) && proof.world.phase === "migrating" && proof.world.setup_completed === false;
51
+ if (object(proof) && proof.rule === "candidate_tool_diagnostic")
52
+ return absoluteFile(proof.file) && proofArtifact(proof.native_artifact)
53
+ && object(proof.tool) && { biome: "2.5.6", typescript: "5.9.3", docs: "1", "value-absence": "1" }[String(proof.tool.name)] === proof.tool.version
54
+ && object(proof.step) && boundedString(proof.step.id, 256) && Boolean(proof.step.id) && Number.isSafeInteger(proof.step.ordinal) && proof.step.ordinal >= 0 && hash(proof.step.command_sha256)
55
+ && boundedString(proof.diagnostic_code, 256) && (proof.tool.name === "biome" ? ["format", "assist/source/organizeImports"].includes(String(proof.diagnostic_code)) || /^lint\/[\w/-]+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "typescript" ? /^TS\d+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "docs" ? ["required_path_missing", "docs_contract"].includes(String(proof.diagnostic_code)) : proof.diagnostic_code === "value_absence")
56
+ && (proof.path_state === "present" && hash(proof.file_sha256) || proof.path_state === "missing" && proof.tool.name === "docs" && proof.diagnostic_code === "required_path_missing" && proof.file_sha256 === null && object(proof.declaration) && absoluteFile(proof.declaration.file) && hash(proof.declaration.sha256));
32
57
  if (!object(proof) || !boundedString(proof.rule, 128) || !boundedString(proof.native_code, 128) || !boundedString(proof.file, 4096) || !object(proof.world))
33
58
  return false;
34
59
  const world = proof.world;
@@ -45,23 +70,132 @@ function candidateFile(root, file) {
45
70
  if (!root || !path.isAbsolute(file))
46
71
  return false;
47
72
  try {
48
- const relative = path.relative(fs.realpathSync(root), fs.realpathSync(file));
49
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) && fs.statSync(file).isFile();
73
+ const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(file));
74
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
75
+ return false;
76
+ let current = file;
77
+ while (fs.realpathSync(current) !== realRoot) {
78
+ if (fs.lstatSync(current).isSymbolicLink())
79
+ return false;
80
+ const parent = path.dirname(current);
81
+ if (parent === current)
82
+ return false;
83
+ current = parent;
84
+ }
85
+ return fs.statSync(file).isFile();
50
86
  }
51
87
  catch {
52
88
  return false;
53
89
  }
54
90
  }
55
91
  export function candidateFailureFiles(evidence, root) {
56
- return [...new Set((evidence?.failures ?? []).flatMap(failure => failure.file && candidateFile(root, failure.file) ? [failure.file] : []))];
92
+ return [...new Set((evidence?.failures ?? []).flatMap(failure => {
93
+ const child = validatedChildren.get(failure);
94
+ if (child)
95
+ return candidateFailureFiles({ ...child.evidence, failures: [child.failure] }, root);
96
+ const proof = failure.repair_evidence;
97
+ const checksum = proof && "file_sha256" in proof ? proof.file_sha256 : undefined;
98
+ try {
99
+ return failure.file && candidateFile(root, failure.file) && (checksum === undefined || checksum === crypto.createHash("sha256").update(fs.readFileSync(failure.file)).digest("hex")) ? [failure.file] : [];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }))];
105
+ }
106
+ function absentCandidate(root, file) {
107
+ if (!root || !absoluteFile(file))
108
+ return false;
109
+ try {
110
+ let current = path.resolve(file);
111
+ try {
112
+ fs.lstatSync(current);
113
+ return false;
114
+ }
115
+ catch (error) {
116
+ if (error.code !== "ENOENT")
117
+ return false;
118
+ }
119
+ for (;;) {
120
+ try {
121
+ if (fs.lstatSync(current).isSymbolicLink())
122
+ return false;
123
+ break;
124
+ }
125
+ catch (error) {
126
+ if (error.code !== "ENOENT")
127
+ return false;
128
+ const parent = path.dirname(current);
129
+ if (parent === current)
130
+ return false;
131
+ current = parent;
132
+ }
133
+ }
134
+ const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(current));
135
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
136
+ return false;
137
+ while (fs.realpathSync(current) !== realRoot) {
138
+ if (fs.lstatSync(current).isSymbolicLink())
139
+ return false;
140
+ current = path.dirname(current);
141
+ }
142
+ return true;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
57
147
  }
58
- function candidateFailure(failure, evidence, root, capturedFiles) {
59
- const owned = (file) => capturedFiles === undefined ? candidateFile(root, file) : capturedFiles.includes(file);
148
+ export function candidateFailureAbsences(evidence, root) {
149
+ return (evidence?.failures ?? []).flatMap(failure => {
150
+ const child = validatedChildren.get(failure);
151
+ if (child)
152
+ return candidateFailureAbsences({ ...child.evidence, failures: [child.failure] }, root);
153
+ try {
154
+ const proof = failure.repair_evidence;
155
+ if (proof?.rule !== "candidate_tool_diagnostic" || proof.path_state !== "missing" || !proof.declaration || !validatedNativeProofs.has(proof)
156
+ || !absentCandidate(root, proof.file) || !candidateFile(root, proof.declaration.file) || crypto.createHash("sha256").update(fs.readFileSync(proof.declaration.file)).digest("hex") !== proof.declaration.sha256)
157
+ return [];
158
+ return [{ file: proof.file, declaration: proof.declaration, path_state: "missing" }];
159
+ }
160
+ catch {
161
+ return [];
162
+ }
163
+ });
164
+ }
165
+ function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences) {
166
+ const child = validatedChildren.get(failure);
167
+ if (child)
168
+ return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences);
169
+ const owned = (file) => {
170
+ if (capturedFiles !== undefined)
171
+ return capturedFiles.includes(file);
172
+ if (!candidateFile(root, file))
173
+ return false;
174
+ const proof = failure.repair_evidence;
175
+ try {
176
+ return !proof || !("file_sha256" in proof) || proof.file_sha256 === crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ };
60
182
  if (failure.origin === "tooling" || failure.origin === "unknown")
61
183
  return false;
62
184
  if (failure.category === "assertion")
63
185
  return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
64
186
  const proof = failure.repair_evidence;
187
+ if (proof?.rule === "candidate_test_runtime_error")
188
+ return validatedNativeProofs.has(proof) && failure.category === "unclassified" && failure.origin === "candidate"
189
+ && failure.phase === proof.phase && failure.native_code === proof.exception_type && failure.file === proof.file && owned(proof.file);
190
+ if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic") {
191
+ if (!validatedNativeProofs.has(proof) || failure.category !== "unclassified" || failure.origin !== "candidate" || failure.file !== proof.file || !["setup", "collection", "test"].includes(failure.phase ?? ""))
192
+ return false;
193
+ if (proof.rule === "candidate_migration_sql")
194
+ return failure.phase === "setup" && failure.native_code === "42601" && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
195
+ if (failure.phase !== "collection" || failure.native_code !== proof.diagnostic_code)
196
+ return false;
197
+ return proof.path_state === "missing" ? Boolean(proof.declaration && (capturedAbsences ?? (root ? candidateFailureAbsences(evidence, root) : [])).some(item => item.file === proof.file && isDeepStrictEqual(item.declaration, proof.declaration))) : owned(proof.file);
198
+ }
65
199
  if (proof?.rule === "candidate_browser_action_inactivity")
66
200
  return validatedBrowserProofs.has(proof) && failure.category === "unclassified" && failure.phase === "test" && failure.origin === "candidate" && failure.native_code === proof.native_code && failure.file === proof.file && failure.test_id === proof.attempt.test_id && proof.attempt.invocation_id === evidence.binding.invocation_id && owned(proof.file);
67
201
  return failure.category === "unclassified" && failure.origin === "candidate" && ["test", "setup"].includes(failure.phase ?? "") && proof?.rule === "candidate_sql_integrity" && ["23502", "23503", "23505", "23514"].includes(proof.native_code) && failure.native_code === proof.native_code && failure.file === proof.file && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
@@ -107,7 +241,7 @@ export function readRunnerProgress(file, invocationId, lastSequence = 0, now = D
107
241
  if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
108
242
  || !Number.isSafeInteger(value.sequence) || value.sequence <= 0 || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at))
109
243
  || !["startup", "execution"].includes(value.phase)
110
- || !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
244
+ || !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_started", "step_completed", "run_completed"].includes(value.event))
111
245
  throw new Error("Malformed official progress record");
112
246
  if (value.sequence <= lastSequence || Date.parse(value.at) > now)
113
247
  continue;
@@ -164,6 +298,9 @@ function readProofArtifact(root, ref, limit = 256 * 1024) {
164
298
  function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
165
299
  function validateBrowserArtifacts(root, proof, binding) {
166
300
  const snapshot = readProofArtifact(root, proof.worker_snapshot), native = readProofArtifact(root, proof.native_result);
301
+ if (proof.observation_contract !== undefined && (proof.observation_contract !== "dd-tasks/browser-observation@2" || snapshot.observation_contract !== proof.observation_contract || native.observation_contract !== proof.observation_contract
302
+ || !isDeepStrictEqual(snapshot.target, proof.target) || !isDeepStrictEqual(native.target, proof.target) || snapshot.window_epoch !== proof.window_epoch || native.window_epoch !== proof.window_epoch))
303
+ return false;
167
304
  if (!Array.isArray(native.errors) || native.errors.length < 1 || native.errors.length > 64 || native.errors.some(error => !boundedString(error, 4096) || !/^(?:Error: )?(?:[A-Za-z][\w.]*: )?Test timeout of [1-9]\d*ms exceeded\.(?:\nCall log:[\s\S]*)?$/.test(stripVTControlCharacters(String(error)))))
168
305
  return false;
169
306
  if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
@@ -175,7 +312,13 @@ function validateBrowserArtifacts(root, proof, binding) {
175
312
  return Number.isSafeInteger(quiet) && quiet > 0 && [last, deadline, observed, health, ended].every(Number.isFinite) && Math.abs(deadline - last - quiet) <= 2 && observed >= last && observed <= deadline && health <= observed && health >= last && deadline - health <= Math.min(1000, quiet / 4) && ended >= deadline && ended <= Date.now() && ended - deadline <= 5000;
176
313
  }
177
314
  export function runnerProofArtifacts(evidence) {
178
- return [...new Set((evidence?.failures ?? []).flatMap(failure => { const proof = failure.repair_evidence; return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : [])]; }))];
315
+ return [...new Set([...(evidence?.failures ?? []).flatMap(failure => {
316
+ const child = validatedChildren.get(failure);
317
+ if (child && failure.child_evidence)
318
+ return [failure.child_evidence.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(failure.child_evidence.report.relative_path), file))];
319
+ const proof = failure.repair_evidence;
320
+ return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : proof && "native_artifact" in proof ? [proof.native_artifact.relative_path] : [])];
321
+ }), ...(evidence ? validatedReports.get(evidence) ?? [] : []).flatMap(child => [child.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(child.report.relative_path), file))])])];
179
322
  }
180
323
  export function runnerContractsValid(value) {
181
324
  if (value === undefined)
@@ -186,13 +329,15 @@ export function runnerContractsValid(value) {
186
329
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
187
330
  return false;
188
331
  const contract = raw;
189
- return alias.startsWith("@check/") && contract.schema_id === "dd-flow/check-runner-evidence@1" && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
332
+ return alias.startsWith("@check/") && ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(String(contract.schema_id)) && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
190
333
  });
191
334
  }
192
- export function readRunnerEvidence(root, contract, binding) {
335
+ export function readRunnerEvidence(root, contract, binding, depth = 0, budget = { reports: 0 }) {
193
336
  try {
337
+ if (depth > 4 || ++budget.reports > 64)
338
+ return null;
194
339
  let current = root;
195
- if (!fs.lstatSync(current).isDirectory())
340
+ if (!fs.lstatSync(current).isDirectory() || fs.lstatSync(current).isSymbolicLink())
196
341
  return null;
197
342
  for (const part of contract.path.split(/[\\/]/)) {
198
343
  current = path.join(current, part);
@@ -200,34 +345,141 @@ export function readRunnerEvidence(root, contract, binding) {
200
345
  return null;
201
346
  }
202
347
  const evidence = JSON.parse(readBoundedProofFile(current, 1024 * 1024).toString("utf8"));
348
+ if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
349
+ return null;
350
+ const partial = evidence.failure_projection;
351
+ if (partial !== undefined && (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !object(partial) || partial.state !== "partial" || !["count_bound", "byte_bound", "interrupt"].includes(partial.reason)
352
+ || ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
353
+ return null;
203
354
  if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
204
355
  || typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
205
356
  || ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
206
357
  || !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
207
- || evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
358
+ || !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
208
359
  return null;
360
+ const children = new Map();
361
+ const consumed = new Map();
362
+ if (evidence.steps !== undefined) {
363
+ if (!Array.isArray(evidence.steps) || evidence.steps.length > 64)
364
+ return null;
365
+ const ids = new Set(), invocations = new Set();
366
+ let stopped = false;
367
+ for (const [ordinal, step] of evidence.steps.entries()) {
368
+ if (!step || !boundedString(step.id, 256) || !step.id || ids.has(step.id) || step.ordinal !== ordinal || !hash(step.command_sha256) || !["passed", "failed", "unreached"].includes(step.state))
369
+ return null;
370
+ ids.add(step.id);
371
+ if (step.state === "unreached") {
372
+ if (!stopped || step.binding !== undefined || step.report !== undefined || step.exit_code !== undefined)
373
+ return null;
374
+ continue;
375
+ }
376
+ if (stopped)
377
+ return null;
378
+ if (step.state === "failed")
379
+ stopped = true;
380
+ if (!step.binding || !proofArtifact(step.report) || !Number.isSafeInteger(step.exit_code) || step.state === "passed" && step.exit_code !== 0 || step.state === "failed" && step.exit_code === 0 || invocations.has(step.binding.invocation_id) || step.binding.invocation_id === binding.invocation_id)
381
+ return null;
382
+ if (step.binding.invocation_id !== `${binding.invocation_id}:step:${step.id}` || step.binding.check_id !== `${binding.check_id}/${step.id}` || step.binding.verification_epoch !== binding.verification_epoch || step.binding.input_hash !== crypto.createHash("sha256").update(JSON.stringify([binding.input_hash, step.command_sha256])).digest("hex"))
383
+ return null;
384
+ if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
385
+ return null;
386
+ invocations.add(step.binding.invocation_id);
387
+ readProofArtifact(root, step.report, 1024 * 1024);
388
+ const child = readRunnerEvidence(path.join(root, path.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget);
389
+ if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
390
+ return null;
391
+ if (!child.completed || child.observation_unknown)
392
+ stopped = true;
393
+ children.set(step.id, child);
394
+ }
395
+ for (const key of ["passed", "failed", "skipped"])
396
+ if (evidence.counts[key] !== [...children.values()].reduce((total, child) => total + child.counts[key], 0))
397
+ return null;
398
+ }
209
399
  for (const failure of evidence.failures) {
400
+ if (failure.child_evidence) {
401
+ const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
402
+ if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
403
+ return null;
404
+ const original = { ...failure };
405
+ delete original.child_evidence;
406
+ const used = consumed.get(child) ?? new Set();
407
+ consumed.set(child, used);
408
+ const index = child.failures.findIndex((item, index) => { const native = { ...item }; delete native.child_evidence; return !used.has(index) && isDeepStrictEqual(native, original); });
409
+ if (index < 0)
410
+ return null;
411
+ used.add(index);
412
+ const matched = child.failures[index];
413
+ if (!matched.child_evidence && matched.repair_evidence?.rule === "candidate_tool_diagnostic" && !isDeepStrictEqual(matched.repair_evidence.step, { id: step.id, ordinal: step.ordinal, command_sha256: step.command_sha256 }))
414
+ return null;
415
+ validatedChildren.set(failure, { failure: matched, evidence: child });
416
+ continue;
417
+ }
210
418
  if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, 2 * 1024 * 1024).binding, binding))
211
419
  return null;
212
420
  const proof = failure.repair_evidence;
421
+ if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
422
+ const native = readProofArtifact(root, proof.native_artifact, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
423
+ if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
424
+ return null;
425
+ if (proof.rule === "candidate_test_runtime_error") {
426
+ if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
427
+ || !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
428
+ return null;
429
+ const firstFrame = String(native.stack).split("\n").find(line => /^\s*at\s/.test(line));
430
+ const frame = firstFrame?.match(/^\s*at\s+(?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/);
431
+ if (!frame || (frame[1].startsWith("file://") ? fileURLToPath(frame[1]) : frame[1]) !== proof.file || Number(frame[2]) !== proof.line || Number(frame[3]) !== proof.column)
432
+ return null;
433
+ }
434
+ validatedNativeProofs.add(proof);
435
+ }
213
436
  if (proof?.rule === "candidate_browser_action_inactivity") {
214
- if (!validateBrowserArtifacts(root, proof, binding))
437
+ if (proof.observation_contract === undefined && evidence.schema_id === "dd-flow/check-runner-evidence@1") {
438
+ // Legacy diagnostics remain readable; they do not prove the new window/target guarantee.
439
+ if (!validateBrowserArtifacts(root, proof, binding))
440
+ return null;
441
+ evidence.completed = false;
442
+ unsupportedObservationContracts.add(evidence);
443
+ continue;
444
+ }
445
+ if (proof.observation_contract !== "dd-tasks/browser-observation@2" || !validateBrowserArtifacts(root, proof, binding))
215
446
  return null;
216
447
  validatedBrowserProofs.add(proof);
217
448
  }
218
449
  }
450
+ const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
451
+ if (partial) {
452
+ if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
453
+ return null;
454
+ evidence.completed = false;
455
+ }
456
+ else if (represented !== childFailures)
457
+ return null;
458
+ // Valid but incomplete native observation is diagnostic evidence, not a
459
+ // malformed schema. Preserve the primary failure and references through
460
+ // every composite, including a parent that incorrectly claims completion.
461
+ if ([...children.values()].some(child => !child.completed || child.observation_unknown)) {
462
+ evidence.completed = false;
463
+ if ([...children.values()].some(child => unsupportedObservationContracts.has(child)))
464
+ unsupportedObservationContracts.add(evidence);
465
+ if (evidence.schema_id === "dd-flow/check-runner-evidence@1" && [...children.values()].some(child => !child.completed && !unsupportedObservationContracts.has(child)) || [...children.values()].some(child => child.observation_unknown))
466
+ evidence.observation_unknown = true;
467
+ }
468
+ validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
219
469
  return evidence;
220
470
  }
221
471
  catch {
222
472
  return null;
223
473
  }
224
474
  }
225
- export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
475
+ export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
226
476
  export function checkRepairDisposition(receipt) {
227
477
  if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
228
478
  return "tooling_blocker";
229
479
  if (receipt.status === "running" || receipt.status === "aborted" || receipt.runner_evidence?.observation_unknown)
230
480
  return "settlement_required";
481
+ if (receipt.runner_evidence && unsupportedObservationContracts.has(receipt.runner_evidence) || receipt.runner_evidence?.failure_projection || receipt.runner_evidence?.schema_id === "dd-flow/check-runner-evidence@2" && !receipt.runner_evidence.completed)
482
+ return "tooling_blocker";
231
483
  if (receipt.status === "passed")
232
484
  return "passed";
233
485
  if (receipt.execution_failures?.length || receipt.mutation_paths?.length || receipt.before_fingerprint !== receipt.after_fingerprint || receipt.diagnostic_evidence_valid === false)
@@ -241,7 +493,7 @@ export function checkRepairDisposition(receipt) {
241
493
  return "tooling_blocker";
242
494
  if (!evidence.completed)
243
495
  return "unclassified";
244
- if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files)))
496
+ if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences)))
245
497
  return "unclassified";
246
498
  return "source_repair";
247
499
  }
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
2
+ import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } 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";
@@ -16,6 +16,7 @@ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, manag
16
16
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
17
17
  import { getResourceDatabase } from "../storage/database.js";
18
18
  import { readBoundedReleaseFile } from "./release-source.js";
19
+ import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
19
20
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
20
21
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
21
22
  // auditable attempt. A second caller must not mistake that small handoff window
@@ -61,10 +62,7 @@ export function terminalCheckSettled(context, record) {
61
62
  }
62
63
  }
63
64
  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" });
65
+ const selected = validateCheckEnvironment(checkEnvironment);
68
66
  const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
69
67
  // The managed CLI's Node must win over a provider app's PATH: a Rosetta
70
68
  // provider can otherwise run x64 checks against arm64-installed modules.
@@ -72,8 +70,7 @@ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
72
70
  delete environment.NODE_PATH;
73
71
  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"])
74
72
  delete environment[key];
75
- if (checkEnvironment)
76
- environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
73
+ Object.assign(environment, selected);
77
74
  return environment;
78
75
  }
79
76
  export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
@@ -302,6 +299,9 @@ export class PreparedCheckEvidence {
302
299
  Object.freeze(this);
303
300
  }
304
301
  static prepare(context, input) {
302
+ const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
303
+ if (checkEnvironment)
304
+ input = { ...input, checkEnvironment };
305
305
  validateCheckEvidenceContext(input.evidenceContext);
306
306
  const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
307
307
  if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
@@ -488,6 +488,9 @@ function checkEvidenceSqlSnapshot(context, input) {
488
488
  }));
489
489
  }
490
490
  export async function runCodeChecks(context, input) {
491
+ const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
492
+ if (checkEnvironment)
493
+ input = { ...input, checkEnvironment };
491
494
  validateCheckEvidenceContext(input.evidenceContext);
492
495
  const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
493
496
  let gate;
@@ -658,8 +661,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
658
661
  if (result.failure)
659
662
  physicalFailures.push(result.failure);
660
663
  const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
664
+ const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
661
665
  if (group.runnerEvidence)
662
- retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles);
666
+ retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
663
667
  const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
664
668
  artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
665
669
  const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
@@ -667,6 +671,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
667
671
  receipt.required_artifacts = requiredArtifacts;
668
672
  receipt.candidate_root = input.workspaceRoot;
669
673
  receipt.candidate_files = candidateFiles;
674
+ receipt.candidate_absences = candidateAbsences;
670
675
  receipt.execution_failures = physicalFailures;
671
676
  context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
672
677
  const completion = readCompletion(completionPath);
@@ -754,6 +759,7 @@ export function reconcileUnfinishedChecks(context, input) {
754
759
  if (scoped && !completionSnapshot)
755
760
  physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
756
761
  let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
762
+ let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
757
763
  if (receipt?.runner_evidence_required) {
758
764
  const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
759
765
  if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
@@ -763,13 +769,14 @@ export function reconcileUnfinishedChecks(context, input) {
763
769
  if (proven.candidate_root) {
764
770
  physicalFailures.push(...(proven.execution_failures ?? []));
765
771
  candidateFiles = proven.candidate_files ?? candidateFiles;
772
+ candidateAbsences = proven.candidate_absences ?? [];
766
773
  }
767
774
  else {
768
775
  physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
769
776
  fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
770
777
  }
771
778
  }
772
- retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
779
+ retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
773
780
  receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
774
781
  artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
775
782
  }
@@ -782,6 +789,7 @@ export function reconcileUnfinishedChecks(context, input) {
782
789
  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: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
783
790
  repaired.candidate_root = input.workspaceRoot;
784
791
  repaired.candidate_files = candidateFiles;
792
+ repaired.candidate_absences = candidateAbsences;
785
793
  repaired.execution_failures = physicalFailures;
786
794
  writeReceipt(pendingReceipt.receipt_path, repaired);
787
795
  if (status === "failed")
@@ -980,12 +988,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
980
988
  if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
981
989
  try {
982
990
  const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
983
- 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 && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && 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"))
991
+ 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 && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && 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)) && (value.candidate_absences === undefined || candidateAbsencesValid(value.candidate_absences)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
984
992
  outcome = value;
985
993
  }
986
994
  catch { /* Missing physical proof cannot grant attribution. */ }
987
995
  }
988
- 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 } : {}) };
996
+ 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 } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
989
997
  }
990
998
  function executionFailures(input) {
991
999
  const failures = [];
@@ -1003,11 +1011,11 @@ function executionFailures(input) {
1003
1011
  failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
1004
1012
  return failures;
1005
1013
  }
1006
- function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
1014
+ function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
1007
1015
  const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
1008
1016
  if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
1009
1017
  throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
1010
- 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, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
1018
+ 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, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, candidate_absences: candidateAbsences, execution_failures: failures }), { flag: "wx", mode: 0o600 });
1011
1019
  fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
1012
1020
  }
1013
1021
  export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
@@ -1162,7 +1170,7 @@ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
1162
1170
  "CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
1163
1171
  return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
1164
1172
  .filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
1165
- && (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1173
+ && (name === "PLAYWRIGHT_BROWSERS_PATH" || inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1166
1174
  .sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
1167
1175
  }
1168
1176
  function acquireWorkspaceGate(context, input) {
@@ -1337,7 +1345,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
1337
1345
  // Receipt annotations are not execution authority. Compare the SQL-owned
1338
1346
  // failures with the independently hash/binding-checked engine sidecar.
1339
1347
  const hasEngineOutcome = receipt.required_artifacts.includes("engine-outcome.json") || receipt.artifacts.some(artifact => artifact.path === "engine-outcome.json" || artifact.path === path.join(path.dirname(receipt.receipt_path), "artifacts", "engine-outcome.json"));
1340
- if (hasEngineOutcome && !isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []))
1348
+ if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
1341
1349
  return false;
1342
1350
  const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
1343
1351
  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 })))
@@ -4,10 +4,11 @@ import { FrozenGenericExecution } from "./generic-execution.js";
4
4
  import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
5
5
  import { readFlowRunExecutionBundle } from "./runs.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
7
8
  /** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
8
9
  * operator file, merge policy or project default participates in this selection.
9
10
  */
10
- export function genericControllerRouting(execution) {
11
+ export function genericControllerRouting(execution, workspaceRoot) {
11
12
  if (!(execution instanceof FrozenGenericExecution) || execution.settings.execution !== "managed") {
12
13
  throw new AppError("controller_execution_invalid", "Managed controller requires managed frozen execution", 1, { effect: "no_effect" });
13
14
  }
@@ -33,14 +34,17 @@ export function genericControllerRouting(execution) {
33
34
  execution.stage(fromStage);
34
35
  return { ...resolveExecutionPolicy({ ddFlowHome: "", policy, profiles, stage }), session_mode: selected.session_mode };
35
36
  },
36
- cwd(stage, _mergeTargetWorkspace, _currentRun) { execution.stage(stage); return path.resolve(snapshot.scope.workspace); }
37
+ cwd(stage, _mergeTargetWorkspace, currentRun) {
38
+ execution.stage(stage);
39
+ return path.resolve(currentRun?.workspace_root ?? workspaceRoot ?? snapshot.scope.workspace);
40
+ }
37
41
  };
38
42
  }
39
43
  /** Restore execution from RUN authority, never from a generic manifest copy.
40
44
  * Legacy SDLC manifests retain their original settings and routing behavior.
41
45
  */
42
46
  export function selectControllerExecution(context, run, manifest) {
43
- if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind)) {
47
+ if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind)) {
44
48
  const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
45
49
  if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
46
50
  throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
@@ -51,7 +55,9 @@ export function selectControllerExecution(context, run, manifest) {
51
55
  || manifest.request?.project_root !== run.project_root || manifest.request?.run_id !== run.id)) {
52
56
  throw new AppError("controller_execution_invalid", "Controller execution binding differs from retained RUN authority", 1);
53
57
  }
54
- return { ...genericControllerRouting(bundle.execution), bundle, binding, legacy: undefined };
58
+ // Bundle admission has already checked the current RUN workspace against
59
+ // its trusted relocation receipts; frozen scope remains historical input.
60
+ return { ...genericControllerRouting(bundle.execution, run.workspace_root), bundle, binding, legacy: undefined };
55
61
  }
56
62
  const index = JSON.parse(run.index_json);
57
63
  const legacy = manifest ? manifest.execution_profile : index.execution_profile;