@deksden-com/dd-flow-cli 0.9.0-beta.149 → 0.9.0-beta.150
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 +8 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/daemon-operations.mjs +21 -4
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -13
- package/dist/harness-runtime/lib/dd-agy.mjs +3 -1
- package/dist/services/check-runner-evidence.js +65 -7
- package/dist/services/code-checks.js +20 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.150
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Retain bound test-runtime failure diagnostics, reconcile AGY native task
|
|
8
|
+
completion and cancellation without model calls, keep cleanup retry available
|
|
9
|
+
until owned resources settle, and disable auto-update for managed AGY launches.
|
|
10
|
+
|
|
3
11
|
## 0.9.0-beta.149
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.150",
|
|
4
|
+
"cli_commit": "ad05c70447949f295e588546fe27f6abc4640c3f",
|
|
5
|
+
"built_at": "2026-10-24T17:47:48.856Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.2",
|
|
8
8
|
"commit": "925ef10ecc0b683f7feba6f74913e15560842500",
|
|
@@ -65,7 +65,7 @@ function ownedSnapshot(snapshot, rootPid) {
|
|
|
65
65
|
const mergeProcesses = (...snapshots) => [...new Map(snapshots.flat().map(item => [`${item.pid}:${item.started}`, item])).values()];
|
|
66
66
|
|
|
67
67
|
/** The same incarnation may retry remaining cleanup, never a native Turn. */
|
|
68
|
-
export async function completeDaemonShutdown(root, state, persist, prepare, phases) {
|
|
68
|
+
export async function completeDaemonShutdown(root, state, persist, prepare, phases, reconcileResources) {
|
|
69
69
|
const daemonId = state.daemon_id ?? state.config?.daemonId;
|
|
70
70
|
let shutdown = state.shutdown;
|
|
71
71
|
if (shutdown && shutdown.schema_id !== NATIVE_SHUTDOWN_CONTRACT) throw Object.assign(new Error("shutdown contract mismatch; retain historical owner evidence"), { code: "native_contract_mismatch" });
|
|
@@ -84,10 +84,23 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
|
|
|
84
84
|
// Compute ancestry within each snapshot, not across recycled parent PIDs.
|
|
85
85
|
const resources = mergeProcesses(shutdown.owned_processes, ownedSnapshot(await processSnapshot(), state.pid));
|
|
86
86
|
shutdown = { ...shutdown, phases: { execution: true }, owned_processes: resources,
|
|
87
|
-
inventory_complete: state.pid === process.pid, tree_observed_at: new Date().toISOString(), result: result ?? {} };
|
|
87
|
+
inventory_complete: state.pid === process.pid, tree_observed_at: new Date().toISOString(), preparation_clean: result?.clean !== false, result: result ?? {} };
|
|
88
88
|
await persist({ shutdown, shutdown_state: "stopping" });
|
|
89
89
|
}
|
|
90
90
|
commitDaemonShutdown(root);
|
|
91
|
+
if (reconcileResources && !shutdown.phases.owned_resources) {
|
|
92
|
+
const revision = state.native_execution_revision, generation = dispatchGenerations.get(root) ?? shutdown.generation;
|
|
93
|
+
const resources = await reconcileResources();
|
|
94
|
+
const prior = shutdown.result?.native_resources;
|
|
95
|
+
if ((state.daemon_id ?? state.config?.daemonId) !== daemonId || state.native_execution_revision !== revision
|
|
96
|
+
|| (dispatchGenerations.get(root) ?? shutdown.generation) !== generation || state.active_tree !== false || state.unclaimed_activity
|
|
97
|
+
|| !resources || resources.schema_id !== 'native-resources@1' || resources.owner_daemon_id !== daemonId
|
|
98
|
+
|| resources.generation !== prior?.generation || resources.revision !== prior?.revision || !Array.isArray(resources.entries)
|
|
99
|
+
|| resources.entries.some(item => item.owner_daemon_id !== daemonId || item.generation !== resources.generation))
|
|
100
|
+
throw Object.assign(new Error('native resource reconciliation lost its owner/execution fence'), { code: 'native_resources_unproven' });
|
|
101
|
+
shutdown = { ...shutdown, result: { ...shutdown.result, native_resources: resources } };
|
|
102
|
+
await persist({ shutdown });
|
|
103
|
+
}
|
|
91
104
|
for (const [phase, action] of phases) {
|
|
92
105
|
if (phase === "daemon_resource" && !shutdown.phases.owned_resources) {
|
|
93
106
|
if (!shutdown.inventory_complete) throw Object.assign(new Error("owned resource coverage is unknown"), { code: "native_resources_unproven" });
|
|
@@ -99,12 +112,16 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
|
|
|
99
112
|
const remaining = await processSnapshot();
|
|
100
113
|
if ((shutdown.owned_processes ?? []).some(retained => remaining.some(item => item.pid === retained.pid && item.started === retained.started && !item.zombie))) throw Object.assign(new Error("owned resources remain live"), { code: "native_resources_unproven" });
|
|
101
114
|
const nativeResources = shutdown.result?.native_resources;
|
|
102
|
-
const resourceGap = nativeResources?.entries?.some(resource => resource.state !== "stopped"
|
|
115
|
+
const resourceGap = nativeResources?.entries?.some(resource => resource.observation_conflict === true || resource.state !== "stopped"
|
|
103
116
|
&& !(resource.classification === "required_work" && shutdown.result?.native_settled === true && shutdown.result?.settled === true)
|
|
104
117
|
&& !(shutdown.owned_processes ?? []).some(item => resource.pid === item.pid && resource.started === item.started));
|
|
105
118
|
shutdown = { ...shutdown, phases: { ...shutdown.phases, owned_resources: !resourceGap },
|
|
106
119
|
resource_retirement: { confirmed: !resourceGap, reason: resourceGap ? "native_resource_identity_unproven" : "owned_processes_retired" } };
|
|
107
120
|
await persist({ shutdown });
|
|
121
|
+
// Keep the technical cleanup owner/endpoint available for a later
|
|
122
|
+
// read-only reconciliation. Retiring its lease here would make the
|
|
123
|
+
// actual daemon exit before the retained resources can converge.
|
|
124
|
+
if (resourceGap) throw Object.assign(new Error("native resource retirement remains unproven"), { code: "native_resources_unproven" });
|
|
108
125
|
}
|
|
109
126
|
if (shutdown.phases[phase]) continue;
|
|
110
127
|
await action();
|
|
@@ -120,7 +137,7 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
|
|
|
120
137
|
shutdown = { ...shutdown, generation: dispatchGenerations.get(root) ?? shutdown.generation };
|
|
121
138
|
const { result: ignored, ...proof } = shutdown; void ignored;
|
|
122
139
|
let result = { ...shutdown.result, daemon_id: daemonId, stopped: true, physical_stopped: physicalStopped,
|
|
123
|
-
settled: nativeSettled, native_settled: nativeSettled, clean: shutdown.result?.clean !== false && nativeSettled && physicalStopped,
|
|
140
|
+
settled: nativeSettled, native_settled: nativeSettled, clean: (shutdown.preparation_clean ?? shutdown.result?.clean !== false) && nativeSettled && physicalStopped,
|
|
124
141
|
resource_retirement: shutdown.resource_retirement,
|
|
125
142
|
shutdown_contract: NATIVE_SHUTDOWN_CONTRACT, shutdown_proof: proof, _shutdown: true };
|
|
126
143
|
shutdown = { ...shutdown, result };
|
|
@@ -22,7 +22,7 @@ import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
|
22
22
|
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { observeModel } from "./model-observations.mjs";
|
|
25
|
-
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
25
|
+
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
|
|
26
26
|
import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
|
|
27
27
|
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
28
28
|
import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
@@ -34,16 +34,43 @@ class DaemonError extends AgyError {}
|
|
|
34
34
|
// classify services. This transcript format has no tool-call IDs: parallel
|
|
35
35
|
// calls therefore retain unknown resources, never FIFO-derived authority.
|
|
36
36
|
export function agyTranscriptFacts(text, source, sessionId) {
|
|
37
|
-
const children = [], resources =
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const children = [], resources = new Map(), calls = [];
|
|
38
|
+
const result = () => ({ children, resources: [...resources.values()] });
|
|
39
|
+
if (!source?.locator || !source?.sha256 || !sessionId) return result();
|
|
40
|
+
const lines = text.split('\n');
|
|
41
|
+
for (let index = 0; index < lines.length; index++) {
|
|
42
|
+
const line = lines[index];
|
|
43
|
+
if (!line && index === lines.length - 1) continue;
|
|
40
44
|
let event;
|
|
41
|
-
try { event = JSON.parse(line); } catch {
|
|
45
|
+
try { event = JSON.parse(line); } catch {
|
|
46
|
+
// A writer may have appended only part of the last record. Retain the
|
|
47
|
+
// valid prefix; a malformed newline-terminated record is a real gap.
|
|
48
|
+
return index === lines.length - 1 ? result() : { ...result(), gap: 'agy_transcript_invalid_record' };
|
|
49
|
+
}
|
|
50
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) return { ...result(), gap: 'agy_transcript_invalid_record' };
|
|
51
|
+
const content = typeof event.content === 'string' ? event.content : '';
|
|
52
|
+
const native_source = { ...source, step_index: event.step_index };
|
|
53
|
+
if (event.source === 'SYSTEM' && event.type === 'SYSTEM_MESSAGE' && event.status === 'DONE') {
|
|
54
|
+
const terminal = content.match(/(?:^|\n)\[Message\] timestamp=[^\n]+ sender=([\w-]+\/task-\d+) priority=MESSAGE_PRIORITY_(?:HIGH|LOW) content=Task id "([\w-]+\/task-\d+)" (finished with result:\nCommand exited with code (-?\d+)\.|was canceled with result:\nTool execution was canceled)(?:\n|$)/);
|
|
55
|
+
if (terminal && terminal[1] === terminal[2]) {
|
|
56
|
+
const prior = resources.get(terminal[1]);
|
|
57
|
+
if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped', native_outcome: terminal[4] === undefined ? 'cancelled' : Number(terminal[4]) === 0 ? 'completed' : 'failed', terminal_source: native_source }));
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
42
61
|
if (event.source !== 'MODEL') continue;
|
|
43
62
|
if (event.type === 'PLANNER_RESPONSE') { calls.length = 0; if (Array.isArray(event.tool_calls) && event.tool_calls.length === 1) calls.push(event.tool_calls[0]); continue; }
|
|
44
63
|
if (event.type !== 'GENERIC') continue;
|
|
45
|
-
const call = calls.shift()
|
|
46
|
-
|
|
64
|
+
const call = calls.shift();
|
|
65
|
+
if (call?.name === 'manage_task' && event.status === 'DONE') {
|
|
66
|
+
const decode = value => { try { return JSON.parse(value); } catch { return value; } };
|
|
67
|
+
const id = decode(call.args?.TaskId), action = decode(call.args?.Action);
|
|
68
|
+
const prior = resources.get(id);
|
|
69
|
+
if (prior && action === 'kill' && content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nTask "([\w-]+\/task-\d+)" cancelled\.(?:\n|$)/)?.[1] === id)
|
|
70
|
+
resources.set(id, mergeAgyResource(prior, { ...prior, state: 'stopped', native_outcome: 'cancelled', terminal_source: native_source }));
|
|
71
|
+
const status = action === 'status' && content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nTask: ([\w-]+\/task-\d+)\nStatus: (COMPLETED|FAILED|CANCELLED)\n/);
|
|
72
|
+
if (prior && status?.[1] === id) resources.set(id, mergeAgyResource(prior, { ...prior, state: 'stopped', native_outcome: status[2].toLowerCase(), terminal_source: native_source }));
|
|
73
|
+
}
|
|
47
74
|
if (call?.name === 'invoke_subagent' && event.status === 'DONE') {
|
|
48
75
|
const match = content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nCreated the following subagent:\n(\{[\s\S]*?\})\nThe subagent will send you a message/);
|
|
49
76
|
if (!match) continue;
|
|
@@ -57,10 +84,28 @@ export function agyTranscriptFacts(text, source, sessionId) {
|
|
|
57
84
|
if (!task || !task[1].startsWith(`${sessionId}/`)) continue;
|
|
58
85
|
let daemon = call?.name === 'run_command' ? call.args?.IsDaemon : null;
|
|
59
86
|
if (typeof daemon === 'string') { try { daemon = JSON.parse(daemon); } catch { daemon = null; } }
|
|
60
|
-
|
|
61
|
-
...(typeof daemon === 'boolean' ? { native_classification: { tool: 'run_command', IsDaemon: daemon } } : {}) }
|
|
87
|
+
const resource = { resource_id: task[1], provider_session_id: sessionId, classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
|
|
88
|
+
...(typeof daemon === 'boolean' ? { native_classification: { tool: 'run_command', IsDaemon: daemon } } : {}) };
|
|
89
|
+
resources.set(resource.resource_id, mergeAgyResource(resources.get(resource.resource_id), resource));
|
|
62
90
|
}
|
|
63
|
-
return
|
|
91
|
+
return result();
|
|
92
|
+
}
|
|
93
|
+
export function mergeAgyResource(prior, next) {
|
|
94
|
+
if (!prior) return next;
|
|
95
|
+
if (prior.provider_session_id !== next.provider_session_id || prior.native_source?.locator !== next.native_source?.locator)
|
|
96
|
+
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
97
|
+
if (prior.native_source?.step_index !== next.native_source?.step_index)
|
|
98
|
+
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
99
|
+
if (prior.observation_conflict || next.observation_conflict || prior.native_outcome && next.native_outcome && prior.native_outcome !== next.native_outcome)
|
|
100
|
+
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
101
|
+
if (next.native_outcome && (!Number.isSafeInteger(next.native_source?.step_index) || !Number.isSafeInteger(next.terminal_source?.step_index) || next.terminal_source.step_index <= next.native_source.step_index))
|
|
102
|
+
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
103
|
+
if (prior.state === 'stopped' && next.state !== 'stopped') {
|
|
104
|
+
if (next.native_source?.step_index > (prior.terminal_source?.step_index ?? prior.native_source?.step_index))
|
|
105
|
+
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
106
|
+
return prior;
|
|
107
|
+
}
|
|
108
|
+
return { ...prior, ...next };
|
|
64
109
|
}
|
|
65
110
|
export const agyTranscriptResources = (text, source, sessionId) => agyTranscriptFacts(text, source, sessionId).resources;
|
|
66
111
|
export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null) {
|
|
@@ -99,7 +144,7 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
|
|
|
99
144
|
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
100
145
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
101
146
|
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
102
|
-
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) env[key] = undefined; return { ...env, ...managedFlowEnv(config) }; }
|
|
147
|
+
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) env[key] = undefined; return managedAgyEnv({ ...env, ...managedFlowEnv(config) }); }
|
|
103
148
|
const errorPayload = errorRecord;
|
|
104
149
|
async function forwardUsage(config, sessionId, result, tools, descendants) {
|
|
105
150
|
if (!config.ddFlowBin || !config.projectRoot) return;
|
|
@@ -439,7 +484,8 @@ export class Runtime {
|
|
|
439
484
|
const events = parent === root && root
|
|
440
485
|
? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors], (text, source) => {
|
|
441
486
|
const facts = agyTranscriptFacts(text, source, child.provider_session_id);
|
|
442
|
-
for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, resource);
|
|
487
|
+
for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, mergeAgyResource(this.nativeResources.get(resource.resource_id), resource));
|
|
488
|
+
if (facts.gap) { this.state.native_topology_error = facts.gap; this.invalidateSettlement(root); }
|
|
443
489
|
for (const discovered of facts.children) {
|
|
444
490
|
const expected = path.join(this.paths.runtime, 'brain', discovered.provider_session_id, '.system_generated', 'logs', 'transcript.jsonl');
|
|
445
491
|
const sourceBrain = path.resolve(path.dirname(fileURLToPath(source.locator)), '../../..');
|
|
@@ -1040,7 +1086,10 @@ async function dispatch(runtime, request) {
|
|
|
1040
1086
|
}
|
|
1041
1087
|
catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
|
|
1042
1088
|
}
|
|
1043
|
-
if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]])
|
|
1089
|
+
if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]], async () => {
|
|
1090
|
+
await runtime.captureChildTools();
|
|
1091
|
+
return runtime.receipt().native_resources;
|
|
1092
|
+
});
|
|
1044
1093
|
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
1045
1094
|
}
|
|
1046
1095
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -8,6 +8,8 @@ import { classifyProviderError, safeProviderError, streamRequestKey } from "./pr
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
10
10
|
export const AGY_VERSION_TIMEOUT_MS = 30_000;
|
|
11
|
+
// Managed binaries are immutable qualification inputs, including doctor probes.
|
|
12
|
+
export const managedAgyEnv = (env = {}) => ({ ...process.env, ...env, AGY_CLI_DISABLE_AUTO_UPDATE: "true" });
|
|
11
13
|
|
|
12
14
|
export class AgyError extends Error {
|
|
13
15
|
constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
|
|
@@ -44,7 +46,7 @@ export function executable(bin, args = []) {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export async function runAgy(bin, args, options = {}) {
|
|
47
|
-
const stdout = await commandText(bin, args, { cwd: options.cwd, env: options.env, input: options.stdin ?? "", timeoutMs: options.timeoutMs, startupMs: options.startupMs ?? options.timeoutMs ?? SETUP_STARTUP_MS, phase: "native-doctor" });
|
|
49
|
+
const stdout = await commandText(bin, args, { cwd: options.cwd, env: managedAgyEnv(options.env), input: options.stdin ?? "", timeoutMs: options.timeoutMs, startupMs: options.startupMs ?? options.timeoutMs ?? SETUP_STARTUP_MS, phase: "native-doctor" });
|
|
48
50
|
return { stdout, stderr: "", code: 0 };
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -40,7 +40,7 @@ export function runnerFailureValid(value) {
|
|
|
40
40
|
if (value.repair_evidence !== undefined) {
|
|
41
41
|
const proof = value.repair_evidence;
|
|
42
42
|
if (object(proof) && proof.rule === "candidate_test_runtime_error")
|
|
43
|
-
return ["TypeError", "ReferenceError"].includes(String(proof.exception_type))
|
|
43
|
+
return ["TypeError", "ReferenceError", "Error"].includes(String(proof.exception_type)) && (proof.exception_type !== "Error" || proof.phase === "test")
|
|
44
44
|
&& absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
|
|
45
45
|
&& [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
|
|
46
46
|
if (object(proof) && proof.rule === "candidate_migration_sql")
|
|
@@ -332,8 +332,33 @@ export function runnerContractsValid(value) {
|
|
|
332
332
|
return alias.startsWith("@check/") && ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(String(contract.schema_id)) && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
|
|
333
333
|
});
|
|
334
334
|
}
|
|
335
|
+
function diagnostic(context, value) {
|
|
336
|
+
// Diagnostics are locators, never another copy of untrusted report content.
|
|
337
|
+
if (context.diagnostics.length >= 64 || context.bytes + Buffer.byteLength(JSON.stringify(value)) > 32 * 1024 - 64) {
|
|
338
|
+
context.truncated = true;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
context.diagnostics.push(value);
|
|
342
|
+
context.bytes += Buffer.byteLength(JSON.stringify(value));
|
|
343
|
+
}
|
|
344
|
+
export function readRunnerEvidenceDetailed(root, contract, binding) {
|
|
345
|
+
const context = { diagnostics: [], bytes: 0, truncated: false };
|
|
346
|
+
const evidence = readRunnerEvidenceInternal(root, contract, binding, 0, { reports: 0 }, context, "");
|
|
347
|
+
return { evidence, diagnostics: context.diagnostics, ...(context.truncated ? { diagnostics_truncated: true } : {}) };
|
|
348
|
+
}
|
|
335
349
|
export function readRunnerEvidence(root, contract, binding, depth = 0, budget = { reports: 0 }) {
|
|
350
|
+
return readRunnerEvidenceInternal(root, contract, binding, depth, budget, { diagnostics: [], bytes: 0, truncated: false }, "");
|
|
351
|
+
}
|
|
352
|
+
function readRunnerEvidenceInternal(root, contract, binding, depth, budget, context, prefix) {
|
|
353
|
+
const report = relativeProofPath(contract.path) ? path.posix.join(prefix, contract.path) : "<invalid-report-path>";
|
|
354
|
+
const count = context.diagnostics.length;
|
|
355
|
+
let accepted = false;
|
|
356
|
+
let failureReason = "invalid_shape";
|
|
357
|
+
const invalid = () => { if (context.diagnostics.length === count)
|
|
358
|
+
diagnostic(context, { reason: failureReason, report }); return null; };
|
|
336
359
|
try {
|
|
360
|
+
if (!relativeProofPath(contract.path))
|
|
361
|
+
return invalid();
|
|
337
362
|
if (depth > 4 || ++budget.reports > 64)
|
|
338
363
|
return null;
|
|
339
364
|
let current = root;
|
|
@@ -344,15 +369,27 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
344
369
|
if (fs.lstatSync(current).isSymbolicLink())
|
|
345
370
|
return null;
|
|
346
371
|
}
|
|
347
|
-
const
|
|
372
|
+
const bytes = readBoundedProofFile(current, 1024 * 1024);
|
|
373
|
+
failureReason = "invalid_json";
|
|
374
|
+
const evidence = JSON.parse(bytes.toString("utf8"));
|
|
375
|
+
failureReason = "invalid_shape";
|
|
376
|
+
if (!object(evidence))
|
|
377
|
+
return invalid();
|
|
348
378
|
if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
|
|
349
379
|
return null;
|
|
350
380
|
const partial = evidence.failure_projection;
|
|
351
381
|
if (partial !== undefined && (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !object(partial) || partial.state !== "partial" || !["count_bound", "byte_bound", "interrupt"].includes(partial.reason)
|
|
352
382
|
|| ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
|
|
353
383
|
return null;
|
|
354
|
-
if (evidence.schema_id !== contract.schema_id
|
|
355
|
-
|
|
384
|
+
if (evidence.schema_id !== contract.schema_id) {
|
|
385
|
+
diagnostic(context, { reason: "invalid_shape", report, expected: contract.schema_id, observed: ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(evidence.schema_id) ? evidence.schema_id : "unsupported" });
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (!evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)) {
|
|
389
|
+
diagnostic(context, { reason: "binding_mismatch", report });
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
356
393
|
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
357
394
|
|| !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
|
|
358
395
|
|| !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
@@ -384,8 +421,10 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
384
421
|
if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
|
|
385
422
|
return null;
|
|
386
423
|
invocations.add(step.binding.invocation_id);
|
|
424
|
+
failureReason = "artifact_mismatch";
|
|
387
425
|
readProofArtifact(root, step.report, 1024 * 1024);
|
|
388
|
-
|
|
426
|
+
failureReason = "invalid_shape";
|
|
427
|
+
const child = readRunnerEvidenceInternal(path.join(root, path.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget, context, path.posix.join(prefix, path.posix.dirname(step.report.relative_path)));
|
|
389
428
|
if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
|
|
390
429
|
return null;
|
|
391
430
|
if (!child.completed || child.observation_unknown)
|
|
@@ -397,6 +436,7 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
397
436
|
return null;
|
|
398
437
|
}
|
|
399
438
|
for (const failure of evidence.failures) {
|
|
439
|
+
failureReason = "artifact_mismatch";
|
|
400
440
|
if (failure.child_evidence) {
|
|
401
441
|
const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
|
|
402
442
|
if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
|
|
@@ -423,6 +463,7 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
423
463
|
if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
|
|
424
464
|
return null;
|
|
425
465
|
if (proof.rule === "candidate_test_runtime_error") {
|
|
466
|
+
failureReason = "attribution_unproven";
|
|
426
467
|
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
427
468
|
|| !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
|
|
428
469
|
return null;
|
|
@@ -447,6 +488,7 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
447
488
|
validatedBrowserProofs.add(proof);
|
|
448
489
|
}
|
|
449
490
|
}
|
|
491
|
+
failureReason = "invalid_shape";
|
|
450
492
|
const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
|
|
451
493
|
if (partial) {
|
|
452
494
|
if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
|
|
@@ -466,10 +508,26 @@ export function readRunnerEvidence(root, contract, binding, depth = 0, budget =
|
|
|
466
508
|
evidence.observation_unknown = true;
|
|
467
509
|
}
|
|
468
510
|
validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
|
|
511
|
+
if (unsupportedObservationContracts.has(evidence))
|
|
512
|
+
diagnostic(context, { reason: "unsupported_observation_contract", report, expected: "dd-tasks/browser-observation@2", observed: "legacy" });
|
|
513
|
+
else if (!evidence.completed || evidence.observation_unknown)
|
|
514
|
+
diagnostic(context, { reason: "observation_incomplete", report });
|
|
515
|
+
for (const failure of evidence.failures)
|
|
516
|
+
if (failure.origin === "unknown" || failure.category === "unclassified" && !failure.repair_evidence) {
|
|
517
|
+
diagnostic(context, { reason: "attribution_unproven", report });
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
accepted = true;
|
|
469
521
|
return evidence;
|
|
470
522
|
}
|
|
471
|
-
catch {
|
|
472
|
-
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (error.code === "ENOENT")
|
|
525
|
+
failureReason = "missing";
|
|
526
|
+
return invalid();
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
if (!accepted && context.diagnostics.length === count)
|
|
530
|
+
diagnostic(context, { reason: failureReason, report });
|
|
473
531
|
}
|
|
474
532
|
}
|
|
475
533
|
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, 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";
|
|
@@ -656,7 +656,8 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
656
656
|
if (mutationPaths.length)
|
|
657
657
|
fs.appendFileSync(stderrPath, `\ncheck inputs changed while running: ${mutationPaths.join(", ")}\n`);
|
|
658
658
|
}
|
|
659
|
-
const
|
|
659
|
+
const runnerRead = group.runnerEvidence ? readRunnerEvidenceDetailed(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
|
|
660
|
+
const runnerEvidence = runnerRead?.evidence;
|
|
660
661
|
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) });
|
|
661
662
|
if (result.failure)
|
|
662
663
|
physicalFailures.push(result.failure);
|
|
@@ -680,6 +681,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
680
681
|
if (group.runnerEvidence) {
|
|
681
682
|
receipt.runner_evidence_required = group.runnerEvidence;
|
|
682
683
|
receipt.runner_evidence = runnerEvidence ?? null;
|
|
684
|
+
receipt.runner_evidence_diagnostics = runnerRead.diagnostics;
|
|
685
|
+
if (runnerRead?.diagnostics_truncated)
|
|
686
|
+
receipt.runner_evidence_diagnostics_truncated = true;
|
|
683
687
|
}
|
|
684
688
|
context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [receipt.status, receipt.exit_code, receipt.after_fingerprint, JSON.stringify(receipt.mutation_paths), JSON.stringify(receipt.artifacts), receipt.finished_at, id, input.projectId]);
|
|
685
689
|
writeReceipt(receiptPath, receipt);
|
|
@@ -752,7 +756,8 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
752
756
|
const mutationPaths = workspaceUnchanged ? [] : scoped && completionSnapshot ? ["<declared check inputs differed at completion; exact delta unavailable>"]
|
|
753
757
|
: scoped && beforeFiles ? changedPaths(beforeFiles, selectedInputFiles(input.workspaceRoot, after.files, receipt.inputs ?? [], input.runHome ? [input.runHome] : []))
|
|
754
758
|
: [scoped ? "<declared check inputs changed; exact manifest unavailable>" : "<workspace changed while detached check ran>"];
|
|
755
|
-
const
|
|
759
|
+
const runnerRead = receipt?.runner_evidence_required ? readRunnerEvidenceDetailed(path.join(path.dirname(pendingReceipt.receipt_path), "artifacts"), receipt.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch }) : undefined;
|
|
760
|
+
const runnerEvidence = runnerRead?.evidence;
|
|
756
761
|
const exitCode = completion?.exit_code ?? null;
|
|
757
762
|
const finishedAt = completion?.finished_at ?? context.now();
|
|
758
763
|
const physicalFailures = executionFailures({ fingerprintScope: scoped ? "check_inputs" : undefined, exitCode, missing: artifacts.missing.filter(file => file !== "engine-outcome.json"), mutations: mutationPaths, invalidRunner: Boolean(receipt?.runner_evidence_required && !runnerEvidence) });
|
|
@@ -786,7 +791,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
786
791
|
if (process)
|
|
787
792
|
finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
|
|
788
793
|
if (receipt) {
|
|
789
|
-
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
|
|
794
|
+
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null, runner_evidence_diagnostics: runnerRead.diagnostics, ...(runnerRead?.diagnostics_truncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
|
|
790
795
|
repaired.candidate_root = input.workspaceRoot;
|
|
791
796
|
repaired.candidate_files = candidateFiles;
|
|
792
797
|
repaired.candidate_absences = candidateAbsences;
|
|
@@ -976,11 +981,19 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
976
981
|
return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
|
|
977
982
|
};
|
|
978
983
|
let evidence = null;
|
|
984
|
+
let diagnostics = [];
|
|
985
|
+
let diagnosticsTruncated = false;
|
|
979
986
|
if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
|
|
980
987
|
const root = path.join(currentRoot, "artifacts");
|
|
981
988
|
const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
|
|
982
|
-
if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256)))
|
|
983
|
-
|
|
989
|
+
if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
990
|
+
const read = readRunnerEvidenceDetailed(root, original.runner_evidence_required, { invocation_id: binding.id, check_id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch });
|
|
991
|
+
evidence = read.evidence;
|
|
992
|
+
diagnostics = read.diagnostics;
|
|
993
|
+
diagnosticsTruncated = Boolean(read.diagnostics_truncated);
|
|
994
|
+
}
|
|
995
|
+
else
|
|
996
|
+
diagnostics = [{ reason: retained.complete ? "artifact_mismatch" : "missing", report: original.runner_evidence_required.path }];
|
|
984
997
|
}
|
|
985
998
|
let outcome = {};
|
|
986
999
|
const outcomePath = path.join(currentRoot, "artifacts", "engine-outcome.json");
|
|
@@ -993,7 +1006,7 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
993
1006
|
}
|
|
994
1007
|
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
995
1008
|
}
|
|
996
|
-
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
|
|
1009
|
+
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 } : {}) };
|
|
997
1010
|
}
|
|
998
1011
|
function executionFailures(input) {
|
|
999
1012
|
const failures = [];
|