@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
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.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -5,8 +5,8 @@ import { parse } from "shell-quote";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
-
import { runnerFailureValid, validateRunnerProof, validatedRunnerChild, hasValidatedNativeProof, hasValidatedBrowserProof, hasUnsupportedObservation } from "./check-runner-proof.js";
|
|
9
|
-
export { candidateAbsencesValid, runnerFailureValid, runnerEvidencePass, runnerProofArtifacts } from "./check-runner-proof.js";
|
|
8
|
+
import { runnerFailureValid, validateRunnerProof, validatedRunnerChild, hasValidatedNativeProof, hasValidatedBrowserProof, hasUnsupportedObservation, runnerLibraryInputs } from "./check-runner-proof.js";
|
|
9
|
+
export { candidateAbsencesValid, candidateLibraryInputsValid, runnerFailureValid, runnerEvidencePass, runnerProofArtifacts } from "./check-runner-proof.js";
|
|
10
10
|
const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
|
|
11
11
|
function candidateFile(root, file) {
|
|
12
12
|
if (!root || !path.isAbsolute(file))
|
|
@@ -45,6 +45,28 @@ export function candidateFailureFiles(evidence, root) {
|
|
|
45
45
|
}
|
|
46
46
|
}))];
|
|
47
47
|
}
|
|
48
|
+
export function candidateLibraryInputs(evidence, root) {
|
|
49
|
+
const inputs = new Map();
|
|
50
|
+
for (const failure of evidence?.failures ?? []) {
|
|
51
|
+
const child = validatedRunnerChild(failure);
|
|
52
|
+
const candidates = child ? candidateLibraryInputs({ ...child.evidence, failures: [child.failure] }, root)
|
|
53
|
+
: failure.repair_evidence?.rule === "candidate_library_query_error" ? runnerLibraryInputs(failure.repair_evidence) ?? [] : [];
|
|
54
|
+
try {
|
|
55
|
+
if (candidates.some(input => !candidateFile(root, input.file) || crypto.createHash("sha256").update(readBoundedProofFile(input.file, 2 * 1024 * 1024)).digest("hex") !== input.sha256))
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const input of candidates)
|
|
62
|
+
inputs.set(input.file, input.sha256);
|
|
63
|
+
}
|
|
64
|
+
return [...inputs].map(([file, sha256]) => ({ file, sha256 }));
|
|
65
|
+
}
|
|
66
|
+
/** Current execution/repair boundary only; historical readers retain captured proof. */
|
|
67
|
+
export function currentCandidateLibraryInputsValid(receipt, workspaceRoot) {
|
|
68
|
+
return isDeepStrictEqual(receipt.candidate_library_inputs ?? [], candidateLibraryInputs(receipt.runner_evidence, workspaceRoot));
|
|
69
|
+
}
|
|
48
70
|
function absentCandidate(root, file) {
|
|
49
71
|
if (!root || !absoluteFile(file))
|
|
50
72
|
return false;
|
|
@@ -104,10 +126,10 @@ export function candidateFailureAbsences(evidence, root) {
|
|
|
104
126
|
}
|
|
105
127
|
});
|
|
106
128
|
}
|
|
107
|
-
function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences) {
|
|
129
|
+
function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences, capturedLibraryInputs) {
|
|
108
130
|
const child = validatedRunnerChild(failure);
|
|
109
131
|
if (child)
|
|
110
|
-
return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences);
|
|
132
|
+
return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences, capturedLibraryInputs);
|
|
111
133
|
const owned = (file) => {
|
|
112
134
|
if (capturedFiles !== undefined)
|
|
113
135
|
return capturedFiles.includes(file);
|
|
@@ -126,6 +148,11 @@ function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsenc
|
|
|
126
148
|
if (failure.category === "assertion")
|
|
127
149
|
return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
|
|
128
150
|
const proof = failure.repair_evidence;
|
|
151
|
+
if (proof?.rule === "candidate_library_query_error") {
|
|
152
|
+
const required = runnerLibraryInputs(proof), captured = capturedFiles !== undefined ? capturedLibraryInputs : root ? candidateLibraryInputs({ ...evidence, failures: [failure] }, root) : undefined;
|
|
153
|
+
return hasValidatedNativeProof(proof) && failure.category === "unclassified" && failure.origin === "candidate" && failure.phase === "test" && failure.native_code === proof.exception_type
|
|
154
|
+
&& failure.test_id === proof.test_id && failure.file === proof.file && owned(proof.file) && Boolean(required?.length && captured && required.every(input => captured.some(item => item.file === input.file && item.sha256 === input.sha256)));
|
|
155
|
+
}
|
|
129
156
|
if (proof?.rule === "candidate_test_runtime_error")
|
|
130
157
|
return hasValidatedNativeProof(proof) && failure.category === "unclassified" && failure.origin === "candidate"
|
|
131
158
|
&& failure.phase === proof.phase && failure.native_code === proof.exception_type && failure.file === proof.file && owned(proof.file);
|
|
@@ -246,6 +273,51 @@ export function readRunnerEvidenceDetailed(root, contract, binding) {
|
|
|
246
273
|
});
|
|
247
274
|
}
|
|
248
275
|
export function readRunnerEvidence(root, contract, binding) { return readRunnerEvidenceDetailed(root, contract, binding).evidence; }
|
|
276
|
+
/** Semantic attribution is diagnostic information, never permission to investigate a failed check. */
|
|
277
|
+
export const checkDiagnosisPolicy = "settled-failure@1";
|
|
278
|
+
export function checkDiagnosisDisposition(receipt) {
|
|
279
|
+
if (receipt.status === "running"
|
|
280
|
+
|| receipt.execution_failures?.some(failure => ["cleanup_unconfirmed", "engine_outcome_unbound"].includes(failure.code)))
|
|
281
|
+
return "settlement_required";
|
|
282
|
+
if (!["passed", "failed", "aborted"].includes(receipt.status))
|
|
283
|
+
return "invalid_binding";
|
|
284
|
+
// Caller supplies the SQL-owned receipt, never a proposed runner report.
|
|
285
|
+
// Missing, malformed or foreign optional reports are diagnostic failures;
|
|
286
|
+
// they cannot change receipt ownership or grant a successful check.
|
|
287
|
+
if (receipt.status === "passed")
|
|
288
|
+
return "passed";
|
|
289
|
+
// New launchers record physical settlement even for spawn errors/signals.
|
|
290
|
+
// Historical aborted receipts with no such proof must first reconcile.
|
|
291
|
+
if (!receipt.execution_failures?.some(failure => failure.code === "check_execution_settled") && !Number.isSafeInteger(receipt.exit_code))
|
|
292
|
+
return "settlement_required";
|
|
293
|
+
return "diagnosis_required";
|
|
294
|
+
}
|
|
295
|
+
export function assertCheckDiagnosisEligible(receipts, _workspaceRoot) {
|
|
296
|
+
const dispositions = receipts.map(receipt => ({ check_receipt_id: receipt.id, disposition: checkDiagnosisDisposition(receipt) }));
|
|
297
|
+
if (!receipts.length || dispositions.some(item => item.disposition !== "diagnosis_required"))
|
|
298
|
+
throw new AppError("check_gate_blocked", "Check diagnosis requires owned, physically settled failed executions", 2, {
|
|
299
|
+
failures: receipts, dispositions, outcome: "blocked", recoverable: false,
|
|
300
|
+
instruction: "Reconcile any running or unknown check process before changing its workspace. Do not treat a foreign receipt or an agent report as proof of completion."
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Human-readable retained facts; tool output remains data, never instructions. */
|
|
304
|
+
export function checkDiagnosisSummary(receipt, workspaceRoot) {
|
|
305
|
+
const available = (file) => {
|
|
306
|
+
try {
|
|
307
|
+
fs.accessSync(file, fs.constants.R_OK);
|
|
308
|
+
return fs.statSync(file).isFile() ? file : `${file} (not a regular file)`;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return `${file} (unavailable; investigate the missing diagnostic, do not invent its content)`;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return [`Check: ${receipt.id}`, `Command: ${receipt.command}`, `cwd: ${workspaceRoot}`,
|
|
315
|
+
`Outcome: ${receipt.status}; exit: ${receipt.exit_code ?? "unavailable"}; epoch: ${receipt.verification_epoch}`,
|
|
316
|
+
`Receipt: ${available(receipt.receipt_path)}`, `stdout: ${available(receipt.stdout_path)}`, `stderr: ${available(receipt.stderr_path)}`,
|
|
317
|
+
`Execution facts: ${JSON.stringify(receipt.execution_failures ?? [])}`,
|
|
318
|
+
`Report: ${receipt.runner_evidence_required?.path ? path.join(path.dirname(receipt.receipt_path), "artifacts", receipt.runner_evidence_required.path) : "not declared"}`].join("\n");
|
|
319
|
+
}
|
|
320
|
+
/** Historical attribution reader only. New repair routing uses checkDiagnosisDisposition. */
|
|
249
321
|
export function checkRepairDisposition(receipt) {
|
|
250
322
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
251
323
|
return "tooling_blocker";
|
|
@@ -266,12 +338,7 @@ export function checkRepairDisposition(receipt) {
|
|
|
266
338
|
return "tooling_blocker";
|
|
267
339
|
if (!evidence.completed)
|
|
268
340
|
return "unclassified";
|
|
269
|
-
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences)))
|
|
341
|
+
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences, receipt.candidate_library_inputs)))
|
|
270
342
|
return "unclassified";
|
|
271
343
|
return "source_repair";
|
|
272
344
|
}
|
|
273
|
-
export function assertSourceRepairEligible(receipts) {
|
|
274
|
-
const blockers = receipts.filter(receipt => checkRepairDisposition(receipt) !== "source_repair");
|
|
275
|
-
if (!receipts.length || blockers.length)
|
|
276
|
-
throw new AppError("check_gate_blocked", "Checks require tooling diagnosis or settlement before product repair; retained nonzero is not a candidate-source verdict", 2, { failures: receipts, dispositions: receipts.map(receipt => ({ check_receipt_id: receipt.id, disposition: checkRepairDisposition(receipt) })), outcome: "blocked", recoverable: false, instruction: "Inspect retained runner evidence. Do not change product code to repair tooling, unknown observation or unclassified failure. Retry only after establishing and correcting the cause." });
|
|
277
|
-
}
|
|
@@ -8,11 +8,13 @@ function boundedString(value, bytes) { return typeof value === "string" && Buffe
|
|
|
8
8
|
const validatedBrowserProofs = new WeakSet();
|
|
9
9
|
const unsupportedObservationContracts = new WeakSet();
|
|
10
10
|
const validatedNativeProofs = new WeakSet();
|
|
11
|
+
const validatedLibraryInputs = new WeakMap();
|
|
11
12
|
const validatedChildren = new WeakMap();
|
|
12
13
|
const validatedReports = new WeakMap();
|
|
13
14
|
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
14
15
|
const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
|
|
15
16
|
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)); }
|
|
17
|
+
export function candidateLibraryInputsValid(value) { return Array.isArray(value) && value.length <= 64 && new Set(value.map(item => object(item) ? item.file : null)).size === value.length && value.every(item => object(item) && absoluteFile(item.file) && hash(item.sha256)); }
|
|
16
18
|
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 !== ".."); }
|
|
17
19
|
function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
|
|
18
20
|
function browserProofShape(proof) {
|
|
@@ -35,6 +37,13 @@ export function runnerFailureValid(value) {
|
|
|
35
37
|
return false;
|
|
36
38
|
if (value.repair_evidence !== undefined) {
|
|
37
39
|
const proof = value.repair_evidence;
|
|
40
|
+
if (object(proof) && proof.rule === "candidate_library_query_error")
|
|
41
|
+
return proof.exception_type === "TestingLibraryElementError" && proof.phase === "test"
|
|
42
|
+
&& boundedString(proof.test_id, 4096) && Boolean(proof.test_id) && absoluteFile(proof.file) && hash(proof.file_sha256)
|
|
43
|
+
&& [proof.line, proof.column].every(n => Number.isSafeInteger(n) && Number(n) > 0) && object(proof.library)
|
|
44
|
+
&& proof.library.name === "@testing-library/dom" && proof.library.version === "10.4.1" && hash(proof.library.package_json_sha256) && hash(proof.library.lockfile_sha256)
|
|
45
|
+
&& Array.isArray(proof.helper_frames) && proof.helper_frames.length >= 2 && proof.helper_frames.length <= 8
|
|
46
|
+
&& proof.helper_frames.every(frame => object(frame) && absoluteFile(frame.file) && hash(frame.file_sha256) && [frame.line, frame.column].every(n => Number.isSafeInteger(n) && Number(n) > 0)) && proofArtifact(proof.native_artifact);
|
|
38
47
|
if (object(proof) && proof.rule === "candidate_test_runtime_error")
|
|
39
48
|
return ["TypeError", "ReferenceError", "Error"].includes(String(proof.exception_type)) && (proof.exception_type !== "Error" || proof.phase === "test")
|
|
40
49
|
&& absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
|
|
@@ -82,6 +91,62 @@ function readProofArtifact(root, ref, context, limit = 2 * 1024 * 1024) {
|
|
|
82
91
|
throw new Error("Invalid proof object");
|
|
83
92
|
return value;
|
|
84
93
|
}
|
|
94
|
+
// Exact known helper bytes keep this a library-specific proof, not permission
|
|
95
|
+
// to skip arbitrary dependency frames. New versions need explicit evidence.
|
|
96
|
+
const queryInputHashes = {
|
|
97
|
+
"dist/config.js": "3627f6265cccd26604777602d7249788c2347a273bdd1cae41719c790f2c5a7a",
|
|
98
|
+
"dist/query-helpers.js": "901f513f2ac901992d7c5002e644aef69286edbcd7d050d3ee7dd6aaaebfc9cc",
|
|
99
|
+
"package.json": "4e62e2aa8b974b6a5d2d532396f7610d1b4e806d9d2aaef3c84221f0d2fab06d",
|
|
100
|
+
};
|
|
101
|
+
function validateLibraryQuery(root, proof, failure, evidence, context) {
|
|
102
|
+
const native = readProofArtifact(root, proof.native_artifact, context, 256 * 1024);
|
|
103
|
+
const facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
104
|
+
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !isDeepStrictEqual(native.binding, evidence.binding)
|
|
105
|
+
|| Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)) || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
106
|
+
|| failure.category !== "unclassified" || failure.origin !== "candidate" || failure.native_code !== proof.exception_type || failure.phase !== "test" || failure.test_id !== proof.test_id || failure.file !== proof.file
|
|
107
|
+
|| native.stack_complete !== true || native.frames_complete !== true || !Array.isArray(native.frame_lines) || native.frame_lines.length !== proof.helper_frames.length + 1
|
|
108
|
+
|| native.frame_lines.some(line => !boundedString(line, 4096)) || Buffer.byteLength(JSON.stringify(native.frame_lines)) > 128 * 1024)
|
|
109
|
+
return false;
|
|
110
|
+
const expectedFrames = [...proof.helper_frames, { file: proof.file, line: proof.line, column: proof.column }];
|
|
111
|
+
for (const [index, line] of native.frame_lines.entries()) {
|
|
112
|
+
const match = String(line).match(/^\s*at (?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/), expected = expectedFrames[index];
|
|
113
|
+
if (!match || (match[1].startsWith("file:") ? fileURLToPath(match[1]) : match[1]) !== expected.file || Number(match[2]) !== expected.line || Number(match[3]) !== expected.column)
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const packageRoot = path.dirname(path.dirname(proof.helper_frames[0].file));
|
|
117
|
+
if (proof.helper_frames[0].file !== path.join(packageRoot, "dist/config.js") || !String(native.frame_lines[0]).includes("getElementError")
|
|
118
|
+
|| proof.helper_frames.slice(1).some(frame => frame.file !== path.join(packageRoot, "dist/query-helpers.js")))
|
|
119
|
+
return false;
|
|
120
|
+
if (!Array.isArray(native.library_inputs) || native.library_inputs.length !== 4)
|
|
121
|
+
return false;
|
|
122
|
+
const inputs = [];
|
|
123
|
+
let packageJson;
|
|
124
|
+
for (const [index, value] of native.library_inputs.entries()) {
|
|
125
|
+
if (!object(value) || !absoluteFile(value.file) || !hash(value.sha256) || !proofArtifact(value.artifact))
|
|
126
|
+
return false;
|
|
127
|
+
const expectedName = Object.keys(queryInputHashes)[index];
|
|
128
|
+
if (expectedName ? value.file !== path.join(packageRoot, expectedName) || value.sha256 !== queryInputHashes[expectedName]
|
|
129
|
+
: path.basename(value.file) !== "pnpm-lock.yaml" || value.sha256 !== proof.library.lockfile_sha256)
|
|
130
|
+
return false;
|
|
131
|
+
const bytes = readBytes(context, path.posix.join(root, value.artifact.relative_path), 2 * 1024 * 1024);
|
|
132
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== value.sha256 || value.artifact.sha256 !== value.sha256)
|
|
133
|
+
return false;
|
|
134
|
+
if (index === 2)
|
|
135
|
+
packageJson = JSON.parse(bytes.toString("utf8"));
|
|
136
|
+
if (index === 3 && !bytes.toString("utf8").includes("@testing-library/dom@10.4.1"))
|
|
137
|
+
return false;
|
|
138
|
+
inputs.push({ file: value.file, sha256: value.sha256, artifact: value.artifact });
|
|
139
|
+
}
|
|
140
|
+
if (packageJson?.name !== proof.library.name || packageJson.version !== proof.library.version || inputs[2].sha256 !== proof.library.package_json_sha256
|
|
141
|
+
|| proof.helper_frames.some(frame => inputs.find(input => input.file === frame.file)?.sha256 !== frame.file_sha256))
|
|
142
|
+
return false;
|
|
143
|
+
const candidateRoot = path.dirname(inputs[3].file);
|
|
144
|
+
if ([proof.file, ...inputs.map(input => input.file)].some(file => { const relative = path.relative(candidateRoot, file); return !relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative); }))
|
|
145
|
+
return false;
|
|
146
|
+
validatedLibraryInputs.set(proof, inputs);
|
|
147
|
+
validatedNativeProofs.add(proof);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
85
150
|
function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
|
|
86
151
|
function validateBrowserArtifacts(root, proof, binding, context) {
|
|
87
152
|
const snapshot = readProofArtifact(root, proof.worker_snapshot, context), native = readProofArtifact(root, proof.native_result, context);
|
|
@@ -104,7 +169,7 @@ export function runnerProofArtifacts(evidence) {
|
|
|
104
169
|
if (child && failure.child_evidence)
|
|
105
170
|
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))];
|
|
106
171
|
const proof = failure.repair_evidence;
|
|
107
|
-
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] : [])];
|
|
172
|
+
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, ...(proof.rule === "candidate_library_query_error" ? (validatedLibraryInputs.get(proof) ?? []).map(input => input.artifact.relative_path) : [])] : [])];
|
|
108
173
|
}), ...(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))])])];
|
|
109
174
|
}
|
|
110
175
|
function diagnostic(context, value) {
|
|
@@ -224,6 +289,8 @@ function readRunnerEvidenceInternal(root, contract, binding, depth, budget, cont
|
|
|
224
289
|
if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, context, 2 * 1024 * 1024).binding, binding))
|
|
225
290
|
return null;
|
|
226
291
|
const proof = failure.repair_evidence;
|
|
292
|
+
if (proof?.rule === "candidate_library_query_error" && !validateLibraryQuery(root, proof, failure, evidence, context))
|
|
293
|
+
return null;
|
|
227
294
|
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
|
|
228
295
|
const native = readProofArtifact(root, proof.native_artifact, context, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
229
296
|
if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
|
|
@@ -301,5 +368,6 @@ function readRunnerEvidenceInternal(root, contract, binding, depth, budget, cont
|
|
|
301
368
|
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
302
369
|
export const validatedRunnerChild = (failure) => validatedChildren.get(failure);
|
|
303
370
|
export const hasValidatedNativeProof = (proof) => validatedNativeProofs.has(proof);
|
|
371
|
+
export const runnerLibraryInputs = (proof) => validatedLibraryInputs.get(proof)?.map(({ file, sha256 }) => ({ file, sha256 }));
|
|
304
372
|
export const hasValidatedBrowserProof = (proof) => validatedBrowserProofs.has(proof);
|
|
305
373
|
export const hasUnsupportedObservation = (evidence) => unsupportedObservationContracts.has(evidence);
|
|
@@ -89,6 +89,10 @@ function isReadOnlyDiagnostic(family, command, action) {
|
|
|
89
89
|
return ["status", "ls", "show"].includes(command ?? "");
|
|
90
90
|
if (family === "stage")
|
|
91
91
|
return command === "fanout" && action === "status";
|
|
92
|
+
if (family === "interactive")
|
|
93
|
+
return command === "inputs" || command === "git-inputs";
|
|
94
|
+
if (family === "delivery")
|
|
95
|
+
return command === "inputs";
|
|
92
96
|
if (family === "lane") {
|
|
93
97
|
return (command === "status" ||
|
|
94
98
|
command === "waiters" ||
|
|
@@ -108,7 +112,8 @@ function isReadOnlyDiagnostic(family, command, action) {
|
|
|
108
112
|
if (family === "dashboard")
|
|
109
113
|
return command === "data" || command === "open";
|
|
110
114
|
if (family === "memory")
|
|
111
|
-
return command === "permissions" && action === "preflight"
|
|
115
|
+
return command === "permissions" && action === "preflight" || command === "distill" && action === "inputs"
|
|
116
|
+
|| command === "fix" && ["context", "scope"].includes(action ?? "");
|
|
112
117
|
if (family === "codex" && command === "hooks")
|
|
113
118
|
return action === "print" || action === "status";
|
|
114
119
|
if (family === "codex" && command === "home")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles,
|
|
3
|
-
export {
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid, checkDiagnosisDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
3
|
+
export { assertCheckDiagnosisEligible, checkDiagnosisDisposition, 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";
|
|
6
6
|
import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
@@ -352,14 +352,16 @@ export class PreparedCheckEvidence {
|
|
|
352
352
|
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
353
353
|
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
354
354
|
});
|
|
355
|
-
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
355
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || input.mode !== "diagnostic" && !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
|
|
356
356
|
fail();
|
|
357
357
|
passed = passed && receipt.status === "passed";
|
|
358
|
-
|
|
358
|
+
const availableArtifacts = input.mode === "diagnostic" ? availableDiagnosticArtifacts(receipt) : receipt.artifacts;
|
|
359
|
+
for (const artifact of availableArtifacts)
|
|
359
360
|
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 });
|
|
360
361
|
const bytes = fs.readFileSync(receipt.receipt_path);
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
if (input.mode !== "diagnostic" || fs.existsSync(receipt.completion_path))
|
|
363
|
+
completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(readBoundedReleaseFile(receipt.completion_path, 1024 * 1024)).digest("hex") });
|
|
364
|
+
if (receipt.input_manifest && (input.mode !== "diagnostic" || fs.existsSync(path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path))))
|
|
363
365
|
completions.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path)).split(path.sep).join("/"), sha256: receipt.input_manifest.sha256 });
|
|
364
366
|
return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
365
367
|
});
|
|
@@ -562,7 +564,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
562
564
|
const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
563
565
|
const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
|
|
564
566
|
&& (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
|
|
565
|
-
||
|
|
567
|
+
|| diagnosisRetryContractMatches(predecessor, candidate, profileHash, checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment, false), checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment))
|
|
566
568
|
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(predecessor, candidate, profileHash, input)));
|
|
567
569
|
if (!group || retries.has(group))
|
|
568
570
|
throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
|
|
@@ -579,7 +581,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
579
581
|
const inputFingerprint = inputFilesFingerprint(inputFiles);
|
|
580
582
|
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
|
|
581
583
|
const epoch = crypto.createHash("sha256").update(`${group.gate}\0${inputFingerprint}`).digest("hex");
|
|
582
|
-
|
|
584
|
+
// Retry preserves the declared check/wave, not the failed conditions being
|
|
585
|
+
// corrected. Observed environment still participates in inputHash/PASS reuse.
|
|
586
|
+
const executionContractHash = checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false);
|
|
583
587
|
const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, inputFiles, inputFingerprint, executionContractHash, profileHash, inputHash, epoch, startedAt };
|
|
584
588
|
delete reservationInput.retryReceiptId;
|
|
585
589
|
const decision = reserveOrReuseReceipt(context, reservationInput);
|
|
@@ -592,6 +596,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
592
596
|
const { id, evidenceDir, stdoutPath, stderrPath, receiptPath, completionPath } = reserved;
|
|
593
597
|
let resources = {};
|
|
594
598
|
let result = { exitCode: null, error: "check did not start", aborted: true };
|
|
599
|
+
let dispatched = false, physicallySettled = false;
|
|
595
600
|
try {
|
|
596
601
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
597
602
|
retainInputManifest(context, reserved);
|
|
@@ -624,6 +629,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
624
629
|
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 } : {};
|
|
625
630
|
if (group.runnerEvidence)
|
|
626
631
|
fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
|
|
632
|
+
dispatched = true;
|
|
627
633
|
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), ...browserCheckCapabilityEnvironment(context, input.runId, input.runHome), 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) => {
|
|
628
634
|
confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
|
|
629
635
|
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
@@ -638,6 +644,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
638
644
|
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
639
645
|
if (processTreeIsAlive(current))
|
|
640
646
|
throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
|
|
647
|
+
physicallySettled = !result.cleanupUnconfirmed;
|
|
641
648
|
allocation.release();
|
|
642
649
|
if (!result.cleanupUnconfirmed)
|
|
643
650
|
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
|
|
@@ -664,10 +671,14 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
664
671
|
const physicalFailures = executionFailures({ fingerprintScope: "check_inputs", exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
|
|
665
672
|
if (result.failure)
|
|
666
673
|
physicalFailures.push(result.failure);
|
|
674
|
+
// This marker is SQL-owned execution metadata, not a semantic runner verdict.
|
|
675
|
+
if (result.aborted && (!dispatched || physicallySettled))
|
|
676
|
+
physicalFailures.push({ code: "check_execution_settled", message: "No owned check process remains; investigate the retained launch or execution failure" });
|
|
667
677
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
668
678
|
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
679
|
+
const libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
|
|
669
680
|
if (group.runnerEvidence)
|
|
670
|
-
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);
|
|
681
|
+
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, libraryInputs);
|
|
671
682
|
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
672
683
|
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
673
684
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
@@ -676,6 +687,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
676
687
|
receipt.candidate_root = input.workspaceRoot;
|
|
677
688
|
receipt.candidate_files = candidateFiles;
|
|
678
689
|
receipt.candidate_absences = candidateAbsences;
|
|
690
|
+
receipt.candidate_library_inputs = libraryInputs;
|
|
679
691
|
receipt.execution_failures = physicalFailures;
|
|
680
692
|
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
|
|
681
693
|
const completion = readCompletion(completionPath);
|
|
@@ -768,6 +780,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
768
780
|
physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
|
|
769
781
|
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
770
782
|
let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
783
|
+
let libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
|
|
771
784
|
if (receipt?.runner_evidence_required) {
|
|
772
785
|
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
773
786
|
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
@@ -778,13 +791,14 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
778
791
|
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
779
792
|
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
780
793
|
candidateAbsences = proven.candidate_absences ?? [];
|
|
794
|
+
libraryInputs = proven.candidate_library_inputs ?? [];
|
|
781
795
|
}
|
|
782
796
|
else {
|
|
783
797
|
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
784
798
|
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
785
799
|
}
|
|
786
800
|
}
|
|
787
|
-
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
801
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences, libraryInputs);
|
|
788
802
|
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
789
803
|
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
790
804
|
}
|
|
@@ -798,6 +812,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
798
812
|
repaired.candidate_root = input.workspaceRoot;
|
|
799
813
|
repaired.candidate_files = candidateFiles;
|
|
800
814
|
repaired.candidate_absences = candidateAbsences;
|
|
815
|
+
repaired.candidate_library_inputs = libraryInputs;
|
|
801
816
|
repaired.execution_failures = physicalFailures;
|
|
802
817
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
803
818
|
if (status === "failed")
|
|
@@ -821,7 +836,7 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
821
836
|
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;
|
|
822
837
|
const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
|
|
823
838
|
const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
|
|
824
|
-
const sourceCorrection = Boolean(retryPredecessor && (
|
|
839
|
+
const sourceCorrection = Boolean(retryPredecessor && (diagnosisRetryContractMatches(retryPredecessor, input.group, input.profileHash, input.executionContractHash, checkInputHash(input.workspaceRoot, input.group, input.profileHash, "", input.evidenceContext, input.checkEnvironment))
|
|
825
840
|
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(retryPredecessor, input.group, input.profileHash, input)));
|
|
826
841
|
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);
|
|
827
842
|
return context.db.writeTransaction(() => {
|
|
@@ -879,9 +894,12 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
879
894
|
return { reserved };
|
|
880
895
|
}, "checks.reserveOrReuseReceipt");
|
|
881
896
|
}
|
|
882
|
-
function
|
|
883
|
-
return
|
|
884
|
-
&&
|
|
897
|
+
function diagnosisRetryContractMatches(receipt, group, profileHash, executionContractHash, legacyExecutionContractHash) {
|
|
898
|
+
return checkDiagnosisDisposition(receipt) === "diagnosis_required" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
|
|
899
|
+
&& [executionContractHash, legacyExecutionContractHash].includes(receipt.execution_contract_hash ?? "")
|
|
900
|
+
&& group.requiredArtifacts.every(artifact => receipt.required_artifacts.includes(artifact))
|
|
901
|
+
&& isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
|
|
902
|
+
&& isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
|
|
885
903
|
}
|
|
886
904
|
function staleCheckRetryEligible(receipt) {
|
|
887
905
|
if (receipt.fingerprint_scope !== "check_inputs" || receipt.status !== "failed" || receipt.exit_code !== 0 || receipt.retry_reason === "check_inputs_changed"
|
|
@@ -892,7 +910,8 @@ function staleCheckRetryEligible(receipt) {
|
|
|
892
910
|
}
|
|
893
911
|
function staleRetryContractMatches(receipt, group, profileHash, input) {
|
|
894
912
|
return group.reusable && staleCheckRetryEligible(receipt) && isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
|
|
895
|
-
&&
|
|
913
|
+
&& [checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false),
|
|
914
|
+
checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment)].includes(receipt.execution_contract_hash ?? "");
|
|
896
915
|
}
|
|
897
916
|
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, fingerprint_scope: "check_inputs", input_manifest: input.inputManifest ?? null, execution_contract_hash: input.executionContractHash, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.inputFingerprint, after_fingerprint: input.status === "running" ? input.inputFingerprint : 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() }; }
|
|
898
917
|
function retainInputManifest(context, reserved) {
|
|
@@ -950,8 +969,6 @@ export function validateFailedCheckRetry(context, input) {
|
|
|
950
969
|
const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
|
|
951
970
|
if (!receipt.check_refs.some((ref) => declared.has(ref)))
|
|
952
971
|
throw new AppError("retry_check_invalid", "--retry-check does not belong to this stage gate", 2, { check_receipt_id: input.receiptId, check_refs: receipt.check_refs });
|
|
953
|
-
if (receipt.fingerprint_scope === "check_inputs" && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed") && input.reason !== "check_inputs_changed")
|
|
954
|
-
throw new AppError("retry_check_invalid", "Scoped drift uses its bounded stale transition, not an environment/source retry", 2, { check_receipt_id: receipt.id });
|
|
955
972
|
if (input.reason === "check_inputs_changed" && (!staleCheckRetryEligible(receipt) || receipt.retry_reason === "check_inputs_changed"))
|
|
956
973
|
throw new AppError("retry_check_invalid", "Scoped input retry requires a bound, safely completed stale predecessor and one unconsumed episode", 2, { check_receipt_id: receipt.id });
|
|
957
974
|
return receipt;
|
|
@@ -1004,12 +1021,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
1004
1021
|
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
1005
1022
|
try {
|
|
1006
1023
|
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
1007
|
-
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"))
|
|
1024
|
+
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)) && (value.candidate_library_inputs === undefined || candidateLibraryInputsValid(value.candidate_library_inputs)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
1008
1025
|
outcome = value;
|
|
1009
1026
|
}
|
|
1010
1027
|
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
1011
1028
|
}
|
|
1012
|
-
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, runner_evidence_diagnostics: diagnostics, ...(diagnosticsTruncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), 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 } : {}) };
|
|
1029
|
+
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, runner_evidence_diagnostics: diagnostics, ...(diagnosticsTruncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), 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 } : {}), ...(outcome.candidate_library_inputs ? { candidate_library_inputs: outcome.candidate_library_inputs } : {}) };
|
|
1013
1030
|
}
|
|
1014
1031
|
function executionFailures(input) {
|
|
1015
1032
|
const failures = [];
|
|
@@ -1027,11 +1044,11 @@ function executionFailures(input) {
|
|
|
1027
1044
|
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
1028
1045
|
return failures;
|
|
1029
1046
|
}
|
|
1030
|
-
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
|
|
1047
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = [], libraryInputs = []) {
|
|
1031
1048
|
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
1032
1049
|
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
1033
1050
|
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
1034
|
-
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 });
|
|
1051
|
+
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, candidate_library_inputs: libraryInputs, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1035
1052
|
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
1036
1053
|
}
|
|
1037
1054
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
@@ -1310,12 +1327,28 @@ function explicitInputFiles(root, inputs, ignoredRoots, sourceFiles) {
|
|
|
1310
1327
|
visit(path.resolve(root, input), input, new Set());
|
|
1311
1328
|
return [...result.entries()];
|
|
1312
1329
|
}
|
|
1313
|
-
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"); }
|
|
1330
|
+
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment, observedEnvironment = true) { 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: observedEnvironment ? environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment) : null, input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
|
|
1314
1331
|
function derivedReceiptArtifacts(group, receipt) { return [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(receipt.runner_evidence), ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])]; }
|
|
1315
1332
|
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: derivedReceiptArtifacts(group, receipt), inputs: group.inputs }; }
|
|
1316
1333
|
export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
|
|
1334
|
+
function availableDiagnosticArtifacts(receipt) {
|
|
1335
|
+
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1336
|
+
return receipt.artifacts.flatMap(artifact => {
|
|
1337
|
+
const relative = path.isAbsolute(artifact.path) ? path.relative(root, artifact.path) : artifact.path;
|
|
1338
|
+
const observed = collectCheckArtifacts(root, [relative]);
|
|
1339
|
+
return observed.items.filter(item => item.sha256 === artifact.sha256);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1317
1342
|
function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
1318
1343
|
try {
|
|
1344
|
+
if (mode === "diagnostic" && receipt.status !== "passed") {
|
|
1345
|
+
if (checkDiagnosisDisposition(receipt) !== "diagnosis_required")
|
|
1346
|
+
return false;
|
|
1347
|
+
// SQL selects this execution. Only its identity must survive in the
|
|
1348
|
+
// diagnostic packet; missing reports/logs cannot erase a failed command.
|
|
1349
|
+
const stored = JSON.parse(readBoundedReleaseFile(receipt.receipt_path, 1024 * 1024).toString("utf8"));
|
|
1350
|
+
return ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code", "started_at", "finished_at"].every(field => (stored[field] ?? null) === (receipt[field] ?? null));
|
|
1351
|
+
}
|
|
1319
1352
|
const staleDiagnostic = mode === "diagnostic" && receipt.fingerprint_scope === "check_inputs" && receipt.status === "failed" && Number.isInteger(receipt.exit_code)
|
|
1320
1353
|
&& receipt.before_fingerprint !== receipt.after_fingerprint && receipt.mutation_paths.length > 0 && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed");
|
|
1321
1354
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
@@ -1361,7 +1394,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
|
1361
1394
|
// Receipt annotations are not execution authority. Compare the SQL-owned
|
|
1362
1395
|
// failures with the independently hash/binding-checked engine sidecar.
|
|
1363
1396
|
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"));
|
|
1364
|
-
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
|
|
1397
|
+
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? []) || !isDeepStrictEqual(retainedProof.candidate_library_inputs ?? [], receipt.candidate_library_inputs ?? [])))
|
|
1365
1398
|
return false;
|
|
1366
1399
|
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1367
1400
|
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 })))
|
|
@@ -19,7 +19,10 @@ export async function verifyCodexHookDelivery(env, payload) {
|
|
|
19
19
|
throw new AppError("native_hook_timeout", "Native hook deadline expired before delivery verification", 1, { phase: "verification", effect: "no_effect", recoverable: false });
|
|
20
20
|
const proof = await callDaemon(env.DD_FLOW_CODEX_STATE_DIR, "hook.verify", {
|
|
21
21
|
daemonId: env.DD_FLOW_DAEMON_ID, rootSessionId: payload.session_id,
|
|
22
|
-
sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id
|
|
22
|
+
sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id,
|
|
23
|
+
// The daemon verifies the native delivery before retaining it; the source
|
|
24
|
+
// command cannot itself grant admission to the CLI.
|
|
25
|
+
nativePayload: payload, nativeCommand: command
|
|
23
26
|
}, Math.min(5000, remaining));
|
|
24
27
|
if (typeof proof.projectRoot !== "string"
|
|
25
28
|
|| typeof proof.ddFlowHome !== "string" || !env.DD_FLOW_HOME || path.resolve(env.DD_FLOW_HOME) !== path.resolve(proof.ddFlowHome)
|
|
@@ -27,6 +30,12 @@ export async function verifyCodexHookDelivery(env, payload) {
|
|
|
27
30
|
|| typeof proof.toolCallId !== "string" || !(proof.parentSessionId === null || typeof proof.parentSessionId === "string")) {
|
|
28
31
|
throw new AppError("native_hook_unproven", "Native hook proof differs from the requested project/runtime", 1, { effect: "no_effect" });
|
|
29
32
|
}
|
|
33
|
+
const binding = proof.nativeHookBinding;
|
|
34
|
+
const retained = binding && typeof binding === "object" && !Array.isArray(binding) ? binding : null;
|
|
30
35
|
return { daemonId: proof.daemonId, rootSessionId: proof.rootSessionId, sessionId: proof.sessionId,
|
|
31
|
-
parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command
|
|
36
|
+
parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command,
|
|
37
|
+
...(retained?.schema_id === "dd-flow/hook-request@1" && retained.source_harness === "codex-desktop"
|
|
38
|
+
&& retained.daemon_id === proof.daemonId && retained.root_provider_session_id === proof.rootSessionId
|
|
39
|
+
&& retained.provider_session_id === proof.sessionId && retained.tool_call_id === proof.toolCallId
|
|
40
|
+
&& retained.native_command === command && typeof retained.request_id === "string" ? { nativeHookBinding: retained } : {}) };
|
|
32
41
|
}
|
|
@@ -5,6 +5,7 @@ import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } fro
|
|
|
5
5
|
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
7
|
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
8
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
8
9
|
/** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
|
|
9
10
|
* operator file, merge policy or project default participates in this selection.
|
|
10
11
|
*/
|
|
@@ -44,7 +45,9 @@ export function genericControllerRouting(execution, workspaceRoot) {
|
|
|
44
45
|
* Legacy SDLC manifests retain their original settings and routing behavior.
|
|
45
46
|
*/
|
|
46
47
|
export function selectControllerExecution(context, run, manifest) {
|
|
47
|
-
|
|
48
|
+
const boundDistill = run.flow_kind === "mb-distill" && JSON.parse(run.index_json).execution_bundle?.flow_id === "mb-distill";
|
|
49
|
+
const boundDelivery = isDeliveryFlow(run.flow_kind) && JSON.parse(run.index_json).execution_bundle?.flow_id === run.flow_kind;
|
|
50
|
+
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade", "mb-fix", "review-fix", "interactive"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind) || boundDistill || boundDelivery) {
|
|
48
51
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
49
52
|
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
|
|
50
53
|
throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
|