@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
@@ -1,4 +1,6 @@
1
1
  import crypto from "node:crypto";
2
+ import { readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
3
+ export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
2
4
  import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
3
5
  import { spawn, spawnSync } from "node:child_process";
4
6
  import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
@@ -63,6 +65,8 @@ export function codeExecutionEnvironment(workspaceRoot) {
63
65
  // provider can otherwise run x64 checks against arm64-installed modules.
64
66
  const environment = { ...process.env, PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
65
67
  delete environment.NODE_PATH;
68
+ for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
69
+ delete environment[key];
66
70
  return environment;
67
71
  }
68
72
  export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
@@ -72,8 +76,13 @@ export function readCodeCheckProfile(workspaceRoot) {
72
76
  return { profile: null, hash: null, file };
73
77
  const bytes = fs.readFileSync(file);
74
78
  const value = JSON.parse(bytes.toString("utf8"));
75
- if (value.schema_id !== "dd-flow/code-check-profile@6")
76
- throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null, required: "dd-flow/code-check-profile@6" });
79
+ if (!["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"].includes(value.schema_id ?? ""))
80
+ throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null, supported: ["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"] });
81
+ if (!runnerContractsValid(value.runner_evidence_by_alias) || value.schema_id === "dd-flow/code-check-profile@6" && value.runner_evidence_by_alias !== undefined)
82
+ throw new AppError("invalid_code_check_profile", "Runner evidence requires valid profile@7 relative artifact contracts", 2, { file });
83
+ for (const alias of Object.keys(value.runner_evidence_by_alias ?? {}))
84
+ if (!value.aliases?.[alias])
85
+ throw new AppError("invalid_code_check_profile", "Runner evidence references an unknown alias", 2, { file, alias });
77
86
  if (!isStringRecord(value.aliases) || !isStringRecord(value.require_alias_for) || !isGateRecord(value.mandatory_by_gate) || !isPortAliasRecord(value.ports_by_alias) || !isInputAliasRecord(workspaceRoot, value.inputs_by_alias))
78
87
  throw new AppError("invalid_code_check_profile", "CODE check profile aliases, require_alias_for, mandatory_by_gate, ports_by_alias or inputs_by_alias are invalid", 2, { file });
79
88
  for (const [gate, aliases] of Object.entries(value.mandatory_by_gate))
@@ -100,15 +109,19 @@ export function codeCheckProfileHash(workspaceRoot, checks = []) {
100
109
  if (!profile)
101
110
  return null;
102
111
  const planned = new Set(checks.filter(check => check.availability === "planned" && check.command.startsWith("@check/")).map(check => check.command));
112
+ const serialize = (value) => profile.schema_id === "dd-flow/code-check-profile@7"
113
+ ? JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right))) : item)
114
+ : JSON.stringify(value);
103
115
  if (!planned.size)
