@deksden-com/dd-flow-cli 0.9.0-beta.75 → 0.9.0-beta.79
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/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +84 -41
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +195 -92
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
1
2
|
export function classifyCliOperation(args, env = process.env) {
|
|
2
3
|
const positional = positionalArgs(args);
|
|
3
4
|
const [family = null, command = null, action = null] = positional;
|
|
@@ -14,19 +15,7 @@ export function classifyCliOperation(args, env = process.env) {
|
|
|
14
15
|
return { mode: "normal_write", operation, family, command, action };
|
|
15
16
|
}
|
|
16
17
|
function positionalArgs(args) {
|
|
17
|
-
|
|
18
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
-
const value = args[index];
|
|
20
|
-
if (!value)
|
|
21
|
-
continue;
|
|
22
|
-
if (value.startsWith("--")) {
|
|
23
|
-
if (!value.includes("=") && args[index + 1] && !args[index + 1]?.startsWith("--"))
|
|
24
|
-
index += 1;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
positional.push(value);
|
|
28
|
-
}
|
|
29
|
-
return positional;
|
|
18
|
+
return parseCommandArgs(args).positional;
|
|
30
19
|
}
|
|
31
20
|
export function isReadOnlyCliOperation(args, env = process.env) {
|
|
32
21
|
return classifyCliOperation(args, env).mode === "read_only_diagnostics";
|
|
@@ -57,7 +46,7 @@ function isMbUpgradeMode(args, env) {
|
|
|
57
46
|
}
|
|
58
47
|
return marked && isUpgradeAllowlisted(args);
|
|
59
48
|
}
|
|
60
|
-
function isUpgradeAllowlisted(args) {
|
|
49
|
+
export function isUpgradeAllowlisted(args) {
|
|
61
50
|
const positional = positionalArgs(args);
|
|
62
51
|
const [family, command] = positional;
|
|
63
52
|
if (family === "status")
|
|
@@ -82,14 +71,9 @@ function hasRunBinding(args) {
|
|
|
82
71
|
return Boolean(optionValue(args, "run") || positionalArgs(args).some((value) => value.startsWith("RUN-")));
|
|
83
72
|
}
|
|
84
73
|
function optionValue(args, name) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return args[index + 1] ?? null;
|
|
89
|
-
if (arg?.startsWith(`--${name}=`))
|
|
90
|
-
return arg.slice(name.length + 3);
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
74
|
+
const values = parseCommandArgs(args).options.get(name);
|
|
75
|
+
const value = values?.[values.length - 1];
|
|
76
|
+
return value && value !== "__dd_flow_flag__" ? value : null;
|
|
93
77
|
}
|
|
94
78
|
function isRouterNative(args) {
|
|
95
79
|
const first = args[0];
|
|
@@ -125,13 +109,17 @@ function isReadOnlyDiagnostic(family, command, action) {
|
|
|
125
109
|
if (family === "canon")
|
|
126
110
|
return command === "status" || command === "resolve";
|
|
127
111
|
if (family === "project")
|
|
128
|
-
return command === "status" || command === "resolve";
|
|
112
|
+
return command === "status" || command === "resolve" || (command === "config" && action === "status");
|
|
129
113
|
if (family === "protocol")
|
|
130
114
|
return ["status", "branch-status", "ready", "blockers", "implement"].includes(command ?? "");
|
|
131
115
|
if (family === "plan")
|
|
132
116
|
return command === "status";
|
|
133
117
|
if (family === "run")
|
|
134
|
-
return
|
|
118
|
+
return ["status", "list", "timeline"].includes(command ?? "") || (["config", "flags", "drive", "control"].includes(command ?? "") && action === "status") || (command === "vars" && ["ls", "get"].includes(action ?? "")) || (command === "recovery" && action === "inspect");
|
|
119
|
+
if (family === "runtime" && command === "process" && ["status", "check-admission"].includes(action ?? ""))
|
|
120
|
+
return true;
|
|
121
|
+
if (family === "runtime" && command === "scope" && action === "status")
|
|
122
|
+
return true;
|
|
135
123
|
if (family === "work")
|
|
136
124
|
return ["status", "ls", "show"].includes(command ?? "");
|
|
137
125
|
if (family === "stage")
|
|
@@ -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);
|
|
@@ -12,7 +12,7 @@ export function controllerStageEntryPrompt(stage, command, responseFile) {
|
|
|
12
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
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
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
|
-
"
|
|
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
16
|
].join("\n\n");
|
|
17
17
|
}
|
|
18
18
|
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
@@ -73,7 +73,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
73
73
|
return { kind: "wait", reason: "work_receipts_pending" };
|
|
74
74
|
if (fanout.works.created)
|
|
75
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.
|
|
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
77
|
}
|
|
78
78
|
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
79
79
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
@@ -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,44 +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));
|
|
22
23
|
if (projectRecoveryGuarded(context, project.id))
|
|
23
24
|
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
24
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);
|
|
25
41
|
if (format === "html") {
|
|
26
42
|
if (input.protocol) {
|
|
27
43
|
return renderProtocolDashboardHtml(context, project, input.protocol, input.output);
|
|
28
44
|
}
|
|
29
45
|
return renderProjectDashboardHtml(context, project, input.output);
|
|
30
46
|
}
|
|
31
|
-
const config = readProjectConfig(context, project.id);
|
|
32
|
-
const output = input.output
|
|
33
|
-
? path.resolve(path.isAbsolute(input.output) ? input.output : path.join(project.root, input.output))
|
|
34
|
-
: dashboardMarkdownPath(project.root, config);
|
|
35
47
|
const markdown = renderProjectDashboardMarkdown(context, project, output);
|
|
36
48
|
writeMarkdown(output, markdown);
|
|
37
49
|
return { ok: true, project_root: project.root, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
38
50
|
}
|
|
39
|
-
export function
|
|
51
|
+
export function prepareGlobalDashboardRender(context, input = {}) {
|
|
40
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);
|
|
41
61
|
if (format === "html") {
|
|
42
62
|
return renderGlobalDashboardHtml(context, input.output);
|
|
43
63
|
}
|
|
44
|
-
const output = input.output
|
|
45
|
-
? path.resolve(input.output)
|
|
46
|
-
: globalDashboardMarkdownPath(context);
|
|
47
64
|
const markdown = renderGlobalDashboardMarkdown(context, output);
|
|
48
65
|
writeMarkdown(output, markdown);
|
|
49
66
|
return { ok: true, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
50
67
|
}
|
|
51
68
|
export function openDashboard(context, input) {
|
|
69
|
+
prepareDashboardOptions(input);
|
|
52
70
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
53
71
|
const config = readProjectConfig(context, project.id);
|
|
54
72
|
const format = parseDashboardFormat(input.format, "html");
|
|
55
|
-
if (input.viewer && input.viewer !== "cmux") {
|
|
56
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
57
|
-
}
|
|
58
73
|
const dashboardPath = format === "html" ? projectDashboardHtmlPath(context, project.id) : dashboardMarkdownPath(project.root, config);
|
|
59
74
|
return {
|
|
60
75
|
ok: true,
|
|
@@ -64,9 +79,7 @@ export function openDashboard(context, input) {
|
|
|
64
79
|
};
|
|
65
80
|
}
|
|
66
81
|
export function openGlobalDashboard(context, input = {}) {
|
|
67
|
-
|
|
68
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
69
|
-
}
|
|
82
|
+
prepareDashboardOptions(input);
|
|
70
83
|
const format = parseDashboardFormat(input.format, "html");
|
|
71
84
|
const rendered = renderGlobalDashboard(context, { format });
|
|
72
85
|
return {
|
|
@@ -76,9 +89,7 @@ export function openGlobalDashboard(context, input = {}) {
|
|
|
76
89
|
};
|
|
77
90
|
}
|
|
78
91
|
export function openProtocolDashboard(context, input) {
|
|
79
|
-
|
|
80
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
81
|
-
}
|
|
92
|
+
prepareDashboardOptions(input);
|
|
82
93
|
const format = parseDashboardFormat(input.format, "html");
|
|
83
94
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
84
95
|
if (format !== "html") {
|
|
@@ -95,14 +106,40 @@ export function openProtocolDashboard(context, input) {
|
|
|
95
106
|
open: openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
|
|
96
107
|
};
|
|
97
108
|
}
|
|
98
|
-
export function
|
|
109
|
+
export function prepareDashboardRefresh(context, input) {
|
|
110
|
+
prepareDashboardOptions(input);
|
|
99
111
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
100
112
|
const config = readProjectConfig(context, project.id);
|
|
101
113
|
const format = parseDashboardFormat(input.format);
|
|
102
114
|
const openMode = input.open ?? "auto";
|
|
103
|
-
if (
|
|
104
|
-
|
|
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);
|
|
105
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);
|
|
106
143
|
if (!config.dashboard.project) {
|
|
107
144
|
return {
|
|
108
145
|
ok: true,
|
|
@@ -145,8 +182,9 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
145
182
|
return { ok: true, project_id: project.id, project_root: project.root, skipped: true, reason: "dashboard_project_disabled" };
|
|
146
183
|
}
|
|
147
184
|
try {
|
|
148
|
-
|
|
185
|
+
prepareRefreshOutputs(context, project, { ...config, dashboard: { ...config.dashboard, global: false } }, format);
|
|
149
186
|
const rendered = renderDashboard(context, { projectRoot: project.root, format });
|
|
187
|
+
const projectSummary = publishProjectSummary(context, { project });
|
|
150
188
|
return { ok: true, project_id: project.id, project_root: project.root, dashboard: rendered.dashboard, project_summary: projectSummary };
|
|
151
189
|
}
|
|
152
190
|
catch (error) {
|
|
@@ -180,6 +218,7 @@ export function autoRefreshDashboards(context, input) {
|
|
|
180
218
|
if (!config.dashboard.auto_refresh) {
|
|
181
219
|
return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
|
|
182
220
|
}
|
|
221
|
+
prepareRefreshOutputs(context, project, config, "markdown");
|
|
183
222
|
const result = { ok: true, project_root: project.root };
|
|
184
223
|
if (config.dashboard.project) {
|
|
185
224
|
publishProjectSummary(context, { project });
|
|
@@ -291,6 +330,14 @@ function detectCmux(context) {
|
|
|
291
330
|
? { available: true, source: "path", version: result.stdout.trim() }
|
|
292
331
|
: { available: false, source: "path", reason: result.error?.message ?? result.stderr.trim() ?? "cmux not found" };
|
|
293
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
|
+
}
|
|
294
341
|
function parseDashboardFormat(value, defaultFormat = "markdown") {
|
|
295
342
|
const format = value ?? defaultFormat;
|
|
296
343
|
if (format !== "markdown" && format !== "html") {
|
|
@@ -303,15 +350,18 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
303
350
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
304
351
|
: projectDashboardHtmlPath(context, project.id);
|
|
305
352
|
const jsonPath = projectDashboardJsonPath(context, project.id);
|
|
353
|
+
prepareOutputFile(htmlPath);
|
|
354
|
+
prepareOutputFile(jsonPath);
|
|
306
355
|
const data = buildProjectDashboardData(context, project, htmlPath);
|
|
307
356
|
const html = renderDashboardHtmlPage(data);
|
|
308
|
-
writeJsonFile(jsonPath, data);
|
|
309
|
-
writeTextFile(htmlPath, html);
|
|
310
357
|
const protocolCards = Array.isArray(data.protocol_cards) ? data.protocol_cards : [];
|
|
311
|
-
const
|
|
358
|
+
const preparedPages = protocolCards
|
|
312
359
|
.filter((card) => card.generate_page !== false)
|
|
313
360
|
.slice(0, 24)
|
|
314
|
-
.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)
|
|
315
365
|
.map((result) => ({
|
|
316
366
|
id: String(result.protocol_id),
|
|
317
367
|
path: result.dashboard.path,
|
|
@@ -330,12 +380,23 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
330
380
|
};
|
|
331
381
|
}
|
|
332
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);
|
|
333
388
|
const htmlPath = output
|
|
334
389
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
335
390
|
: protocolDashboardHtmlPath(context, project.id, protocolId);
|
|
336
391
|
const jsonPath = protocolDashboardJsonPath(context, project.id, protocolId);
|
|
392
|
+
prepareOutputFile(htmlPath);
|
|
393
|
+
prepareOutputFile(jsonPath);
|
|
337
394
|
const data = buildProtocolDashboardData(context, project, protocolId, htmlPath);
|
|
338
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;
|
|
339
400
|
writeJsonFile(jsonPath, data);
|
|
340
401
|
writeTextFile(htmlPath, html);
|
|
341
402
|
return {
|
|
@@ -353,6 +414,8 @@ function renderProtocolDashboardHtml(context, project, protocolId, output) {
|
|
|
353
414
|
function renderGlobalDashboardHtml(context, output) {
|
|
354
415
|
const htmlPath = output ? path.resolve(output) : globalDashboardHtmlPath(context);
|
|
355
416
|
const jsonPath = globalDashboardJsonPath(context);
|
|
417
|
+
prepareOutputFile(htmlPath);
|
|
418
|
+
prepareOutputFile(jsonPath);
|
|
356
419
|
const data = buildGlobalDashboardData(context, htmlPath);
|
|
357
420
|
const html = renderDashboardHtmlPage(data);
|
|
358
421
|
writeJsonFile(jsonPath, data);
|
|
@@ -897,12 +960,12 @@ function protocolsForProject(context, projectId) {
|
|
|
897
960
|
FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
|
|
898
961
|
}
|
|
899
962
|
function planSummaryForProtocol(context, projectId, protocolId) {
|
|
900
|
-
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]);
|
|
901
964
|
if (!protocol) {
|
|
902
965
|
return { plan_id: "none", total: 0, done: 0, blocked: 0 };
|
|
903
966
|
}
|
|
904
967
|
try {
|
|
905
|
-
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 });
|
|
906
969
|
return { plan_id: summary.plan_id, total: summary.total, done: summary.done, blocked: summary.blocked };
|
|
907
970
|
}
|
|
908
971
|
catch {
|
|
@@ -912,13 +975,13 @@ function planSummaryForProtocol(context, projectId, protocolId) {
|
|
|
912
975
|
function activePlanItems(context, projectId, protocols) {
|
|
913
976
|
const items = [];
|
|
914
977
|
for (const protocol of protocols) {
|
|
915
|
-
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]);
|
|
916
979
|
if (!record) {
|
|
917
980
|
continue;
|
|
918
981
|
}
|
|
919
982
|
let plan;
|
|
920
983
|
try {
|
|
921
|
-
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;
|
|
922
985
|
}
|
|
923
986
|
catch {
|
|
924
987
|
continue;
|