@deksden-com/dd-flow-cli 0.9.0-beta.140 → 0.9.0-beta.142
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 +14 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/command-inputs.js +1 -1
- package/dist/cli/command-option-descriptions.js +1 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/run-cli.js +9 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +7 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +3 -3
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +13 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +64 -12
- package/dist/harness-runtime/lib/dd-opencode.mjs +12 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +3 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +36 -7
- package/dist/harness-runtime/lib/operation-errors.d.mts +2 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +11 -0
- package/dist/services/check-runner-evidence.js +102 -20
- package/dist/services/code-checks.js +69 -33
- package/dist/services/runtime-budget.js +16 -0
- package/dist/services/stage-lifecycle.js +57 -13
- package/dist/services/tooling.js +14 -12
- package/dist/services/verification-matrix.js +3 -2
- package/dist/services/vnext-plan.js +1 -2
- package/dist/services/vnext-protocolize.js +121 -10
- package/dist/services/worktrees.js +8 -5
- package/dist/shared/errors.js +11 -1
- package/package.json +1 -1
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { parse } from "shell-quote";
|
|
4
6
|
import { AppError } from "../shared/errors.js";
|
|
5
7
|
import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
6
8
|
function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
7
9
|
function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
|
|
10
|
+
const validatedBrowserProofs = new WeakSet();
|
|
11
|
+
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
|
+
function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
|
|
13
|
+
function browserProofShape(proof) {
|
|
14
|
+
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)
|
|
16
|
+
&& [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
|
|
17
|
+
&& 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
|
+
&& proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
|
|
19
|
+
}
|
|
8
20
|
export function runnerFailureValid(value) {
|
|
9
21
|
if (!object(value) || typeof value.category !== "string" || !["assertion", "tooling", "unclassified"].includes(value.category))
|
|
10
22
|
return false;
|
|
23
|
+
if (value.diagnostic_artifact !== undefined && !proofArtifact(value.diagnostic_artifact))
|
|
24
|
+
return false;
|
|
11
25
|
for (const key of ["code", "message", "file", "test_id", "native_code", "classification_reason"])
|
|
12
26
|
if (value[key] !== undefined && !boundedString(value[key], 4096))
|
|
13
27
|
return false;
|
|
@@ -20,6 +34,10 @@ export function runnerFailureValid(value) {
|
|
|
20
34
|
const world = proof.world;
|
|
21
35
|
if (world.schema_id !== "dd-tasks/test-world-ready@1" || !boundedString(world.invocation_id, 4096) || typeof world.world_id !== "string" || !/^[a-f0-9]{32}$/.test(world.world_id) || typeof world.migrations_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(world.migrations_sha256) || world.ready !== true || world.setup_completed !== true || !boundedString(world.role, 128) || !world.role)
|
|
22
36
|
return false;
|
|
37
|
+
if (proof.rule === "candidate_browser_action_inactivity" && !browserProofShape(proof))
|
|
38
|
+
return false;
|
|
39
|
+
if (!["candidate_sql_integrity", "candidate_browser_action_inactivity"].includes(String(proof.rule)))
|
|
40
|
+
return false;
|
|
23
41
|
}
|
|
24
42
|
return true;
|
|
25
43
|
}
|
|
@@ -44,12 +62,14 @@ function candidateFailure(failure, evidence, root, capturedFiles) {
|
|
|
44
62
|
if (failure.category === "assertion")
|
|
45
63
|
return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
|
|
46
64
|
const proof = failure.repair_evidence;
|
|
65
|
+
if (proof?.rule === "candidate_browser_action_inactivity")
|
|
66
|
+
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);
|
|
47
67
|
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);
|
|
48
68
|
}
|
|
49
69
|
export function runnerObservationDurations(environment) {
|
|
50
70
|
// Both observers leave the launcher its bounded physical cleanup tail.
|
|
51
71
|
return { startupMs: validateObservationDuration(validateObservationDuration(Number(environment.DD_FLOW_TEST_STARTUP_SILENCE_MS ?? 120_000)) + 10_000),
|
|
52
|
-
executionMs: validateObservationDuration(validateObservationDuration(Number(environment.DD_FLOW_TEST_EXECUTION_SILENCE_MS ?? 600_000)) +
|
|
72
|
+
executionMs: validateObservationDuration(validateObservationDuration(Number(environment.DD_FLOW_TEST_EXECUTION_SILENCE_MS ?? 600_000)) + 15_000) };
|
|
53
73
|
}
|
|
54
74
|
/** Baseline uses argv rather than a shell alias. Recognize only a literal,
|
|
55
75
|
* unique execution contract; never guess shell expansion or compound syntax. */
|
|
@@ -72,35 +92,90 @@ export function runnerContractForArgv(profile, command, args) {
|
|
|
72
92
|
throw new AppError("runner_evidence_ambiguous", "Baseline argv matches conflicting runner evidence contracts", 2, { aliases: matches.map(match => match.alias) });
|
|
73
93
|
return matches[0] ?? null;
|
|
74
94
|
}
|
|
75
|
-
/** Shared by CODE and EVAL baseline.
|
|
76
|
-
*
|
|
95
|
+
/** Shared by CODE and EVAL baseline. The sole publisher owns this structured
|
|
96
|
+
* journal; logs belong on stdout/stderr. Partial final lines await append. */
|
|
77
97
|
export function readRunnerProgress(file, invocationId, lastSequence = 0, now = Date.now()) {
|
|
78
98
|
try {
|
|
79
|
-
const
|
|
80
|
-
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
81
|
-
return [];
|
|
99
|
+
const text = readBoundedProofFile(file, 4 * 1024 * 1024).toString("utf8");
|
|
82
100
|
const accepted = [];
|
|
83
|
-
for (const line of
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
catch {
|
|
101
|
+
for (const line of text.split("\n").slice(0, -1)) {
|
|
102
|
+
if (Buffer.byteLength(line) > 16 * 1024)
|
|
103
|
+
throw new Error("Progress record exceeds 16 KiB");
|
|
104
|
+
const value = JSON.parse(line);
|
|
105
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.invocation_id === "string" && value.invocation_id !== invocationId)
|
|
89
106
|
continue;
|
|
90
|
-
}
|
|
91
107
|
if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
|
|
92
|
-
|| !Number.isSafeInteger(value.sequence) || value.sequence <=
|
|
108
|
+
|| !Number.isSafeInteger(value.sequence) || value.sequence <= 0 || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at))
|
|
93
109
|
|| !["startup", "execution"].includes(value.phase)
|
|
94
110
|
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
|
|
111
|
+
throw new Error("Malformed official progress record");
|
|
112
|
+
if (value.sequence <= lastSequence || Date.parse(value.at) > now)
|
|
95
113
|
continue;
|
|
96
114
|
accepted.push(value);
|
|
97
115
|
lastSequence = value.sequence;
|
|
98
116
|
}
|
|
99
117
|
return accepted;
|
|
100
118
|
}
|
|
101
|
-
catch {
|
|
102
|
-
|
|
119
|
+
catch (cause) {
|
|
120
|
+
if (cause.code === "ENOENT")
|
|
121
|
+
return [];
|
|
122
|
+
throw new AppError("runner_progress_invalid", "Runner progress cannot be safely observed", 1, { cause: cause.code ?? cause.message });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function readBoundedProofFile(file, limit) {
|
|
126
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.fstatSync(fd);
|
|
129
|
+
if (!stat.isFile() || stat.size > limit)
|
|
130
|
+
throw new Error("Evidence is not a bounded regular file");
|
|
131
|
+
const buffer = Buffer.alloc(Math.min(limit + 1, stat.size + 1));
|
|
132
|
+
let length = 0;
|
|
133
|
+
while (length < buffer.length) {
|
|
134
|
+
const read = fs.readSync(fd, buffer, length, buffer.length - length, null);
|
|
135
|
+
if (!read)
|
|
136
|
+
break;
|
|
137
|
+
length += read;
|
|
138
|
+
}
|
|
139
|
+
if (length > limit || length > stat.size)
|
|
140
|
+
throw new Error("Evidence grew during observation");
|
|
141
|
+
return buffer.subarray(0, length);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
fs.closeSync(fd);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function readProofArtifact(root, ref, limit = 256 * 1024) {
|
|
148
|
+
let current = root;
|
|
149
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
150
|
+
throw new Error("Symlink evidence root");
|
|
151
|
+
for (const part of ref.relative_path.split("/")) {
|
|
152
|
+
current = path.join(current, part);
|
|
153
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
154
|
+
throw new Error("Symlink proof artifact");
|
|
103
155
|
}
|
|
156
|
+
const bytes = readBoundedProofFile(current, limit);
|
|
157
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== ref.sha256)
|
|
158
|
+
throw new Error("Browser proof hash differs");
|
|
159
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
160
|
+
if (!object(value))
|
|
161
|
+
throw new Error("Invalid proof object");
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
|
|
165
|
+
function validateBrowserArtifacts(root, proof, binding) {
|
|
166
|
+
const snapshot = readProofArtifact(root, proof.worker_snapshot), native = readProofArtifact(root, proof.native_result);
|
|
167
|
+
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
|
+
return false;
|
|
169
|
+
if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
|
|
170
|
+
|| !matches(snapshot.step, proof.step) || !matches(native.step, proof.step) || !matches(snapshot.world, proof.world) || proof.world.invocation_id !== binding.invocation_id
|
|
171
|
+
|| native.status !== "timedOut" || native.phase !== "test" || native.file !== proof.file || snapshot.phase !== "body" || snapshot.observation_gap !== false || !matches(snapshot.browser, { connected: true, page_closed: false, crashed: false })
|
|
172
|
+
|| !matches(snapshot.world_health, { api: true, web: true, db: true }))
|
|
173
|
+
return false;
|
|
174
|
+
const quiet = snapshot.quiet_ms, last = Date.parse(String(snapshot.last_progress_at)), deadline = Date.parse(String(snapshot.deadline_at)), observed = Date.parse(String(snapshot.observed_at)), health = Date.parse(String(snapshot.health_observed_at)), ended = Date.parse(String(native.ended_at));
|
|
175
|
+
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
|
+
}
|
|
177
|
+
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] : [])]; }))];
|
|
104
179
|
}
|
|
105
180
|
export function runnerContractsValid(value) {
|
|
106
181
|
if (value === undefined)
|
|
@@ -124,16 +199,23 @@ export function readRunnerEvidence(root, contract, binding) {
|
|
|
124
199
|
if (fs.lstatSync(current).isSymbolicLink())
|
|
125
200
|
return null;
|
|
126
201
|
}
|
|
127
|
-
const
|
|
128
|
-
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
129
|
-
return null;
|
|
130
|
-
const evidence = JSON.parse(fs.readFileSync(current, "utf8"));
|
|
202
|
+
const evidence = JSON.parse(readBoundedProofFile(current, 1024 * 1024).toString("utf8"));
|
|
131
203
|
if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
|
|
132
204
|
|| typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
133
205
|
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
134
206
|
|| !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
|
|
135
207
|
|| evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
136
208
|
return null;
|
|
209
|
+
for (const failure of evidence.failures) {
|
|
210
|
+
if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, 2 * 1024 * 1024).binding, binding))
|
|
211
|
+
return null;
|
|
212
|
+
const proof = failure.repair_evidence;
|
|
213
|
+
if (proof?.rule === "candidate_browser_action_inactivity") {
|
|
214
|
+
if (!validateBrowserArtifacts(root, proof, binding))
|
|
215
|
+
return null;
|
|
216
|
+
validatedBrowserProofs.add(proof);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
137
219
|
return evidence;
|
|
138
220
|
}
|
|
139
221
|
catch {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
|
|
2
|
+
import { 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";
|
|
@@ -622,10 +622,12 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
622
622
|
}
|
|
623
623
|
const runnerEvidence = group.runnerEvidence ? readRunnerEvidence(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
|
|
624
624
|
const physicalFailures = executionFailures({ exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
|
|
625
|
+
if (result.failure)
|
|
626
|
+
physicalFailures.push(result.failure);
|
|
625
627
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
626
628
|
if (group.runnerEvidence)
|
|
627
629
|
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch }, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
628
|
-
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
630
|
+
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
629
631
|
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
630
632
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
631
633
|
const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
|
|
@@ -714,7 +716,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
714
716
|
}
|
|
715
717
|
}
|
|
716
718
|
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
717
|
-
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), "engine-outcome.json"])];
|
|
719
|
+
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
718
720
|
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
719
721
|
}
|
|
720
722
|
const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && physicalFailures.length === 0 && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
@@ -796,7 +798,7 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
796
798
|
}
|
|
797
799
|
function sourceRepairContractMatches(receipt, group, profileHash) {
|
|
798
800
|
return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
|
|
799
|
-
&& isDeepStrictEqual(
|
|
801
|
+
&& isDeepStrictEqual(derivedReceiptArtifacts(group, receipt).sort(), [...receipt.required_artifacts].sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
|
|
800
802
|
}
|
|
801
803
|
function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
|
|
802
804
|
function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
@@ -1136,7 +1138,8 @@ function explicitInputFiles(root, inputs, ignoredRoots) {
|
|
|
1136
1138
|
return [...result.entries()];
|
|
1137
1139
|
}
|
|
1138
1140
|
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
|
|
1139
|
-
function
|
|
1141
|
+
function derivedReceiptArtifacts(group, receipt) { return [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(receipt.runner_evidence), ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])]; }
|
|
1142
|
+
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 }; }
|
|
1140
1143
|
export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
|
|
1141
1144
|
function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
1142
1145
|
try {
|
|
@@ -1239,8 +1242,10 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
|
|
|
1239
1242
|
const started = Date.now();
|
|
1240
1243
|
const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
1241
1244
|
let error = null;
|
|
1245
|
+
let failure;
|
|
1242
1246
|
let stopping = false;
|
|
1243
1247
|
let closed = false;
|
|
1248
|
+
let admitted = false;
|
|
1244
1249
|
let progress;
|
|
1245
1250
|
let cleanup;
|
|
1246
1251
|
let cleanupUnconfirmed = false;
|
|
@@ -1259,6 +1264,8 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
|
|
|
1259
1264
|
return;
|
|
1260
1265
|
stopping = true;
|
|
1261
1266
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
1267
|
+
if (cause instanceof AppError)
|
|
1268
|
+
failure = { code: cause.code, message: cause.message };
|
|
1262
1269
|
if (progress) {
|
|
1263
1270
|
clearInterval(progress);
|
|
1264
1271
|
progress = undefined;
|
|
@@ -1270,22 +1277,28 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
|
|
|
1270
1277
|
cleanup = stopProcessGroup(child, 5_000).catch(cause => {
|
|
1271
1278
|
cleanupUnconfirmed = true;
|
|
1272
1279
|
error = `${error}; cleanup: ${JSON.stringify(errorRecord(cause))}`;
|
|
1273
|
-
resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed });
|
|
1280
|
+
resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed, ...(failure ? { failure } : {}) });
|
|
1274
1281
|
});
|
|
1275
1282
|
};
|
|
1276
1283
|
const observationTimer = setInterval(() => {
|
|
1277
|
-
if (stopping || closed || observation.phase?.() === "serving")
|
|
1284
|
+
if (stopping || closed || !admitted || observation.phase?.() === "serving")
|
|
1278
1285
|
return;
|
|
1279
1286
|
if (runnerClock && runnerProgress) {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1287
|
+
try {
|
|
1288
|
+
for (const event of readRunnerProgress(runnerProgress, environment.DD_FLOW_CHECK_INVOCATION_ID ?? "", runnerSequence)) {
|
|
1289
|
+
runnerSequence = event.sequence;
|
|
1290
|
+
if (runnerPhase !== "execution")
|
|
1291
|
+
runnerPhase = event.phase;
|
|
1292
|
+
runnerClock.sample(runnerSequence, Date.parse(event.at));
|
|
1293
|
+
}
|
|
1294
|
+
// The outer guard includes the runner's bounded cleanup tail.
|
|
1295
|
+
runnerClock.timeoutMs = runnerPhase === "execution" ? runnerExecutionMs : runnerStartupMs;
|
|
1296
|
+
if (runnerClock.sample(runnerSequence))
|
|
1297
|
+
stop(new AppError("operation_observation_lost", "Structured test runner stopped making progress", 1, { phase: runnerPhase, sequence: runnerSequence }));
|
|
1298
|
+
}
|
|
1299
|
+
catch (cause) {
|
|
1300
|
+
stop(cause);
|
|
1284
1301
|
}
|
|
1285
|
-
// The outer guard includes the runner's bounded cleanup tail.
|
|
1286
|
-
runnerClock.timeoutMs = runnerPhase === "execution" ? runnerExecutionMs : runnerStartupMs;
|
|
1287
|
-
if (runnerClock.sample(runnerSequence))
|
|
1288
|
-
stop(new AppError("operation_observation_lost", "Structured test runner stopped making progress", 1, { phase: runnerPhase, sequence: runnerSequence }));
|
|
1289
1302
|
return;
|
|
1290
1303
|
}
|
|
1291
1304
|
if (clock.sample(outputCursor))
|
|
@@ -1313,28 +1326,12 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
|
|
|
1313
1326
|
clearInterval(observationTimer);
|
|
1314
1327
|
await cleanup;
|
|
1315
1328
|
await Promise.all(flushed);
|
|
1316
|
-
resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}) });
|
|
1329
|
+
resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}), ...(failure ? { failure } : {}) });
|
|
1317
1330
|
});
|
|
1318
1331
|
if (!child.pid) {
|
|
1319
1332
|
error = "check process did not provide a PID";
|
|
1320
1333
|
return;
|
|
1321
1334
|
}
|
|
1322
|
-
try {
|
|
1323
|
-
onSpawn(child.pid);
|
|
1324
|
-
}
|
|
1325
|
-
catch (cause) {
|
|
1326
|
-
stop(cause);
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
if (observation.leaseExpiresAt) {
|
|
1330
|
-
const expiry = observation.leaseExpiresAt();
|
|
1331
|
-
if (!expiry || !renewalConfirmed(renewal, performance.now(), expiry)) {
|
|
1332
|
-
stop("check confirmation has no current authority");
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
child.stdin.end("admitted\n");
|
|
1337
|
-
clock.sample(outputCursor);
|
|
1338
1335
|
const renew = () => {
|
|
1339
1336
|
if (stopping || closed)
|
|
1340
1337
|
return;
|
|
@@ -1382,7 +1379,46 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
|
|
|
1382
1379
|
stop(cause);
|
|
1383
1380
|
}
|
|
1384
1381
|
};
|
|
1385
|
-
|
|
1382
|
+
// The shell is held on stdin: a no-effect identity read may be retried,
|
|
1383
|
+
// but never the command or an admission callback with unknown effects.
|
|
1384
|
+
const confirmation = createRenewalState();
|
|
1385
|
+
confirmation.uncertainSince = performance.now();
|
|
1386
|
+
let confirmationError;
|
|
1387
|
+
const admit = () => {
|
|
1388
|
+
renewalTimer = undefined;
|
|
1389
|
+
if (stopping || closed)
|
|
1390
|
+
return;
|
|
1391
|
+
if (confirmationError && !renewalRemaining(confirmation)) {
|
|
1392
|
+
stop(confirmationError);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
onSpawn(child.pid);
|
|
1397
|
+
}
|
|
1398
|
+
catch (cause) {
|
|
1399
|
+
if (cause instanceof AppError && cause.code === "process_ownership_unknown"
|
|
1400
|
+
&& cause.details.reason === "physical_identity_unavailable" && maintenanceRetryable(cause)
|
|
1401
|
+
&& renewalFailure(confirmation)) {
|
|
1402
|
+
confirmationError = cause;
|
|
1403
|
+
renewalTimer = setTimeout(admit, Math.min(renewalDelay(confirmation), renewalRemaining(confirmation)));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
stop(cause);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (observation.leaseExpiresAt) {
|
|
1410
|
+
const expiry = observation.leaseExpiresAt();
|
|
1411
|
+
if (!expiry || !renewalConfirmed(renewal, performance.now(), expiry)) {
|
|
1412
|
+
stop("check confirmation has no current authority");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
admitted = true;
|
|
1417
|
+
child.stdin.end("admitted\n");
|
|
1418
|
+
clock.sample(++outputCursor);
|
|
1419
|
+
progress = setInterval(renew, 15_000);
|
|
1420
|
+
};
|
|
1421
|
+
admit();
|
|
1386
1422
|
});
|
|
1387
1423
|
}
|
|
1388
1424
|
function checkShellScript() {
|
|
@@ -199,6 +199,22 @@ export function admitRuntimeCheck(context, id, leaseToken) {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
/** Scope selection uses retained registration, never a current profile or PID search. */
|
|
202
|
+
export function prepareRuntimeScopeStatus(scopeId, scopeIdsJson) {
|
|
203
|
+
if (Boolean(scopeId) === Boolean(scopeIdsJson))
|
|
204
|
+
throw new AppError("usage", "Use exactly one of --scope-id or --scope-ids-json", 2);
|
|
205
|
+
let ids;
|
|
206
|
+
try {
|
|
207
|
+
ids = scopeId ? [scopeId] : JSON.parse(scopeIdsJson);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new AppError("usage", "Scope inventory requires JSON IDs", 2);
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(ids) || !ids.length || ids.length > 16 || ids.some(id => typeof id !== "string") || new Set(ids).size !== ids.length)
|
|
213
|
+
throw new AppError("usage", "Scope inventory requires 1..16 unique scope IDs", 2);
|
|
214
|
+
for (const id of ids)
|
|
215
|
+
parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: id, per_harness: {} }));
|
|
216
|
+
return ids;
|
|
217
|
+
}
|
|
202
218
|
export function runtimeBudgetStatus(context, scopeId) {
|
|
203
219
|
parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
|
|
204
220
|
return withResourceReadSnapshot(resourceHome(context), db => {
|
|
@@ -13,7 +13,7 @@ import { reconcileSessionCoverage, registerFlowSession } from "./sessions.js";
|
|
|
13
13
|
import { usageForRun } from "./usage.js";
|
|
14
14
|
import { boundCanonicalPlan, prepareCanonicalPlan } from "./plan-runtime.js";
|
|
15
15
|
import { prepareJsonFile, prepareJsonSchema, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
|
|
16
|
-
import { AppError } from "../shared/errors.js";
|
|
16
|
+
import { AppError, errorRecord, processEvidence, diagnosticJson } from "../shared/errors.js";
|
|
17
17
|
import { planJsonPath, projectHome, resolveProjectRoot } from "../storage/paths.js";
|
|
18
18
|
import { resolveCanonRoot } from "./canon.js";
|
|
19
19
|
import { preflightMemoryPermissions } from "./memory-permissions.js";
|
|
@@ -295,7 +295,7 @@ export async function finishStage(context, input, prepared = prepareStageFinish(
|
|
|
295
295
|
if (coverage.jobs.missing?.length) {
|
|
296
296
|
throw new AppError("worker_jobs_incomplete", "Stage finish requires all declared worker jobs", 1, { coverage });
|
|
297
297
|
}
|
|
298
|
-
const lint = await runTargetedMemoryBankLint(projectRoot);
|
|
298
|
+
const lint = await runTargetedMemoryBankLint(projectRoot, undefined, path.join(stageRoot, "diagnostics"));
|
|
299
299
|
if (planFinish)
|
|
300
300
|
boundCanonicalPlan(context, { projectId: view.run.project_id, protocolId: view.run.subject.id, planPath: planFinish.plan_path }, planFinish.canonical);
|
|
301
301
|
const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
|
|
@@ -836,7 +836,7 @@ function workerCoverage(context, projectId, runId) {
|
|
|
836
836
|
jobs: { expected: jobs.map((job) => job.job_id), missing: missingJobs }
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
|
-
export async function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
|
|
839
|
+
export async function runTargetedMemoryBankLint(projectRoot, selectedFiles, diagnosticRoot) {
|
|
840
840
|
const files = [...new Set((selectedFiles ?? gitChangedFiles(projectRoot)).filter((file) => file.startsWith(".memory-bank/") && file.toLowerCase().endsWith(".md") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
|
|
841
841
|
for (const file of files)
|
|
842
842
|
ensureWithin(projectRoot, path.resolve(projectRoot, file), "lint target");
|
|
@@ -844,27 +844,71 @@ export async function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
|
|
|
844
844
|
return { ok: true, status: "not_applicable_no_changed_docs", files, finding_count: 0 };
|
|
845
845
|
}
|
|
846
846
|
const binary = process.env.MB_LINT_BIN ?? "mb-lint";
|
|
847
|
+
const fail = (code, message, details) => {
|
|
848
|
+
const error = new AppError(code, message, 1, { binary, files, ...details });
|
|
849
|
+
if (diagnosticRoot) {
|
|
850
|
+
try {
|
|
851
|
+
fs.mkdirSync(diagnosticRoot, { recursive: true });
|
|
852
|
+
const file = path.join(diagnosticRoot, `lint-${crypto.randomUUID()}.json`);
|
|
853
|
+
if (details.report) {
|
|
854
|
+
const reportFile = file.replace(/\.json$/, ".report.json"), report = diagnosticJson(details.report);
|
|
855
|
+
const reportBytes = Buffer.from(report);
|
|
856
|
+
fs.writeFileSync(reportFile, reportBytes.length <= 1024 * 1024 ? report : JSON.stringify({ truncated: true, bytes: reportBytes.length, excerpt: reportBytes.subarray(0, 128 * 1024).toString("utf8") }), { flag: "wx", mode: 0o600 });
|
|
857
|
+
error.details.report_path = reportFile;
|
|
858
|
+
}
|
|
859
|
+
delete error.details.report;
|
|
860
|
+
fs.writeFileSync(file, diagnosticJson(errorRecord(error)), { flag: "wx", mode: 0o600 });
|
|
861
|
+
error.details.diagnostic_path = file;
|
|
862
|
+
}
|
|
863
|
+
catch (capture) {
|
|
864
|
+
delete error.details.report;
|
|
865
|
+
error.details.capture_error = errorRecord(capture);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
delete error.details.report;
|
|
869
|
+
throw error;
|
|
870
|
+
};
|
|
847
871
|
let result;
|
|
848
872
|
try {
|
|
849
873
|
result = await commandResult(binary, ["--root", projectRoot, "--files", ...files, "--format", "json"], { cwd: projectRoot, startupMs: SETUP_STARTUP_MS });
|
|
850
874
|
}
|
|
851
875
|
catch (cause) {
|
|
852
|
-
const
|
|
853
|
-
|
|
876
|
+
const recorded = errorRecord(cause), unavailable = ["ENOENT", "EACCES", "ENOEXEC"].includes(recorded.code);
|
|
877
|
+
fail(unavailable ? "lint_gate_unavailable" : "lint_gate_tool_failed", "Selected-file mb-lint could not execute", { cause: recorded });
|
|
854
878
|
}
|
|
855
|
-
|
|
879
|
+
const evidence = processEvidence(result);
|
|
880
|
+
if (result.signal || result.exit_code !== 0 && result.exit_code !== 1)
|
|
881
|
+
fail("lint_gate_tool_failed", "Selected-file mb-lint did not complete normally", { process: evidence });
|
|
882
|
+
let parsed;
|
|
856
883
|
try {
|
|
857
|
-
parsed =
|
|
884
|
+
parsed = JSON.parse(result.stdout);
|
|
858
885
|
}
|
|
859
886
|
catch {
|
|
860
|
-
|
|
887
|
+
fail(result.exit_code === 0 ? "lint_gate_invalid_output" : "lint_gate_tool_failed", "Selected-file mb-lint did not return a report", { process: evidence });
|
|
861
888
|
}
|
|
862
|
-
|
|
863
|
-
|
|
889
|
+
const report = parsed;
|
|
890
|
+
const severities = { error: "errors", warning: "warnings", info: "info", suggestion: "suggestions" };
|
|
891
|
+
const matchingRoot = () => { try {
|
|
892
|
+
return typeof report?.root === "string" && fs.realpathSync(path.resolve(projectRoot, report.root)) === fs.realpathSync(projectRoot);
|
|
864
893
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
894
|
+
catch {
|
|
895
|
+
return false;
|
|
896
|
+
} };
|
|
897
|
+
const valid = report && report.tool === "mb-lint" && typeof report.version === "string" && report.version.trim()
|
|
898
|
+
&& matchingRoot()
|
|
899
|
+
&& report.memoryBank === ".memory-bank" && report.summary && Array.isArray(report.findings)
|
|
900
|
+
&& report.findings.every(finding => finding && typeof finding === "object" && typeof finding.severity === "string" && Object.hasOwn(severities, finding.severity)
|
|
901
|
+
&& [finding.ruleId, finding.file, finding.message].every(value => typeof value === "string" && value.trim()))
|
|
902
|
+
&& Object.entries(severities).every(([severity, key]) => Number.isSafeInteger(report.summary[key]) && Number(report.summary[key]) >= 0
|
|
903
|
+
&& report.summary[key] === report.findings.filter(finding => finding.severity === severity).length);
|
|
904
|
+
if (!valid)
|
|
905
|
+
fail(result.exit_code === 0 ? "lint_gate_invalid_output" : "lint_gate_tool_failed", "Selected-file mb-lint returned an invalid report", { process: evidence });
|
|
906
|
+
const errors = Number(report.summary.errors);
|
|
907
|
+
if ((errors > 0 ? 1 : 0) !== result.exit_code)
|
|
908
|
+
fail("lint_gate_invalid_output", "Selected-file mb-lint report contradicts its exit status", { process: evidence });
|
|
909
|
+
if (errors > 0)
|
|
910
|
+
fail("lint_gate_failed", "Selected-file mb-lint reported blocking findings", { process: evidence, report: parsed, summary: report.summary, finding_count: report.findings.length });
|
|
911
|
+
const findingCount = report.findings.length;
|
|
868
912
|
return { ok: true, status: "passed", binary, files, finding_count: findingCount, result: parsed };
|
|
869
913
|
}
|
|
870
914
|
function renderMarkdown(report) {
|
package/dist/services/tooling.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { commandResult, commandText } from '../harness-runtime/lib/process-json.mjs';
|
|
6
6
|
import { publishSetup, SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
|
|
7
|
-
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { AppError, processEvidence } from "../shared/errors.js";
|
|
8
8
|
import { ensureDir } from "../storage/paths.js";
|
|
9
9
|
const manifestRelativePath = path.join("tools", "worktrunk-manifest.json");
|
|
10
10
|
const managedToolRootName = "wt";
|
|
@@ -206,24 +206,26 @@ function verifyCachedWorktrunk(context, manifest, platform) {
|
|
|
206
206
|
binary_sha256: actualBinaryChecksum
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
-
async function extractArchive(archive, destination, format) {
|
|
209
|
+
export async function extractArchive(archive, destination, format) {
|
|
210
210
|
if (format === "tar.xz") {
|
|
211
211
|
const listing = await commandResult("tar", ["-tJf", archive], { startupMs: SETUP_STARTUP_MS });
|
|
212
|
-
if (listing.exit_code !== 0 || listing.
|
|
213
|
-
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive
|
|
214
|
-
|
|
212
|
+
if (listing.exit_code !== 0 || listing.signal)
|
|
213
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive listing failed", 1, { process: processEvidence(listing) });
|
|
214
|
+
if (listing.stdout.split("\n").some(unsafeArchivePath))
|
|
215
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe file list", 1, { phase: "archive_path_validation" });
|
|
215
216
|
const extracted = await commandResult("tar", ["-xJvf", archive, "-C", destination], { startupMs: SETUP_STARTUP_MS });
|
|
216
|
-
if (extracted.exit_code !== 0)
|
|
217
|
-
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, {
|
|
217
|
+
if (extracted.exit_code !== 0 || extracted.signal)
|
|
218
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { process: processEvidence(extracted) });
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
221
|
const listing = await commandResult("unzip", ["-Z1", archive], { startupMs: SETUP_STARTUP_MS });
|
|
221
|
-
if (listing.exit_code !== 0 || listing.
|
|
222
|
-
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive
|
|
223
|
-
|
|
222
|
+
if (listing.exit_code !== 0 || listing.signal)
|
|
223
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive listing failed", 1, { process: processEvidence(listing) });
|
|
224
|
+
if (listing.stdout.split("\n").some(unsafeArchivePath))
|
|
225
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe file list", 1, { phase: "archive_path_validation" });
|
|
224
226
|
const extracted = await commandResult("unzip", [archive, "-d", destination], { startupMs: SETUP_STARTUP_MS });
|
|
225
|
-
if (extracted.exit_code !== 0)
|
|
226
|
-
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, {
|
|
227
|
+
if (extracted.exit_code !== 0 || extracted.signal)
|
|
228
|
+
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { process: processEvidence(extracted) });
|
|
227
229
|
}
|
|
228
230
|
function findBinary(root) {
|
|
229
231
|
const expected = process.platform === "win32" ? "wt.exe" : "wt";
|
|
@@ -52,9 +52,10 @@ export function renderVerificationMatrix(matrix) {
|
|
|
52
52
|
return [`# Verification matrix`, "", `RUN: ${escape(h.run_id)}; stage: ${escape(h.stage)} / ${escape(h.stage_attempt)}; role: ${h.role}; completeness: ${h.completeness}.`, "", "This generated view is not semantic acceptance. A retained passed check proves only its declared contour and proof limits.", "", "## Requirements", "", "| Protocol | Requirement | Statement | PLAN items / checks |", "| --- | --- | --- | --- |", ...matrix.requirements.map(row => `| ${escape(row.protocol_id)} | ${escape(row.id)} | ${escape(row.statement)} | ${escape(row.plan_items.map(item => `${item.id}: ${item.check_refs.join(", ")}`).join("; "))} |`), "", "## Acceptance criteria", "", "| Protocol | Criterion | Gate | Checks / native results | Proof limits |", "| --- | --- | --- | --- | --- |", ...matrix.criteria.map(row => `| ${escape(row.protocol_id)} | ${escape(row.criterion_id)} | ${escape(row.gate ?? "not declared")} | ${escape(row.checks.map(check => `${check.canonical_ref}: ${status(check)}`).join("; "))} | ${escape(JSON.stringify(row.declaration?.proof_limits ?? []))} |`), "", "## Policy checks", "", ...matrix.policy_checks.map(check => `- ${escape(check.canonical_ref)}: ${escape(status(check))}`), "", "## Sources", "", ...matrix.sources.map(source => `- ${escape(source.role)}: ${escape(`${source.root}:${source.path}`)} (${source.sha256})`), ""].join("\n");
|
|
53
53
|
}
|
|
54
54
|
export function verificationMatrixInstruction(metadata, runHome, task) {
|
|
55
|
+
const contract = `<verification_evidence_contract>\nCheck prerequisites must be executable in the check command or supported project policy; purpose prose and list order do not prepare an environment. A readiness gate is final verification, not implicit setup before the first CODE Work. PLAN and PLAN-REVIEW must compare claimed prerequisites with the actual launch path: a description saying migrate-before-check cannot repair a command that checks before migration. Prefer the existing self-contained project verification alias.\nBrowser checks must prove the expected surface exists and the measured set is nonempty before asserting layout geometry. An empty DOM is not evidence of no overlap. Derive navigation from the actual UI and accepted specification. Review whether the test can fail for the intended defect; a green named contour does not prove complete product semantics. Keep native test outcome separate from observation/cleanup: passed assertions with unknown cleanup are neither PASS nor a product assertion failure. Use retained native diagnostics and proof limits; generic timeouts or infrastructure errors do not authorize source repair. Do not patch the candidate product outside the assigned flow Work.\n</verification_evidence_contract>\n`;
|
|
55
56
|
if (!metadata)
|
|
56
|
-
return
|
|
57
|
-
return
|
|
57
|
+
return contract;
|
|
58
|
+
return `${contract}<verification_matrix>\nRuntime generated the technical verification matrix: ${safePath(runHome, metadata.markdown.path, false)}; structured data: ${safePath(runHome, metadata.json.path, false)}. Source fingerprint: ${metadata.source_fingerprint}; completeness: ${metadata.completeness}. Read it before ${task}. This is a derived view of the named accepted requirements, PLAN and receipts, not semantic acceptance. Do not edit these generated files or manually duplicate receipt IDs/hashes/results. Record insufficient proof or violated criteria in the ordinary Stage/Work result with evidence references; modify only your assigned source/test/result documents. Review inputs remain read-only. A passed check proves only its named contour and limits, not all business semantics or production readiness.\n</verification_matrix>\n`;
|
|
58
59
|
}
|
|
59
60
|
/** Attachment and packet authority share a short transaction, then both files
|
|
60
61
|
* must be visible before the prompt claims they are ready. */
|
|
@@ -120,8 +120,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
|
|
|
120
120
|
: `PLAN-REVIEW mode is ${planReviewMode}: include review groups only for genuinely applicable aspects; an empty set is allowed.`;
|
|
121
121
|
fs.writeFileSync(promptPath, `${prompt}\n<review_mode_rule>${reviewModeRule}</review_mode_rule>\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
|
|
122
122
|
const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
123
|
-
|
|
124
|
-
fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix.metadata, home, "designing the source PLAN and its tests"));
|
|
123
|
+
fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix?.metadata ?? null, home, "designing the source PLAN and its tests"));
|
|
125
124
|
fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
|
|
126
125
|
const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
127
126
|
const attachmentInput = { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" };
|