104
- return crypto.createHash("sha256").update(JSON.stringify(profile)).digest("hex");
116
+ return crypto.createHash("sha256").update(serialize(profile)).digest("hex");
105
117
  const canonical = {
106
118
  ...profile,
107
119
  aliases: Object.fromEntries(Object.entries(profile.aliases).filter(([alias]) => !planned.has(alias))),
108
120
  ports_by_alias: Object.fromEntries(Object.entries(profile.ports_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
109
- inputs_by_alias: Object.fromEntries(Object.entries(profile.inputs_by_alias ?? {}).filter(([alias]) => !planned.has(alias)))
121
+ inputs_by_alias: Object.fromEntries(Object.entries(profile.inputs_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
122
+ ...(profile.schema_id === "dd-flow/code-check-profile@7" ? { runner_evidence_by_alias: Object.fromEntries(Object.entries(profile.runner_evidence_by_alias ?? {}).filter(([alias]) => !planned.has(alias))) } : {})
110
123
  };
111
- return crypto.createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
124
+ return crypto.createHash("sha256").update(serialize(canonical)).digest("hex");
112
125
  }
113
126
  /** The integration target starts at its baseline profile; the reviewed source
114
127
  * may add aliases, but must not change existing check policy or bindings. */
@@ -120,7 +133,7 @@ export function mergeCheckProfileHash(sourceWorkspace, targetWorkspace, checks)
120
133
  if (!source || !target)
121
134
  throw new AppError("check_profile_drift", "Source and integration check profiles differ", 1);
122
135
  const retained = structuredClone(source);
123
- for (const field of ["aliases", "ports_by_alias", "inputs_by_alias"]) {
136
+ for (const field of ["aliases", "ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"]) {
124
137
  const old = target[field];
125
138
  if (!old) {
126
139
  delete retained[field];
@@ -130,7 +143,7 @@ export function mergeCheckProfileHash(sourceWorkspace, targetWorkspace, checks)
130
143
  retained[field] = Object.fromEntries(Object.keys(old).filter(key => key in current).map(key => [key, current[key]]));
131
144
  }
132
145
  const existingAliases = new Set(Object.keys(target.aliases));
133
- const changedExistingMetadata = ["ports_by_alias", "inputs_by_alias"].some(field => Object.keys(source[field] ?? {}).some(alias => existingAliases.has(alias) && !(alias in (target[field] ?? {}))));
146
+ const changedExistingMetadata = ["ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"].some(field => Object.keys(source[field] ?? {}).some(alias => existingAliases.has(alias) && !(alias in (target[field] ?? {}))));
134
147
  if (changedExistingMetadata || !isDeepStrictEqual(retained, target)) {
135
148
  throw new AppError("check_profile_drift", "Source check profile changes existing integration policy", 1, { source_profile_hash: codeCheckProfileHash(sourceWorkspace, checks), target_profile_hash: codeCheckProfileHash(targetWorkspace, checks) });
136
149
  }
@@ -171,6 +184,8 @@ export function validateCheckPlacement(workspaceRoot, checks) { const { profile
171
184
  export function validateCheckDeclaration(workspaceRoot, check) {
172
185
  if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
173
186
  throw new AppError("unsupported_check_gate", "Check requires an autonomous executable gate; replan the incompatible declaration", 2, { check_id: check.id, run_at: check.run_at });
187
+ if (check.runner_evidence !== undefined && !runnerContractsValid({ "@check/declaration": check.runner_evidence }))
188
+ throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
174
189
  const { profile } = readCodeCheckProfile(workspaceRoot);
175
190
  const resolvedCommand = profile?.aliases[check.command] ?? check.command;
176
191
  validateTerminatingCheckCommand(resolvedCommand, check.id);
@@ -274,7 +289,13 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
274
289
  const profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
275
290
  if (input.expectedProfileHash !== undefined && input.expectedProfileHash !== profileHash)
276
291
  throw new AppError("check_profile_drift", "The frozen check profile differs from the workspace profile", 1, { expected_profile_hash: input.expectedProfileHash, actual_profile_hash: profileHash });
277
- const checks = input.checks.map((check) => { const ports = profile?.ports_by_alias?.[check.command]; const inputs = profile?.inputs_by_alias?.[check.command]; return { ...check, ...(check.ports || !ports ? {} : { ports }), ...(check.inputs || !inputs ? {} : { inputs }) }; });
292
+ const checks = input.checks.map(check => {
293
+ const ports = profile?.ports_by_alias?.[check.command], inputs = profile?.inputs_by_alias?.[check.command];
294
+ const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
295
+ if (runner && !runnerContractsValid({ "@check/declaration": runner }))
296
+ throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
297
+ return { ...check, ...(check.ports || !ports ? {} : { ports }), ...(check.inputs || !inputs ? {} : { inputs }), ...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
298
+ });
278
299
  const resolved = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map((check) => check.command));
279
300
  assertConsistentDeclarations(checks, resolved);
280
301
  const groups = deduplicate(checks, resolved), receipts = [];
@@ -313,8 +334,16 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
313
334
  let result = { exitCode: null, error: "check did not start", aborted: true };
314
335
  try {
315
336
  fs.mkdirSync(evidenceDir, { recursive: true });
316
- writeReceipt(receiptPath, receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] }));
337
+ const startingReceipt = receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] });
338
+ if (group.runnerEvidence)
339
+ startingReceipt.runner_evidence_required = group.runnerEvidence;
340
+ writeReceipt(receiptPath, startingReceipt);
317
341
  const budget = runtimeBudgetForRun(context, input.projectId, input.runId);
342
+ if (group.runnerEvidence) {
343
+ // Reject configuration before acquiring a check lease/ports, so there
344
+ // is no unconfirmed managed child to strand on a validation error.
345
+ runnerObservationDurations(process.env);
346
+ }
318
347
  const managed = registerManagedProcess(context, { kind: "check", ownerPid: process.pid, ownerId: `check:${id}`, operationId: workspaceOperationId(input.workspaceRoot), projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command, role: "check", workspace_root: input.workspaceRoot, workspace_gate_id: workspaceGateId, budget } });
319
348
  const allocation = await reservePorts(context, { ownerId: managed.owner_id, processId: managed.id, names: group.ports });
320
349
  resources = allocation.ports;
@@ -329,13 +358,17 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
329
358
  }
330
359
  const stdout = fs.openSync(stdoutPath, "a");
331
360
  const stderr = fs.openSync(stderrPath, "a");
361
+ let confirmedExpiry = null;
332
362
  try {
333
- result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
334
- confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid });
363
+ const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
364
+ if (group.runnerEvidence)
365
+ fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
366
+ result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
367
+ confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
335
368
  assertRunMutationAllowed(context, input.projectId, input.runId, generation);
