@deksden-com/dd-flow-cli 0.1.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 (37) hide show
  1. package/README.md +13 -0
  2. package/dist/build-info.json +15 -0
  3. package/dist/cli/help.js +133 -14
  4. package/dist/cli/run-cli.js +242 -8
  5. package/dist/schemas/archived-flow-manifest.schema.json +112 -0
  6. package/dist/schemas/compatibility.schema.json +26 -0
  7. package/dist/schemas/flow-guidance.schema.json +73 -0
  8. package/dist/schemas/flow-run-index.schema.json +30 -4
  9. package/dist/schemas/global-dashboard-data.schema.json +68 -0
  10. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  11. package/dist/schemas/merge-stage-report.schema.json +61 -1
  12. package/dist/schemas/plan-stage-report.schema.json +255 -0
  13. package/dist/schemas/project-dashboard-data.schema.json +98 -0
  14. package/dist/schemas/project-flow-pack-manifest.schema.json +127 -0
  15. package/dist/schemas/protocol-dashboard-data.schema.json +89 -0
  16. package/dist/schemas/status-report.schema.json +186 -0
  17. package/dist/schemas/version-report.schema.json +22 -0
  18. package/dist/services/build-info.js +114 -0
  19. package/dist/services/canon.js +298 -0
  20. package/dist/services/cleanup.js +14 -1
  21. package/dist/services/config.js +25 -0
  22. package/dist/services/dashboard.js +655 -10
  23. package/dist/services/flow-guidance.js +214 -0
  24. package/dist/services/ids.js +106 -0
  25. package/dist/services/lanes.js +6 -2
  26. package/dist/services/merge-queue.js +68 -4
  27. package/dist/services/merge-worker.js +177 -0
  28. package/dist/services/projects.js +7 -1
  29. package/dist/services/protocols.js +648 -17
  30. package/dist/services/runs.js +98 -21
  31. package/dist/services/schema-validation.js +88 -9
  32. package/dist/services/sessions.js +3 -2
  33. package/dist/services/status.js +306 -0
  34. package/dist/services/version-status.js +284 -0
  35. package/dist/storage/database.js +13 -0
  36. package/dist/storage/paths.js +21 -0
  37. package/package.json +2 -2
