@deksden-com/dd-flow-cli 0.2.0 → 0.3.0

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 (33) hide show
  1. package/README.md +2 -0
  2. package/dist/build-info.json +10 -6
  3. package/dist/cli/help.js +70 -13
  4. package/dist/cli/run-cli.js +140 -10
  5. package/dist/schemas/compatibility.schema.json +26 -0
  6. package/dist/schemas/flow-guidance.schema.json +73 -0
  7. package/dist/schemas/flow-run-index.schema.json +30 -4
  8. package/dist/schemas/global-dashboard-data.schema.json +68 -0
  9. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  10. package/dist/schemas/plan-stage-report.schema.json +83 -0
  11. package/dist/schemas/project-dashboard-data.schema.json +98 -0
  12. package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
  13. package/dist/schemas/protocol-dashboard-data.schema.json +89 -0
  14. package/dist/schemas/status-report.schema.json +34 -0
  15. package/dist/schemas/version-report.schema.json +22 -0
  16. package/dist/services/build-info.js +26 -3
  17. package/dist/services/canon.js +93 -22
  18. package/dist/services/cleanup.js +14 -1
  19. package/dist/services/config.js +25 -0
  20. package/dist/services/dashboard.js +655 -10
  21. package/dist/services/flow-guidance.js +214 -0
  22. package/dist/services/ids.js +106 -0
  23. package/dist/services/merge-queue.js +9 -1
  24. package/dist/services/merge-worker.js +31 -2
  25. package/dist/services/projects.js +7 -1
  26. package/dist/services/protocols.js +637 -6
  27. package/dist/services/runs.js +98 -21
  28. package/dist/services/schema-validation.js +84 -4
  29. package/dist/services/status.js +189 -1
  30. package/dist/services/version-status.js +28 -2
  31. package/dist/storage/database.js +6 -0
  32. package/dist/storage/paths.js +21 -0
  33. package/package.json +1 -1
@@ -18,6 +18,8 @@
18
18
  "properties": {
19
19
  "version": { "type": "string", "minLength": 1 },
20
20
  "package_name": { "type": "string", "minLength": 1 },
21
+ "compatibility": { "$ref": "#/definitions/cliCompatibility" },
22
+ "registry": { "$ref": "#/definitions/registryStatus" },
21
23
  "build": {
22
24
  "type": "object",
23
25
  "additionalProperties": true,
@@ -85,6 +87,38 @@
85
87
  }
86
88
  },
87
89
  "definitions": {
90
+ "cliCompatibility": {
91
+ "type": "object",
92
+ "additionalProperties": true,
93
+ "required": ["verdict", "reason", "package_name", "installed_version"],
94
+ "properties": {
95
+ "verdict": { "type": "string", "enum": ["ok", "outdated", "incompatible", "missing", "unknown"] },
96
+ "reason": { "type": "string", "minLength": 1 },
97
+ "package_name": { "type": "string", "minLength": 1 },
98
+ "expected_package_name": { "type": "string" },
99
+ "installed_version": { "type": "string", "minLength": 1 },
100
+ "memory_bank_version": { "type": ["string", "null"] },
101
+ "min_version": { "type": ["string", "null"] },
102
+ "recommended_version": { "type": ["string", "null"] },
103
+ "status_contract": { "type": ["string", "null"] },
104
+ "version_contract": { "type": ["string", "null"] },
105
+ "flow_contract": { "type": ["string", "null"] },
106
+ "update_command": { "type": ["string", "null"] }
107
+ }
108
+ },
109
+ "registryStatus": {
110
+ "type": "object",
111
+ "additionalProperties": true,
112
+ "required": ["package_name", "latest", "checked_at", "source", "status"],
113
+ "properties": {
114
+ "package_name": { "type": "string", "minLength": 1 },
115
+ "latest": { "type": ["string", "null"] },
116
+ "checked_at": { "type": "string", "minLength": 1 },
117
+ "source": { "type": "string", "enum": ["npm", "npm_cache"] },
118
+ "status": { "type": "string", "enum": ["ok", "degraded"] },
119
+ "reason": { "type": "string" }
120
+ }
121
+ },
88
122
  "memoryBank": {
89
123
  "type": ["object", "null"],
90
124
  "additionalProperties": true,
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/version-report@1",
4
+ "title": "dd-flow version report",
5
+ "description": "Small read-only JSON contract for dd-flow version --json.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["ok", "schema_id", "cli"],
9
+ "properties": {
10
+ "ok": { "const": true },
11
+ "schema_id": { "const": "dd-flow/version-report@1" },
12
+ "cli": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["package_name", "version"],
16
+ "properties": {
17
+ "package_name": { "type": "string", "minLength": 1 },
18
+ "version": { "type": "string", "minLength": 1 }
19
+ }
20
+ }
21
+ }
22
+ }
@@ -21,6 +21,10 @@ export function getCliBuildInfo() {
21
21
  version: null,
22
22
  commit: null,
23
23
  flow_contract: null,
24
+ repo_root: null,
25
+ memorybank_root: null,
26
+ flow_root: null,
27
+ layout: null,
24
28
  status: "unknown",
25
29
  reason: "build_info_missing"
26
30
  }