336
369
  if (budget)
337
370
  admitRuntimeCheck(context, managed.id, managed.lease_token);
338
- }, attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }));
371
+ }, attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }), { leaseExpiresAt: () => confirmedExpiry });
339
372
  }
340
373
  finally {
341
374
  fs.closeSync(stdout);
@@ -364,8 +397,13 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
364
397
  if (mutationPaths.length)
365
398
  fs.appendFileSync(stderrPath, `\ncheck mutated project workspace: ${mutationPaths.join(", ")}\n`);
366
399
  }
367
- const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 ? "passed" : "failed";
400
+ const runnerEvidence = group.runnerEvidence ? readRunnerEvidence(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
401
+ const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
368
402
  const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
403
+ if (group.runnerEvidence) {
404
+ receipt.runner_evidence_required = group.runnerEvidence;
405
+ receipt.runner_evidence = runnerEvidence ?? null;
406
+ }
369
407
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [receipt.status, receipt.exit_code, receipt.after_fingerprint, JSON.stringify(receipt.mutation_paths), JSON.stringify(receipt.artifacts), receipt.finished_at, id, input.projectId]);
370
408
  writeReceipt(receiptPath, receipt);
371
409
  receipts.push(receipt);
@@ -420,14 +458,15 @@ export function reconcileUnfinishedChecks(context, input) {
420
458
  const after = workspaceState(input.workspaceRoot, input.runHome ? [input.runHome] : []);
421
459
  const artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
422
460
  const workspaceUnchanged = receipt?.before_fingerprint === after.fingerprint;
423
- const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete ? "passed" : "failed";
461
+ const runnerEvidence = receipt?.runner_evidence_required ? readRunnerEvidence(path.join(path.dirname(pendingReceipt.receipt_path), "artifacts"), receipt.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch }) : undefined;
462
+ const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
424
463
  const exitCode = completion?.exit_code ?? null;
425
464
  const finishedAt = completion?.finished_at ?? context.now();
