@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134
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 +687 -0
- package/README.md +101 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +306 -0
- package/dist/cli/help.js +88 -12
- package/dist/cli/hook-ingress.js +78 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +80 -0
- package/dist/cli/run-cli.js +1465 -450
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli.js +17 -2
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -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 +190 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
- package/dist/harness-runtime/lib/model-observations.mjs +101 -0
- package/dist/harness-runtime/lib/native-children.d.mts +14 -0
- package/dist/harness-runtime/lib/native-children.mjs +94 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -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/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/flow-run.schema.json +2 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +11 -8
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +851 -109
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +13 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +136 -0
- package/dist/services/dashboard.js +135 -56
- package/dist/services/engines.js +245 -119
- package/dist/services/eval-snapshots.js +1146 -105
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +230 -0
- package/dist/services/harness-adapter.js +329 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +753 -325
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +1828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +409 -51
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +145 -0
- package/dist/services/repair-intents.js +42 -0
- package/dist/services/review-copy.js +157 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +411 -0
- package/dist/services/run-control.js +985 -0
- package/dist/services/run-controller-adapter.js +399 -0
- package/dist/services/run-controller-capture.js +179 -0
- package/dist/services/run-controller-process.js +187 -0
- package/dist/services/run-controller-recovery.js +289 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1035 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +349 -0
- package/dist/services/runs.js +558 -217
- package/dist/services/runtime-budget.js +430 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +599 -0
- package/dist/services/runtime-scope-stop.js +180 -0
- package/dist/services/runtime-scope-worker.js +287 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +185 -19
- package/dist/services/stage-lifecycle.js +98 -107
- package/dist/services/stage-pause.js +89 -75
- package/dist/services/stage-report-renderer.js +158 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/tooling.js +32 -23
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +374 -0
- package/dist/services/vnext-code-review.js +222 -87
- package/dist/services/vnext-code.js +324 -251
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +79 -4
- package/dist/services/vnext-fanout.js +182 -17
- package/dist/services/vnext-merge.js +335 -113
- package/dist/services/vnext-plan-review.js +276 -126
- package/dist/services/vnext-plan.js +227 -102
- package/dist/services/vnext-protocolize.js +335 -82
- package/dist/services/vnext-specify.js +119 -81
- package/dist/services/work-registry.js +1083 -194
- package/dist/services/workspace-bootstrap.js +87 -0
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +98 -44
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +785 -37
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +30 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
|
+
import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
3
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
4
|
+
import { getFlowRunVariables } from "./runs.js";
|
|
5
|
+
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
6
|
+
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
7
|
+
import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
8
|
+
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
9
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
10
|
+
import { stageWorkIds } from "./stage-work-graph.js";
|
|
11
|
+
function semanticCompletionPrompt(stage, completed) {
|
|
12
|
+
const task = stage === "plan-review"
|
|
13
|
+
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
14
|
+
: stage === "code-review"
|
|
15
|
+
? "Classify the review findings and write or retain the authoritative review decision."
|
|
16
|
+
: "Integrate the accepted child results and update the Stage's authoritative verification evidence.";
|
|
17
|
+
return [
|
|
18
|
+
`All declared ${stage} child Work has settled (${completed} completed).`,
|
|
19
|
+
task,
|
|
20
|
+
"Then invoke the exact Stage finish command from the original authoritative packet.",
|
|
21
|
+
"If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
|
|
22
|
+
"Do not create speculative repair Work or start a successor Stage."
|
|
23
|
+
].join(" ");
|
|
24
|
+
}
|
|
25
|
+
/** Stage entry acknowledges the packet before the controller issues child commands. */
|
|
26
|
+
export function controllerStageEntryPrompt(stage, command) {
|
|
27
|
+
return [
|
|
28
|
+
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
29
|
+
command,
|
|
30
|
+
"Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
|
|
31
|
+
"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).",
|
|
32
|
+
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
33
|
+
stageLifecycleInstruction
|
|
34
|
+
].join("\n\n");
|
|
35
|
+
}
|
|
36
|
+
export function controllerStageResumePrompt(pauseId, command) {
|
|
37
|
+
return [
|
|
38
|
+
`The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
|
|
39
|
+
command,
|
|
40
|
+
"Do not alter the answer. Follow the returned packet, then stop at this Stage boundary.",
|
|
41
|
+
stageLifecycleInstruction
|
|
42
|
+
].join("\n\n");
|
|
43
|
+
}
|
|
44
|
+
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
45
|
+
export async function nextControllerFanout(context, input) {
|
|
46
|
+
// Tree settlement is not a Work result. Keep these children visible to the
|
|
47
|
+
// association check, especially when a hook rejected work start before CLI.
|
|
48
|
+
const children = input.children;
|
|
49
|
+
const observations = { harness_id: input.harness, parent_session_id: input.sessionId, children };
|
|
50
|
+
if (children.length) {
|
|
51
|
+
const observed = observeVnextNativeChildren(context, { ...input, observations });
|
|
52
|
+
const issues = observed.issues;
|
|
53
|
+
if (issues.length)
|
|
54
|
+
throw new AppError("fanout_reconciliation_required", "Native children have unresolved lifecycle ownership", 1, { issues });
|
|
55
|
+
}
|
|
56
|
+
let fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
57
|
+
if (!fanout)
|
|
58
|
+
return { kind: "none" };
|
|
59
|
+
if (fanout.state === "compatibility_required")
|
|
60
|
+
throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
|
|
61
|
+
if (children.length) {
|
|
62
|
+
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
63
|
+
const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
|
|
64
|
+
if (issues.length)
|
|
65
|
+
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
66
|
+
if (children.some(child => child.status === "running" || child.status === "unknown"))
|
|
67
|
+
return { kind: "wait", reason: "native_children_unsettled" };
|
|
68
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
69
|
+
}
|
|
70
|
+
if (fanout.state === "dispatch_required") {
|
|
71
|
+
dispatchVnextPlanReview(context, input);
|
|
72
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
73
|
+
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
74
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
75
|
+
}
|
|
76
|
+
const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
|
|
77
|
+
if (fanout.state === "coordinator_required")
|
|
78
|
+
return prompt("materialize_graph", `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.`);
|
|
79
|
+
if (fanout.works.failed || fanout.works.cancelled) {
|
|
80
|
+
const projectId = requireProjectByRoot(context, input.projectRoot).id;
|
|
81
|
+
const members = stageWorkIds(context, projectId, input.runId, input.stage);
|
|
82
|
+
const failures = context.db.all("SELECT work_id,status,result FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND status IN ('failed','cancelled')", [projectId, input.runId, fanout.parent_work_id]).filter(work => members.has(work.work_id)).map(work => {
|
|
83
|
+
let receipt = {};
|
|
84
|
+
try {
|
|
85
|
+
const value = work.result ? JSON.parse(work.result) : null;
|
|
86
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
87
|
+
receipt = value;
|
|
88
|
+
}
|
|
89
|
+
catch { /* Missing legacy metadata is unknown. */ }
|
|
90
|
+
const providerError = safeNativeFailure(receipt.provider_error);
|
|
91
|
+
return { work_id: work.work_id, status: work.status, ...(providerError ? { provider_error: providerError } : {}),
|
|
92
|
+
...Object.fromEntries(["terminal_turn_id", "terminal_observed_at"].filter(key => typeof receipt[key] === "string" && receipt[key].length <= 256).map(key => [key, receipt[key]])) };
|
|
93
|
+
});
|
|
94
|
+
throw new AppError("fanout_child_failed", "A child Work failed or was cancelled; no retry authority has been issued", 1, { stage: input.stage, run_id: input.runId, parent_work_id: fanout.parent_work_id, failed: fanout.works.failed, cancelled: fanout.works.cancelled, failures, effect: "unknown" });
|
|
95
|
+
}
|
|
96
|
+
if (fanout.works.ready.length) {
|
|
97
|
+
const choices = fanout.works.ready.map(work => {
|
|
98
|
+
const payload = context.db.get("SELECT payload_json FROM works WHERE work_id = ?", [work.work_id])?.payload_json;
|
|
99
|
+
const override = payload ? JSON.parse(payload).execution_override : undefined;
|
|
100
|
+
return { work, resolved: resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: input.policy, profiles: input.profiles, stage: input.stage, ...(override ? { work: override } : {}) }) };
|
|
101
|
+
});
|
|
102
|
+
const external = choices.filter(choice => choice.resolved.delegation.mode === "external");
|
|
103
|
+
if (external.length) {
|
|
104
|
+
const maximum = Math.min(...external.map(choice => choice.resolved.delegation.max_parallel ?? 1));
|
|
105
|
+
return { kind: "external", works: external.slice(0, maximum).map(choice => choice.work) };
|
|
106
|
+
}
|
|
107
|
+
const variables = getFlowRunVariables(context, input);
|
|
108
|
+
const capacity = variables.variables[subagentCapacityKey];
|
|
109
|
+
if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1)
|
|
110
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
111
|
+
const wave = choices.slice(0, capacity).map(choice => choice.work);
|
|
112
|
+
for (const work of wave)
|
|
113
|
+
if (!work.work_id || !work.start_command)
|
|
114
|
+
throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
|
|
115
|
+
if (input.stage === "plan-review" || input.stage === "code-review")
|
|
116
|
+
for (const work of wave)
|
|
117
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
118
|
+
const totalWorks = fanout.works.created + fanout.works.running + fanout.works.completed + fanout.works.failed + fanout.works.cancelled;
|
|
119
|
+
return prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length }));
|
|
120
|
+
}
|
|
121
|
+
if (fanout.works.running)
|
|
122
|
+
return { kind: "wait", reason: "work_receipts_pending" };
|
|
123
|
+
if (fanout.works.created)
|
|
124
|
+
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
125
|
+
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
|
|
126
|
+
}
|
|
127
|
+
/** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
|
|
128
|
+
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
129
|
+
try {
|
|
130
|
+
return normalizeNativeChildren(receipt, rootSessionId);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const record = errorRecord(error);
|
|
134
|
+
throw Object.assign(new AppError(record.code, record.message, record.exitCode, record.details), record);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
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
|
-
import {
|
|
7
|
+
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
8
|
+
import { ensureDir, resolveProjectRoot, resolveProtocolPlanPath } from "../storage/paths.js";
|
|
7
9
|
import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, projectSummaryJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
|
|
8
10
|
import { queueForProject } from "./merge-queue.js";
|
|
9
11
|
import { publishProjectSummary } from "./project-summary.js";
|
|
@@ -13,70 +15,82 @@ import { activeFlowSessionsForProject } from "./sessions.js";
|
|
|
13
15
|
import { usageForRun } from "./usage.js";
|
|
14
16
|
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
15
17
|
import { planSummary, planWithProgress } from "./plan-runtime.js";
|
|
16
|
-
export function getCmuxStatus(context, input) {
|
|
18
|
+
export async function getCmuxStatus(context, input) {
|
|
17
19
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
18
|
-
return { ok: true, project_root: project.root, cmux: detectCmux(context) };
|
|
20
|
+
return { ok: true, project_root: project.root, cmux: await detectCmux(context) };
|
|
19
21
|
}
|
|
20
|
-
export function
|
|
22
|
+
export function prepareDashboardRender(context, input) {
|
|
21
23
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
24
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
25
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
22
26
|
const format = parseDashboardFormat(input.format);
|
|
27
|
+
if (input.protocol)
|
|
28
|
+
requireProtocol(context, input.protocol, project.id);
|
|
29
|
+
const config = readProjectConfig(context, project.id);
|
|
30
|
+
const output = input.output
|
|
31
|
+
? path.resolve(project.root, input.output)
|
|
32
|
+
: format === "html"
|
|
33
|
+
? input.protocol ? protocolDashboardHtmlPath(context, project.id, input.protocol) : projectDashboardHtmlPath(context, project.id)
|
|
34
|
+
: dashboardMarkdownPath(project.root, config);
|
|
35
|
+
prepareOutputFile(output);
|
|
36
|
+
if (format === "html")
|
|
37
|
+
prepareOutputFile(input.protocol ? protocolDashboardJsonPath(context, project.id, input.protocol) : projectDashboardJsonPath(context, project.id));
|
|
38
|
+
return { project, format, output };
|
|
39
|
+
}
|
|
40
|
+
export function renderDashboard(context, input) {
|
|
41
|
+
const { project, format, output } = prepareDashboardRender(context, input);
|
|
23
42
|
if (format === "html") {
|
|
24
43
|
if (input.protocol) {
|
|
25
44
|
return renderProtocolDashboardHtml(context, project, input.protocol, input.output);
|
|
26
45
|
}
|
|
27
46
|
return renderProjectDashboardHtml(context, project, input.output);
|
|
28
47
|
}
|
|
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
48
|
const markdown = renderProjectDashboardMarkdown(context, project, output);
|
|
34
49
|
writeMarkdown(output, markdown);
|
|
35
50
|
return { ok: true, project_root: project.root, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
36
51
|
}
|
|
37
|
-
export function
|
|
52
|
+
export function prepareGlobalDashboardRender(context, input = {}) {
|
|
38
53
|
const format = parseDashboardFormat(input.format);
|
|
54
|
+
const output = input.output ? path.resolve(input.output) : format === "html" ? globalDashboardHtmlPath(context) : globalDashboardMarkdownPath(context);
|
|
55
|
+
prepareOutputFile(output);
|
|
56
|
+
if (format === "html")
|
|
57
|
+
prepareOutputFile(globalDashboardJsonPath(context));
|
|
58
|
+
return { format, output };
|
|
59
|
+
}
|
|
60
|
+
export function renderGlobalDashboard(context, input = {}) {
|
|
61
|
+
const { format, output } = prepareGlobalDashboardRender(context, input);
|
|
39
62
|
if (format === "html") {
|
|
40
63
|
return renderGlobalDashboardHtml(context, input.output);
|
|
41
64
|
}
|
|
42
|
-
const output = input.output
|
|
43
|
-
? path.resolve(input.output)
|
|
44
|
-
: globalDashboardMarkdownPath(context);
|
|
45
65
|
const markdown = renderGlobalDashboardMarkdown(context, output);
|
|
46
66
|
writeMarkdown(output, markdown);
|
|
47
67
|
return { ok: true, dashboard: { path: output, bytes: Buffer.byteLength(markdown) } };
|
|
48
68
|
}
|
|
49
|
-
export function openDashboard(context, input) {
|
|
69
|
+
export async function openDashboard(context, input) {
|
|
70
|
+
prepareDashboardOptions(input);
|
|
50
71
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
51
72
|
const config = readProjectConfig(context, project.id);
|
|
52
73
|
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
74
|
const dashboardPath = format === "html" ? projectDashboardHtmlPath(context, project.id) : dashboardMarkdownPath(project.root, config);
|
|
57
75
|
return {
|
|
58
76
|
ok: true,
|
|
59
77
|
project_root: project.root,
|
|
60
78
|
dashboard: { path: dashboardPath },
|
|
61
|
-
open: openCmuxDashboard(context, project.root, dashboardPath, format)
|
|
79
|
+
open: await openCmuxDashboard(context, project.root, dashboardPath, format)
|
|
62
80
|
};
|
|
63
81
|
}
|
|
64
|
-
export function openGlobalDashboard(context, input = {}) {
|
|
65
|
-
|
|
66
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
67
|
-
}
|
|
82
|
+
export async function openGlobalDashboard(context, input = {}) {
|
|
83
|
+
prepareDashboardOptions(input);
|
|
68
84
|
const format = parseDashboardFormat(input.format, "html");
|
|
69
85
|
const rendered = renderGlobalDashboard(context, { format });
|
|
70
86
|
return {
|
|
71
87
|
ok: true,
|
|
72
88
|
dashboard: rendered.dashboard,
|
|
73
|
-
open: openDashboardPath(context, rendered.dashboard.path, format)
|
|
89
|
+
open: await openDashboardPath(context, rendered.dashboard.path, format)
|
|
74
90
|
};
|
|
75
91
|
}
|
|
76
|
-
export function openProtocolDashboard(context, input) {
|
|
77
|
-
|
|
78
|
-
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
79
|
-
}
|
|
92
|
+
export async function openProtocolDashboard(context, input) {
|
|
93
|
+
prepareDashboardOptions(input);
|
|
80
94
|
const format = parseDashboardFormat(input.format, "html");
|
|
81
95
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
82
96
|
if (format !== "html") {
|
|
@@ -90,17 +104,43 @@ export function openProtocolDashboard(context, input) {
|
|
|
90
104
|
project_root: project.root,
|
|
91
105
|
protocol_id: input.protocol,
|
|
92
106
|
dashboard: rendered.dashboard,
|
|
93
|
-
open: openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
|
|
107
|
+
open: await openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
|
|
94
108
|
};
|
|
95
109
|
}
|
|
96
|
-
export function
|
|
110
|
+
export function prepareDashboardRefresh(context, input) {
|
|
111
|
+
prepareDashboardOptions(input);
|
|
97
112
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
98
113
|
const config = readProjectConfig(context, project.id);
|
|
99
114
|
const format = parseDashboardFormat(input.format);
|
|
100
115
|
const openMode = input.open ?? "auto";
|
|
101
|
-
if (
|
|
102
|
-
|
|
116
|
+
if (config.dashboard.project) {
|
|
117
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
118
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
119
|
+
if (input.protocol)
|
|
120
|
+
requireProtocol(context, input.protocol, project.id);
|
|
121
|
+
prepareRefreshOutputs(context, project, config, format, input.protocol);
|
|
122
|
+
}
|
|
123
|
+
return { project, config, format, openMode };
|
|
124
|
+
}
|
|
125
|
+
function prepareRefreshOutputs(context, project, config, format, protocol) {
|
|
126
|
+
if (config.dashboard.project) {
|
|
127
|
+
if (!protocol)
|
|
128
|
+
prepareOutputFile(projectSummaryJsonPath(context, project.id));
|
|
129
|
+
if (format === "html") {
|
|
130
|
+
prepareOutputFile(protocol ? protocolDashboardHtmlPath(context, project.id, protocol) : projectDashboardHtmlPath(context, project.id));
|
|
131
|
+
prepareOutputFile(protocol ? protocolDashboardJsonPath(context, project.id, protocol) : projectDashboardJsonPath(context, project.id));
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
prepareOutputFile(dashboardMarkdownPath(project.root, config));
|
|
135
|
+
}
|
|
136
|
+
if (config.dashboard.global) {
|
|
137
|
+
prepareOutputFile(format === "html" ? globalDashboardHtmlPath(context) : globalDashboardMarkdownPath(context, config));
|
|
138
|
+
if (format === "html")
|
|
139
|
+
prepareOutputFile(globalDashboardJsonPath(context));
|
|
103
140
|
}
|
|
141
|
+
}
|
|
142
|
+
export async function refreshDashboard(context, input) {
|
|
143
|
+
const { project, config, format, openMode } = prepareDashboardRefresh(context, input);
|
|
104
144
|
if (!config.dashboard.project) {
|
|
105
145
|
return {
|
|
106
146
|
ok: true,
|
|
@@ -124,7 +164,7 @@ export function refreshDashboard(context, input) {
|
|
|
124
164
|
dashboard: rendered.dashboard,
|
|
125
165
|
project_summary: projectSummary,
|
|
126
166
|
global_dashboard: global.dashboard,
|
|
127
|
-
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" }
|
|
128
168
|
};
|
|
129
169
|
}
|
|
130
170
|
export function refreshGlobalDashboard(context, input = {}) {
|
|
@@ -143,8 +183,9 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
143
183
|
return { ok: true, project_id: project.id, project_root: project.root, skipped: true, reason: "dashboard_project_disabled" };
|
|
144
184
|
}
|
|
145
185
|
try {
|
|
146
|
-
|
|
186
|
+
prepareRefreshOutputs(context, project, { ...config, dashboard: { ...config.dashboard, global: false } }, format);
|
|
147
187
|
const rendered = renderDashboard(context, { projectRoot: project.root, format });
|
|
188
|
+
const projectSummary = publishProjectSummary(context, { project });
|
|
148
189
|
return { ok: true, project_id: project.id, project_root: project.root, dashboard: rendered.dashboard, project_summary: projectSummary };
|
|
149
190
|
}
|
|
150
191
|
catch (error) {
|
|
@@ -170,10 +211,15 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
170
211
|
}
|
|
171
212
|
export function autoRefreshDashboards(context, input) {
|
|
172
213
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
214
|
+
// Hooks and usage ingestion continue to record receipts after capture.
|
|
215
|
+
// Their derived timestamp must not mutate the captured workspace.
|
|
216
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
217
|
+
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
173
218
|
const config = readProjectConfig(context, project.id);
|
|
174
219
|
if (!config.dashboard.auto_refresh) {
|
|
175
220
|
return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
|
|
176
221
|
}
|
|
222
|
+
prepareRefreshOutputs(context, project, config, "markdown");
|
|
177
223
|
const result = { ok: true, project_root: project.root };
|
|
178
224
|
if (config.dashboard.project) {
|
|
179
225
|
publishProjectSummary(context, { project });
|
|
@@ -196,6 +242,9 @@ export function autoRefreshDashboards(context, input) {
|
|
|
196
242
|
}
|
|
197
243
|
return result;
|
|
198
244
|
}
|
|
245
|
+
function projectRecoveryGuarded(context, projectId) {
|
|
246
|
+
return Boolean(context.db.get("SELECT 1 FROM run_recovery_guards WHERE project_id = ? AND status IN ('draining', 'sealed', 'resuming') LIMIT 1", [projectId]));
|
|
247
|
+
}
|
|
199
248
|
export function dashboardSummary(context, projectId, projectRoot) {
|
|
200
249
|
const config = readProjectConfig(context, projectId);
|
|
201
250
|
return {
|
|
@@ -221,13 +270,13 @@ export function dashboardData(context, input) {
|
|
|
221
270
|
}
|
|
222
271
|
return { ok: true, dashboard: buildProjectDashboardData(context, project, projectDashboardHtmlPath(context, project.id)) };
|
|
223
272
|
}
|
|
224
|
-
function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdown") {
|
|
273
|
+
async function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdown") {
|
|
225
274
|
const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
|
|
226
275
|
const config = readProjectConfig(context, project.id);
|
|
227
276
|
if (config.integrations.cmux.mode === "off") {
|
|
228
277
|
return { ok: true, skipped: true, reason: "cmux_off" };
|
|
229
278
|
}
|
|
230
|
-
const cmux = detectCmux(context);
|
|
279
|
+
const cmux = await detectCmux(context);
|
|
231
280
|
if (!cmux.available) {
|
|
232
281
|
if (config.integrations.cmux.mode === "required") {
|
|
233
282
|
throw new AppError("cmux_unavailable", "cmux is required by project config but is unavailable", 1, { cmux });
|
|
@@ -238,20 +287,20 @@ function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdo
|
|
|
238
287
|
return { ok: true, opened: false, dry_run: true, command: cmuxOpenCommand(format, dashboardPath) };
|
|
239
288
|
}
|
|
240
289
|
const command = cmuxOpenCommand(format, dashboardPath);
|
|
241
|
-
const result =
|
|
242
|
-
if (result.
|
|
290
|
+
const result = await commandResult(command[0], command.slice(1), { startupMs: SETUP_STARTUP_MS });
|
|
291
|
+
if (result.exit_code !== 0) {
|
|
243
292
|
if (config.integrations.cmux.mode === "required") {
|
|
244
293
|
throw new AppError("cmux_open_failed", "cmux markdown open failed", 1, {
|
|
245
|
-
status: result.
|
|
294
|
+
status: result.exit_code,
|
|
246
295
|
stderr: result.stderr.trim()
|
|
247
296
|
});
|
|
248
297
|
}
|
|
249
|
-
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() };
|
|
250
299
|
}
|
|
251
300
|
return { ok: true, opened: true, command };
|
|
252
301
|
}
|
|
253
|
-
function openDashboardPath(context, dashboardPath, format) {
|
|
254
|
-
const cmux = detectCmux(context);
|
|
302
|
+
async function openDashboardPath(context, dashboardPath, format) {
|
|
303
|
+
const cmux = await detectCmux(context);
|
|
255
304
|
if (!cmux.available) {
|
|
256
305
|
return { ok: true, skipped: true, reason: "cmux_unavailable", cmux };
|
|
257
306
|
}
|
|
@@ -259,9 +308,9 @@ function openDashboardPath(context, dashboardPath, format) {
|
|
|
259
308
|
return { ok: true, opened: false, dry_run: true, command: cmuxOpenCommand(format, dashboardPath) };
|
|
260
309
|
}
|
|
261
310
|
const command = cmuxOpenCommand(format, dashboardPath);
|
|
262
|
-
const result =
|
|
263
|
-
if (result.
|
|
264
|
-
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() };
|
|
265
314
|
}
|
|
266
315
|
return { ok: true, opened: true, command };
|
|
267
316
|
}
|
|
@@ -270,17 +319,31 @@ function cmuxOpenCommand(format, dashboardPath) {
|
|
|
270
319
|
? ["cmux", "browser", "open", pathToFileUrl(dashboardPath)]
|
|
271
320
|
: ["cmux", "markdown", "open", dashboardPath];
|
|
272
321
|
}
|
|
273
|
-
function detectCmux(context) {
|
|
322
|
+
async function detectCmux(context) {
|
|
274
323
|
if (context.env.DD_FLOW_CMUX_AVAILABLE === "true") {
|
|
275
324
|
return { available: true, source: "env" };
|
|
276
325
|
}
|
|
277
326
|
if (context.env.DD_FLOW_CMUX_AVAILABLE === "false") {
|
|
278
327
|
return { available: false, source: "env", reason: "forced_unavailable" };
|
|
279
328
|
}
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
282
337
|
? { available: true, source: "path", version: result.stdout.trim() }
|
|
283
|
-
: { available: false, source: "path", reason: result.
|
|
338
|
+
: { available: false, source: "path", reason: result.stderr.trim() || "cmux not found" };
|
|
339
|
+
}
|
|
340
|
+
export function prepareDashboardOptions(input) {
|
|
341
|
+
if (input.format !== undefined)
|
|
342
|
+
parseDashboardFormat(input.format);
|
|
343
|
+
if (input.viewer !== undefined && input.viewer !== "cmux")
|
|
344
|
+
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
345
|
+
if (input.open !== undefined && !["auto", "true", "false"].includes(input.open))
|
|
346
|
+
throw new AppError("validation", "--open must be auto, true, or false", 2);
|
|
284
347
|
}
|
|
285
348
|
function parseDashboardFormat(value, defaultFormat = "markdown") {
|
|
286
349
|
const format = value ?? defaultFormat;
|
|
@@ -294,15 +357,18 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
294
357
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
295
358
|
: projectDashboardHtmlPath(context, project.id);
|
|
296
359
|
const jsonPath = projectDashboardJsonPath(context, project.id);
|
|
360
|
+
prepareOutputFile(htmlPath);
|
|
361
|
+
prepareOutputFile(jsonPath);
|
|
297
362
|
const data = buildProjectDashboardData(context, project, htmlPath);
|
|
298
363
|
const html = renderDashboardHtmlPage(data);
|
|
299
|
-
writeJsonFile(jsonPath, data);
|
|
300
|
-
writeTextFile(htmlPath, html);
|
|
301
364
|
const protocolCards = Array.isArray(data.protocol_cards) ? data.protocol_cards : [];
|
|
302
|
-
const
|
|
365
|
+
const preparedPages = protocolCards
|
|
303
366
|
.filter((card) => card.generate_page !== false)
|
|
304
367
|
.slice(0, 24)
|
|
305
|
-
.map((card) =>
|
|
368
|
+
.map((card) => prepareProtocolDashboardHtml(context, project, String(card.id)));
|
|
369
|
+
writeJsonFile(jsonPath, data);
|
|
370
|
+
writeTextFile(htmlPath, html);
|
|
371
|
+
const protocolPages = preparedPages.map(writeProtocolDashboardHtml)
|
|
306
372
|
.map((result) => ({
|
|
307
373
|
id: String(result.protocol_id),
|
|
308
374
|
path: result.dashboard.path,
|
|
@@ -321,12 +387,23 @@ function renderProjectDashboardHtml(context, project, output) {
|
|
|
321
387
|
};
|
|
322
388
|
}
|
|
323
389
|
function renderProtocolDashboardHtml(context, project, protocolId, output) {
|
|
390
|
+
return writeProtocolDashboardHtml(prepareProtocolDashboardHtml(context, project, protocolId, output));
|
|
391
|
+
}
|
|
392
|
+
function prepareProtocolDashboardHtml(context, project, protocolId, output) {
|
|
393
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
394
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
324
395
|
const htmlPath = output
|
|
325
396
|
? path.resolve(path.isAbsolute(output) ? output : path.join(project.root, output))
|
|
326
397
|
: protocolDashboardHtmlPath(context, project.id, protocolId);
|
|
327
398
|
const jsonPath = protocolDashboardJsonPath(context, project.id, protocolId);
|
|
399
|
+
prepareOutputFile(htmlPath);
|
|
400
|
+
prepareOutputFile(jsonPath);
|
|
328
401
|
const data = buildProtocolDashboardData(context, project, protocolId, htmlPath);
|
|
329
402
|
const html = renderDashboardHtmlPage(data);
|
|
403
|
+
return { project, protocolId, htmlPath, jsonPath, data, html };
|
|
404
|
+
}
|
|
405
|
+
function writeProtocolDashboardHtml(prepared) {
|
|
406
|
+
const { project, protocolId, htmlPath, jsonPath, data, html } = prepared;
|
|
330
407
|
writeJsonFile(jsonPath, data);
|
|
331
408
|
writeTextFile(htmlPath, html);
|
|
332
409
|
return {
|
|
@@ -344,6 +421,8 @@ function renderProtocolDashboardHtml(context, project, protocolId, output) {
|
|
|
344
421
|
function renderGlobalDashboardHtml(context, output) {
|
|
345
422
|
const htmlPath = output ? path.resolve(output) : globalDashboardHtmlPath(context);
|
|
346
423
|
const jsonPath = globalDashboardJsonPath(context);
|
|
424
|
+
prepareOutputFile(htmlPath);
|
|
425
|
+
prepareOutputFile(jsonPath);
|
|
347
426
|
const data = buildGlobalDashboardData(context, htmlPath);
|
|
348
427
|
const html = renderDashboardHtmlPage(data);
|
|
349
428
|
writeJsonFile(jsonPath, data);
|
|
@@ -888,12 +967,12 @@ function protocolsForProject(context, projectId) {
|
|
|
888
967
|
FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
|
|
889
968
|
}
|
|
890
969
|
function planSummaryForProtocol(context, projectId, protocolId) {
|
|
891
|
-
const protocol = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
970
|
+
const protocol = context.db.get("SELECT plan_path, project_root FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
892
971
|
if (!protocol) {
|
|
893
972
|
return { plan_id: "none", total: 0, done: 0, blocked: 0 };
|
|
894
973
|
}
|
|
895
974
|
try {
|
|
896
|
-
const summary = planSummary(context, { projectId, protocolId, planPath: protocol.plan_path }, { bind: false });
|
|
975
|
+
const summary = planSummary(context, { projectId, protocolId, planPath: resolveProtocolPlanPath(protocol.project_root, protocolId, protocol.plan_path) }, { bind: false });
|
|
897
976
|
return { plan_id: summary.plan_id, total: summary.total, done: summary.done, blocked: summary.blocked };
|
|
898
977
|
}
|
|
899
978
|
catch {
|
|
@@ -903,13 +982,13 @@ function planSummaryForProtocol(context, projectId, protocolId) {
|
|
|
903
982
|
function activePlanItems(context, projectId, protocols) {
|
|
904
983
|
const items = [];
|
|
905
984
|
for (const protocol of protocols) {
|
|
906
|
-
const record = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocol.id]);
|
|
985
|
+
const record = context.db.get("SELECT plan_path, project_root FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocol.id]);
|
|
907
986
|
if (!record) {
|
|
908
987
|
continue;
|
|
909
988
|
}
|
|
910
989
|
let plan;
|
|
911
990
|
try {
|
|
912
|
-
plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: record.plan_path }, { bind: false }).plan;
|
|
991
|
+
plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: resolveProtocolPlanPath(record.project_root, protocol.id, record.plan_path) }, { bind: false }).plan;
|
|
913
992
|
}
|
|
914
993
|
catch {
|
|
915
994
|
continue;
|