@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80
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 +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1342 -375
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +553 -285
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +342 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +65 -23
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +707 -156
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { callDaemon } from "../harness-runtime/lib/dd-codex-daemon.mjs";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand } from "./lifecycle-command.js";
|
|
5
|
+
/** The hook asks the existing daemon about native evidence; it cannot mint it. */
|
|
6
|
+
export async function verifyCodexHookDelivery(env, payload) {
|
|
7
|
+
const command = lifecycleCommandFromPayload(payload);
|
|
8
|
+
const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
|
|
9
|
+
if (parsed.kind === "none")
|
|
10
|
+
return undefined;
|
|
11
|
+
const managed = commandOption(parsed.invocation, "invocation-id") || env.DD_FLOW_DAEMON_ID;
|
|
12
|
+
if (!managed)
|
|
13
|
+
return undefined; // Explicit legacy/unmanaged transport.
|
|
14
|
+
if (!env.DD_FLOW_CODEX_STATE_DIR || !env.DD_FLOW_DAEMON_ID)
|
|
15
|
+
throw new AppError("native_hook_unproven", "Managed hook has no owning native daemon", 1, { effect: "no_effect" });
|
|
16
|
+
const proof = await callDaemon(env.DD_FLOW_CODEX_STATE_DIR, "hook.verify", {
|
|
17
|
+
daemonId: env.DD_FLOW_DAEMON_ID, rootSessionId: payload.session_id,
|
|
18
|
+
sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id
|
|
19
|
+
}, 5000);
|
|
20
|
+
if (typeof proof.projectRoot !== "string"
|
|
21
|
+
|| typeof proof.ddFlowHome !== "string" || !env.DD_FLOW_HOME || path.resolve(env.DD_FLOW_HOME) !== path.resolve(proof.ddFlowHome)
|
|
22
|
+
|| proof.daemonId !== env.DD_FLOW_DAEMON_ID || typeof proof.rootSessionId !== "string" || typeof proof.sessionId !== "string"
|
|
23
|
+
|| typeof proof.toolCallId !== "string" || !(proof.parentSessionId === null || typeof proof.parentSessionId === "string")) {
|
|
24
|
+
throw new AppError("native_hook_unproven", "Native hook proof differs from the requested project/runtime", 1, { effect: "no_effect" });
|
|
25
|
+
}
|
|
26
|
+
return { daemonId: proof.daemonId, rootSessionId: proof.rootSessionId, sessionId: proof.sessionId,
|
|
27
|
+
parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command };
|
|
28
|
+
}
|
package/dist/services/config.js
CHANGED
|
@@ -28,7 +28,8 @@ export function getProjectConfigStatus(context, project) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
export function setProjectConfigValue(context, project, input) {
|
|
31
|
-
const value =
|
|
31
|
+
const value = prepareProjectConfigValue(input.key, input.value);
|
|
32
|
+
migrateLegacyConfig(context, project.id);
|
|
32
33
|
const now = context.now();
|
|
33
34
|
context.db.run(`INSERT INTO project_config (project_id, key, value_json, created_at, updated_at)
|
|
34
35
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -38,13 +39,15 @@ export function setProjectConfigValue(context, project, input) {
|
|
|
38
39
|
return getProjectConfigStatus(context, project);
|
|
39
40
|
}
|
|
40
41
|
export function readProjectConfig(context, projectId) {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
migrateLegacyConfig(context, projectId);
|
|
42
|
+
// Project legacy values without changing storage. Explicit writes migrate
|
|
43
|
+
// them; a canonical override always wins over its legacy alias.
|
|
44
|
+
const rows = configOverrides(context, projectId), keys = new Set(rows.map(row => row.key));
|
|
45
45
|
const config = structuredClone(defaultProjectConfig);
|
|
46
|
-
for (const row of
|
|
47
|
-
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const key = legacyConfigKey(row.key);
|
|
48
|
+
if (key !== row.key && keys.has(key))
|
|
49
|
+
continue;
|
|
50
|
+
applyConfigValue(config, key, JSON.parse(row.value_json));
|
|
48
51
|
}
|
|
49
52
|
return config;
|
|
50
53
|
}
|
|
@@ -89,7 +92,7 @@ export function globalDashboardHtmlPath(context) {
|
|
|
89
92
|
function configOverrides(context, projectId) {
|
|
90
93
|
return context.db.all("SELECT key, value_json, updated_at FROM project_config WHERE project_id = ? ORDER BY key ASC", [projectId]);
|
|
91
94
|
}
|
|
92
|
-
function
|
|
95
|
+
export function prepareProjectConfigValue(key, value) {
|
|
93
96
|
if (key === "integrations.cmux.mode") {
|
|
94
97
|
if (!cmuxModes.includes(value)) {
|
|
95
98
|
throw new AppError("validation", "--value for integrations.cmux.mode must be off, auto, or required", 2);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
3
|
+
import { getFlowRunVariables } from "./runs.js";
|
|
4
|
+
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
|
+
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
|
+
import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
|
+
/** Stage entry acknowledges the packet before the controller issues child commands. */
|
|
8
|
+
export function controllerStageEntryPrompt(stage, command, responseFile) {
|
|
9
|
+
return [
|
|
10
|
+
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
11
|
+
command,
|
|
12
|
+
`Read the complete authoritative packet saved to ${JSON.stringify(responseFile)}, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.`,
|
|
13
|
+
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
14
|
+
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
15
|
+
"Execute each runtime-issued lifecycle operation and target as one standalone call. Internal invocation IDs are intentionally absent; never add, copy or invent one. For a no-effect input rejection, correct the input and use its returned retry_command. If a required command is missing or no safe retry is returned, report the exact error and end the Turn so the controller can handle it."
|
|
16
|
+
].join("\n\n");
|
|
17
|
+
}
|
|
18
|
+
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
19
|
+
export async function nextControllerFanout(context, input) {
|
|
20
|
+
const children = input.children.filter(child => child.status !== "settled_by_root");
|
|
21
|
+
const observations = { harness_id: input.harness, parent_session_id: input.sessionId, children };
|
|
22
|
+
if (children.length) {
|
|
23
|
+
const observed = observeVnextNativeChildren(context, { ...input, observations });
|
|
24
|
+
const issues = observed.issues;
|
|
25
|
+
if (issues.length)
|
|
26
|
+
throw new AppError("fanout_reconciliation_required", "Native children have unresolved lifecycle ownership", 1, { issues });
|
|
27
|
+
}
|
|
28
|
+
let fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
29
|
+
if (!fanout)
|
|
30
|
+
return { kind: "none" };
|
|
31
|
+
if (children.length) {
|
|
32
|
+
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
33
|
+
const issues = reconciled.issues.filter(issue => issue.code !== "child_check_in_progress");
|
|
34
|
+
if (issues.length)
|
|
35
|
+
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
36
|
+
if (children.some(child => child.status === "running" || child.status === "unknown"))
|
|
37
|
+
return { kind: "wait", reason: "native_children_unsettled" };
|
|
38
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
39
|
+
}
|
|
40
|
+
if (fanout.state === "dispatch_required") {
|
|
41
|
+
dispatchVnextPlanReview(context, input);
|
|
42
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
43
|
+
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
44
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
45
|
+
}
|
|
46
|
+
const prompt = (text) => ({ kind: "prompt", prompt: text, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, works: fanout.works, text }) });
|
|
47
|
+
if (fanout.state === "coordinator_required")
|
|
48
|
+
return prompt(`Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
49
|
+
if (fanout.works.failed || fanout.works.cancelled)
|
|
50
|
+
return prompt(`Some ${input.stage} child Work failed or was cancelled. Inspect its recorded receipts. Choose a legal scoped repair/retry or block the Stage with the concrete reason. Do not relaunch settled siblings or infer success from a child that omitted work finish.`);
|
|
51
|
+
if (fanout.works.ready.length) {
|
|
52
|
+
const choices = fanout.works.ready.map(work => {
|
|
53
|
+
const payload = context.db.get("SELECT payload_json FROM works WHERE work_id = ?", [work.work_id])?.payload_json;
|
|
54
|
+
const override = payload ? JSON.parse(payload).execution_override : undefined;
|
|
55
|
+
return { work, resolved: resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: input.policy, profiles: input.profiles, stage: input.stage, ...(override ? { work: override } : {}) }) };
|
|
56
|
+
});
|
|
57
|
+
const external = choices.filter(choice => choice.resolved.delegation.mode === "external");
|
|
58
|
+
if (external.length) {
|
|
59
|
+
const maximum = Math.min(...external.map(choice => choice.resolved.delegation.max_parallel ?? 1));
|
|
60
|
+
return { kind: "external", works: external.slice(0, maximum).map(choice => choice.work) };
|
|
61
|
+
}
|
|
62
|
+
const variables = getFlowRunVariables(context, input);
|
|
63
|
+
const capacity = variables.variables[subagentCapacityKey];
|
|
64
|
+
if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1)
|
|
65
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
66
|
+
const wave = choices.slice(0, capacity).map(choice => choice.work);
|
|
67
|
+
for (const work of wave)
|
|
68
|
+
if (!work.work_id || !work.start_command)
|
|
69
|
+
throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
|
|
70
|
+
return prompt(renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave }));
|
|
71
|
+
}
|
|
72
|
+
if (fanout.works.running)
|
|
73
|
+
return { kind: "wait", reason: "work_receipts_pending" };
|
|
74
|
+
if (fanout.works.created)
|
|
75
|
+
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
76
|
+
return prompt(`All declared ${input.stage} child Work has settled (${fanout.works.completed} completed). Read the accepted result receipts and continue this Stage's semantic decision under its original packet. A rejected stage finish may return either a repair creation command or an already-registered repair Work. Execute only a returned repair creation command, then return. If it reports repair_required with an existing repair Work/start_command, do not invoke that start_command in this coordinator Session: return immediately so the controller can launch a fresh child. Do not start a successor Stage.`);
|
|
77
|
+
}
|
|
78
|
+
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
79
|
+
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
80
|
+
const candidates = [];
|
|
81
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
82
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
83
|
+
const text = (value, keys) => { for (const key of keys)
|
|
84
|
+
if (typeof value[key] === "string" && value[key])
|
|
85
|
+
return value[key]; return null; };
|
|
86
|
+
const settledTree = new Set(list(receipt.tree_observations)
|
|
87
|
+
.filter(value => object(value).settled === true)
|
|
88
|
+
.map(value => text(object(value), ["provider_session_id", "session_id"]))
|
|
89
|
+
.filter((id) => id !== null));
|
|
90
|
+
const add = (raw, source, scopedParent = null, fallback = "unknown") => {
|
|
91
|
+
const value = object(raw);
|
|
92
|
+
const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
|
|
93
|
+
const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
|
|
94
|
+
if (!id || parent !== rootSessionId)
|
|
95
|
+
return;
|
|
96
|
+
const state = String(value.status ?? value.state ?? fallback).toLowerCase();
|
|
97
|
+
const status = ["completed", "complete", "done", "idle", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
|
|
98
|
+
candidates.push({ session_id: id, parent_session_id: parent, status: status === "unknown" && settledTree.has(id) ? "settled_by_root" : status, source });
|
|
99
|
+
};
|
|
100
|
+
for (const value of list(receipt.descendants))
|
|
101
|
+
add(value, "adapter.descendants");
|
|
102
|
+
for (const value of list(receipt.children))
|
|
103
|
+
add(value, "opencode.session.children");
|
|
104
|
+
const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
|
|
105
|
+
for (const value of list(subagents.running))
|
|
106
|
+
add(value, "zcode/session/subagents", rootSessionId, "running");
|
|
107
|
+
for (const value of [...list(subagents.completed), ...list(object(subagents.ended).items)])
|
|
108
|
+
add(value, "zcode/session/subagents", rootSessionId, "completed");
|
|
109
|
+
const unique = new Map();
|
|
110
|
+
for (const child of candidates) {
|
|
111
|
+
const previous = unique.get(child.session_id);
|
|
112
|
+
if (!previous || previous.status === "unknown" || previous.status === "running" && child.status !== "running")
|
|
113
|
+
unique.set(child.session_id, child);
|
|
114
|
+
}
|
|
115
|
+
return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
|
|
116
|
+
}
|
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { isActiveProtocolStatus, isActiveQueueStatus, isQueueHistoryStatus } from "../domain/contracts.js";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
7
|
+
import { ensureDir, resolveProjectRoot, resolveProtocolPlanPath } from "../storage/paths.js";
|
|
7
8
|
import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, projectSummaryJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
|
|
8
9
|
import { queueForProject } from "./merge-queue.js";
|
|
9
10
|
import { publishProjectSummary } from "./project-summary.js";
|
|
@@ -17,42 +18,58 @@ export function getCmuxStatus(context, input) {
|
|
|
17
18
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
18
19
|
return { ok: true, project_root: project.root, cmux: detectCmux(context) };
|
|
19
20
|
}
|
|
20
|
-
export function
|
|
21
|
+
export function prepareDashboardRender(context, input) {
|
|
21
22
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
23
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
24
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
22
25
|
const format = parseDashboardFormat(input.format);
|
|
26
|
+
if (input.protocol)
|
|
27
|
+
requireProtocol(context, input.protocol, project.id);
|
|
28
|
+
const config = readProjectConfig(context, project.id);
|
|
29
|
+
const output = input.output
|
|
30
|
+
? path.resolve(project.root, input.output)
|
|
31
|
+
: format === "html"
|
|
32
|
+
? input.protocol ? protocolDashboardHtmlPath(context, project.id, input.protocol) : projectDashboardHtmlPath(context, project.id)
|
|
33
|
+
: dashboardMarkdownPath(project.root, config);
|
|
34
|
+
prepareOutputFile(output);
|
|
35
|
+
if (format === "html")
|
|
36
|
+
prepareOutputFile(input.protocol ? protocolDashboardJsonPath(context, project.id, input.protocol) : projectDashboardJsonPath(context, project.id));
|
|
37
|
+
return { project, format, output };
|
|
38
|
+
}
|
|
39
|
+
export function renderDashboard(context, input) {
|
|
40
|
+
const { project, format, output } = prepareDashboardRender(context, input);
|
|
23
41
|
if (format === "html") {
|
|
24
42
|
if (input.protocol) {
|
|
25
43
|
return renderProtocolDashboardHtml(context, project, input.protocol, input.output);
|
|
26
44
|
}
|
|
27
45
|
return renderProjectDashboardHtml(context, project, input.output);
|
|
28
46
|
}
|
|
29
|
-
const config = readProjectConfig(context, project.id);
|
|
30
|
-
const output = input.output
|
|
31
|
-
? path.resolve(path.isAbsolute(input.output) ? input.output : path.join(project.root, input.output))
|
|
32
|
-
: dashboardMarkdownPath(project.root, config);
|
|
33
47
|
const markdown = renderProjectDashboardMarkdown(context, project, output);
|
|
34
48
|
writeMarkdown(output, markdown);
|
|
35
49
|
return { ok: true, project_root: project.root, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
36
50
|
}
|
|
37
|
-
export function
|
|
51
|
+
export function prepareGlobalDashboardRender(context, input = {}) {
|
|
38
52
|
const format = parseDashboardFormat(input.format);
|
|
53
|
+
const output = input.output ? path.resolve(input.output) : format === "html" ? globalDashboardHtmlPath(context) : globalDashboardMarkdownPath(context);
|
|
54
|
+
prepareOutputFile(output);
|
|
55
|
+
if (format === "html")
|
|
56
|
+
prepareOutputFile(globalDashboardJsonPath(context));
|
|
57
|
+
return { format, output };
|
|
58
|
+
}
|
|
59
|
+
export function renderGlobalDashboard(context, input = {}) {
|
|
60
|
+
const { format, output } = prepareGlobalDashboardRender(context, input);
|
|
39
61
|
if (format === "html") {
|
|
40
62
|
return renderGlobalDashboardHtml(context, input.output);
|
|
41
63
|
}
|
|
42
|
-
const output = input.output
|
|
43
|
-
? path.resolve(input.output)
|
|
44
|
-
: globalDashboardMarkdownPath(context);
|
|
45
64
|
const markdown = renderGlobalDashboardMarkdown(context, output);
|
|
46
65
|
writeMarkdown(output, markdown);
|
|
47
66
|
return { ok: true, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
48
67
|
}
|
|
49
68
|
export function openDashboard(context, input) {
|
|
69
|
+
prepareDashboardOptions(input);
|
|
50
70
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
51
71
|
const config = readProjectConfig(context, project.id);
|
|
52
72
|
const format = parseDashboardFormat(input.format, "html");
|
|
53
|
-
if (input.viewer && input.viewer !== "cmux") {
|
|
54
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
55
|
-
}
|
|
56
73
|
const dashboardPath = format === "html" ? projectDashboardHtmlPath(context, project.id) : dashboardMarkdownPath(project.root, config);
|
|
57
74
|
return {
|
|
58
75
|
ok: true,
|
|
@@ -62,9 +79,7 @@ export function openDashboard(context, input) {
|
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
export function openGlobalDashboard(context, input = {}) {
|
|
65
|
-
|
|
66
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
67
|
-
}
|
|
82
|
+
prepareDashboardOptions(input);
|
|
68
83
|
const format = parseDashboardFormat(input.format, "html");
|
|
69
84
|
const rendered = renderGlobalDashboard(context, { format });
|
|
70
85
|
return {
|
|
@@ -74,9 +89,7 @@ export function openGlobalDashboard(context, input = {}) {
|
|
|
74
89
|
};
|
|
75
90
|
}
|
|
76
91
|
export function openProtocolDashboard(context, input) {
|
|
77
|
-
|
|
78
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
79
|
-
}
|
|
92
|
+
prepareDashboardOptions(input);
|
|
80
93
|
const format = parseDashboardFormat(input.format, "html");
|
|
81
94
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
82
95
|
if (format !== "html") {
|
|
@@ -93,14 +106,40 @@ export function openProtocolDashboard(context, input) {
|
|
|
93
106
|
open: openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
|
-
export function
|
|
109
|
+
export function prepareDashboardRefresh(context, input) {
|
|
110
|
+
prepareDashboardOptions(input);
|
|
97
111
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
98
112
|
const config = readProjectConfig(context, project.id);
|
|
99
113
|
const format = parseDashboardFormat(input.format);
|
|
100
114
|
const openMode = input.open ?? "auto";
|
|
101
|
-
if (
|
|
102
|
-
|
|
115
|
+
if (config.dashboard.project) {
|
|
116
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
117
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
118
|
+
if (input.protocol)
|
|
119
|
+
requireProtocol(context, input.protocol, project.id);
|
|
120
|
+
prepareRefreshOutputs(context, project, config, format, input.protocol);
|
|
103
121
|
}
|
|
122
|
+
return { project, config, format, openMode };
|
|
123
|
+
}
|
|
124
|
+
function prepareRefreshOutputs(context, project, config, format, protocol) {
|
|
125
|
+
if (config.dashboard.project) {
|
|
126
|
+
if (!protocol)
|
|
127
|
+
prepareOutputFile(projectSummaryJsonPath(context, project.id));
|
|
128
|
+
if (format === "html") {
|
|
129
|
+
prepareOutputFile(protocol ? protocolDashboardHtmlPath(context, project.id, protocol) : projectDashboardHtmlPath(context, project.id));
|
|
130
|
+
prepareOutputFile(protocol ? protocolDashboardJsonPath(context, project.id, protocol) : projectDashboardJsonPath(context, project.id));
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
prepareOutputFile(dashboardMarkdownPath(project.root, config));
|
|
134
|
+
}
|
|
135
|
+
if (config.dashboard.global) {
|
|
136
|
+
prepareOutputFile(format === "html" ? globalDashboardHtmlPath(context) : globalDashboardMarkdownPath(context, config));
|
|
137
|
+
if (format === "html")
|
|
138
|
+
prepareOutputFile(globalDashboardJsonPath(context));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function refreshDashboard(context, input) {
|
|
142
|
+
const { project, config, format, openMode } = prepareDashboardRefresh(context, input);
|
|
104
143
|
if (!config.dashboard.project) {
|
|
105
144
|
return {
|
|
106
145
|
ok: true,
|
|
@@ -143,8 +182,9 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
143
182
|
return { ok: true, project_id: project.id, project_root: project.root, skipped: true, reason: "dashboard_project_disabled" };
|
|
144
183
|
}
|
|
145
184
|
try {
|
|
146
|
-
|
|
185
|
+
prepareRefreshOutputs(context, project, { ...config, dashboard: { ...config.dashboard, global: false } }, format);
|
|
147
186
|
const rendered = renderDashboard(context, { projectRoot: project.root, format });
|
|
187
|
+
const projectSummary = publishProjectSummary(context, { project });
|
|
148
188
|
return { ok: true, project_id: project.id, project_root: project.root, dashboard: rendered.dashboard, project_summary: projectSummary };
|
|
149
189
|
}
|
|
150
190
|
catch (error) {
|
|
@@ -170,10 +210,15 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
170
210
|
}
|
|
171
211
|
export function autoRefreshDashboards(context, input) {
|
|
172
212
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
213
|
+
// Hooks and usage ingestion continue to record receipts after capture.
|
|
214
|
+
// Their derived timestamp must not mutate the captured workspace.
|
|
215
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
216
|
+
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
173
217
|
const config = readProjectConfig(context, project.id);
|
|
174
218
|
if (!config.dashboard.auto_refresh) {
|
|
175
219
|
return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
|
|
176
220
|
}
|
|
221
|
+
prepareRefreshOutputs(context, project, config, "markdown");
|
|
177
222
|
const result = { ok: true, project_root: project.root };
|
|
178
223
|
if (config.dashboard.project) {
|
|
179
224
|
publishProjectSummary(context, { project });
|
|
@@ -196,6 +241,9 @@ export function autoRefreshDashboards(context, input) {
|
|
|
196
241
|
}
|
|
197
242
|
return result;
|
|
198
243
|
}
|
|
244
|
+
function projectRecoveryGuarded(context, projectId) {
|
|
245
|
+
return Boolean(context.db.get("SELECT 1 FROM run_recovery_guards WHERE project_id = ? AND status IN ('draining', 'sealed', 'resuming') LIMIT 1", [projectId]));
|
|
246
|
+
}
|
|
199
247
|
export function dashboardSummary(context, projectId, projectRoot) {
|
|
200
248
|
const config = readProjectConfig(context, projectId);
|
|
201
249
|
return {
|
|
@@ -282,6 +330,14 @@ function detectCmux(context) {
|
|
|
282
330
|
? { available: true, source: "path", version: result.stdout.trim() }
|
|
283
331
|
: { available: false, source: "path", reason: result.error?.message ?? result.stderr.trim() ?? "cmux not found" };
|
|
284
332
|
}
|
|
333
|
+
export function prepareDashboardOptions(input) {
|
|
334
|
+
if (input.format !== undefined)
|
|
335
|
+
parseDashboardFormat(input.format);
|
|
336
|
+
if (input.viewer !== undefined && input.viewer !== "cmux")
|
|
337
|
+
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
338
|
+
if (input.open !== undefined && !["auto", "true", "false"].includes(input.open))
|
|
339
|
+
throw new AppError("validation", "--open must be auto, true, or false", 2);
|
|
340
|
+
}
|
|
285
341
|
function parseDashboardFormat(value, defaultFormat = "markdown") {
|
|
286
342
|
const format = value ?? defaultFormat;
|
|
287
343
|
if (format !== "markdown" && format !== "html") {
|
|
@@ -294,15 +350,18 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
294
350
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
295
351
|
: projectDashboardHtmlPath(context, project.id);
|
|
296
352
|
const jsonPath = projectDashboardJsonPath(context, project.id);
|
|
353
|
+
prepareOutputFile(htmlPath);
|
|
354
|
+
prepareOutputFile(jsonPath);
|
|
297
355
|
const data = buildProjectDashboardData(context, project, htmlPath);
|
|
298
356
|
const html = renderDashboardHtmlPage(data);
|
|
299
|
-
writeJsonFile(jsonPath, data);
|
|
300
|
-
writeTextFile(htmlPath, html);
|
|
301
357
|
const protocolCards = Array.isArray(data.protocol_cards) ? data.protocol_cards : [];
|
|
302
|
-
const
|
|
358
|
+
const preparedPages = protocolCards
|
|
303
359
|
.filter((card) => card.generate_page !== false)
|
|
304
360
|
.slice(0, 24)
|
|
305
|
-
.map((card) =>
|
|
361
|
+
.map((card) => prepareProtocolDashboardHtml(context, project, String(card.id)));
|
|
362
|
+
writeJsonFile(jsonPath, data);
|
|
363
|
+
writeTextFile(htmlPath, html);
|
|
364
|
+
const protocolPages = preparedPages.map(writeProtocolDashboardHtml)
|
|
306
365
|
.map((result) => ({
|
|
307
366
|
id: String(result.protocol_id),
|
|
308
367
|
path: result.dashboard.path,
|
|
@@ -321,12 +380,23 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
321
380
|
};
|
|
322
381
|
}
|
|
323
382
|
function renderProtocolDashboardHtml(context, project, protocolId, output) {
|
|
383
|
+
return writeProtocolDashboardHtml(prepareProtocolDashboardHtml(context, project, protocolId, output));
|
|
384
|
+
}
|
|
385
|
+
function prepareProtocolDashboardHtml(context, project, protocolId, output) {
|
|
386
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
387
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
324
388
|
const htmlPath = output
|
|
325
389
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
326
390
|
: protocolDashboardHtmlPath(context, project.id, protocolId);
|
|
327
391
|
const jsonPath = protocolDashboardJsonPath(context, project.id, protocolId);
|
|
392
|
+
prepareOutputFile(htmlPath);
|
|
393
|
+
prepareOutputFile(jsonPath);
|
|
328
394
|
const data = buildProtocolDashboardData(context, project, protocolId, htmlPath);
|
|
329
395
|
const html = renderDashboardHtmlPage(data);
|
|
396
|
+
return { project, protocolId, htmlPath, jsonPath, data, html };
|
|
397
|
+
}
|
|
398
|
+
function writeProtocolDashboardHtml(prepared) {
|
|
399
|
+
const { project, protocolId, htmlPath, jsonPath, data, html } = prepared;
|
|
330
400
|
writeJsonFile(jsonPath, data);
|
|
331
401
|
writeTextFile(htmlPath, html);
|
|
332
402
|
return {
|
|
@@ -344,6 +414,8 @@ function renderProtocolDashboardHtml(context, project, protocolId, output) {
|
|
|
344
414
|
function renderGlobalDashboardHtml(context, output) {
|
|
345
415
|
const htmlPath = output ? path.resolve(output) : globalDashboardHtmlPath(context);
|
|
346
416
|
const jsonPath = globalDashboardJsonPath(context);
|
|
417
|
+
prepareOutputFile(htmlPath);
|
|
418
|
+
prepareOutputFile(jsonPath);
|
|
347
419
|
const data = buildGlobalDashboardData(context, htmlPath);
|
|
348
420
|
const html = renderDashboardHtmlPage(data);
|
|
349
421
|
writeJsonFile(jsonPath, data);
|
|
@@ -888,12 +960,12 @@ function protocolsForProject(context, projectId) {
|
|
|
888
960
|
FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
|
|
889
961
|
}
|
|
890
962
|
function planSummaryForProtocol(context, projectId, protocolId) {
|
|
891
|
-
const protocol = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
963
|
+
const protocol = context.db.get("SELECT plan_path, project_root FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
892
964
|
if (!protocol) {
|
|
893
965
|
return { plan_id: "none", total: 0, done: 0, blocked: 0 };
|
|
894
966
|
}
|
|
895
967
|
try {
|
|
896
|
-
const summary = planSummary(context, { projectId, protocolId, planPath: protocol.plan_path }, { bind: false });
|
|
968
|
+
const summary = planSummary(context, { projectId, protocolId, planPath: resolveProtocolPlanPath(protocol.project_root, protocolId, protocol.plan_path) }, { bind: false });
|
|
897
969
|
return { plan_id: summary.plan_id, total: summary.total, done: summary.done, blocked: summary.blocked };
|
|
898
970
|
}
|
|
899
971
|
catch {
|
|
@@ -903,13 +975,13 @@ function planSummaryForProtocol(context, projectId, protocolId) {
|
|
|
903
975
|
function activePlanItems(context, projectId, protocols) {
|
|
904
976
|
const items = [];
|
|
905
977
|
for (const protocol of protocols) {
|
|
906
|
-
const record = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocol.id]);
|
|
978
|
+
const record = context.db.get("SELECT plan_path, project_root FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocol.id]);
|
|
907
979
|
if (!record) {
|
|
908
980
|
continue;
|
|
909
981
|
}
|
|
910
982
|
let plan;
|
|
911
983
|
try {
|
|
912
|
-
plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: record.plan_path }, { bind: false }).plan;
|
|
984
|
+
plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: resolveProtocolPlanPath(record.project_root, protocol.id, record.plan_path) }, { bind: false }).plan;
|
|
913
985
|
}
|
|
914
986
|
catch {
|
|
915
987
|
continue;
|