@deksden-com/dd-flow-cli 0.3.1 → 0.4.1
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 +28 -0
- package/README.md +25 -9
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +58 -32
- package/dist/cli/run-cli.js +282 -50
- package/dist/domain/entity-ids.js +4 -4
- package/dist/domain/flow-contract.js +502 -36
- package/dist/domain/validation.js +34 -0
- package/dist/protocol/local-files.js +8 -6
- package/dist/schemas/code-stage-report.schema.json +197 -2
- package/dist/schemas/flow-contract.schema.json +126 -0
- package/dist/schemas/flow-run-index-v3.schema.json +203 -0
- package/dist/schemas/flow-run-index.schema.json +22 -2
- package/dist/schemas/flow-run.schema.json +36 -0
- package/dist/schemas/merge-stage-report.schema.json +213 -2
- package/dist/schemas/plan-stage-report.schema.json +156 -2
- package/dist/schemas/release-impact.schema.json +16 -0
- package/dist/services/audit.js +3 -3
- package/dist/services/branch-context.js +266 -0
- package/dist/services/canon.js +0 -1
- package/dist/services/cleanup.js +6 -6
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/compatibility-preflight.js +8 -77
- package/dist/services/dashboard.js +48 -11
- package/dist/services/engines.js +123 -13
- package/dist/services/hooks.js +6 -6
- package/dist/services/ids.js +40 -49
- package/dist/services/merge-queue.js +240 -20
- package/dist/services/merge-worker.js +12 -4
- package/dist/services/migrations.js +64 -0
- package/dist/services/plans.js +23 -16
- package/dist/services/projects.js +2 -2
- package/dist/services/prompts.js +322 -0
- package/dist/services/protocols.js +78 -42
- package/dist/services/run-projection.js +80 -0
- package/dist/services/runs.js +360 -22
- package/dist/services/schema-validation.js +35 -12
- package/dist/services/sessions.js +81 -3
- package/dist/services/status.js +32 -1
- package/dist/services/usage.js +233 -0
- package/dist/services/worktrees.js +24 -19
- package/dist/storage/database.js +223 -9
- package/package.json +1 -1
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { getCliBuildInfo } from "./build-info.js";
|
|
3
3
|
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
4
|
-
import { selectEngine } from "./engines.js";
|
|
5
|
-
|
|
6
|
-
export function preflightCliCompatibility(context, args, env = process.env) {
|
|
4
|
+
import { resolveOperationProjectRoot, selectEngine } from "./engines.js";
|
|
5
|
+
export function preflightCliCompatibility(context, args, env = process.env, resolvedProjectRoot) {
|
|
7
6
|
const classification = classifyCliOperation(args, env);
|
|
8
7
|
if (classification.mode === "router_native") {
|
|
9
8
|
return { ok: true, classification, compatibility: null };
|
|
10
9
|
}
|
|
11
|
-
const projectRoot = resolveOperationProjectRoot(context, args);
|
|
10
|
+
const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
|
|
12
11
|
if (!projectRoot) {
|
|
13
12
|
return { ok: true, classification, compatibility: null };
|
|
14
13
|
}
|
|
@@ -27,6 +26,7 @@ export function preflightCliCompatibility(context, args, env = process.env) {
|
|
|
27
26
|
}
|
|
28
27
|
export function compatibilityReport(selection, classification) {
|
|
29
28
|
const build = getCliBuildInfo();
|
|
29
|
+
const normalWriteAllowed = selection.status === "selected" && Boolean(selection.selected);
|
|
30
30
|
return {
|
|
31
31
|
verdict: selection.status === "selected" ? "ok" : "incompatible",
|
|
32
32
|
memorybank_version: selection.memory_bank_version,
|
|
@@ -36,9 +36,11 @@ export function compatibilityReport(selection, classification) {
|
|
|
36
36
|
engine_resolution: selection.status,
|
|
37
37
|
required_engine_range: selection.required_range,
|
|
38
38
|
recommended_engine_version: selection.recommended_version,
|
|
39
|
-
allowed_modes:
|
|
39
|
+
allowed_modes: normalWriteAllowed
|
|
40
|
+
? ["read_only_diagnostics", "normal_write", "mb_upgrade"]
|
|
41
|
+
: ["read_only_diagnostics", "mb_upgrade"],
|
|
40
42
|
operation_mode: classification.mode,
|
|
41
|
-
blocked_operation: classification.mode === "normal_write" ? classification.operation : null,
|
|
43
|
+
blocked_operation: classification.mode === "normal_write" && !normalWriteAllowed ? classification.operation : null,
|
|
42
44
|
install_hint: selection.install_hint,
|
|
43
45
|
project_root: selection.project_root,
|
|
44
46
|
diagnostics: selection.diagnostics
|
|
@@ -51,74 +53,3 @@ function remediationForSelection(selection) {
|
|
|
51
53
|
diagnostics: selection.diagnostics
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
|
-
function resolveOperationProjectRoot(context, args) {
|
|
55
|
-
const [family, command, ...rest] = args;
|
|
56
|
-
const parsed = parseLightArgs(rest);
|
|
57
|
-
const explicitRoot = option(parsed, "project-root") ?? option(parsed, "root");
|
|
58
|
-
if (explicitRoot)
|
|
59
|
-
return explicitRoot;
|
|
60
|
-
if (family === "protocol") {
|
|
61
|
-
const protocolId = positional(parsed, 0);
|
|
62
|
-
if (protocolId && command && ["ready-for-merge", "cancel", "status"].includes(command)) {
|
|
63
|
-
return requireProtocol(context, protocolId).project_root;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (family === "transition") {
|
|
67
|
-
const transitionParsed = parseLightArgs([command ?? "", ...rest]);
|
|
68
|
-
const protocolId = positional(transitionParsed, 0);
|
|
69
|
-
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
70
|
-
}
|
|
71
|
-
if (family === "plan" && (command === "set" || command === "status")) {
|
|
72
|
-
const protocolId = positional(parsed, 0);
|
|
73
|
-
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
74
|
-
}
|
|
75
|
-
if (family === "plan" && command === "item") {
|
|
76
|
-
const protocolId = positional(parsed, 1);
|
|
77
|
-
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
78
|
-
}
|
|
79
|
-
if (family === "merge-queue" && ["complete", "fail", "cancel", "note"].includes(command ?? "")) {
|
|
80
|
-
const protocolId = positional(parsed, 0);
|
|
81
|
-
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
82
|
-
}
|
|
83
|
-
if (family === "worktree") {
|
|
84
|
-
const protocolId = option(parsed, "protocol-id");
|
|
85
|
-
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
function parseLightArgs(args) {
|
|
90
|
-
const positional = [];
|
|
91
|
-
const options = new Map();
|
|
92
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
93
|
-
const value = args[index];
|
|
94
|
-
if (value?.startsWith("--")) {
|
|
95
|
-
const equal = value.indexOf("=");
|
|
96
|
-
if (equal > 2) {
|
|
97
|
-
const key = value.slice(2, equal);
|
|
98
|
-
options.set(key, [...(options.get(key) ?? []), value.slice(equal + 1)]);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const key = value.slice(2);
|
|
102
|
-
const next = args[index + 1];
|
|
103
|
-
if (!next || next.startsWith("--")) {
|
|
104
|
-
options.set(key, [...(options.get(key) ?? []), ""]);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
options.set(key, [...(options.get(key) ?? []), next]);
|
|
108
|
-
index += 1;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else if (value) {
|
|
112
|
-
positional.push(value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return { positional, options };
|
|
116
|
-
}
|
|
117
|
-
function option(parsed, key) {
|
|
118
|
-
const values = parsed.options.get(key);
|
|
119
|
-
const value = values?.[values.length - 1];
|
|
120
|
-
return value ? value : null;
|
|
121
|
-
}
|
|
122
|
-
function positional(parsed, index) {
|
|
123
|
-
return parsed.positional[index] ?? null;
|
|
124
|
-
}
|
|
@@ -10,6 +10,7 @@ import { publishProjectSummary } from "./project-summary.js";
|
|
|
10
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
11
|
import { protocolRunDiagnostics, protocolSetBoardForProtocol, protocolSetBoardsForProject, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
12
12
|
import { activeFlowSessionsForProject } from "./sessions.js";
|
|
13
|
+
import { usageForRun } from "./usage.js";
|
|
13
14
|
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
14
15
|
export function getCmuxStatus(context, input) {
|
|
15
16
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -617,7 +618,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
617
618
|
if (!protocol) {
|
|
618
619
|
throw new AppError("not_found", `Protocol is not registered: ${protocolId}`, 1, { protocol_id: protocolId });
|
|
619
620
|
}
|
|
620
|
-
const runtimeProtocol = requireProtocol(context, protocol.id);
|
|
621
|
+
const runtimeProtocol = requireProtocol(context, protocol.id, project.id);
|
|
621
622
|
const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
|
|
622
623
|
const lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
|
|
623
624
|
const queueItem = queueForProject(context, project.id).find((item) => item.protocol_id === protocolId) ?? null;
|
|
@@ -626,6 +627,9 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
626
627
|
FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
627
628
|
ORDER BY updated_at DESC, id DESC`, [project.id, protocolId]);
|
|
628
629
|
const primaryRun = runs.find((run) => run.status === "running") ?? runs[0];
|
|
630
|
+
const observability = primaryRun
|
|
631
|
+
? usageForRun(context, { projectId: project.id, runId: primaryRun.id, groupBy: "role" })
|
|
632
|
+
: { status: "not_observable", reason: "no_protocol_run" };
|
|
629
633
|
const warnings = [...runDiagnostics.diagnostics];
|
|
630
634
|
const protocolSetBoard = protocolSetBoardForProtocol(context, project.id, project.root, protocolId);
|
|
631
635
|
const reviewRuns = recentReviewRunsForProtocol(context, project.id, protocolId, 8);
|
|
@@ -643,6 +647,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
643
647
|
run_index_path: run.run_index_path,
|
|
644
648
|
updated_at: run.updated_at,
|
|
645
649
|
completed_at: run.completed_at,
|
|
650
|
+
flow_flags: flowFlagsProjection(index),
|
|
646
651
|
stages: Array.isArray(index?.stage_runs) ? index.stage_runs.map((stage) => stageLink(project.root, run, stage)) : []
|
|
647
652
|
};
|
|
648
653
|
});
|
|
@@ -696,6 +701,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
696
701
|
},
|
|
697
702
|
primary_run_id: primaryRun?.id ?? null,
|
|
698
703
|
primary_run_reason: primaryRun ? (primaryRun.status === "running" ? "latest running run" : "latest updated run") : "no runs registered",
|
|
704
|
+
observability,
|
|
699
705
|
protocol_set_board: protocolSetBoard,
|
|
700
706
|
review_runs: reviewRuns,
|
|
701
707
|
run_history: runHistory,
|
|
@@ -730,7 +736,7 @@ function renderProjectDashboardMarkdown(context, project, output) {
|
|
|
730
736
|
const config = readProjectConfig(context, project.id);
|
|
731
737
|
const globalOutput = globalDashboardMarkdownPath(context, config);
|
|
732
738
|
const latestProtocol = protocols.find((protocol) => isActiveProtocolStatus(protocol.status)) ?? protocols[0];
|
|
733
|
-
const latestProtocolSummary = latestProtocol ? planSummaryForProtocol(context, latestProtocol.id) : null;
|
|
739
|
+
const latestProtocolSummary = latestProtocol ? planSummaryForProtocol(context, project.id, latestProtocol.id) : null;
|
|
734
740
|
const lines = [
|
|
735
741
|
"# 🧭 dd-flow Dashboard",
|
|
736
742
|
"",
|
|
@@ -750,14 +756,14 @@ function renderProjectDashboardMarkdown(context, project, output) {
|
|
|
750
756
|
if (activeProtocols.length || sessions.length) {
|
|
751
757
|
lines.push("| --- | --- | --- | --- |");
|
|
752
758
|
for (const protocol of activeProtocols.slice(0, 5)) {
|
|
753
|
-
const summary = planSummaryForProtocol(context, protocol.id);
|
|
759
|
+
const summary = planSummaryForProtocol(context, project.id, protocol.id);
|
|
754
760
|
lines.push(`| ${statusIcon(protocol.status)} ${cell(shortId(protocol.id))} | ${cell(`${protocol.stage}/${protocol.status} · ${summary.done}/${summary.total}`)} | ${cell(protocol.next_action ?? "none")} | ${cell(workspaceForProtocol(worktrees, protocol.id) ?? "-")} |`);
|
|
755
761
|
}
|
|
756
762
|
for (const session of sessions.slice(0, 6)) {
|
|
757
763
|
lines.push(`| ${statusIcon(session.status)} ${cell(session.flow_kind)} | ${cell(session.worker_id ?? shortId(session.session_id))} | ${cell(session.next_action ?? "none")} | ${cell(compactPath(session.workspace_path))} |`);
|
|
758
764
|
}
|
|
759
765
|
}
|
|
760
|
-
const activeItems = activePlanItems(context, activeProtocols);
|
|
766
|
+
const activeItems = activePlanItems(context, project.id, activeProtocols);
|
|
761
767
|
lines.push("", "## ✅ Plan", activeItems.length ? "| protocol | item | status | summary |" : "No active plan items.");
|
|
762
768
|
if (activeItems.length) {
|
|
763
769
|
lines.push("| --- | --- | --- | --- |");
|
|
@@ -793,7 +799,7 @@ function renderProjectDashboardMarkdown(context, project, output) {
|
|
|
793
799
|
if (recentProtocols.length) {
|
|
794
800
|
lines.push("| --- | --- | --- | --- |");
|
|
795
801
|
for (const protocol of recentProtocols) {
|
|
796
|
-
const summary = planSummaryForProtocol(context, protocol.id);
|
|
802
|
+
const summary = planSummaryForProtocol(context, project.id, protocol.id);
|
|
797
803
|
lines.push(`| ${cell(shortId(protocol.id))} | ${statusIcon(protocol.status)} ${cell(protocol.stage)}/${cell(protocol.status)} | ${cell(`${summary.done}/${summary.total}`)} | ${cell(protocol.updated_at)} |`);
|
|
798
804
|
}
|
|
799
805
|
}
|
|
@@ -874,8 +880,8 @@ function protocolsForProject(context, projectId) {
|
|
|
874
880
|
return context.db.all(`SELECT id, status, stage, next_action, blockers_json, active_def_json, updated_at
|
|
875
881
|
FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
|
|
876
882
|
}
|
|
877
|
-
function planSummaryForProtocol(context, protocolId) {
|
|
878
|
-
const row = context.db.get("SELECT plan_json FROM plans WHERE protocol_id = ?", [protocolId]);
|
|
883
|
+
function planSummaryForProtocol(context, projectId, protocolId) {
|
|
884
|
+
const row = context.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
879
885
|
if (!row) {
|
|
880
886
|
return { plan_id: "none", total: 0, done: 0, blocked: 0 };
|
|
881
887
|
}
|
|
@@ -888,10 +894,10 @@ function planSummaryForProtocol(context, protocolId) {
|
|
|
888
894
|
blocked: items.filter((item) => item.status === "blocked").length
|
|
889
895
|
};
|
|
890
896
|
}
|
|
891
|
-
function activePlanItems(context, protocols) {
|
|
897
|
+
function activePlanItems(context, projectId, protocols) {
|
|
892
898
|
const items = [];
|
|
893
899
|
for (const protocol of protocols) {
|
|
894
|
-
const row = context.db.get("SELECT plan_json FROM plans WHERE protocol_id = ?", [protocol.id]);
|
|
900
|
+
const row = context.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [projectId, protocol.id]);
|
|
895
901
|
if (!row) {
|
|
896
902
|
continue;
|
|
897
903
|
}
|
|
@@ -1031,7 +1037,7 @@ function link(label, href, status, reason) {
|
|
|
1031
1037
|
};
|
|
1032
1038
|
}
|
|
1033
1039
|
function buildProtocolCard(context, project, protocol, generatePage) {
|
|
1034
|
-
const summary = planSummaryForProtocol(context, protocol.id);
|
|
1040
|
+
const summary = planSummaryForProtocol(context, project.id, protocol.id);
|
|
1035
1041
|
const activeDefCount = jsonArray(protocol.active_def_json).length;
|
|
1036
1042
|
const blockerCount = jsonArray(protocol.blockers_json).length;
|
|
1037
1043
|
const latestRun = context.db.get(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
@@ -1040,7 +1046,7 @@ function buildProtocolCard(context, project, protocol, generatePage) {
|
|
|
1040
1046
|
let diagnostics = [];
|
|
1041
1047
|
let lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
1042
1048
|
try {
|
|
1043
|
-
const runtimeProtocol = requireProtocol(context, protocol.id);
|
|
1049
|
+
const runtimeProtocol = requireProtocol(context, protocol.id, project.id);
|
|
1044
1050
|
const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
|
|
1045
1051
|
lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
|
|
1046
1052
|
diagnostics = [...protocolRunDiagnostics(context, runtimeProtocol, runtimeState).diagnostics, ...lifecycle.diagnostics];
|
|
@@ -1116,11 +1122,42 @@ function reviewRunSummary(run) {
|
|
|
1116
1122
|
verdict: run.verdict,
|
|
1117
1123
|
next_action: run.next_action,
|
|
1118
1124
|
run_index_path: run.run_index_path,
|
|
1125
|
+
flow_flags: flowFlagsProjection(index),
|
|
1119
1126
|
report: reviewStage ? stageLink("", run, reviewStage).report : link("Run index", run.run_index_path, fs.existsSync(run.run_index_path) ? "available" : "missing"),
|
|
1120
1127
|
updated_at: run.updated_at,
|
|
1121
1128
|
completed_at: run.completed_at
|
|
1122
1129
|
};
|
|
1123
1130
|
}
|
|
1131
|
+
function flowFlagsProjection(index) {
|
|
1132
|
+
const flags = index?.flow_flags;
|
|
1133
|
+
if (!flags || typeof flags !== "object" || Array.isArray(flags))
|
|
1134
|
+
return null;
|
|
1135
|
+
const object = flags;
|
|
1136
|
+
const values = object.values;
|
|
1137
|
+
const projectedValues = {};
|
|
1138
|
+
if (values && typeof values === "object" && !Array.isArray(values)) {
|
|
1139
|
+
for (const [key, raw] of Object.entries(values)) {
|
|
1140
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1141
|
+
continue;
|
|
1142
|
+
const value = raw;
|
|
1143
|
+
const source = value.source;
|
|
1144
|
+
projectedValues[key] = {
|
|
1145
|
+
value: value.value ?? null,
|
|
1146
|
+
source: source && typeof source === "object" && !Array.isArray(source)
|
|
1147
|
+
? { kind: source.kind ?? null, ref: source.ref ?? null }
|
|
1148
|
+
: null
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
snapshot_revision: object.snapshot_revision ?? null,
|
|
1154
|
+
snapshot_checksum: object.snapshot_checksum ?? null,
|
|
1155
|
+
resolution_status: object.resolution_status ?? "legacy_incomplete",
|
|
1156
|
+
preset: object.preset ?? null,
|
|
1157
|
+
floors_applied: Array.isArray(object.floors_applied) ? object.floors_applied : [],
|
|
1158
|
+
values: projectedValues
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1124
1161
|
function safeParseIndex(text) {
|
|
1125
1162
|
try {
|
|
1126
1163
|
const parsed = JSON.parse(text);
|
package/dist/services/engines.js
CHANGED
|
@@ -2,13 +2,16 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { getCliBuildInfo } from "./build-info.js";
|
|
6
7
|
import { AppError } from "../shared/errors.js";
|
|
7
8
|
import { ensureDir, engineStoreRoot, engineVersionRoot, resolveProjectRoot } from "../storage/paths.js";
|
|
8
9
|
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
10
|
+
import { requireProtocol } from "./protocols.js";
|
|
9
11
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
10
12
|
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
11
13
|
const sourceFile = fileURLToPath(import.meta.url);
|
|
14
|
+
const packageRequire = createRequire(sourceFile);
|
|
12
15
|
export function isEngineMode(env) {
|
|
13
16
|
return env.DD_FLOW_ENGINE_MODE === "1";
|
|
14
17
|
}
|
|
@@ -115,11 +118,11 @@ export function doctorEngines(context, input = {}) {
|
|
|
115
118
|
exit_code: checks.every((check) => check.status === "ok") && selection.status !== "missing" ? 0 : 1
|
|
116
119
|
};
|
|
117
120
|
}
|
|
118
|
-
export function routeArgsThroughEngine(context, args, io, stdin, env) {
|
|
121
|
+
export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
|
|
119
122
|
if (isEngineMode(env) || isRouterNativeCommand(args))
|
|
120
123
|
return Promise.resolve(null);
|
|
121
124
|
installCurrentEngine(context);
|
|
122
|
-
const selection = selectEngine(context, { projectRoot:
|
|
125
|
+
const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveOperationProjectRoot(context, args) ?? undefined });
|
|
123
126
|
if (selection.status === "missing") {
|
|
124
127
|
const classification = classifyCliOperation(args, env);
|
|
125
128
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
@@ -170,6 +173,33 @@ export function selectEngine(context, input) {
|
|
|
170
173
|
diagnostics: selected ? [] : [`No installed engine satisfies ${packageName} ${requiredRange}`]
|
|
171
174
|
};
|
|
172
175
|
}
|
|
176
|
+
export function resolveOperationProjectRoot(context, args) {
|
|
177
|
+
const [family, command, ...rest] = args;
|
|
178
|
+
const parsed = parseLightArgs(rest);
|
|
179
|
+
const explicitRoot = option(parsed, "project-root") ?? option(parsed, "root");
|
|
180
|
+
if (explicitRoot)
|
|
181
|
+
return explicitRoot;
|
|
182
|
+
let protocolId = null;
|
|
183
|
+
if (family === "protocol" && ["status", "branch-status", "transition", "sync-from-run", "ready-for-merge", "cancel", "implement"].includes(command ?? "")) {
|
|
184
|
+
protocolId = positional(parsed, 0);
|
|
185
|
+
}
|
|
186
|
+
else if (family === "transition") {
|
|
187
|
+
protocolId = positional(parseLightArgs([command ?? "", ...rest]), 0);
|
|
188
|
+
}
|
|
189
|
+
else if (family === "plan" && ["set", "status"].includes(command ?? "")) {
|
|
190
|
+
protocolId = positional(parsed, 0);
|
|
191
|
+
}
|
|
192
|
+
else if (family === "plan" && command === "item") {
|
|
193
|
+
protocolId = positional(parsed, 1);
|
|
194
|
+
}
|
|
195
|
+
else if (family === "worktree") {
|
|
196
|
+
protocolId = option(parsed, "protocol-id");
|
|
197
|
+
}
|
|
198
|
+
else if (family === "merge-queue" && ["complete", "note", "fail", "cancel"].includes(command ?? "")) {
|
|
199
|
+
protocolId = positional(parsed, 0);
|
|
200
|
+
}
|
|
201
|
+
return protocolId ? requireProtocol(context, protocolId).project_root : null;
|
|
202
|
+
}
|
|
173
203
|
function readCompatibilityForProject(projectRoot) {
|
|
174
204
|
let resolvedProjectRoot = null;
|
|
175
205
|
if (projectRoot) {
|
|
@@ -191,17 +221,38 @@ function readCompatibilityForProject(projectRoot) {
|
|
|
191
221
|
}
|
|
192
222
|
return null;
|
|
193
223
|
}
|
|
194
|
-
function
|
|
224
|
+
function parseLightArgs(args) {
|
|
225
|
+
const positional = [];
|
|
226
|
+
const options = new Map();
|
|
195
227
|
for (let index = 0; index < args.length; index += 1) {
|
|
196
|
-
const
|
|
197
|
-
if ((
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
228
|
+
const value = args[index];
|
|
229
|
+
if (value?.startsWith("--")) {
|
|
230
|
+
const equal = value.indexOf("=");
|
|
231
|
+
if (equal > 2) {
|
|
232
|
+
const key = value.slice(2, equal);
|
|
233
|
+
options.set(key, [...(options.get(key) ?? []), value.slice(equal + 1)]);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const key = value.slice(2);
|
|
237
|
+
const next = args[index + 1];
|
|
238
|
+
if (!next || next.startsWith("--"))
|
|
239
|
+
options.set(key, [...(options.get(key) ?? []), ""]);
|
|
240
|
+
else {
|
|
241
|
+
options.set(key, [...(options.get(key) ?? []), next]);
|
|
242
|
+
index += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (value)
|
|
246
|
+
positional.push(value);
|
|
203
247
|
}
|
|
204
|
-
return
|
|
248
|
+
return { positional, options };
|
|
249
|
+
}
|
|
250
|
+
function option(parsed, key) {
|
|
251
|
+
const values = parsed.options.get(key);
|
|
252
|
+
return values?.[values.length - 1] || null;
|
|
253
|
+
}
|
|
254
|
+
function positional(parsed, index) {
|
|
255
|
+
return parsed.positional[index] ?? null;
|
|
205
256
|
}
|
|
206
257
|
function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot) {
|
|
207
258
|
return {
|
|
@@ -242,6 +293,63 @@ function copyPackageSnapshot(packageRoot, target) {
|
|
|
242
293
|
fs.copyFileSync(from, to);
|
|
243
294
|
}
|
|
244
295
|
}
|
|
296
|
+
copyProductionDependencies(packageRoot, target);
|
|
297
|
+
}
|
|
298
|
+
function copyProductionDependencies(packageRoot, target) {
|
|
299
|
+
const queue = [{ sourceRoot: packageRoot, targetRoot: target, ancestors: new Set() }];
|
|
300
|
+
const copied = new Set();
|
|
301
|
+
while (queue.length > 0) {
|
|
302
|
+
const current = queue.shift();
|
|
303
|
+
if (!current)
|
|
304
|
+
continue;
|
|
305
|
+
for (const dependency of readProductionDependencies(current.sourceRoot)) {
|
|
306
|
+
const sourceRoot = resolvePackageRoot(dependency, current.sourceRoot);
|
|
307
|
+
if (!sourceRoot)
|
|
308
|
+
throw new AppError("engine_install_failed", `Cannot resolve runtime dependency: ${dependency}`, 1);
|
|
309
|
+
const targetRoot = path.join(current.targetRoot, "node_modules", dependency);
|
|
310
|
+
const copyKey = `${sourceRoot}\n${targetRoot}`;
|
|
311
|
+
if (copied.has(copyKey))
|
|
312
|
+
continue;
|
|
313
|
+
copied.add(copyKey);
|
|
314
|
+
copyDir(sourceRoot, targetRoot, true);
|
|
315
|
+
if (current.ancestors.has(sourceRoot))
|
|
316
|
+
continue;
|
|
317
|
+
queue.push({ sourceRoot, targetRoot, ancestors: new Set([...current.ancestors, sourceRoot]) });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function readProductionDependencies(packageRoot) {
|
|
322
|
+
try {
|
|
323
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
324
|
+
const dependencies = new Set();
|
|
325
|
+
for (const field of ["dependencies", "optionalDependencies"]) {
|
|
326
|
+
const value = manifest[field];
|
|
327
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
328
|
+
continue;
|
|
329
|
+
for (const name of Object.keys(value))
|
|
330
|
+
dependencies.add(name);
|
|
331
|
+
}
|
|
332
|
+
return [...dependencies];
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function resolvePackageRoot(name, from) {
|
|
339
|
+
try {
|
|
340
|
+
let current = path.dirname(packageRequire.resolve(name, { paths: [from] }));
|
|
341
|
+
while (true) {
|
|
342
|
+
if (fs.existsSync(path.join(current, "package.json")))
|
|
343
|
+
return fs.realpathSync(current);
|
|
344
|
+
const parent = path.dirname(current);
|
|
345
|
+
if (parent === current)
|
|
346
|
+
return null;
|
|
347
|
+
current = parent;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
245
353
|
}
|
|
246
354
|
function inferInstallSource(packageRoot, ddFlowHome) {
|
|
247
355
|
const normalized = path.resolve(packageRoot);
|
|
@@ -265,13 +373,15 @@ function hasAncestorFile(start, name) {
|
|
|
265
373
|
current = parent;
|
|
266
374
|
}
|
|
267
375
|
}
|
|
268
|
-
function copyDir(from, to) {
|
|
376
|
+
function copyDir(from, to, skipNodeModules = false) {
|
|
269
377
|
ensureDir(to);
|
|
270
378
|
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
379
|
+
if (skipNodeModules && entry.name === "node_modules")
|
|
380
|
+
continue;
|
|
271
381
|
const source = path.join(from, entry.name);
|
|
272
382
|
const target = path.join(to, entry.name);
|
|
273
383
|
if (entry.isDirectory())
|
|
274
|
-
copyDir(source, target);
|
|
384
|
+
copyDir(source, target, skipNodeModules);
|
|
275
385
|
else if (entry.isFile())
|
|
276
386
|
fs.copyFileSync(source, target);
|
|
277
387
|
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -695,7 +695,7 @@ function stopDecision(context, project, sessionId) {
|
|
|
695
695
|
if (!session || ["waiting_user", "blocked", "stopping", "stopped", "closed"].includes(session.status)) {
|
|
696
696
|
return {};
|
|
697
697
|
}
|
|
698
|
-
if (session.protocol_id && protocolAllowsStop(context, session.protocol_id)) {
|
|
698
|
+
if (session.protocol_id && protocolAllowsStop(context, project.id, session.protocol_id)) {
|
|
699
699
|
return {};
|
|
700
700
|
}
|
|
701
701
|
if (session.flow_kind === "merge_worker" && session.continuation_policy === "merge_queue") {
|
|
@@ -704,17 +704,17 @@ function stopDecision(context, project, sessionId) {
|
|
|
704
704
|
if (session.flow_kind === "merge_job" && session.continuation_policy === "merge_job") {
|
|
705
705
|
return stopContinuationDecision(context, project.id, sessionId, `merge-job:${session.protocol_id ?? session.session_id}:${session.next_action ?? "none"}`, `Continue dd-flow merge job ${session.protocol_id ?? ""}: complete merge work, close the protocol, and report terminal state.`);
|
|
706
706
|
}
|
|
707
|
-
const nextAction = session.next_action ?? nextActionFromProtocol(context, session.protocol_id);
|
|
707
|
+
const nextAction = session.next_action ?? nextActionFromProtocol(context, project.id, session.protocol_id);
|
|
708
708
|
if (!nextAction || nextAction === "none" || session.continuation_policy === "none") {
|
|
709
709
|
return {};
|
|
710
710
|
}
|
|
711
711
|
return stopContinuationDecision(context, project.id, sessionId, `${session.flow_kind}:${session.protocol_id ?? session.session_id}:${nextAction}`, `Continue dd-flow ${session.flow_kind}${session.protocol_id ? ` ${session.protocol_id}` : ""}: ${nextAction}`);
|
|
712
712
|
}
|
|
713
|
-
function nextActionFromProtocol(context, protocolId) {
|
|
713
|
+
function nextActionFromProtocol(context, projectId, protocolId) {
|
|
714
714
|
if (!protocolId) {
|
|
715
715
|
return null;
|
|
716
716
|
}
|
|
717
|
-
const protocol = context.db.get("SELECT id, status, stage, next_action FROM protocols WHERE id = ?", [protocolId]);
|
|
717
|
+
const protocol = context.db.get("SELECT id, status, stage, next_action FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
718
718
|
if (!protocol) {
|
|
719
719
|
return null;
|
|
720
720
|
}
|
|
@@ -723,8 +723,8 @@ function nextActionFromProtocol(context, protocolId) {
|
|
|
723
723
|
}
|
|
724
724
|
return protocol.next_action;
|
|
725
725
|
}
|
|
726
|
-
function protocolAllowsStop(context, protocolId) {
|
|
727
|
-
const protocol = context.db.get("SELECT status, stage FROM protocols WHERE id = ?", [protocolId]);
|
|
726
|
+
function protocolAllowsStop(context, projectId, protocolId) {
|
|
727
|
+
const protocol = context.db.get("SELECT status, stage FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
728
728
|
return Boolean(protocol && (["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage)));
|
|
729
729
|
}
|
|
730
730
|
function stopContinuationDecision(context, projectId, sessionId, actionKey, reason, options = {}) {
|
package/dist/services/ids.js
CHANGED
|
@@ -2,10 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { formatFullId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
6
|
const kindConfig = {
|
|
7
7
|
protocol: { type: "PRT", table: "protocols", fileRoot: ".memory-bank/protocol" },
|
|
8
|
-
run: { type: "RUN", table: "flow_runs"
|
|
8
|
+
run: { type: "RUN", table: "flow_runs" }
|
|
9
9
|
};
|
|
10
10
|
export function previewNextEntityId(context, input) {
|
|
11
11
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -13,51 +13,31 @@ export function previewNextEntityId(context, input) {
|
|
|
13
13
|
const slug = normalizeSlug(input.slug);
|
|
14
14
|
const config = kindConfig[kind];
|
|
15
15
|
const used = new Set();
|
|
16
|
-
|
|
16
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
17
|
+
for (const id of databaseIds(context, config.table, config.type, project?.id)) {
|
|
17
18
|
addSequence(used, id, config.type);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
if (kind === "run") {
|
|
23
|
-
for (const id of homeRunIds(context, projectRoot)) {
|
|
20
|
+
if (config.fileRoot) {
|
|
21
|
+
for (const id of filesystemIds(projectRoot, config.fileRoot, config.type)) {
|
|
24
22
|
addSequence(used, id, config.type);
|
|
25
23
|
}
|
|
26
|
-
for (const id of filesystemIds(projectRoot, ".tasks", config.type)) {
|
|
27
|
-
addSequence(used, id, config.type);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
for (let sequence = 1; sequence <= 999; sequence += 1) {
|
|
31
|
-
if (used.has(sequence))
|
|
32
|
-
continue;
|
|
33
|
-
const id = formatFullId(config.type, sequence, slug);
|
|
34
|
-
const parsed = parseFullEntityId(id);
|
|
35
|
-
return {
|
|
36
|
-
ok: true,
|
|
37
|
-
project_root: projectRoot,
|
|
38
|
-
entity: {
|
|
39
|
-
kind,
|
|
40
|
-
type: config.type,
|
|
41
|
-
id,
|
|
42
|
-
short_id: parsed.shortId,
|
|
43
|
-
slug,
|
|
44
|
-
sequence,
|
|
45
|
-
reserved: false
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
throw new AppError("validation", `${config.type} id sequence exhausted`, 2);
|
|
50
|
-
}
|
|
51
|
-
function homeRunIds(context, projectRoot) {
|
|
52
|
-
const project = context.db.get("SELECT id FROM projects WHERE root = ? AND status = 'active'", [projectRoot]);
|
|
53
|
-
if (!project) {
|
|
54
|
-
return [];
|
|
55
24
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
25
|
+
const sequence = Math.max(0, ...used) + 1;
|
|
26
|
+
const id = formatFullId(config.type, sequence, slug);
|
|
27
|
+
const parsed = parseFullEntityId(id);
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
project_root: projectRoot,
|
|
31
|
+
entity: {
|
|
32
|
+
kind,
|
|
33
|
+
type: config.type,
|
|
34
|
+
id,
|
|
35
|
+
short_id: parsed.shortId,
|
|
36
|
+
slug,
|
|
37
|
+
sequence,
|
|
38
|
+
reserved: false
|
|
39
|
+
}
|
|
40
|
+
};
|
|
61
41
|
}
|
|
62
42
|
function parseEntityKind(value) {
|
|
63
43
|
const normalized = value.toLowerCase();
|
|
@@ -67,20 +47,28 @@ function parseEntityKind(value) {
|
|
|
67
47
|
return "run";
|
|
68
48
|
throw new AppError("validation", "--type must be protocol or run", 2, { type: value });
|
|
69
49
|
}
|
|
70
|
-
function databaseIds(context, table, type) {
|
|
71
|
-
|
|
50
|
+
function databaseIds(context, table, type, projectId) {
|
|
51
|
+
if (!projectId)
|
|
52
|
+
return [];
|
|
53
|
+
return context.db
|
|
54
|
+
.all(`SELECT id FROM ${table} WHERE project_id = ? AND id LIKE ?`, [projectId, `${type}-%`])
|
|
55
|
+
.map((row) => row.id);
|
|
72
56
|
}
|
|
73
57
|
function filesystemIds(projectRoot, relativeRoot, type) {
|
|
74
58
|
const root = path.join(projectRoot, relativeRoot);
|
|
75
|
-
|
|
59
|
+
try {
|
|
60
|
+
return fs
|
|
61
|
+
.readdirSync(root)
|
|
62
|
+
.filter((entry) => entry.startsWith(`${type}-`))
|
|
63
|
+
.map((entry) => entry.replace(/\.md$/, ""));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
76
66
|
return [];
|
|
77
67
|
}
|
|
78
|
-
return fs
|
|
79
|
-
.readdirSync(root)
|
|
80
|
-
.filter((entry) => entry.startsWith(`${type}-`))
|
|
81
|
-
.map((entry) => entry.replace(/\.md$/, ""));
|
|
82
68
|
}
|
|
83
69
|
function addSequence(used, id, type) {
|
|
70
|
+
if (type === "PRT" && isLegacyDateProtocolId(id))
|
|
71
|
+
return;
|
|
84
72
|
try {
|
|
85
73
|
const parsed = parseFullEntityId(id);
|
|
86
74
|
if (parsed.type !== type)
|
|
@@ -91,6 +79,9 @@ function addSequence(used, id, type) {
|
|
|
91
79
|
return;
|
|
92
80
|
}
|
|
93
81
|
}
|
|
82
|
+
function isLegacyDateProtocolId(id) {
|
|
83
|
+
return /^PRT-\d{4}-\d{2}-\d{2}(?:-|$)/.test(id);
|
|
84
|
+
}
|
|
94
85
|
function normalizeSlug(value) {
|
|
95
86
|
const slug = value
|
|
96
87
|
.normalize("NFKD")
|