@deksden-com/dd-flow-cli 0.9.0-beta.127 → 0.9.0-beta.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +2 -2
- package/dist/cli/run-cli.js +6 -6
- package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
- package/dist/cli/runtime-maintenance-ingress.js +5 -4
- package/dist/cli/runtime-process.js +27 -2
- package/dist/cli.js +7 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
- package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
- package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +41 -9
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +113 -31
- package/dist/services/compatibility-preflight.js +9 -2
- package/dist/services/dashboard.js +33 -26
- package/dist/services/engines.js +88 -57
- package/dist/services/lifecycle-invocations.js +4 -2
- package/dist/services/managed-lease-monitor.js +9 -4
- package/dist/services/prompts.js +3 -1
- package/dist/services/protocols.js +3 -1
- package/dist/services/repair-intents.js +4 -2
- package/dist/services/run-control-worker.js +2 -1
- package/dist/services/run-controller.js +20 -17
- package/dist/services/run-fork.js +2 -2
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-lifecycle.js +13 -7
- package/dist/services/tooling.js +32 -23
- package/dist/services/vnext-code-review.js +2 -0
- package/dist/services/vnext-code.js +5 -1
- package/dist/services/vnext-merge.js +4 -1
- package/dist/services/vnext-protocolize.js +17 -16
- package/dist/services/work-registry.js +3 -1
- package/dist/services/worktrees.js +16 -14
- package/package.json +1 -1
|
@@ -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 (
|
|
76
|
-
throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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) {
|
|
766
|
-
|
|
767
|
-
|
|
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", () => {
|
|
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 (!
|
|
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 {
|
|
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 {
|
|
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 =
|
|
290
|
-
if (result.
|
|
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.
|
|
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.
|
|
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 =
|
|
311
|
-
if (result.
|
|
312
|
-
return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.
|
|
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
|
-
|
|
329
|
-
|
|
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.
|
|
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)
|