426
465
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [status, exitCode, after.fingerprint, JSON.stringify(workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"]), JSON.stringify(artifacts.items), finishedAt, pendingReceipt.id, input.projectId]);
427
466
  if (process)
428
467
  finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
429
468
  if (receipt) {
430
- const repaired = { ...receipt, completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
469
+ const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
431
470
  writeReceipt(pendingReceipt.receipt_path, repaired);
432
471
  reconciled.push(repaired);
433
472
  }
@@ -472,10 +511,10 @@ function reserveOrReuseReceipt(context, input) {
472
511
  throw new AppError("retry_check_invalid", "The requested retry already has a terminal outcome; choose its failed receipt for another retry", 2, { check_receipt_id: successor.id, retry_of_receipt_id: retryId, status: successor.status });
473
512
  }
474
513
  }
475
- // Only success is reusable evidence. A new finish attempt must execute a
476
- // failed check again: repairing dependencies/services need not change any
477
- // tracked input. Lifecycle invocation replay owns command idempotency;
478
- // the running-receipt guard above still prevents concurrent execution.
514
+ // An unchanged failed contract needs a concrete restored-condition retry,
515
+ // not repeated coordinator finish until it happens to become green.
516
+ if (prior && ["failed", "aborted"].includes(prior.status) && !retryId)
517
+ return { receipt: prior };
479
518
  if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
480
519
  return { receipt: prior };
481
520
  let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
@@ -522,10 +561,10 @@ export function validateFailedCheckRetry(context, input) {
522
561
  export function checkReceipts(context, input) {
523
562
  const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
524
563
  const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
525
- return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json)) }));
564
+ return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json), row) }));
526
565
  }
527
566
  /** File bytes remain immutable; only the reader projects their current location. */
528
- function resolvedReceiptArtifacts(receiptPath, artifacts) {
567
+ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
529
568
  let original = {};
530
569
  try {
531
570
  original = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
@@ -537,7 +576,14 @@ function resolvedReceiptArtifacts(receiptPath, artifacts) {
537
576
  const relative = path.relative(oldRoot, file);
538
577
  return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
539
578
  };
540
- return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })) };
579
+ let evidence = null;
580
+ if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
581
+ const root = path.join(currentRoot, "artifacts");
582
+ const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
583
+ if (retained.complete && JSON.stringify(retained.items) === JSON.stringify(artifacts))
584
+ evidence = readRunnerEvidence(root, original.runner_evidence_required, { invocation_id: binding.id, check_id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch });
585
+ }
586
+ return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}) };
541
587
  }
542
588
  export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