@@ -31,10 +35,14 @@ export function getCliBuildInfo() {
31
35
  const canonVersion = stringOrNull(builtWithCanon?.version);
32
36
  const canonCommit = stringOrNull(builtWithCanon?.commit);
33
37
  const flowContract = stringOrNull(builtWithCanon?.flow_contract);
34
- const canonStatus = canonVersion && canonCommit ? "present" : "degraded";
38
+ const repoRoot = stringOrNull(builtWithCanon?.repo_root);
39
+ const memorybankRoot = stringOrNull(builtWithCanon?.memorybank_root);
40
+ const flowRoot = stringOrNull(builtWithCanon?.flow_root);
41
+ const layout = stringOrNull(builtWithCanon?.layout);
42
+ const canonStatus = canonVersion && canonCommit && flowContract ? "present" : "degraded";
35
43
  return {
36
- version: stringOr(buildInfo.cli_version, version),
37
- package_name: stringOr(buildInfo.cli_package, packageName),
44
+ version,
45
+ package_name: packageName,
38
46
  build: {
39
47
  status: "present",
40
48
  built_at: stringOrNull(buildInfo.built_at),
@@ -43,12 +51,27 @@ export function getCliBuildInfo() {
43
51
  version: canonVersion,
44
52
  commit: canonCommit,
45
53
  flow_contract: flowContract,
54
+ repo_root: repoRoot,
55
+ memorybank_root: memorybankRoot,
56
+ flow_root: flowRoot,
57
+ layout,
46
58
  status: canonStatus,
47
59
  ...(canonStatus === "degraded" ? { reason: "build_canon_metadata_incomplete" } : {})
48
60
  }
49
61
  }
50
62
  };
51
63
  }
