@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +25 -9
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +58 -32
  5. package/dist/cli/run-cli.js +282 -50
  6. package/dist/domain/entity-ids.js +4 -4
  7. package/dist/domain/flow-contract.js +502 -36
  8. package/dist/domain/validation.js +34 -0
  9. package/dist/protocol/local-files.js +8 -6
  10. package/dist/schemas/code-stage-report.schema.json +197 -2
  11. package/dist/schemas/flow-contract.schema.json +126 -0
  12. package/dist/schemas/flow-run-index-v3.schema.json +203 -0
  13. package/dist/schemas/flow-run-index.schema.json +22 -2
  14. package/dist/schemas/flow-run.schema.json +36 -0
  15. package/dist/schemas/merge-stage-report.schema.json +213 -2
  16. package/dist/schemas/plan-stage-report.schema.json +156 -2
  17. package/dist/schemas/release-impact.schema.json +16 -0
  18. package/dist/services/audit.js +3 -3
  19. package/dist/services/branch-context.js +266 -0
  20. package/dist/services/canon.js +0 -1
  21. package/dist/services/cleanup.js +6 -6
  22. package/dist/services/cli-operation-classifier.js +1 -1
  23. package/dist/services/compatibility-preflight.js +8 -77
  24. package/dist/services/dashboard.js +48 -11
  25. package/dist/services/engines.js +123 -13
  26. package/dist/services/hooks.js +6 -6
  27. package/dist/services/ids.js +40 -49
  28. package/dist/services/merge-queue.js +240 -20
  29. package/dist/services/merge-worker.js +12 -4
  30. package/dist/services/migrations.js +64 -0
  31. package/dist/services/plans.js +23 -16
  32. package/dist/services/projects.js +2 -2
  33. package/dist/services/prompts.js +322 -0
  34. package/dist/services/protocols.js +78 -42
  35. package/dist/services/run-projection.js +80 -0
  36. package/dist/services/runs.js +360 -22
  37. package/dist/services/schema-validation.js +35 -12
  38. package/dist/services/sessions.js +81 -3
  39. package/dist/services/status.js +32 -1
  40. package/dist/services/usage.js +233 -0
  41. package/dist/services/worktrees.js +24 -19
  42. package/dist/storage/database.js +223 -9
  43. 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
- import { requireProtocol } from "./protocols.js";
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: ["read_only_diagnostics", "mb_upgrade"],
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);
@@ -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: projectRootFromArgs(args) });
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 projectRootFromArgs(args) {
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 arg = args[index];
197
- if ((arg === "--project-root" || arg === "--root") && args[index + 1])
198
- return args[index + 1];
199
- if (arg?.startsWith("--project-root="))
200
- return arg.slice("--project-root=".length);
201
- if (arg?.startsWith("--root="))
202
- return arg.slice("--root=".length);
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 undefined;
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
  }
@@ -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 = {}) {
@@ -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 { projectRunHome, resolveProjectRoot } from "../storage/paths.js";
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", fileRoot: ".tasks/dd-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
- for (const id of databaseIds(context, config.table, config.type)) {
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
- for (const id of filesystemIds(projectRoot, config.fileRoot, config.type)) {
20
- addSequence(used, id, config.type);
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 root = projectRunHome(context.ddFlowHome, project.id, "__placeholder__").replace(/__placeholder__$/, "");
57
- if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
58
- return [];
59
- }
60
- return fs.readdirSync(root).filter((entry) => entry.startsWith("RUN-"));
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
- return context.db.all(`SELECT id FROM ${table} WHERE id LIKE ?`, [`${type}-%`]).map((row) => row.id);
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
- if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
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")