543
589
  /** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
@@ -604,12 +650,12 @@ function deduplicate(checks, commands) {
604
650
  const reusable = check.reuse === "deterministic";
605
651
  // Stage labels identify obligations, not different processes in this gate.
606
652
  // Cross-invocation reuse still requires the explicit deterministic contract.
607
- const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable]);
653
+ const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, check.runner_evidence ?? null]);
608
654
  const current = groups.get(key);
609
655
  if (current)
610
656
  current.declarations.push(check);
611
657
  else
612
- groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable });
658
+ groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
613
659
  });
614
660
  return [...groups.values()].map(group => ({ ...group, gate: [...group.declarations].map(check => check.run_at).sort()[0], declarations: [...group.declarations].sort((left, right) => checkRef(left).localeCompare(checkRef(right))) }));
615
661
  }
@@ -657,7 +703,7 @@ function checkRef(check) { return check.canonical_ref ?? check.id; }
657
703
  function assertConsistentDeclarations(checks, commands) {
658
704
  const seen = new Map();
659
705
  for (const [index, check] of checks.entries()) {
660
- const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null]);
706
+ const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, check.runner_evidence ?? null]);
661
707
  const ref = checkRef(check), prior = seen.get(ref);
662
708
  if (prior && prior !== contract)
663
709
  throw new AppError("check_declaration_conflict", "The same check reference has conflicting execution definitions", 2, { check_ref: ref });
@@ -760,11 +806,20 @@ function explicitInputFiles(root, inputs, ignoredRoots) {
760
806
  visit(path.resolve(root, input), input, new Set());
761
807
  return [...result.entries()];
762
808
  }
763
- function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot), input_fingerprint: inputFingerprint })).digest("hex"); }
809
+ function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot), input_fingerprint: inputFingerprint })).digest("hex"); }
764
810
  function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: group.requiredArtifacts, inputs: group.inputs }; }
765
- export function checkReceiptEvidenceValid(receipt) { if (receipt.status !== "passed" || receipt.exit_code !== 0 || receipt.before_fingerprint !== receipt.after_fingerprint || !fs.existsSync(receipt.completion_path) || readCompletion(receipt.completion_path)?.exit_code !== 0)
766
- return false; const stored = readReceipt(receipt.receipt_path); if (!stored || stored.status !== "passed" || stored.input_hash !== receipt.input_hash || stored.profile_hash !== receipt.profile_hash)
767
- return false; const artifacts = collectCheckArtifacts(path.join(path.dirname(receipt.receipt_path), "artifacts"), stored.required_artifacts ?? []); return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(stored.artifacts ?? []); }
811
+ export function checkReceiptEvidenceValid(receipt) {
812
+ if (receipt.status !== "passed" || receipt.exit_code !== 0 || receipt.before_fingerprint !== receipt.after_fingerprint || !fs.existsSync(receipt.completion_path) || readCompletion(receipt.completion_path)?.exit_code !== 0)
813
+ return false;
814
+ const stored = readReceipt(receipt.receipt_path);
815
+ if (!stored || stored.status !== "passed" || stored.input_hash !== receipt.input_hash || stored.profile_hash !== receipt.profile_hash)
816
+ return false;
817
+ const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
818
+ if (stored.runner_evidence_required && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
819
+ return false;
820
+ const artifacts = collectCheckArtifacts(root, stored.required_artifacts ?? []);
821
+ return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(stored.artifacts ?? []);
822
+ }
768
823
  function parseArray(value, fallback) { try {
769
824
  const parsed = JSON.parse(value);
770
825
  return Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ? parsed : fallback;
@@ -826,6 +881,7 @@ function listFiles(root) {
826
881
  }
827
882
  export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease, observation = {}) {
828
883
  const inactivityTimeoutMs = validateObservationDuration(observation.inactivityTimeoutMs ?? 30 * 60_000);
884
+ const { startupMs: runnerStartupMs, executionMs: runnerExecutionMs } = runnerObservationDurations(environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH ? environment : {});
829
885
  return new Promise((resolve) => {
830
886
  const started = Date.now();
831
887
  const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
@@ -839,6 +895,10 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
839
895
  let renewalTimer;
840
896
  const clock = new ObservationClock({ timeoutMs: inactivityTimeoutMs });
841
897
  let outputCursor = 0;
898
+ let runnerSequence = 0;
899
+ let runnerPhase = "startup";
900
+ const runnerProgress = environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH;
901
+ const runnerClock = runnerProgress ? new ObservationClock({ timeoutMs: runnerStartupMs }) : null;
842
902
  const outputs = [fs.createWriteStream("", { fd: stdout, autoClose: false }), fs.createWriteStream("", { fd: stderr, autoClose: false })];
843
903
  const flushed = outputs.map(output => new Promise(resolve => { output.once("finish", resolve); output.once("error", () => resolve()); }));
844
904
  const stop = (cause) => {
@@ -863,6 +923,18 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
863
923
  const observationTimer = setInterval(() => {
864
924
  if (stopping || closed || observation.phase?.() === "serving")
865
925
  return;
926
+ if (runnerClock && runnerProgress) {
927
+ for (const event of readRunnerProgress(runnerProgress, environment.DD_FLOW_CHECK_INVOCATION_ID ?? "", runnerSequence)) {
928
+ runnerSequence = event.sequence;
929
+ runnerPhase = event.phase;
930
+ runnerClock.sample(runnerSequence, Date.parse(event.at));
931
+ }
932
+ // The outer guard includes the runner's bounded cleanup tail.
933
+ runnerClock.timeoutMs = runnerPhase === "execution" ? runnerExecutionMs : runnerStartupMs;
934
+ if (runnerClock.sample(runnerSequence))
935
+ stop(new AppError("operation_observation_lost", "Structured test runner stopped making progress", 1, { phase: runnerPhase, sequence: runnerSequence }));
936
+ return;
937
+ }
866
938
  if (clock.sample(outputCursor))
867
939
  stop(new AppError("operation_observation_lost", "Owned command has no confirmed progress within its inactivity window", 1, { phase: observation.phase?.() ?? "finite", inactivity_timeout_ms: inactivityTimeoutMs }));
868
940
  }, Math.min(1000, inactivityTimeoutMs));
@@ -871,7 +943,10 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
871
943
  child.once("error", (value) => { error = value.message; });
872
944
  for (const [index, stream] of [child.stdout, child.stderr].entries()) {
873
945
  outputs[index].once("error", stop);
874
- stream.on("data", () => { outputCursor++; clock.sample(outputCursor); });
946
+ stream.on("data", () => { if (!runnerClock) {
947
+ outputCursor++;
948
+ clock.sample(outputCursor);
949
+ } });
875
950
  stream.pipe(outputs[index]);
876
951
  }
877
952
  child.stdin.on("error", stop);
@@ -898,6 +973,13 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
898
973
  stop(cause);
899
974
  return;
900
975
  }
976
+ if (observation.leaseExpiresAt) {
977
+ const expiry = observation.leaseExpiresAt();
978
+ if (!expiry || !renewalConfirmed(renewal, performance.now(), expiry)) {
979
+ stop("check confirmation has no current authority");
980
+ return;
981
+ }
982
+ }
901
983
  child.stdin.end("admitted\n");
902
984
  clock.sample(outputCursor);
903
985
  const renew = () => {
@@ -926,7 +1008,7 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
926
1008
  if (expiry <= Date.now())
927
1009
  throw Object.assign(new Error("Check renewal receipt expired before acknowledgement"), { code: "process_maintenance_timeout" });
928
1010
  }
929
- if (!renewalRemaining(renewal) || !renewalConfirmed(renewal)) {
1011
+ if (!renewalConfirmed(renewal, performance.now(), typeof receipt === "object" && receipt.owned ? receipt.lease_expires_at : null)) {
930
1012
  stop("managed process renewal budget exhausted");
931
1013
  return;
932
1014
  }
@@ -1,12 +1,19 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { getCliBuildInfo } from "./build-info.js";
3
3
  import { classifyCliOperation } from "./cli-operation-classifier.js";
4
- import { resolveOperationProjectRoot, selectEngine } from "./engines.js";
5
- export function preflightCliCompatibility(context, args, env = process.env, resolvedProjectRoot) {
4
+ import { parseCommandArgs } from "./lifecycle-command.js";
5
+ export async function preflightCliCompatibility(context, args, env = process.env, resolvedProjectRoot) {
6
6
  const classification = classifyCliOperation(args, env);
7
7
  if (classification.mode === "router_native") {
8
8
  return { ok: true, classification, compatibility: null };
9
9
  }
10
+ // Internal scope-free ownership maintenance has no project compatibility
11
+ // work. Keep the engine/protocol/canon graph out of that cold path.
12
+ const parsed = parseCommandArgs(args);
13
+ if (resolvedProjectRoot === null && parsed.positional[0] === "runtime" && parsed.positional[1] === "process"
14
+ && !parsed.options.has("project-root") && !parsed.options.has("root"))
15
+ return { ok: true, classification, compatibility: null };
16
+ const { resolveOperationProjectRoot, selectEngine } = await import("./engines.js");
10
17
  const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
11
18
  if (!projectRoot) {
12
19
  return { ok: true, classification, compatibility: null };
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { spawnSync } from "node:child_process";
3
+ import { commandResult } from '../harness-runtime/lib/process-json.mjs';
4
+ import { SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
4
5
  import { isActiveProtocolStatus, isActiveQueueStatus, isQueueHistoryStatus } from "../domain/contracts.js";
5
6
  import { AppError } from "../shared/errors.js";
6
7
  import { prepareOutputFile } from "../cli/input-preparation.js";
@@ -14,9 +15,9 @@ import { activeFlowSessionsForProject } from "./sessions.js";
14
15
  import { usageForRun } from "./usage.js";
15
16
  import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
16
17
  import { planSummary, planWithProgress } from "./plan-runtime.js";
17
- export function getCmuxStatus(context, input) {
18
+ export async function getCmuxStatus(context, input) {
18
19
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
19
- return { ok: true, project_root: project.root, cmux: detectCmux(context) };
20
+ return { ok: true, project_root: project.root, cmux: await detectCmux(context) };
20
21
  }
21
22
  export function prepareDashboardRender(context, input) {
22
23
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -65,7 +66,7 @@ export function renderGlobalDashboard(context, input = {}) {
65
66
  writeMarkdown(output, markdown);
66
67
  return { ok: true, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
67
68
  }
68
- export function openDashboard(context, input) {
69
+ export async function openDashboard(context, input) {
69
70
  prepareDashboardOptions(input);
70
71
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
71
72
  const config = readProjectConfig(context, project.id);
@@ -75,20 +76,20 @@ export function openDashboard(context, input) {
75
76
  ok: true,
76
77
  project_root: project.root,
77
78
  dashboard: { path: dashboardPath },
78
- open: openCmuxDashboard(context, project.root, dashboardPath, format)
79
+ open: await openCmuxDashboard(context, project.root, dashboardPath, format)
79
80
  };
80
81
  }
81
- export function openGlobalDashboard(context, input = {}) {
82
+ export async function openGlobalDashboard(context, input = {}) {
82
83
  prepareDashboardOptions(input);
83
84
  const format = parseDashboardFormat(input.format, "html");
84
85
  const rendered = renderGlobalDashboard(context, { format });
85
86
  return {
86
87
  ok: true,
87
88
  dashboard: rendered.dashboard,
88
- open: openDashboardPath(context, rendered.dashboard.path, format)
89
+ open: await openDashboardPath(context, rendered.dashboard.path, format)
89
90
  };
90
91
  }
91
- export function openProtocolDashboard(context, input) {
92
+ export async function openProtocolDashboard(context, input) {
92
93
  prepareDashboardOptions(input);
93
94
  const format = parseDashboardFormat(input.format, "html");
94
95
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -103,7 +104,7 @@ export function openProtocolDashboard(context, input) {
103
104
  project_root: project.root,
104
105
  protocol_id: input.protocol,
105
106
  dashboard: rendered.dashboard,
106
- open: openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
107
+ open: await openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
107
108
  };
108
109
  }
109
110
  export function prepareDashboardRefresh(context, input) {
@@ -138,7 +139,7 @@ function prepareRefreshOutputs(context, project, config, format, protocol) {
138
139
  prepareOutputFile(globalDashboardJsonPath(context));
139
140
  }
140
141
  }
141
- export function refreshDashboard(context, input) {
142
+ export async function refreshDashboard(context, input) {
142
143
  const { project, config, format, openMode } = prepareDashboardRefresh(context, input);
143
144
  if (!config.dashboard.project) {
144
145
  return {
@@ -163,7 +164,7 @@ export function refreshDashboard(context, input) {
163
164
  dashboard: rendered.dashboard,
164
165
  project_summary: projectSummary,
165
166
  global_dashboard: global.dashboard,
166
- open: shouldOpen ? openCmuxDashboard(context, project.root, rendered.dashboard.path, format) : { ok: true, skipped: true, reason: "open_disabled" }
167
+ open: shouldOpen ? await openCmuxDashboard(context, project.root, rendered.dashboard.path, format) : { ok: true, skipped: true, reason: "open_disabled" }
167
168
  };
168
169
  }
169
170
  export function refreshGlobalDashboard(context, input = {}) {
@@ -269,13 +270,13 @@ export function dashboardData(context, input) {
269
270
  }
270
271
  return { ok: true, dashboard: buildProjectDashboardData(context, project, projectDashboardHtmlPath(context, project.id)) };
271
272
  }
272
- function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdown") {
273
+ async function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdown") {
273
274
  const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
274
275
  const config = readProjectConfig(context, project.id);
275
276
  if (config.integrations.cmux.mode === "off") {
276
277
  return { ok: true, skipped: true, reason: "cmux_off" };
277
278
  }
278
- const cmux = detectCmux(context);
279
+ const cmux = await detectCmux(context);
279
280
  if (!cmux.available) {
280
281
  if (config.integrations.cmux.mode === "required") {
281
282
  throw new AppError("cmux_unavailable", "cmux is required by project config but is unavailable", 1, { cmux });
@@ -286,20 +287,20 @@ function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdo
286
287
  return { ok: true, opened: false, dry_run: true, command: cmuxOpenCommand(format, dashboardPath) };
287
288
  }
288
289
  const command = cmuxOpenCommand(format, dashboardPath);
289
- const result = spawnSync(command[0], command.slice(1), { encoding: "utf8" });
290
- if (result.status !== 0) {
290
+ const result = await commandResult(command[0], command.slice(1), { startupMs: SETUP_STARTUP_MS });
291
+ if (result.exit_code !== 0) {
291
292
  if (config.integrations.cmux.mode === "required") {
292
293
  throw new AppError("cmux_open_failed", "cmux markdown open failed", 1, {
293
- status: result.status,
294
+ status: result.exit_code,
294
295
  stderr: result.stderr.trim()
295
296
  });
296
297
  }
297
- return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.status, stderr: result.stderr.trim() };
298
+ return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.exit_code, stderr: result.stderr.trim() };
298
299
  }
299
300
  return { ok: true, opened: true, command };
300
301
  }
301
- function openDashboardPath(context, dashboardPath, format) {
302
- const cmux = detectCmux(context);
302
+ async function openDashboardPath(context, dashboardPath, format) {
303
+ const cmux = await detectCmux(context);
303
304
  if (!cmux.available) {
304
305
  return { ok: true, skipped: true, reason: "cmux_unavailable", cmux };
305
306
  }
@@ -307,9 +308,9 @@ function openDashboardPath(context, dashboardPath, format) {
307
308
  return { ok: true, opened: false, dry_run: true, command: cmuxOpenCommand(format, dashboardPath) };
308
309
  }
309
310
  const command = cmuxOpenCommand(format, dashboardPath);
310
- const result = spawnSync(command[0], command.slice(1), { encoding: "utf8" });
311
- if (result.status !== 0) {
312
- return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.status, stderr: result.stderr.trim() };
311
+ const result = await commandResult(command[0], command.slice(1), { startupMs: SETUP_STARTUP_MS });
312
+ if (result.exit_code !== 0) {
313
+ return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.exit_code, stderr: result.stderr.trim() };
313
314
  }
314
315
  return { ok: true, opened: true, command };
315
316
  }
@@ -318,17 +319,23 @@ function cmuxOpenCommand(format, dashboardPath) {
318
319
  ? ["cmux", "browser", "open", pathToFileUrl(dashboardPath)]
319
320
  : ["cmux", "markdown", "open", dashboardPath];
320
321
  }
321
- function detectCmux(context) {
322
+ async function detectCmux(context) {
322
323
  if (context.env.DD_FLOW_CMUX_AVAILABLE === "true") {
323
324
  return { available: true, source: "env" };
324
325
  }
325
326
  if (context.env.DD_FLOW_CMUX_AVAILABLE === "false") {
326
327
  return { available: false, source: "env", reason: "forced_unavailable" };
327
328
  }
328
- const result = spawnSync("cmux", ["--version"], { encoding: "utf8" });
329
- return result.status === 0
329
+ let result;
330
+ try {
331
+ result = await commandResult("cmux", ["--version"], { startupMs: SETUP_STARTUP_MS, phase: 'native-version' });
332
+ }
333
+ catch (error) {
334
+ return { available: false, source: 'path', reason: error instanceof Error ? error.message : String(error) };
335
+ }
336
+ return result.exit_code === 0
330
337
  ? { available: true, source: "path", version: result.stdout.trim() }
331
- : { available: false, source: "path", reason: result.error?.message ?? result.stderr.trim() ?? "cmux not found" };
338
+ : { available: false, source: "path", reason: result.stderr.trim() || "cmux not found" };
332
339
  }
333
340
  export function prepareDashboardOptions(input) {
334
341
  if (input.format !== undefined)