64
+ export function getCliVersionReport() {
65
+ const cli = getCliBuildInfo();
66
+ return {
67
+ ok: true,
68
+ schema_id: "dd-flow/version-report@1",
69
+ cli: {
70
+ package_name: cli.package_name,
71
+ version: cli.version
72
+ }
73
+ };
74
+ }
52
75
  function findPackageRoot(start) {
53
76
  let current = path.resolve(start);
54
77
  while (true) {
@@ -46,14 +46,21 @@ export function resolveCanonRoot(context, input = {}) {
46
46
  throw new Error("Expected one canonical candidate.");
47
47
  }
48
48
  const root = candidate.realpath ?? candidate.root;
49
- const metadata = readCanonMetadata(root);
49
+ const flowRoot = candidate.flow_root ?? path.join(root, "dd-flow");
50
+ const memorybankRoot = candidate.memorybank_root ?? root;
51
+ const mbbRoot = candidate.mbb_root ?? path.join(memorybankRoot, "mbb");
52
+ const layout = candidate.layout ?? "legacy_root";
53
+ const metadata = readCanonMetadata(root, flowRoot);
50
54
  return {
51
55
  ok: true,
52
56
  canon: {
53
57
  root,
58
+ repo_root: root,
54
59
  source: candidate.source,
55
- flow_root: path.join(root, "dd-flow"),
56
- memorybank_root: root,
60
+ layout,
61
+ flow_root: flowRoot,
62
+ mbb_root: mbbRoot,
63
+ memorybank_root: memorybankRoot,
57
64
  version: metadata.version,
58
65
  commit: metadata.commit,
59
66
  flow_contract: metadata.flow_contract,
@@ -121,24 +128,28 @@ function validateCanonCandidate(source, root) {
121
128
  if (!fs.statSync(realpath).isDirectory()) {
122
129
  blockers.push(`${source} canonical root is not a directory: ${realpath}`);
123
130
  }
124
- const requiredFiles = [
125
- "dd-flow/README.md",
126
- "mbb/index.md",
127
- "protocol/index.md",
128
- "dd-flow/mb-init.md",
129
- "dd-flow/mb-upgrade.md",
130
- "dd-flow/mb-upgrade-review.md",
131
- "dd-flow/mb-distill.md"
132
- ];
133
- for (const relativePath of requiredFiles) {
134
- const file = path.join(realpath, relativePath);
135
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
136
- blockers.push(`${source} canonical root is missing ${relativePath}: ${realpath}`);
137
- }
131
+ if (path.basename(realpath) === ".memory-bank" && fs.existsSync(path.join(path.dirname(realpath), "VERSION"))) {
132
+ blockers.push(`${source} canonical root points to .memory-bank; use the dd-memorybank repository root instead: ${path.dirname(realpath)}`);
138
133
  }
139
- return { source, root: absolute, realpath, valid: blockers.length === 0, blockers };
134
+ const layout = assessCanonLayout(realpath);
135
+ blockers.push(...layout.blockers.map((blocker) => `${source} ${blocker}`));
136
+ return {
137
+ source,
138
+ root: absolute,
139
+ realpath,
140
+ valid: blockers.length === 0 && layout.valid,
141
+ blockers,
142
+ ...(layout.valid
143
+ ? {
144
+ layout: layout.layout,
145
+ memorybank_root: layout.memorybankRoot,
146
+ flow_root: layout.flowRoot,
147
+ mbb_root: layout.mbbRoot
148
+ }
149
+ : {})
150
+ };
140
151
  }
141
- export function readCanonMetadata(root) {
152
+ export function readCanonMetadata(root, flowRoot = path.join(root, "dd-flow")) {
142
153
  const diagnostics = [];
143
154
  const version = readTrimmed(path.join(root, "VERSION"));
144
155
  if (!version)
@@ -146,7 +157,7 @@ export function readCanonMetadata(root) {
146
157
  const commit = gitCommit(root);
147
158
  if (!commit)
148
159
  diagnostics.push("canon_commit_unknown");
149
- const flowContract = readFlowContractId(root);
160
+ const flowContract = readFlowContractId(flowRoot);
150
161
  if (!flowContract)
151
162
  diagnostics.push("flow_contract_unknown");
152
163
  return {
@@ -166,9 +177,69 @@ function readTrimmed(file) {
166
177
  return null;
167
178
  }
168
179
  }
169
- function readFlowContractId(root) {
180
+ export function resolveCanonLayout(root) {
181
+ return assessCanonLayout(path.resolve(root));
182
+ }
183
+ function assessCanonLayout(root) {
184
+ const legacy = assessLayout(root, "legacy_root", root);
185
+ const dot = assessLayout(root, "dot_memory_bank", path.join(root, ".memory-bank"));
186
+ if (dot.valid && !legacy.valid)
187
+ return dot;
188
+ if (legacy.valid && !dot.valid)
189
+ return legacy;
190
+ if (dot.valid && legacy.valid) {
191
+ if (dot.flowContract && legacy.flowContract && dot.flowContract !== legacy.flowContract) {
192
+ return {
193
+ ...dot,
194
+ valid: false,
195
+ blockers: [
196
+ `canonical root has conflicting legacy_root and dot_memory_bank flow contracts: ${legacy.flowContract} vs ${dot.flowContract}`
197
+ ]
198
+ };
199
+ }
200
+ return dot;
201
+ }
202
+ return {
203
+ ...dot,
204
+ valid: false,
205
+ blockers: [
206
+ "canonical root does not match legacy_root or dot_memory_bank layout",
207
+ ...legacy.blockers.map((blocker) => `legacy_root: ${blocker}`),
208
+ ...dot.blockers.map((blocker) => `dot_memory_bank: ${blocker}`)
209
+ ]
210
+ };
211
+ }
212
+ function assessLayout(root, layout, memorybankRoot) {
213
+ const blockers = [];
214
+ const requiredFiles = [
215
+ "dd-flow/README.md",
216
+ "mbb/index.md",
217
+ "protocol/index.md",
218
+ "dd-flow/mb-init.md",
219
+ "dd-flow/mb-upgrade.md",
220
+ "dd-flow/mb-upgrade-review.md",
221
+ "dd-flow/mb-distill.md"
222
+ ];
223
+ for (const relativePath of requiredFiles) {
224
+ const file = path.join(memorybankRoot, relativePath);
225
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
226
+ blockers.push(`canonical root is missing ${layout} ${relativePath}: ${root}`);
227
+ }
228
+ }
229
+ const flowRoot = path.join(memorybankRoot, "dd-flow");
230
+ return {
231
+ layout,
232
+ memorybankRoot,
233
+ flowRoot,
234
+ mbbRoot: path.join(memorybankRoot, "mbb"),
235
+ valid: blockers.length === 0,
236
+ blockers,
237
+ flowContract: readFlowContractId(flowRoot)
238
+ };
239
+ }
240
+ function readFlowContractId(flowRoot) {
170
241
  try {
171
- const parsed = JSON.parse(fs.readFileSync(path.join(root, "dd-flow", "flow-contract.json"), "utf8"));
242
+ const parsed = JSON.parse(fs.readFileSync(path.join(flowRoot, "flow-contract.json"), "utf8"));
172
243
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
173
244
  const id = parsed.id;
174
245
  return typeof id === "string" && id.length > 0 ? id : null;
@@ -7,7 +7,7 @@ import { ensureReadableFile } from "../storage/database.js";
7
7
  import { projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
8
8
  import { appendAudit } from "./audit.js";
9
9
  import { requireProjectByRoot } from "./projects.js";
10
- import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
10
+ import { protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
11
11
  const actionKinds = new Set([
12
12
  "repair_protocol_state",
13
13
  "cancel_queue_job",
@@ -39,6 +39,19 @@ export function cleanupScan(context, input) {
39
39
  });
40
40
  }
41
41
  }
42
+ const fullProtocol = requireProtocol(context, protocol.id);
43
+ const state = readProtocolRuntimeState(context, fullProtocol).state;
44
+ const diagnostics = protocolRunDiagnostics(context, fullProtocol, state).diagnostics;
45
+ for (const diagnostic of diagnostics.filter((entry) => entry.code === "protocol_run_stage_mismatch")) {
46
+ findings.push({
47
+ code: "protocol_run_stage_mismatch",
48
+ severity: diagnostic.severity === "error" ? "warning" : "info",
49
+ protocol_id: protocol.id,
50
+ protocol_stage: state.stage,
51
+ latest_run: diagnostic.latest_run,
52
+ recommended_commands: diagnostic.recommended_commands
53
+ });
54
+ }
42
55
  }
43
56
  const orphanQueueJobs = context.db.all(`SELECT mq.protocol_id, mq.status FROM merge_queue mq
44
57
  LEFT JOIN protocols p ON p.id = mq.protocol_id
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { cmuxModes } from "../domain/contracts.js";
3
3
  import { AppError } from "../shared/errors.js";
4
+ import { projectHome } from "../storage/paths.js";
4
5
  export const defaultProjectConfig = {
5
6
  dashboard: {
6
7
  auto_refresh: true,
@@ -52,6 +53,30 @@ export function globalDashboardMarkdownPath(context, config) {
52
53
  const configured = config?.dashboard.global_markdown_path ?? defaultProjectConfig.dashboard.global_markdown_path;
53
54
  return path.isAbsolute(configured) ? configured : path.join(context.ddFlowHome, configured);
54
55
  }
56
+ export function projectDashboardDir(ddFlowHome, projectId) {
57
+ return path.join(projectHome(ddFlowHome, projectId), "dashboard");
58
+ }
59
+ export function projectDashboardJsonPath(context, projectId) {
60
+ return path.join(projectDashboardDir(context.ddFlowHome, projectId), "project-dashboard.json");
61
+ }
62
+ export function projectDashboardHtmlPath(context, projectId) {
63
+ return path.join(projectDashboardDir(context.ddFlowHome, projectId), "project-dashboard.html");
64
+ }
65
+ export function protocolDashboardJsonPath(context, projectId, protocolId) {
66
+ return path.join(projectDashboardDir(context.ddFlowHome, projectId), "protocols", `${protocolId}.json`);
67
+ }
68
+ export function protocolDashboardHtmlPath(context, projectId, protocolId) {
69
+ return path.join(projectDashboardDir(context.ddFlowHome, projectId), "protocols", `${protocolId}.html`);
70
+ }
71
+ export function globalDashboardDir(context) {
72
+ return path.join(context.ddFlowHome, "dashboard");
73
+ }
74
+ export function globalDashboardJsonPath(context) {
75
+ return path.join(globalDashboardDir(context), "global-dashboard.json");
76
+ }
77
+ export function globalDashboardHtmlPath(context) {
78
+ return path.join(globalDashboardDir(context), "global-dashboard.html");
79
+ }
55
80
  function configOverrides(context, projectId) {
56
81
  return context.db.all("SELECT key, value_json, updated_at FROM project_config WHERE project_id = ? ORDER BY key ASC", [projectId]);
57
82
  }