@@ -0,0 +1,186 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/status-report@1",
4
+ "title": "dd-flow status report",
5
+ "description": "Read-only status contract for CLI/canon/project Memory Bank version visibility and drift diagnostics.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": ["ok", "schema_id", "dd_flow_home", "cwd", "cli", "project", "canon"],
9
+ "properties": {
10
+ "ok": { "type": "boolean" },
11
+ "schema_id": { "const": "dd-flow/status-report@1" },
12
+ "dd_flow_home": { "type": "string", "minLength": 1 },
13
+ "cwd": { "type": "string", "minLength": 1 },
14
+ "cli": {
15
+ "type": "object",
16
+ "additionalProperties": true,
17
+ "required": ["version", "package_name", "build"],
18
+ "properties": {
19
+ "version": { "type": "string", "minLength": 1 },
20
+ "package_name": { "type": "string", "minLength": 1 },
21
+ "compatibility": { "$ref": "#/definitions/cliCompatibility" },
22
+ "registry": { "$ref": "#/definitions/registryStatus" },
23
+ "build": {
24
+ "type": "object",
25
+ "additionalProperties": true,
26
+ "required": ["status", "built_with_canon"],
27
+ "properties": {
28
+ "status": { "type": "string", "enum": ["present", "degraded"] },
29
+ "built_at": { "type": ["string", "null"] },
30
+ "commit": { "type": ["string", "null"] },
31
+ "reason": { "type": "string" },
32
+ "built_with_canon": {
33
+ "type": "object",
34
+ "additionalProperties": true,
35
+ "required": ["version", "commit", "flow_contract", "status"],
36
+ "properties": {
37
+ "version": { "type": ["string", "null"] },
38
+ "commit": { "type": ["string", "null"] },
39
+ "flow_contract": { "type": ["string", "null"] },
40
+ "status": { "type": "string", "enum": ["present", "unknown", "degraded"] },
41
+ "reason": { "type": "string" }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "project": {
49
+ "type": "object",
50
+ "additionalProperties": true,
51
+ "required": ["requested_root", "root", "root_source", "registered", "memory_bank", "flow_pack", "drift"],
52
+ "properties": {
53
+ "requested_root": { "type": "string", "minLength": 1 },
54
+ "root": { "type": ["string", "null"] },
55
+ "root_source": { "type": "string", "enum": ["explicit", "git", "cwd"] },
56
+ "registered": { "type": "boolean" },
57
+ "id": { "type": ["string", "null"] },
58
+ "status": { "type": ["string", "null"] },
59
+ "memory_bank": { "$ref": "#/definitions/memoryBank" },
60
+ "flow_pack": { "$ref": "#/definitions/flowPack" },
61
+ "drift": { "$ref": "#/definitions/drift" }
62
+ }
63
+ },
64
+ "canon": {
65
+ "type": "object",
66
+ "additionalProperties": true,
67
+ "required": ["ok", "resolved", "compatibility"],
68
+ "properties": {
69
+ "ok": { "type": "boolean" },
70
+ "resolved": {
71
+ "type": ["object", "null"],
72
+ "additionalProperties": true
73
+ },
74
+ "compatibility": {
75
+ "type": "object",
76
+ "additionalProperties": true,
77
+ "required": ["cli_built_with_resolved_canon", "reason"],
78
+ "properties": {
79
+ "cli_built_with_resolved_canon": {
80
+ "type": "string",
81
+ "enum": ["same", "behind", "ahead", "unknown"]
82
+ },
83
+ "reason": { "type": "string", "minLength": 1 }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ },
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
+ },
122
+ "memoryBank": {
123
+ "type": ["object", "null"],
124
+ "additionalProperties": true,
125
+ "required": ["root", "folder_name", "version", "source", "status"],
126
+ "properties": {
127
+ "root": { "type": ["string", "null"] },
128
+ "folder_name": { "type": ["string", "null"], "enum": [".memory-bank", "memory-bank", null] },
129
+ "version": { "type": ["string", "null"] },
130
+ "source": { "type": ["string", "null"] },
131
+ "status": { "type": "string", "enum": ["present", "missing", "invalid", "unknown"] },
132
+ "reason": { "type": "string" }
133
+ }
134
+ },
135
+ "flowPack": {
136
+ "type": ["object", "null"],
137
+ "additionalProperties": true,
138
+ "required": ["manifest_path", "schema_id", "pack_version", "source_commit", "canon_version_at_source_commit", "status", "canonical_only_files"],
139
+ "properties": {
140
+ "manifest_path": { "type": ["string", "null"] },
141
+ "schema_id": { "type": ["string", "null"] },
142
+ "pack_version": { "type": ["string", "null"] },
143
+ "source_commit": { "type": ["string", "null"] },
144
+ "canon_version_at_source_commit": { "type": ["string", "null"] },
145
+ "status": { "type": "string", "enum": ["present", "missing", "invalid", "degraded"] },
146
+ "reason": { "type": "string" },
147
+ "canonical_only_files": {
148
+ "type": "array",
149
+ "items": { "type": "string", "minLength": 1 }
150
+ }
151
+ }
152
+ },
153
+ "drift": {
154
+ "type": ["object", "null"],
155
+ "additionalProperties": true,
156
+ "required": ["memory_bank_version", "flow_pack_commit", "overall", "next_action"],
157
+ "properties": {
158
+ "memory_bank_version": { "$ref": "#/definitions/comparison" },
159
+ "flow_pack_commit": { "$ref": "#/definitions/comparison" },
160
+ "overall": {
161
+ "type": "string",
162
+ "enum": ["same", "behind", "ahead", "diverged", "missing", "invalid", "unknown", "not_applicable"]
163
+ },
164
+ "next_action": { "type": "string", "minLength": 1 }
165
+ }
166
+ },
167
+ "comparison": {
168
+ "type": "object",
169
+ "additionalProperties": true,
170
+ "required": ["status", "confidence", "reason"],
171
+ "properties": {
172
+ "status": {
173
+ "type": "string",
174
+ "enum": ["same", "behind", "ahead", "diverged", "missing", "invalid", "unknown", "not_applicable"]
175
+ },
176
+ "confidence": {
177
+ "type": "string",
178
+ "enum": ["exact", "semver", "git_ancestry", "declared", "none"]
179
+ },
180
+ "reason": { "type": "string", "minLength": 1 },
181
+ "current": { "type": ["string", "null"] },
182
+ "target": { "type": ["string", "null"] }
183
+ }
184
+ }
185
+ }
186
+ }
@@ -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
+ }
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function getCliBuildInfo() {
5
+ const packageRoot = findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
6
+ const packageJson = readPackageJson(packageRoot);
7
+ const version = stringOr(packageJson.version, "0.0.0");
8
+ const packageName = stringOr(packageJson.name, "dd-flow-cli");
9
+ const buildInfoPath = packageRoot ? path.join(packageRoot, "dist", "build-info.json") : null;
10
+ const buildInfo = buildInfoPath && fs.existsSync(buildInfoPath) ? readBuildInfo(buildInfoPath) : null;
11
+ if (!buildInfo) {
12
+ return {
13
+ version,
14
+ package_name: packageName,
15
+ build: {
16
+ status: "degraded",
17
+ built_at: null,
18
+ commit: null,
19
+ reason: "build_info_missing",
20
+ built_with_canon: {
21
+ version: null,
22
+ commit: null,
23
+ flow_contract: null,
24
+ repo_root: null,
25
+ memorybank_root: null,
26
+ flow_root: null,
27
+ layout: null,
28
+ status: "unknown",
29
+ reason: "build_info_missing"
30
+ }
31
+ }
32
+ };
33
+ }
34
+ const builtWithCanon = asRecord(buildInfo.built_with_canon);
35
+ const canonVersion = stringOrNull(builtWithCanon?.version);
36
+ const canonCommit = stringOrNull(builtWithCanon?.commit);
37
+ const flowContract = stringOrNull(builtWithCanon?.flow_contract);
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";
43
+ return {
44
+ version,
45
+ package_name: packageName,
46
+ build: {
47
+ status: "present",
48
+ built_at: stringOrNull(buildInfo.built_at),
49
+ commit: stringOrNull(buildInfo.cli_commit),
50
+ built_with_canon: {
51
+ version: canonVersion,
52
+ commit: canonCommit,
53
+ flow_contract: flowContract,
54
+ repo_root: repoRoot,
55
+ memorybank_root: memorybankRoot,
56
+ flow_root: flowRoot,
57
+ layout,
58
+ status: canonStatus,
59
+ ...(canonStatus === "degraded" ? { reason: "build_canon_metadata_incomplete" } : {})
60
+ }
61
+ }
62
+ };
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
+ }
75
+ function findPackageRoot(start) {
76
+ let current = path.resolve(start);
77
+ while (true) {
78
+ const candidate = path.join(current, "package.json");
79
+ if (fs.existsSync(candidate))
80
+ return current;
81
+ const parent = path.dirname(current);
82
+ if (parent === current)
83
+ return null;
84
+ current = parent;
85
+ }
86
+ }
87
+ function readPackageJson(packageRoot) {
88
+ if (!packageRoot)
89
+ return {};
90
+ try {
91
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
92
+ }
93
+ catch {
94
+ return {};
95
+ }
96
+ }
97
+ function readBuildInfo(file) {
98
+ try {
99
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
100
+ return asRecord(parsed) ? parsed : null;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function asRecord(value) {
107
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
108
+ }
109
+ function stringOr(value, fallback) {
110
+ return typeof value === "string" && value.length > 0 ? value : fallback;
111
+ }
112
+ function stringOrNull(value) {
113
+ return typeof value === "string" && value.length > 0 ? value : null;
114
+ }
@@ -0,0 +1,298 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ const canonRootKey = "canon.root";
5
+ export function registerCanonRoot(context, input) {
6
+ const resolution = resolveCanonRoot(context, { explicitRoot: input.root, allowRegistered: false });
7
+ if (!resolution.ok || !resolution.canon) {
8
+ return {
9
+ ...resolution,
10
+ action: "register",
11
+ exit_code: 1
12
+ };
13
+ }
14
+ const now = context.now();
15
+ context.db.run(`INSERT INTO runtime_config (key, value_json, created_at, updated_at)
16
+ VALUES (?, ?, ?, ?)
17
+ ON CONFLICT(key) DO UPDATE SET
18
+ value_json = excluded.value_json,
19
+ updated_at = excluded.updated_at`, [canonRootKey, JSON.stringify({ root: resolution.canon.root }), now, now]);
20
+ return {
21
+ ok: true,
22
+ action: "register",
23
+ canon: {
24
+ ...resolution.canon,
25
+ source: "registered"
26
+ },
27
+ db_path: context.db.path
28
+ };
29
+ }
30
+ export function getCanonStatus(context, input = {}) {
31
+ const resolution = resolveCanonRoot(context, { explicitRoot: input.root });
32
+ return {
33
+ ...resolution,
34
+ dd_flow_home: context.ddFlowHome,
35
+ registered: readRegisteredCanonRoot(context)
36
+ };
37
+ }
38
+ export function resolveCanonRoot(context, input = {}) {
39
+ const candidates = canonCandidates(context, input);
40
+ const validCandidates = candidates.filter((candidate) => candidate.valid && candidate.realpath);
41
+ const uniqueValid = uniqueBy(validCandidates, (candidate) => candidate.realpath ?? candidate.root);
42
+ const blockers = candidates.flatMap((candidate) => candidate.blockers);
43
+ if (uniqueValid.length === 1) {
44
+ const candidate = uniqueValid[0];
45
+ if (!candidate) {
46
+ throw new Error("Expected one canonical candidate.");
47
+ }
48
+ const root = candidate.realpath ?? candidate.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);
54
+ return {
55
+ ok: true,
56
+ canon: {
57
+ root,
58
+ repo_root: root,
59
+ source: candidate.source,
60
+ layout,
61
+ flow_root: flowRoot,
62
+ mbb_root: mbbRoot,
63
+ memorybank_root: memorybankRoot,
64
+ version: metadata.version,
65
+ commit: metadata.commit,
66
+ flow_contract: metadata.flow_contract,
67
+ metadata
68
+ },
69
+ candidates,
70
+ blockers: [],
71
+ bootstrap: []
72
+ };
73
+ }
74
+ if (uniqueValid.length > 1) {
75
+ return {
76
+ ok: false,
77
+ exit_code: 1,
78
+ candidates,
79
+ blockers: [
80
+ "Multiple different canonical Memory Bank roots are configured. Pass --root explicitly or make DD_MEMORYBANK and the registered root point to the same checkout."
81
+ ],
82
+ bootstrap: [
83
+ "dd-flow canon register --root \"$DD_MEMORYBANK\" --json",
84
+ "dd-flow canon resolve --root /absolute/path/to/dd-memorybank --json"
85
+ ]
86
+ };
87
+ }
88
+ return {
89
+ ok: false,
90
+ exit_code: 1,
91
+ candidates,
92
+ blockers: blockers.length > 0 ? blockers : ["Canonical Memory Bank root is not configured."],
93
+ bootstrap: [
94
+ "Clone the canonical dd-memorybank repository locally.",
95
+ "export DD_MEMORYBANK=/absolute/path/to/dd-memorybank",
96
+ "dd-flow canon register --root \"$DD_MEMORYBANK\" --json"
97
+ ]
98
+ };
99
+ }
100
+ function canonCandidates(context, input) {
101
+ const candidates = [];
102
+ if (input.explicitRoot) {
103
+ candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
104
+ return candidates;
105
+ }
106
+ if (context.env.DD_MEMORYBANK) {
107
+ candidates.push(validateCanonCandidate("env", context.env.DD_MEMORYBANK));
108
+ }
109
+ if (input.allowRegistered !== false) {
110
+ const registered = readRegisteredCanonRoot(context);
111
+ if (registered?.root) {
112
+ candidates.push(validateCanonCandidate("registered", registered.root));
113
+ }
114
+ }
115
+ for (const known of knownCanonRoots()) {
116
+ candidates.push(validateCanonCandidate("known", known));
117
+ }
118
+ return uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`);
119
+ }
120
+ function validateCanonCandidate(source, root) {
121
+ const absolute = path.resolve(root);
122
+ const blockers = [];
123
+ let realpath = null;
124
+ if (!fs.existsSync(absolute)) {
125
+ return { source, root: absolute, realpath, valid: false, blockers: [`${source} canonical root does not exist: ${absolute}`] };
126
+ }
127
+ realpath = fs.realpathSync(absolute);
128
+ if (!fs.statSync(realpath).isDirectory()) {
129
+ blockers.push(`${source} canonical root is not a directory: ${realpath}`);
130
+ }
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)}`);
133
+ }
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
+ };
151
+ }
152
+ export function readCanonMetadata(root, flowRoot = path.join(root, "dd-flow")) {
153
+ const diagnostics = [];
154
+ const version = readTrimmed(path.join(root, "VERSION"));
155
+ if (!version)
156
+ diagnostics.push("canon_version_missing");
157
+ const commit = gitCommit(root);
158
+ if (!commit)
159
+ diagnostics.push("canon_commit_unknown");
160
+ const flowContract = readFlowContractId(flowRoot);
161
+ if (!flowContract)
162
+ diagnostics.push("flow_contract_unknown");
163
+ return {
164
+ version,
165
+ commit,
166
+ flow_contract: flowContract,
167
+ status: version && commit && flowContract ? "present" : diagnostics.length > 0 ? "degraded" : "unknown",
168
+ diagnostics
169
+ };
170
+ }
171
+ function readTrimmed(file) {
172
+ try {
173
+ const value = fs.readFileSync(file, "utf8").trim();
174
+ return value.length > 0 ? value : null;
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
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) {
241
+ try {
242
+ const parsed = JSON.parse(fs.readFileSync(path.join(flowRoot, "flow-contract.json"), "utf8"));
243
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
244
+ const id = parsed.id;
245
+ return typeof id === "string" && id.length > 0 ? id : null;
246
+ }
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ return null;
252
+ }
253
+ function gitCommit(root) {
254
+ const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
255
+ return result.status === 0 ? result.stdout.trim() || null : null;
256
+ }
257
+ function readRegisteredCanonRoot(context) {
258
+ const row = context.db.get("SELECT key, value_json, updated_at FROM runtime_config WHERE key = ?", [canonRootKey]);
259
+ if (!row)
260
+ return null;
261
+ const value = JSON.parse(row.value_json);
262
+ return typeof value.root === "string" ? { root: value.root, updated_at: row.updated_at } : null;
263
+ }
264
+ function knownCanonRoots() {
265
+ const cwd = process.cwd();
266
+ const candidates = [];
267
+ for (const dir of ancestorDirs(cwd)) {
268
+ if (path.basename(dir) === "dd-memorybank") {
269
+ candidates.push(dir);
270
+ }
271
+ candidates.push(path.join(dir, "dd-memorybank"));
272
+ }
273
+ return uniqueBy(candidates.filter((candidate) => fs.existsSync(candidate)), (candidate) => fs.realpathSync(candidate));
274
+ }
275
+ function ancestorDirs(start) {
276
+ const dirs = [];
277
+ let current = path.resolve(start);
278
+ while (true) {
279
+ dirs.push(current);
280
+ const parent = path.dirname(current);
281
+ if (parent === current)
282
+ break;
283
+ current = parent;
284
+ }
285
+ return dirs;
286
+ }
287
+ function uniqueBy(items, keyFor) {
288
+ const seen = new Set();
289
+ const result = [];
290
+ for (const item of items) {
291
+ const key = keyFor(item);
292
+ if (!seen.has(key)) {
293
+ seen.add(key);
294
+ result.push(item);
295
+ }
296
+ }
297
+ return result;
298
+ }
@@ -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