@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/command-inputs.js +2 -2
  4. package/dist/cli/run-cli.js +6 -6
  5. package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
  6. package/dist/cli/runtime-maintenance-ingress.js +5 -4
  7. package/dist/cli/runtime-process.js +27 -2
  8. package/dist/cli.js +7 -1
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
  10. package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
  12. package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  14. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  15. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
  16. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  17. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
  18. package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
  19. package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
  20. package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
  21. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  22. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  23. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
  24. package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
  25. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  26. package/dist/harness-runtime/lib/process-json.mjs +41 -9
  27. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  28. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  29. package/dist/services/branch-context.js +3 -3
  30. package/dist/services/check-runner-evidence.js +121 -0
  31. package/dist/services/cleanup.js +1 -1
  32. package/dist/services/code-checks.js +113 -31
  33. package/dist/services/compatibility-preflight.js +9 -2
  34. package/dist/services/dashboard.js +33 -26
  35. package/dist/services/engines.js +88 -57
  36. package/dist/services/lifecycle-invocations.js +4 -2
  37. package/dist/services/managed-lease-monitor.js +9 -4
  38. package/dist/services/prompts.js +3 -1
  39. package/dist/services/protocols.js +3 -1
  40. package/dist/services/repair-intents.js +4 -2
  41. package/dist/services/run-control-worker.js +2 -1
  42. package/dist/services/run-controller.js +20 -17
  43. package/dist/services/run-fork.js +2 -2
  44. package/dist/services/runtime-scope-worker.js +2 -2
  45. package/dist/services/stage-lifecycle.js +13 -7
  46. package/dist/services/tooling.js +32 -23
  47. package/dist/services/vnext-code-review.js +2 -0
  48. package/dist/services/vnext-code.js +5 -1
  49. package/dist/services/vnext-merge.js +4 -1
  50. package/dist/services/vnext-protocolize.js +17 -16
  51. package/dist/services/work-registry.js +3 -1
  52. package/dist/services/worktrees.js +16 -14
  53. 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
- if (!maintenanceRetryable(error) || !renewalFailure(renewal)) throw error;
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
- return await bridge.start();
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 uncertainty episode while native work is active.
354
- // Waiting for the next 60s tick would exhaust the 30s budget after one BUSY.
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
- if (!renewalConfirmed(lease.renewal)) requireTimelyMaintenance(lease.renewal, "heartbeat", result, record);
379
- retainReceipt(record, result); lease.error = null; lease.expiresAt = Date.parse(record.lease_expires_at);
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 is evidence, but cannot revive an expired authority episode.
432
- // Cleanup-only finish/release receipts remain facts, not productive grants.
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
- if (renewalRemaining(renewal)) return;
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 + maintenanceProgress.inactivityMs - now : Infinity);
179
- if (remaining <= 0) { stop(Object.assign(new Error("Bounded CLI request timed out"), { code: timeoutCode })); return; }
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 >= maintenanceProgress.inactivityMs) { arm(); return; }
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 !["native_hook_phase", "process_maintenance_phase"].includes(JSON.parse(line)?.kind); }
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 (["native_hook_phase", "process_maintenance_phase"].includes(value?.kind)) continue;
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: { phase, timeout_ms: timeoutMs, observation_lost: clock.observationLost } }));
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 (status !== 0) return finish(reportedFailure(stdout, stderr, `${executable} exited ${status}`, json ? "flow_reconciliation_failed" : "command_failed", retainedError));
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
+ }
@@ -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;