@deksden-com/dd-flow-cli 0.9.0-beta.127 → 0.9.0-beta.133
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 +58 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +2 -2
- package/dist/cli/run-cli.js +6 -6
- package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
- package/dist/cli/runtime-maintenance-ingress.js +5 -4
- package/dist/cli/runtime-process.js +27 -2
- package/dist/cli.js +7 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
- package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
- package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +41 -9
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +113 -31
- package/dist/services/compatibility-preflight.js +9 -2
- package/dist/services/dashboard.js +33 -26
- package/dist/services/engines.js +88 -57
- package/dist/services/lifecycle-invocations.js +4 -2
- package/dist/services/managed-lease-monitor.js +9 -4
- package/dist/services/prompts.js +3 -1
- package/dist/services/protocols.js +3 -1
- package/dist/services/repair-intents.js +4 -2
- package/dist/services/run-control-worker.js +2 -1
- package/dist/services/run-controller.js +20 -17
- package/dist/services/run-fork.js +2 -2
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-lifecycle.js +13 -7
- package/dist/services/tooling.js +32 -23
- package/dist/services/vnext-code-review.js +2 -0
- package/dist/services/vnext-code.js +5 -1
- package/dist/services/vnext-merge.js +4 -1
- package/dist/services/vnext-protocolize.js +17 -16
- package/dist/services/work-registry.js +3 -1
- package/dist/services/worktrees.js +16 -14
- package/package.json +1 -1
|
@@ -75,9 +75,15 @@ export async function assertDaemonOwnership(stateDir, dispatch = {}, assertCurre
|
|
|
75
75
|
const record = state.resource_process;
|
|
76
76
|
if (owner.state_dir !== stateDir || !record?.id || !record.lease_token) throw Object.assign(new Error("owned daemon has no confirmed resource binding"), { code: "process_ownership_unknown" });
|
|
77
77
|
scheduleDaemonProcessHeartbeat(state.config, record);
|
|
78
|
-
const admissionRenewal = createRenewalState();
|
|
78
|
+
const admissionRenewal = createRenewalState({ leaseExpiresAt: record.lease_expires_at });
|
|
79
79
|
for (;;) {
|
|
80
80
|
await gate();
|
|
81
|
+
// daemon.json is a snapshot, not the current authority. gate() verified
|
|
82
|
+
// the exact retained owner and its latest committed renewal above.
|
|
83
|
+
const retained = leases.get(leaseKey(state.config, record));
|
|
84
|
+
if (!retained || record.registration_sha256 && record.registration_sha256 !== retained.record.registration_sha256) throw Object.assign(new Error("Admission registration differs from confirmed owner"), { code: "process_maintenance_receipt_invalid" });
|
|
85
|
+
retainReceipt(record, retained.record);
|
|
86
|
+
if (!renewalConfirmed(admissionRenewal, performance.now(), record.lease_expires_at)) requireTimelyMaintenance(admissionRenewal, "admission", retained.record, record);
|
|
81
87
|
admissionRenewal.uncertainSince ??= performance.now();
|
|
82
88
|
let result;
|
|
83
89
|
try { result = await runtimeProcess(state.config, "admission", { id: record.id, "lease-token": record.lease_token, owner: owner.owner_id, "state-dir": stateDir, "daemon-id": state.daemon_id ?? state.config?.daemonId, ...dispatch }, { timeoutMs: renewalRemaining(admissionRenewal) }); }
|
|
@@ -213,8 +219,16 @@ export async function runtimeProcess(config, action, options = {}, { timeoutMs =
|
|
|
213
219
|
return await executeRuntimeProcess(config, action, options, { timeoutMs: remaining, signal });
|
|
214
220
|
} catch (error) {
|
|
215
221
|
const lease = leases.get(key);
|
|
222
|
+
// A settled transport may have lost its ACK after COMMIT. Observe the exact
|
|
223
|
+
// owner, never replay the writer. Productive admission still checks RUN.
|
|
224
|
+
const registration = lease?.record.registration_sha256 ?? options["registration-sha256"];
|
|
225
|
+
if (action === "heartbeat" && error.details?.effect === "unknown" && error.details?.cleanup_unconfirmed !== true && /^[a-f0-9]{64}$/.test(registration ?? "")) {
|
|
226
|
+
const readback = await executeRuntimeProcess(config, "status", { id: options.id, "lease-token": options["lease-token"], "registration-sha256": registration }, { timeoutMs: RENEWAL_POLICY.budgetMs, signal });
|
|
227
|
+
if (readback?.ok === true && readback.process_id === options.id && readback.registration_sha256 === registration) return readback;
|
|
228
|
+
throw Object.assign(new Error("Managed heartbeat readback did not establish the exact effect"), { code: "process_maintenance_receipt_invalid" });
|
|
229
|
+
}
|
|
216
230
|
const text = value => typeof value === "string" ? value.slice(0, 128) : undefined;
|
|
217
|
-
const request = { phase: started ? "request" : "queue", action, process_id: text(options.id), operation_id: text(options["operation-id"] ?? config.runtime_owner?.operation_id), role: text(lease?.record.kind ?? config.runtime_owner?.role), attempt_kind: prior ? "join" : "fresh", attempt: (lease?.renewal.attempt ?? 0) + 1, remaining_ms: Math.max(0, Math.round(deadline - performance.now())), native_dispatch_started: false, maintenance_effect: started ? "unknown" : "no_effect" };
|
|
231
|
+
const request = { phase: started ? "request" : "queue", action, process_id: text(options.id), operation_id: text(options["operation-id"] ?? options.operation ?? config.runtime_owner?.operation_id), role: text(lease?.record.kind ?? options.role ?? config.runtime_owner?.role), attempt_kind: prior ? "join" : "fresh", attempt: (lease?.renewal.attempt ?? 0) + 1, remaining_ms: Math.max(0, Math.round(deadline - performance.now())), native_dispatch_started: false, maintenance_effect: started ? "unknown" : "no_effect" };
|
|
218
232
|
// Parent queue/import failures precede any child trace. Keep only bounded,
|
|
219
233
|
// credential-free context and the existing child phase allowlist.
|
|
220
234
|
const childPhases = Array.isArray(error.details?.hook_diagnostics) ? error.details.hook_diagnostics : [];
|
|
@@ -231,6 +245,16 @@ export async function runtimeProcess(config, action, options = {}, { timeoutMs =
|
|
|
231
245
|
}
|
|
232
246
|
}
|
|
233
247
|
|
|
248
|
+
/** Only after the original maintenance transport physically settled. This
|
|
249
|
+
* endpoint reads exact current authority and cannot renew or reserve a turn. */
|
|
250
|
+
export async function readbackDaemonProcess(config, record, { signal } = {}) {
|
|
251
|
+
if (!record?.id || !record.lease_token || !/^[a-f0-9]{64}$/.test(record.registration_sha256 ?? "")) throw Object.assign(new Error("Readback has no exact registration"), { code: "process_maintenance_receipt_invalid" });
|
|
252
|
+
const result = await runtimeProcess(config, "status", { id: record.id, "lease-token": record.lease_token, "registration-sha256": record.registration_sha256 }, { timeoutMs: RENEWAL_POLICY.budgetMs, signal });
|
|
253
|
+
if (result?.ok !== true || result.process_id !== record.id || result.registration_sha256 !== record.registration_sha256) throw Object.assign(new Error("Readback returned foreign authority"), { code: "process_maintenance_receipt_invalid" });
|
|
254
|
+
retainReceipt(record, result);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
234
258
|
async function executeRuntimeProcess(config, action, options = {}, { timeoutMs, signal }) {
|
|
235
259
|
const owner = config.runtime_owner;
|
|
236
260
|
const resourceHome = owner?.resource_home ?? config.resourceHome;
|
|
@@ -251,7 +275,7 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
|
|
|
251
275
|
try { output = await runBoundedCommand({ bin: ddFlowBin, args: [...args, "--json"], cwd: config.cwd,
|
|
252
276
|
env: { ...(config.env ?? {}), DD_FLOW_HOME: ddFlowHome, DD_FLOW_RESOURCE_HOME: resourceHome }, timeoutMs,
|
|
253
277
|
timeoutCode: action === "heartbeat" ? "process_heartbeat_timeout" : "process_maintenance_timeout", inheritDeadline: false, cleanupWithinBudget: true,
|
|
254
|
-
maintenanceProgress: { action, processId: options.id ?? null, inactivityMs: RENEWAL_POLICY.attemptMs }, signal }); }
|
|
278
|
+
maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: RENEWAL_POLICY.attemptMs }, signal }); }
|
|
255
279
|
catch (error) {
|
|
256
280
|
if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), error);
|
|
257
281
|
throw error;
|
|
@@ -279,7 +303,8 @@ export async function registerDaemonProcess(config, input) {
|
|
|
279
303
|
for (;;) {
|
|
280
304
|
try { result = await runtimeProcess(config, "register", options, { timeoutMs: renewalRemaining(renewal) }); break; }
|
|
281
305
|
catch (error) {
|
|
282
|
-
|
|
306
|
+
const unknownSettled = error.details?.effect === "unknown" && error.details?.cleanup_unconfirmed !== true;
|
|
307
|
+
if ((!maintenanceRetryable(error) && !unknownSettled) || !renewalFailure(renewal)) throw error;
|
|
283
308
|
// The child has exited before a timeout is retryable. Reconcile the exact
|
|
284
309
|
// startup identity, not an arbitrary PID or another operation's owner.
|
|
285
310
|
let status;
|
|
@@ -327,14 +352,19 @@ export async function startManagedBridge({ config, state, bridge, kind, publish
|
|
|
327
352
|
const provider = config.runtime_owner ? await registerDaemonProcess(config, { kind, owner: config.runtime_owner.owner_id, operation: kind }) : null;
|
|
328
353
|
try {
|
|
329
354
|
state.provider_process = provider;
|
|
355
|
+
state.startup_phase = "owner_registered";
|
|
330
356
|
await publish(state);
|
|
331
357
|
bridge.options.onSpawn = async child => {
|
|
332
358
|
await confirmDaemonProcess(config, provider, child);
|
|
333
359
|
state.provider_pid = child.pid;
|
|
360
|
+
state.startup_phase = "provider_spawned";
|
|
334
361
|
await publish(state);
|
|
335
362
|
scheduleDaemonProcessHeartbeat(config, provider);
|
|
336
363
|
};
|
|
337
|
-
|
|
364
|
+
const initialized = await bridge.start();
|
|
365
|
+
state.startup_phase = "native_initialized";
|
|
366
|
+
await publish(state);
|
|
367
|
+
return initialized;
|
|
338
368
|
}
|
|
339
369
|
catch (error) { await cleanupFailedStart(config, provider, bridge.child, error); throw error; }
|
|
340
370
|
}
|
|
@@ -348,10 +378,10 @@ export function scheduleDaemonProcessHeartbeat(config, record) {
|
|
|
348
378
|
if (!record?.id || !record.lease_token) return;
|
|
349
379
|
const key = leaseKey(config, record);
|
|
350
380
|
if (leases.has(key)) return;
|
|
351
|
-
const lease = { pending: null, error: null, timer: null, lost: false, closing: false, expiresAt: Date.parse(record.lease_expires_at ?? ""), renewal: createRenewalState(), stateDir: path.resolve(config.runtime_owner?.state_dir ?? config.stateDir ?? config.cwd), config, record };
|
|
381
|
+
const lease = { pending: null, error: null, timer: null, lost: false, closing: false, expiresAt: Date.parse(record.lease_expires_at ?? ""), renewal: createRenewalState({ leaseExpiresAt: record.lease_expires_at }), stateDir: path.resolve(config.runtime_owner?.state_dir ?? config.stateDir ?? config.cwd), config, record };
|
|
352
382
|
leases.set(key, lease);
|
|
353
|
-
// Retry inside the same
|
|
354
|
-
//
|
|
383
|
+
// Retry no-effect contention inside the same authority episode while native
|
|
384
|
+
// work is active; scheduling ticks are not ownership evidence.
|
|
355
385
|
lease.timer = setInterval(() => reconfirmLease(lease, () => {}).catch(() => {}), 60_000);
|
|
356
386
|
lease.timer.unref();
|
|
357
387
|
}
|
|
@@ -375,8 +405,9 @@ export async function heartbeatDaemonProcess(config, record) {
|
|
|
375
405
|
}
|
|
376
406
|
if (result?.ok !== true || result.process_id !== record.id) throw Object.assign(new Error("Managed renewal response belongs to another process"), { code: "process_maintenance_receipt_invalid", details: { process_id: record.id } });
|
|
377
407
|
if (!lease.closing) {
|
|
378
|
-
|
|
379
|
-
|
|
408
|
+
retainReceipt(record, result);
|
|
409
|
+
if (!renewalConfirmed(lease.renewal, performance.now(), record.lease_expires_at)) requireTimelyMaintenance(lease.renewal, "heartbeat", result, record);
|
|
410
|
+
lease.error = null; lease.expiresAt = Date.parse(record.lease_expires_at);
|
|
380
411
|
}
|
|
381
412
|
} catch (cause) {
|
|
382
413
|
if (!lease.closing) renewalFailure(lease.renewal);
|
|
@@ -389,9 +420,11 @@ export async function heartbeatDaemonProcess(config, record) {
|
|
|
389
420
|
|
|
390
421
|
export async function finishDaemonProcess(config, process, state = "stopped", reason = "daemon_stopped") {
|
|
391
422
|
if (process?.id && process.lease_token) {
|
|
392
|
-
const settlement = createRenewalState(); settlement.uncertainSince = performance.now();
|
|
393
423
|
const key = leaseKey(config, process), lease = leases.get(key);
|
|
394
424
|
if (lease) { lease.closing = true; clearInterval(lease.timer); await lease.pending?.catch(() => {}); if (lease.error?.cause?.details?.cleanup_unconfirmed) throw lease.error; }
|
|
425
|
+
// A predecessor owns its transport fence; finish gets an independent
|
|
426
|
+
// settlement window only after that flight physically settled.
|
|
427
|
+
const settlement = createRenewalState(); settlement.uncertainSince = performance.now();
|
|
395
428
|
const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement);
|
|
396
429
|
if (result?.ok !== true) throw Object.assign(new Error("managed process finish was not accepted"), { code: "process_lease_lost", details: { process_id: process.id } });
|
|
397
430
|
leases.delete(key);
|
|
@@ -401,7 +434,7 @@ export async function finishDaemonProcess(config, process, state = "stopped", re
|
|
|
401
434
|
function leaseKey(config, record) { return JSON.stringify([path.resolve(config.runtime_owner?.resource_home ?? config.resourceHome), record.id, record.lease_token]); }
|
|
402
435
|
function retainReceipt(record, receipt) {
|
|
403
436
|
const expiresAt = Date.parse(receipt.lease_expires_at ?? "");
|
|
404
|
-
if (!Number.isFinite(expiresAt) || !/^[a-f0-9]{64}$/.test(receipt.registration_sha256 ?? "") || record.registration_sha256 && record.registration_sha256 !== receipt.registration_sha256) throw Object.assign(new Error("Managed lease receipt has no authoritative expiry/binding"), { code: "process_maintenance_receipt_invalid", details: { process_id: record.id } });
|
|
437
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now() + RENEWAL_POLICY.safetyMs || !/^[a-f0-9]{64}$/.test(receipt.registration_sha256 ?? "") || record.registration_sha256 && record.registration_sha256 !== receipt.registration_sha256) throw Object.assign(new Error("Managed lease receipt has no authoritative expiry/binding"), { code: "process_maintenance_receipt_invalid", details: { process_id: record.id } });
|
|
405
438
|
record.lease_expires_at = receipt.lease_expires_at;
|
|
406
439
|
record.registration_sha256 = receipt.registration_sha256;
|
|
407
440
|
}
|
|
@@ -428,10 +461,12 @@ async function retryBookkeeping(action, renewal = createRenewalState()) {
|
|
|
428
461
|
}
|
|
429
462
|
}
|
|
430
463
|
|
|
431
|
-
// A committed ACK
|
|
432
|
-
//
|
|
464
|
+
// A current exact committed ACK establishes authority, not an arbitrary
|
|
465
|
+
// elapsed-time cutoff. Cleanup finish/release remains a non-productive fact.
|
|
433
466
|
function requireTimelyMaintenance(renewal, action, receipt, record) {
|
|
434
|
-
|
|
467
|
+
const expiry = receipt?.process?.lease_expires_at ?? receipt?.lease_expires_at;
|
|
468
|
+
if (expiry && renewalConfirmed(renewal, performance.now(), expiry)) return;
|
|
469
|
+
if (!expiry && renewalRemaining(renewal)) return;
|
|
435
470
|
const safeText = value => typeof value === "string" ? value.slice(0, 128) : undefined;
|
|
436
471
|
const hash = receipt?.process?.registration_sha256 ?? receipt?.registration_sha256 ?? record?.registration_sha256;
|
|
437
472
|
throw Object.assign(new Error("Managed ownership confirmation arrived after its uncertainty budget"), {
|
|
@@ -4,4 +4,4 @@ export function readNativeHookInput(stream: NodeJS.ReadStream, budget: { remaini
|
|
|
4
4
|
export function acceptNativeHookRequest(stateDir: string, binding: Record<string, unknown>, current?: { requests: Map<string, { binding: Record<string, unknown> }>; accepting?: Map<string, Promise<Record<string, unknown>>> } | null): Promise<Record<string, unknown>>;
|
|
5
5
|
export function writeNativeHookFailure(stateDir: string, binding: Record<string, unknown> | undefined, error: unknown, budget: ReturnType<typeof hookBudget>): Promise<void>;
|
|
6
6
|
export function readNativeHookFailures(stateDir: string, requests: Map<string, { current: unknown; binding: Record<string, unknown> }>, current: unknown, reject: (error: Error & { code?: string; details?: Record<string, unknown> }) => void): Promise<void>;
|
|
7
|
-
export function runBoundedCommand(input: { bin: string; args: string[]; input?: string | null; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; timeoutCode?: string; inheritDeadline?: boolean; cleanupWithinBudget?: boolean; maintenanceProgress?: { action: string; processId: string | null; inactivityMs: number } | null; signal?: AbortSignal }): Promise<string>;
|
|
7
|
+
export function runBoundedCommand(input: { bin: string; args: string[]; input?: string | null; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; timeoutCode?: string; inheritDeadline?: boolean; cleanupWithinBudget?: boolean; maintenanceProgress?: { action: string; processId: string | null; startupMs?: number; inactivityMs: number } | null; signal?: AbortSignal }): Promise<string>;
|
|
@@ -127,6 +127,7 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
127
127
|
}
|
|
128
128
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw Object.assign(new Error("Native hook budget exhausted before subprocess launch"), { code: timeoutCode, details: { phase: "subprocess", effect: "no_effect" } });
|
|
129
129
|
if (maintenanceProgress && (!Number.isFinite(maintenanceProgress.inactivityMs) || maintenanceProgress.inactivityMs <= 0
|
|
130
|
+
|| maintenanceProgress.startupMs !== undefined && (!Number.isFinite(maintenanceProgress.startupMs) || maintenanceProgress.startupMs <= 0)
|
|
130
131
|
|| typeof maintenanceProgress.action !== "string" || !(maintenanceProgress.processId === null || typeof maintenanceProgress.processId === "string")))
|
|
131
132
|
throw Object.assign(new Error("Invalid maintenance progress budget"), { code: "process_maintenance_timeout", details: { effect: "no_effect", native_dispatch_started: false } });
|
|
132
133
|
const started = performance.now();
|
|
@@ -142,6 +143,8 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
142
143
|
let stdout = "", stderr = "", failure, cleanupTimer, settled = false, termination;
|
|
143
144
|
let timer, phaseBuffer = "", lastProgress = started;
|
|
144
145
|
const progressPhases = new Set();
|
|
146
|
+
let importsReady = maintenanceProgress?.startupMs === undefined;
|
|
147
|
+
const silenceMs = () => importsReady ? maintenanceProgress?.inactivityMs : maintenanceProgress.startupMs;
|
|
145
148
|
const commandDeadline = started + commandMs;
|
|
146
149
|
const finish = (error, value) => {
|
|
147
150
|
if (settled) return;
|
|
@@ -156,7 +159,7 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
156
159
|
if (termination || settled) return;
|
|
157
160
|
failure ??= error;
|
|
158
161
|
clearTimeout(timer);
|
|
159
|
-
failure.details = { ...failure.details, elapsed_ms: Math.round(performance.now() - started), effect: "unknown", subprocess_pid: child.pid ?? null };
|
|
162
|
+
failure.details = { ...failure.details, failure_at: new Date().toISOString(), elapsed_ms: Math.round(performance.now() - started), effect: "unknown", subprocess_pid: child.pid ?? null };
|
|
160
163
|
termination = terminateCommandTree(child).catch(cleanup => {
|
|
161
164
|
if (cleanup.code === "ESRCH") return;
|
|
162
165
|
failure.details.cleanup_error = { code: cleanup.code, message: cleanup.message };
|
|
@@ -175,8 +178,10 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
175
178
|
const arm = () => {
|
|
176
179
|
clearTimeout(timer);
|
|
177
180
|
const now = performance.now();
|
|
178
|
-
const remaining = Math.min(commandDeadline - now, maintenanceProgress ? lastProgress +
|
|
179
|
-
if (remaining <= 0) { stop(Object.assign(new Error("Bounded CLI request timed out"), { code: timeoutCode
|
|
181
|
+
const remaining = Math.min(commandDeadline - now, maintenanceProgress ? lastProgress + silenceMs() - now : Infinity);
|
|
182
|
+
if (remaining <= 0) { stop(Object.assign(new Error("Bounded CLI request timed out"), { code: timeoutCode,
|
|
183
|
+
details: maintenanceProgress ? { phase: importsReady ? "storage" : "startup", inactivity_ms: silenceMs(),
|
|
184
|
+
ownership_horizon_ms: commandMs } : {} })); return; }
|
|
180
185
|
timer = setTimeout(arm, Math.ceil(remaining));
|
|
181
186
|
};
|
|
182
187
|
arm();
|
|
@@ -196,13 +201,16 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
196
201
|
let phase; try { phase = JSON.parse(line); } catch { continue; }
|
|
197
202
|
if (phase?.kind !== "process_maintenance_phase" || phase.action !== maintenanceProgress.action
|
|
198
203
|
|| (phase.process_id ?? null) !== maintenanceProgress.processId || phase.native_dispatch_started !== false
|
|
199
|
-
|| !["request", "store_open", "store_validation", "lookup", "writer_reservation", "writer_reserved", "write", "commit", "committed", "ack"].includes(phase.phase)
|
|
204
|
+
|| !["request", "imports_ready", "store_open", "store_validation", "lookup", "writer_reservation", "writer_reserved", "write", "commit", "committed", "ack"].includes(phase.phase)
|
|
200
205
|
|| phase.store_kind !== undefined && !["run", "resource"].includes(phase.store_kind)) continue;
|
|
201
206
|
const key = `${phase.store_kind ?? ""}:${phase.phase}`;
|
|
202
207
|
if (progressPhases.has(key)) continue;
|
|
203
208
|
// Delayed observation cannot revive an already-expired silence window.
|
|
204
209
|
const now = performance.now();
|
|
205
|
-
if (now >= commandDeadline || now - lastProgress >=
|
|
210
|
+
if (now >= commandDeadline || now - lastProgress >= silenceMs()) { arm(); return; }
|
|
211
|
+
// Cold imports are not a stalled storage operation. Readiness is an
|
|
212
|
+
// actual boundary, never a timer heartbeat; the ownership horizon stays fixed.
|
|
213
|
+
if (["imports_ready", "store_open"].includes(phase.phase)) importsReady = true;
|
|
206
214
|
progressPhases.add(key); lastProgress = now; arm();
|
|
207
215
|
}
|
|
208
216
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type Options = { cwd?: string; env?: NodeJS.ProcessEnv; input?: string | null; onProgress?: (value: unknown) => void; progress?: () => number; startupMs?: number | null; timeoutMs?: number | null; phase?: string; signal?: AbortSignal; onDiagnostic?: (data: string, stream: string) => void };
|
|
2
|
+
export function reportedFailure(stdout: string, stderr: string, fallback: string, fallbackCode?: string, retainedError?: Record<string, unknown> | null): Error & import('./operation-errors.mjs').ErrorRecord;
|
|
3
|
+
export function commandJson(bin: string, args: string[], options?: Options): Promise<Record<string, unknown>>;
|
|
4
|
+
export function commandText(bin: string, args: string[], options?: Options): Promise<string>;
|
|
5
|
+
export function commandResult(bin: string, args: string[], options?: Options): Promise<{exit_code: number | null; signal: string | null; stdout: string; stderr: string}>;
|
|
@@ -2,10 +2,13 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { errorRecord, reportedError } from "./operation-errors.mjs";
|
|
3
3
|
import { ObservationClock, validateObservationDuration } from "./observation-clock.mjs";
|
|
4
4
|
import { terminateCommandTree } from "./native-hook-command.mjs";
|
|
5
|
+
import { SETUP_STARTUP_MS, publishSetup, setupObserver, observationDetails, isSetupPhase } from './setup-progress.mjs';
|
|
6
|
+
let probeSequence = 0;
|
|
5
7
|
|
|
6
8
|
function failure(message, code) {
|
|
7
9
|
return Object.assign(new Error(message), { code });
|
|
8
10
|
}
|
|
11
|
+
const phaseDiagnostic = value => isSetupPhase(value) || ["native_hook_phase", "process_maintenance_phase"].includes(value?.kind);
|
|
9
12
|
|
|
10
13
|
export function reportedFailure(stdout, stderr, fallback, fallbackCode = "flow_reconciliation_failed", retainedError = null) {
|
|
11
14
|
for (const text of [stdout, stderr]) {
|
|
@@ -13,13 +16,13 @@ export function reportedFailure(stdout, stderr, fallback, fallbackCode = "flow_r
|
|
|
13
16
|
// pretty-printed error envelope. Remove only our diagnostics, not arbitrary
|
|
14
17
|
// text or a substring that might masquerade as an authoritative receipt.
|
|
15
18
|
const envelope = text.split(/\n/).filter(line => {
|
|
16
|
-
try { return !
|
|
19
|
+
try { return !phaseDiagnostic(JSON.parse(line)); }
|
|
17
20
|
catch { return true; }
|
|
18
21
|
}).join("\n").trim();
|
|
19
22
|
for (const line of [text.trim(), envelope, ...text.trim().split(/\n/).reverse()]) {
|
|
20
23
|
try {
|
|
21
24
|
const value = JSON.parse(line);
|
|
22
|
-
if (
|
|
25
|
+
if (phaseDiagnostic(value)) continue;
|
|
23
26
|
const error = value?.error;
|
|
24
27
|
if (typeof error?.code === "string" && error.code) return reportedError(error, fallback);
|
|
25
28
|
if (typeof value?.code === "string" && value.code) return reportedError({ ...value, message: typeof error === "string" ? error : value.message }, fallback);
|
|
@@ -36,10 +39,14 @@ function processTarget(executable, args) {
|
|
|
36
39
|
|
|
37
40
|
// These are utility/transport children, never a second productive-work owner.
|
|
38
41
|
async function command(bin, args, json, { cwd, env = {}, input = null, onProgress = null,
|
|
39
|
-
timeoutMs = 30_000, phase = "utility", signal = null, onDiagnostic = null } = {}) {
|
|
42
|
+
timeoutMs = 30_000, startupMs = null, phase = "utility", signal = null, onDiagnostic = null, result = false, progress = null } = {}) {
|
|
43
|
+
if (startupMs !== null) validateObservationDuration(startupMs);
|
|
40
44
|
if (timeoutMs !== null) validateObservationDuration(timeoutMs);
|
|
41
45
|
if (timeoutMs === null && phase !== "native-work") throw new RangeError("only native-work may omit the utility quiet timeout");
|
|
42
46
|
if (signal?.aborted) throw signal.reason ?? failure("command aborted", "command_aborted");
|
|
47
|
+
const setupProbe = ['native-version', 'native-doctor'].includes(phase);
|
|
48
|
+
const step = setupProbe ? `version-${++probeSequence}` : null;
|
|
49
|
+
if (setupProbe) publishSetup('probe_started', step);
|
|
43
50
|
const executable = bin === "dd-flow" && typeof env.DD_FLOW_BIN === "string" ? env.DD_FLOW_BIN : bin;
|
|
44
51
|
const target = processTarget(executable, json ? [...args, "--json"] : args);
|
|
45
52
|
return await new Promise((resolve, reject) => {
|
|
@@ -49,11 +56,13 @@ async function command(bin, args, json, { cwd, env = {}, input = null, onProgres
|
|
|
49
56
|
let stdout = "", stderr = "", pendingStderr = "", settled = false, primary = null, cursor = 0, droppingStructured = false;
|
|
50
57
|
let outputBytes = 0, cleanupTimer = null, inputError = null, termination = null, retainedError = null, prettyError = "";
|
|
51
58
|
const phases = new Set();
|
|
52
|
-
const clock = timeoutMs === null ? null : new ObservationClock({ timeoutMs });
|
|
59
|
+
const clock = timeoutMs === null ? null : new ObservationClock({ timeoutMs: startupMs ?? timeoutMs });
|
|
60
|
+
const observeSetup = setupObserver(env.DD_EVAL_OPERATION_ID ?? process.env.DD_EVAL_OPERATION_ID);
|
|
53
61
|
const productive = () => { cursor++; clock?.sample(cursor, Date.now()); };
|
|
54
62
|
const finish = (error, value) => {
|
|
55
63
|
if (settled) return; settled = true;
|
|
56
64
|
clearInterval(timer); clearTimeout(cleanupTimer); signal?.removeEventListener("abort", abort);
|
|
65
|
+
if (!error && setupProbe) publishSetup('probe_completed', step);
|
|
57
66
|
error ? reject(error) : resolve(value);
|
|
58
67
|
};
|
|
59
68
|
const stop = error => {
|
|
@@ -80,6 +89,15 @@ async function command(bin, args, json, { cwd, env = {}, input = null, onProgres
|
|
|
80
89
|
prettyError = candidate.length > maxTail ? null : candidate; return;
|
|
81
90
|
}
|
|
82
91
|
const kind = value?.kind;
|
|
92
|
+
if (kind === 'setup_phase') {
|
|
93
|
+
if (observeSetup(value)) {
|
|
94
|
+
if (clock) clock.setupProgress = { phase: value.phase, sequence: value.sequence, step: value.step, operation_id: value.operation_id };
|
|
95
|
+
if (clock) clock.timeoutMs = ['ingress_entered', 'probe_started'].includes(value.phase) ? startupMs ?? timeoutMs : timeoutMs;
|
|
96
|
+
productive();
|
|
97
|
+
if (setupProbe || phase === 'engine-probe') publishSetup(value.phase, `${step ?? 'engine'}:${value.sequence}:${value.step ?? ''}`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
83
101
|
const passive = ["heartbeat", "poll", "status", "elapsed", "wait", "waiting"].some(marker => [kind, value?.phase, value?.action].includes(marker));
|
|
84
102
|
if (!["native_hook_phase", "process_maintenance_phase"].includes(kind)) {
|
|
85
103
|
const error = typeof value?.error?.code === "string" && value.error.code ? value.error
|
|
@@ -110,9 +128,17 @@ async function command(bin, args, json, { cwd, env = {}, input = null, onProgres
|
|
|
110
128
|
}
|
|
111
129
|
} catch (error) { stop(error); }
|
|
112
130
|
};
|
|
131
|
+
let observedProgress = 0;
|
|
113
132
|
const timer = clock ? setInterval(() => {
|
|
133
|
+
if (progress) {
|
|
134
|
+
try {
|
|
135
|
+
const next = progress();
|
|
136
|
+
if (!Number.isSafeInteger(next) || next < 0) throw new RangeError('progress must be a non-negative integer');
|
|
137
|
+
if (next > observedProgress) { observedProgress = next; productive(); }
|
|
138
|
+
} catch (error) { stop(error); return; }
|
|
139
|
+
}
|
|
114
140
|
if (clock.sample(cursor)) stop(Object.assign(failure("command observation became quiet", "command_observation_lost"),
|
|
115
|
-
{ details:
|
|
141
|
+
{ details: observationDetails(clock, phase, child.pid) }));
|
|
116
142
|
}, Math.max(1, Math.min(1_000, timeoutMs / 4))) : null;
|
|
117
143
|
clock?.sample(cursor);
|
|
118
144
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -148,10 +174,15 @@ async function command(bin, args, json, { cwd, env = {}, input = null, onProgres
|
|
|
148
174
|
child.stdin.on("error", error => { inputError = error; if (error.code !== "EPIPE") stop(error); });
|
|
149
175
|
child.stdin.end(input);
|
|
150
176
|
}
|
|
151
|
-
child.once("close", status => {
|
|
177
|
+
child.once("close", (status, signal) => {
|
|
152
178
|
if (pendingStderr) deliverProgress(pendingStderr);
|
|
153
179
|
if (primary) { void (termination ?? Promise.resolve()).then(() => finish(primary)); return; }
|
|
154
|
-
if (
|
|
180
|
+
if (result) return finish(inputError, { exit_code: status, signal, stdout: stdout.trim(), stderr });
|
|
181
|
+
if (status !== 0) {
|
|
182
|
+
const error = reportedFailure(stdout, stderr, `${executable} exited ${status}`, json ? "flow_reconciliation_failed" : "command_failed", retainedError);
|
|
183
|
+
error.details = { reason: 'child_exit', exit_code: status, signal, phase, pid: child.pid, ...error.details };
|
|
184
|
+
return finish(error);
|
|
185
|
+
}
|
|
155
186
|
if (!json) return finish(inputError, stdout.trim());
|
|
156
187
|
try {
|
|
157
188
|
const value = JSON.parse(stdout.trim());
|
|
@@ -165,5 +196,6 @@ async function command(bin, args, json, { cwd, env = {}, input = null, onProgres
|
|
|
165
196
|
});
|
|
166
197
|
});
|
|
167
198
|
}
|
|
168
|
-
export async function commandJson(bin, args, options = {}) { return await command(bin, args, true, options); }
|
|
169
|
-
export async function commandText(bin, args, options = {}) { return await command(bin, args, false, options); }
|
|
199
|
+
export async function commandJson(bin, args, options = {}) { return await command(bin, args, true, { startupMs: options.timeoutMs ?? SETUP_STARTUP_MS, ...options }); }
|
|
200
|
+
export async function commandText(bin, args, options = {}) { return await command(bin, args, false, { startupMs: options.timeoutMs ?? SETUP_STARTUP_MS, ...options }); }
|
|
201
|
+
export async function commandResult(bin, args, options = {}) { return await command(bin, args, false, { startupMs: options.timeoutMs ?? SETUP_STARTUP_MS, ...options, result: true }); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const SETUP_STARTUP_MS: number;
|
|
2
|
+
export const SETUP_QUIET_MS: number;
|
|
3
|
+
export const setupPhases: Set<string>;
|
|
4
|
+
export function setupReporter(operationId?: string): (phase: string, step?: string | number | null) => void;
|
|
5
|
+
export const publishSetup: ReturnType<typeof setupReporter>;
|
|
6
|
+
export function isSetupPhase(value: unknown): boolean;
|
|
7
|
+
export function startupObserver(stateDir: string, daemonId: string): () => Promise<number>;
|
|
8
|
+
export function setupObserver(operationId: string): (value: unknown) => boolean;
|
|
9
|
+
export function observationDetails(clock: import('./observation-clock.mjs').ObservationClock, phase: string | null, pid: number | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const SETUP_STARTUP_MS = 120_000;
|
|
4
|
+
export const SETUP_QUIET_MS = 30_000;
|
|
5
|
+
export const setupPhases = new Set(['ingress_entered', 'imports_ready', 'probe_started', 'probe_completed', 'dependency_check_completed', 'transfer_progress', 'identity_digest_progress', 'identity_digest_completed', 'native_initialized', 'ready']);
|
|
6
|
+
export function setupReporter(operationId = process.env.DD_EVAL_OPERATION_ID) {
|
|
7
|
+
if (!operationId) return () => {};
|
|
8
|
+
let sequence = 0;
|
|
9
|
+
return (phase, step = null) => {
|
|
10
|
+
if (!setupPhases.has(phase)) throw new Error('Invalid setup phase');
|
|
11
|
+
process.stderr.write(JSON.stringify({ kind: 'setup_phase', operation_id: operationId, phase, step, sequence: ++sequence, observed_at: new Date().toISOString() }) + '\n');
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export const publishSetup = setupReporter();
|
|
15
|
+
export function isSetupPhase(value) {
|
|
16
|
+
return value?.kind === 'setup_phase' && typeof value.operation_id === 'string' && value.operation_id.length > 0 && setupPhases.has(value.phase)
|
|
17
|
+
&& Number.isSafeInteger(value.sequence) && value.sequence > 0 && value.sequence <= 1_000_000
|
|
18
|
+
&& (value.step === null || typeof value.step === 'string' && value.step.length <= 4096 || Number.isSafeInteger(value.step) && value.step >= 0)
|
|
19
|
+
&& typeof value.observed_at === 'string' && Number.isFinite(Date.parse(value.observed_at))
|
|
20
|
+
&& !Object.hasOwn(value, 'error') && !Object.hasOwn(value, 'code');
|
|
21
|
+
}
|
|
22
|
+
export function startupObserver(stateDir, daemonId) {
|
|
23
|
+
const seen = new Set(); let sequence = 0, binding;
|
|
24
|
+
return async () => {
|
|
25
|
+
let state;
|
|
26
|
+
try { state = JSON.parse(await readFile(path.join(stateDir, 'daemon.json'), 'utf8')); }
|
|
27
|
+
catch (error) { if (error.code === 'ENOENT') return sequence; throw error; }
|
|
28
|
+
if (state.daemon_id !== daemonId) throw Object.assign(new Error('Startup daemon identity changed'), { code: 'operation_binding_conflict' });
|
|
29
|
+
const owner = state.config?.runtime_owner;
|
|
30
|
+
const identity = JSON.stringify([state.started_at ?? null, owner?.owner_id ?? null, owner?.operation_id ?? null, state.generation ?? null]);
|
|
31
|
+
binding ??= identity;
|
|
32
|
+
if (identity !== binding) throw Object.assign(new Error('Startup ownership binding changed'), { code: 'operation_binding_conflict' });
|
|
33
|
+
const marker = JSON.stringify([state.pid ?? null, state.provider_pid ?? null, state.shutdown_state ?? null, state.startup_phase ?? null]);
|
|
34
|
+
if (!seen.has(marker)) { seen.add(marker); sequence++; publishSetup(state.shutdown_state === 'running' ? 'native_initialized' : 'probe_started', `${daemonId}:${sequence}`); }
|
|
35
|
+
return sequence;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function setupObserver(operationId) {
|
|
39
|
+
const startedAt = Date.now(); let sequence = 0; const markers = new Set();
|
|
40
|
+
return value => {
|
|
41
|
+
if (typeof operationId !== 'string' || markers.size >= 4096 || !isSetupPhase(value) || value.operation_id !== operationId
|
|
42
|
+
|| value.sequence <= sequence || Date.parse(value.observed_at) < startedAt || Date.parse(value.observed_at) > Date.now()) return false;
|
|
43
|
+
const marker = JSON.stringify([value.phase, value.step]);
|
|
44
|
+
if (markers.has(marker)) return false;
|
|
45
|
+
markers.add(marker); sequence = value.sequence; return true;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function observationDetails(clock, phase, pid) {
|
|
49
|
+
return { reason: clock.observationLost ? 'observation_gap' : 'inactivity', phase, pid,
|
|
50
|
+
timeout_ms: clock.timeoutMs, observation_lost: clock.observationLost, observation_clock: clock.state(), ...(clock.setupProgress ? { last_setup: clock.setupProgress } : {}), effect: 'unknown' };
|
|
51
|
+
}
|
|
@@ -166,11 +166,11 @@ function gitFacts(workspacePath) {
|
|
|
166
166
|
if (!fs.existsSync(workspacePath)) {
|
|
167
167
|
return { status: "missing_workspace", actual_branch: null, dirty: null, path: workspacePath };
|
|
168
168
|
}
|
|
169
|
-
const branch = spawnSync("git", ["-C", workspacePath, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
169
|
+
const branch = spawnSync("git", ["-C", workspacePath, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8", timeout: 5000 });
|
|
170
170
|
if (branch.status !== 0) {
|
|
171
|
-
return { status: "not_git_checkout", actual_branch: null, dirty: null, path: workspacePath };
|
|
171
|
+
return { status: branch.error || branch.signal ? "observation_unavailable" : "not_git_checkout", actual_branch: null, dirty: null, path: workspacePath };
|
|
172
172
|
}
|
|
173
|
-
const status = spawnSync("git", ["-C", workspacePath, "status", "--porcelain"], { encoding: "utf8" });
|
|
173
|
+
const status = spawnSync("git", ["-C", workspacePath, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
|
|
174
174
|
return {
|
|
175
175
|
status: "ok",
|
|
176
176
|
actual_branch: branch.stdout.trim() || null,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "shell-quote";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
6
|
+
export function runnerObservationDurations(environment) {
|
|
7
|
+
// Both observers leave the launcher its bounded physical cleanup tail.
|
|
8
|
+
return { startupMs: validateObservationDuration(validateObservationDuration(Number(environment.DD_FLOW_TEST_STARTUP_SILENCE_MS ?? 120_000)) + 10_000),
|
|
9
|
+
executionMs: validateObservationDuration(validateObservationDuration(Number(environment.DD_FLOW_TEST_EXECUTION_SILENCE_MS ?? 600_000)) + 10_000) };
|
|
10
|
+
}
|
|
11
|
+
/** Baseline uses argv rather than a shell alias. Recognize only a literal,
|
|
12
|
+
* unique execution contract; never guess shell expansion or compound syntax. */
|
|
13
|
+
export function runnerContractForArgv(profile, command, args) {
|
|
14
|
+
if (profile.schema_id !== "dd-flow/code-check-profile@7" || !profile.runner_evidence_by_alias)
|
|
15
|
+
return null;
|
|
16
|
+
if (!runnerContractsValid(profile.runner_evidence_by_alias))
|
|
17
|
+
throw new AppError("invalid_code_check_profile", "Invalid baseline runner evidence contract", 2);
|
|
18
|
+
const matches = [];
|
|
19
|
+
for (const [alias, contract] of Object.entries(profile.runner_evidence_by_alias)) {
|
|
20
|
+
const source = profile.aliases?.[alias];
|
|
21
|
+
if (typeof source !== "string")
|
|
22
|
+
throw new AppError("invalid_code_check_profile", "Baseline runner evidence references an unknown alias", 2, { alias });
|
|
23
|
+
let expansion = false;
|
|
24
|
+
const tokens = parse(source, () => { expansion = true; return ""; });
|
|
25
|
+
if (!expansion && tokens.every(token => typeof token === "string") && JSON.stringify(tokens) === JSON.stringify([command, ...args]))
|
|
26
|
+
matches.push({ alias, command: source, contract });
|
|
27
|
+
}
|
|
28
|
+
if (matches.some(match => JSON.stringify(match.contract) !== JSON.stringify(matches[0].contract)))
|
|
29
|
+
throw new AppError("runner_evidence_ambiguous", "Baseline argv matches conflicting runner evidence contracts", 2, { aliases: matches.map(match => match.alias) });
|
|
30
|
+
return matches[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
/** Shared by CODE and EVAL baseline. Logs, repeated cursors and foreign records
|
|
33
|
+
* are diagnostic bytes, not runner progress. Partial final lines await append. */
|
|
34
|
+
export function readRunnerProgress(file, invocationId, lastSequence = 0, now = Date.now()) {
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs.lstatSync(file);
|
|
37
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
38
|
+
return [];
|
|
39
|
+
const accepted = [];
|
|
40
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n").slice(0, -1)) {
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
|
|
49
|
+
|| !Number.isSafeInteger(value.sequence) || value.sequence <= lastSequence || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at)) || Date.parse(value.at) > now
|
|
50
|
+
|| !["startup", "execution"].includes(value.phase)
|
|
51
|
+
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
|
|
52
|
+
continue;
|
|
53
|
+
accepted.push(value);
|
|
54
|
+
lastSequence = value.sequence;
|
|
55
|
+
}
|
|
56
|
+
return accepted;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function runnerContractsValid(value) {
|
|
63
|
+
if (value === undefined)
|
|
64
|
+
return true;
|
|
65
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
66
|
+
return false;
|
|
67
|
+
return Object.entries(value).every(([alias, raw]) => {
|
|
68
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
69
|
+
return false;
|
|
70
|
+
const contract = raw;
|
|
71
|
+
return alias.startsWith("@check/") && contract.schema_id === "dd-flow/check-runner-evidence@1" && typeof contract.path === "string" && contract.path.length > 0 && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function readRunnerEvidence(root, contract, binding) {
|
|
75
|
+
try {
|
|
76
|
+
let current = root;
|
|
77
|
+
if (!fs.lstatSync(current).isDirectory())
|
|
78
|
+
return null;
|
|
79
|
+
for (const part of contract.path.split(/[\\/]/)) {
|
|
80
|
+
current = path.join(current, part);
|
|
81
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const stat = fs.statSync(current);
|
|
85
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
86
|
+
return null;
|
|
87
|
+
const evidence = JSON.parse(fs.readFileSync(current, "utf8"));
|
|
88
|
+
if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
|
|
89
|
+
|| typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
90
|
+
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
91
|
+
|| !Array.isArray(evidence.failures) || evidence.failures.some(f => !f || !["assertion", "tooling", "unclassified"].includes(f.category))
|
|
92
|
+
|| evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
93
|
+
return null;
|
|
94
|
+
return evidence;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
101
|
+
export function checkRepairDisposition(receipt) {
|
|
102
|
+
if (receipt.status === "running" || receipt.status === "aborted" || receipt.runner_evidence?.observation_unknown)
|
|
103
|
+
return "settlement_required";
|
|
104
|
+
if (receipt.status === "passed")
|
|
105
|
+
return "passed";
|
|
106
|
+
const evidence = receipt.runner_evidence;
|
|
107
|
+
if (!evidence || !Array.isArray(evidence.failures) || !evidence.failures.length || receipt.exit_code === 0 || evidence.failures.some(f => !f || !["assertion", "tooling", "unclassified"].includes(f.category)))
|
|
108
|
+
return "unclassified";
|
|
109
|
+
if (evidence.failures.some(f => f.category === "tooling"))
|
|
110
|
+
return "tooling_blocker";
|
|
111
|
+
if (!evidence.completed)
|
|
112
|
+
return "unclassified";
|
|
113
|
+
if (evidence.failures.some(f => f.category === "unclassified"))
|
|
114
|
+
return "unclassified";
|
|
115
|
+
return "source_repair";
|
|
116
|
+
}
|
|
117
|
+
export function assertSourceRepairEligible(receipts) {
|
|
118
|
+
const blockers = receipts.filter(receipt => checkRepairDisposition(receipt) !== "source_repair");
|
|
119
|
+
if (!receipts.length || blockers.length)
|
|
120
|
+
throw new AppError("check_gate_blocked", "Checks require tooling diagnosis or settlement before product repair; retained nonzero is not an assertion 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." });
|
|
121
|
+
}
|
package/dist/services/cleanup.js
CHANGED
|
@@ -512,7 +512,7 @@ function dirtyGit(worktreePath) {
|
|
|
512
512
|
if (!fs.existsSync(worktreePath)) {
|
|
513
513
|
return false;
|
|
514
514
|
}
|
|
515
|
-
const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8" });
|
|
515
|
+
const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
|
|
516
516
|
if (result.error || result.status !== 0)
|
|
517
517
|
throw new AppError("workspace_status_failed", "Cannot establish worktree status before cleanup", 1, { path: worktreePath, cause: result.error?.message ?? result.stderr });
|
|
518
518
|
return result.stdout.trim().length > 0;
|