@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,306 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { getCanonStatus } from "./canon.js";
5
+ import { getCliBuildInfo } from "./build-info.js";
6
+ import { getProjectVersionStatus, resolveStatusProjectRoot } from "./version-status.js";
7
+ import { findProjectByRoot } from "./projects.js";
8
+ export function getRuntimeStatus(context, input = {}) {
9
+ const cwd = process.cwd();
10
+ const projectRootResolution = resolveStatusProjectRoot({ requestedRoot: input.projectRoot, cwd });
11
+ const projectRoot = projectRootResolution.root;
12
+ const project = projectRoot ? findProjectByRoot(context, projectRoot) : undefined;
13
+ const canonStatus = getCanonStatus(context);
14
+ const canon = asRecord(canonStatus)?.canon;
15
+ const resolvedCanonForProject = canonForProjectStatus(canon);
16
+ const projectVersionStatus = getProjectVersionStatus({
17
+ projectRoot,
18
+ rootSource: projectRootResolution.root_source,
19
+ canon: resolvedCanonForProject
20
+ });
21
+ const cli = getCliBuildInfo();
22
+ const compatibilityManifest = resolvedCanonForProject ? readCompatibilityManifest(canon) : null;
23
+ const cliCompatibility = cliCompatibilityVerdict(cli, compatibilityManifest);
24
+ const registry = input.checkRegistry ? checkNpmRegistry(context, cli.package_name) : undefined;
25
+ return {
26
+ ok: true,
27
+ schema_id: "dd-flow/status-report@1",
28
+ dd_flow_home: context.ddFlowHome,
29
+ cwd,
30
+ cli: {
31
+ ...cli,
32
+ compatibility: cliCompatibility,
33
+ ...(registry ? { registry } : {})
34
+ },
35
+ project: {
36
+ requested_root: projectRootResolution.requested_root,
37
+ root: projectRoot,
38
+ root_source: projectRootResolution.root_source,
39
+ registered: Boolean(project),
40
+ id: project?.id ?? null,
41
+ status: project?.status ?? null,
42
+ memory_bank: projectVersionStatus?.memory_bank ?? null,
43
+ flow_pack: projectVersionStatus?.flow_pack ?? null,
44
+ drift: projectVersionStatus?.drift ?? null
45
+ },
46
+ canon: {
47
+ ...(asRecord(canonStatus) ?? {}),
48
+ resolved: canon ?? null,
49
+ compatibility: cliCanonCompatibility(cli, resolvedCanonForProject?.metadata.version ?? null)
50
+ }
51
+ };
52
+ }
53
+ function asRecord(value) {
54
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
55
+ }
56
+ function stringValue(value) {
57
+ return typeof value === "string" && value.length > 0 ? value : null;
58
+ }
59
+ function readCompatibilityManifest(canon) {
60
+ const record = asRecord(canon);
61
+ const memorybankRoot = stringValue(record?.memorybank_root);
62
+ if (!memorybankRoot)
63
+ return null;
64
+ const file = path.join(memorybankRoot, "dd-flow", "compatibility.json");
65
+ try {
66
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
67
+ const root = asRecord(parsed);
68
+ const cli = asRecord(root?.dd_flow_cli);
69
+ if (!root || !cli)
70
+ return null;
71
+ return {
72
+ schema_id: stringValue(root.schema_id),
73
+ memory_bank_version: stringValue(root.memory_bank_version),
74
+ dd_flow_cli: {
75
+ package_name: stringValue(cli.package_name),
76
+ min_version: stringValue(cli.min_version),
77
+ recommended_version: stringValue(cli.recommended_version),
78
+ status_contract: stringValue(cli.status_contract),
79
+ version_contract: stringValue(cli.version_contract),
80
+ flow_contract: stringValue(cli.flow_contract)
81
+ }
82
+ };
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ function cliCompatibilityVerdict(cli, manifest) {
89
+ if (!manifest) {
90
+ return {
91
+ verdict: "unknown",
92
+ reason: "compatibility_manifest_missing",
93
+ package_name: cli.package_name,
94
+ installed_version: cli.version,
95
+ update_command: null
96
+ };
97
+ }
98
+ const expectedPackage = manifest.dd_flow_cli.package_name;
99
+ if (expectedPackage && expectedPackage !== cli.package_name) {
100
+ return {
101
+ verdict: "incompatible",
102
+ reason: "package_name_mismatch",
103
+ package_name: cli.package_name,
104
+ expected_package_name: expectedPackage,
105
+ installed_version: cli.version,
106
+ memory_bank_version: manifest.memory_bank_version,
107
+ min_version: manifest.dd_flow_cli.min_version,
108
+ recommended_version: manifest.dd_flow_cli.recommended_version,
109
+ update_command: updateCommand(expectedPackage, manifest.dd_flow_cli.recommended_version ?? manifest.dd_flow_cli.min_version)
110
+ };
111
+ }
112
+ const minVersion = manifest.dd_flow_cli.min_version;
113
+ const recommendedVersion = manifest.dd_flow_cli.recommended_version;
114
+ const installed = parseSemver(cli.version);
115
+ const min = minVersion ? parseSemver(minVersion) : null;
116
+ const recommended = recommendedVersion ? parseSemver(recommendedVersion) : null;
117
+ if (!installed || (minVersion && !min) || (recommendedVersion && !recommended)) {
118
+ return {
119
+ verdict: "unknown",
120
+ reason: "invalid_semver",
121
+ package_name: cli.package_name,
122
+ installed_version: cli.version,
123
+ memory_bank_version: manifest.memory_bank_version,
124
+ min_version: minVersion,
125
+ recommended_version: recommendedVersion,
126
+ update_command: null
127
+ };
128
+ }
129
+ if (min && compareSemver(installed, min) < 0) {
130
+ return {
131
+ verdict: "incompatible",
132
+ reason: "installed_below_min_version",
133
+ package_name: cli.package_name,
134
+ installed_version: cli.version,
135
+ memory_bank_version: manifest.memory_bank_version,
136
+ min_version: minVersion,
137
+ recommended_version: recommendedVersion,
138
+ update_command: updateCommand(cli.package_name, recommendedVersion ?? minVersion)
139
+ };
140
+ }
141
+ if (recommended && compareSemver(installed, recommended) < 0) {
142
+ return {
143
+ verdict: "outdated",
144
+ reason: "installed_below_recommended_version",
145
+ package_name: cli.package_name,
146
+ installed_version: cli.version,
147
+ memory_bank_version: manifest.memory_bank_version,
148
+ min_version: minVersion,
149
+ recommended_version: recommendedVersion,
150
+ update_command: updateCommand(cli.package_name, recommendedVersion)
151
+ };
152
+ }
153
+ return {
154
+ verdict: "ok",
155
+ reason: "installed_satisfies_memory_bank_compatibility",
156
+ package_name: cli.package_name,
157
+ installed_version: cli.version,
158
+ memory_bank_version: manifest.memory_bank_version,
159
+ min_version: minVersion,
160
+ recommended_version: recommendedVersion,
161
+ status_contract: manifest.dd_flow_cli.status_contract,
162
+ version_contract: manifest.dd_flow_cli.version_contract,
163
+ flow_contract: manifest.dd_flow_cli.flow_contract,
164
+ update_command: null
165
+ };
166
+ }
167
+ function updateCommand(packageName, version) {
168
+ return `pnpm add -g ${packageName}${version ? `@${version}` : "@latest"}`;
169
+ }
170
+ function checkNpmRegistry(context, packageName) {
171
+ const cachePath = path.join(context.ddFlowHome, "cache", "npm", `${safeCacheName(packageName)}.json`);
172
+ const cached = readRegistryCache(cachePath);
173
+ const now = new Date().toISOString();
174
+ if (cached && Date.now() - cached.checkedAtMs < 15 * 60 * 1000) {
175
+ return { ...cached.payload, source: "npm_cache" };
176
+ }
177
+ const result = spawnSync("npm", ["view", packageName, "version", "--json"], { encoding: "utf8", timeout: 5000 });
178
+ if (result.status !== 0) {
179
+ return {
180
+ package_name: packageName,
181
+ latest: null,
182
+ checked_at: now,
183
+ source: "npm",
184
+ status: "degraded",
185
+ reason: result.error ? String(result.error) : result.stderr.trim() || "npm_view_failed"
186
+ };
187
+ }
188
+ const latest = parseRegistryVersion(result.stdout);
189
+ const payload = {
190
+ package_name: packageName,
191
+ latest,
192
+ checked_at: now,
193
+ source: "npm",
194
+ status: latest ? "ok" : "degraded",
195
+ ...(latest ? {} : { reason: "npm_view_returned_no_version" })
196
+ };
197
+ writeRegistryCache(cachePath, payload);
198
+ return payload;
199
+ }
200
+ function readRegistryCache(file) {
201
+ try {
202
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
203
+ const record = asRecord(parsed);
204
+ const payload = asRecord(record?.payload);
205
+ const checkedAt = stringValue(record?.checked_at);
206
+ if (!payload || !checkedAt)
207
+ return null;
208
+ const checkedAtMs = Date.parse(checkedAt);
209
+ return Number.isNaN(checkedAtMs) ? null : { checkedAtMs, payload };
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ function writeRegistryCache(file, payload) {
216
+ try {
217
+ fs.mkdirSync(path.dirname(file), { recursive: true });
218
+ fs.writeFileSync(file, `${JSON.stringify({ checked_at: payload.checked_at, payload }, null, 2)}\n`);
219
+ }
220
+ catch {
221
+ // Registry cache is best effort; status remains valid without it.
222
+ }
223
+ }
224
+ function parseRegistryVersion(value) {
225
+ try {
226
+ const parsed = JSON.parse(value);
227
+ return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
228
+ }
229
+ catch {
230
+ const trimmed = value.trim().replace(/^"|"$/g, "");
231
+ return trimmed.length > 0 ? trimmed : null;
232
+ }
233
+ }
234
+ function safeCacheName(value) {
235
+ return value.replace(/[^a-zA-Z0-9_.-]+/g, "_");
236
+ }
237
+ function canonForProjectStatus(value) {
238
+ const record = asRecord(value);
239
+ if (!record || typeof record.root !== "string")
240
+ return null;
241
+ const metadata = asRecord(record.metadata);
242
+ const status = metadata?.status;
243
+ if (metadata && (status === "present" || status === "degraded" || status === "unknown")) {
244
+ return {
245
+ root: record.root,
246
+ metadata: {
247
+ version: stringValue(metadata.version),
248
+ commit: stringValue(metadata.commit),
249
+ flow_contract: stringValue(metadata.flow_contract),
250
+ status,
251
+ diagnostics: Array.isArray(metadata.diagnostics) ? metadata.diagnostics.filter((item) => typeof item === "string") : []
252
+ }
253
+ };
254
+ }
255
+ const version = stringValue(record.version);
256
+ const commit = stringValue(record.commit);
257
+ const flowContract = stringValue(record.flow_contract);
258
+ const diagnostics = [
259
+ ...(version ? [] : ["canon_version_unknown"]),
260
+ ...(commit ? [] : ["canon_commit_unknown"]),
261
+ ...(flowContract ? [] : ["flow_contract_unknown"])
262
+ ];
263
+ return {
264
+ root: record.root,
265
+ metadata: {
266
+ version,
267
+ commit,
268
+ flow_contract: flowContract,
269
+ status: diagnostics.length === 0 ? "present" : "degraded",
270
+ diagnostics
271
+ }
272
+ };
273
+ }
274
+ function cliCanonCompatibility(cli, resolvedCanonVersion) {
275
+ const builtWithVersion = cli.build.built_with_canon.version;
276
+ if (!builtWithVersion || !resolvedCanonVersion) {
277
+ return { cli_built_with_resolved_canon: "unknown", reason: "version_missing" };
278
+ }
279
+ const built = parseSemver(builtWithVersion);
280
+ const resolved = parseSemver(resolvedCanonVersion);
281
+ if (!built || !resolved)
282
+ return { cli_built_with_resolved_canon: "unknown", reason: "invalid_semver" };
283
+ const comparison = compareSemver(built, resolved);
284
+ if (comparison === 0)
285
+ return { cli_built_with_resolved_canon: "same", reason: "semver_equal" };
286
+ return {
287
+ cli_built_with_resolved_canon: comparison < 0 ? "behind" : "ahead",
288
+ reason: comparison < 0 ? "cli_built_with_older_canon" : "cli_built_with_newer_canon"
289
+ };
290
+ }
291
+ function parseSemver(version) {
292
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$/);
293
+ if (!match?.[1] || !match[2] || !match[3])
294
+ return null;
295
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
296
+ }
297
+ function compareSemver(left, right) {
298
+ for (let index = 0; index < 3; index += 1) {
299
+ const leftPart = left[index] ?? 0;
300
+ const rightPart = right[index] ?? 0;
301
+ const diff = leftPart - rightPart;
302
+ if (diff !== 0)
303
+ return diff;
304
+ }
305
+ return 0;
306
+ }
@@ -0,0 +1,284 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { parse as parseYaml } from "yaml";
5
+ const canonicalOnlyFlowNames = ["mb-init.md", "mb-upgrade.md", "mb-upgrade-review.md", "mb-distill.md"];
6
+ export function resolveStatusProjectRoot(input) {
7
+ if (input.requestedRoot) {
8
+ const absolute = path.resolve(input.requestedRoot);
9
+ return { requested_root: absolute, root: existingDirectoryRealpath(absolute), root_source: "explicit" };
10
+ }
11
+ const gitRoot = nearestGitRoot(input.cwd);
12
+ if (gitRoot) {
13
+ return { requested_root: path.resolve(input.cwd), root: gitRoot, root_source: "git" };
14
+ }
15
+ const cwdRoot = existingDirectoryRealpath(input.cwd);
16
+ return { requested_root: path.resolve(input.cwd), root: cwdRoot, root_source: "cwd" };
17
+ }
18
+ export function getProjectVersionStatus(input) {
19
+ if (!input.projectRoot)
20
+ return null;
21
+ const memoryBank = readMemoryBankMetadata(input.projectRoot);
22
+ const flowPack = readFlowPackMetadata(input.projectRoot, memoryBank.root);
23
+ const memoryBankDrift = compareMemoryBankVersion(memoryBank.version, input.canon?.metadata.version ?? null, memoryBank.status);
24
+ const flowPackDrift = compareFlowPackCommit(flowPack.source_commit, input.canon?.metadata.commit ?? null, input.canon?.root ?? null, flowPack.status);
25
+ const overall = overallDrift(memoryBankDrift, flowPackDrift, memoryBank.status, flowPack.status);
26
+ return {
27
+ root: input.projectRoot,
28
+ root_source: input.rootSource,
29
+ memory_bank: memoryBank,
30
+ flow_pack: flowPack,
31
+ drift: {
32
+ memory_bank_version: memoryBankDrift,
33
+ flow_pack_commit: flowPackDrift,
34
+ overall,
35
+ next_action: nextAction(overall, memoryBank.status, flowPack.status)
36
+ }
37
+ };
38
+ }
39
+ function readMemoryBankMetadata(projectRoot) {
40
+ const dotRoot = path.join(projectRoot, ".memory-bank");
41
+ const legacyRoot = path.join(projectRoot, "memory-bank");
42
+ const root = fs.existsSync(dotRoot) && fs.statSync(dotRoot).isDirectory() ? dotRoot : fs.existsSync(legacyRoot) && fs.statSync(legacyRoot).isDirectory() ? legacyRoot : null;
43
+ const folderName = root ? path.basename(root) : null;
44
+ if (!root) {
45
+ return { root: null, folder_name: null, version: null, source: null, status: "missing", reason: "no_memory_bank" };
46
+ }
47
+ const indexPath = path.join(root, "index.md");
48
+ const indexVersion = fs.existsSync(indexPath) ? readMemoryBankVersionFromIndex(indexPath) : null;
49
+ if (indexVersion) {
50
+ return { root, folder_name: folderName, version: indexVersion, source: path.relative(projectRoot, indexPath), status: "present" };
51
+ }
52
+ const mbbPath = path.join(root, "mbb", "index.md");
53
+ const mbbVersion = fs.existsSync(mbbPath) ? readMemoryBankCanonTextVersion(mbbPath) : null;
54
+ if (mbbVersion) {
55
+ return { root, folder_name: folderName, version: mbbVersion, source: path.relative(projectRoot, mbbPath), status: "present" };
56
+ }
57
+ return { root, folder_name: folderName, version: null, source: null, status: "unknown", reason: "memory_bank_version_missing" };
58
+ }
59
+ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
60
+ if (!memoryBankRoot) {
61
+ return {
62
+ manifest_path: null,
63
+ schema_id: null,
64
+ pack_version: null,
65
+ canon_root: null,
66
+ canon_memory_bank_root: null,
67
+ canon_flow_root: null,
68
+ source_commit: null,
69
+ canon_version_at_source_commit: null,
70
+ status: "missing",
71
+ reason: "no_memory_bank",
72
+ canonical_only_files: []
73
+ };
74
+ }
75
+ const flowRoot = path.join(memoryBankRoot, "dd-flow");
76
+ const manifestPath = path.join(flowRoot, "manifest.json");
77
+ const canonicalOnlyFiles = canonicalOnlyFlowNames.filter((file) => fs.existsSync(path.join(flowRoot, file)));
78
+ if (!fs.existsSync(manifestPath)) {
79
+ return {
80
+ manifest_path: path.relative(projectRoot, manifestPath),
81
+ schema_id: null,
82
+ pack_version: null,
83
+ canon_root: null,
84
+ canon_memory_bank_root: null,
85
+ canon_flow_root: null,
86
+ source_commit: null,
87
+ canon_version_at_source_commit: null,
88
+ status: "missing",
89
+ reason: "flow_pack_manifest_missing",
90
+ canonical_only_files: canonicalOnlyFiles
91
+ };
92
+ }
93
+ let manifest;
94
+ try {
95
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
96
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
97
+ throw new Error("manifest is not an object");
98
+ manifest = parsed;
99
+ }
100
+ catch {
101
+ return {
102
+ manifest_path: path.relative(projectRoot, manifestPath),
103
+ schema_id: null,
104
+ pack_version: null,
105
+ canon_root: null,
106
+ canon_memory_bank_root: null,
107
+ canon_flow_root: null,
108
+ source_commit: null,
109
+ canon_version_at_source_commit: null,
110
+ status: "invalid",
111
+ reason: "flow_pack_manifest_invalid_json",
112
+ canonical_only_files: canonicalOnlyFiles
113
+ };
114
+ }
115
+ const schemaId = stringOrNull(manifest.schema_id);
116
+ const canonVersion = stringOrNull(manifest.canon_version_at_source_commit) ?? stringOrNull(manifest.canon_version);
117
+ const sourceCommit = stringOrNull(manifest.source_commit);
118
+ const canonRoot = stringOrNull(manifest.canon_root);
119
+ const canonMemoryBankRoot = stringOrNull(manifest.canon_memory_bank_root);
120
+ const canonFlowRoot = stringOrNull(manifest.canon_flow_root);
121
+ const status = schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion && canonMemoryBankRoot && canonFlowRoot
122
+ ? "present"
123
+ : schemaId === "dd-flow/project-flow-pack-manifest@1" ||
124
+ (schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion && (!canonMemoryBankRoot || !canonFlowRoot))
125
+ ? "degraded"
126
+ : "invalid";
127
+ const reason = status === "degraded" && schemaId === "dd-flow/project-flow-pack-manifest@2"
128
+ ? "flow_pack_manifest_missing_explicit_canon_roots"
129
+ : status === "degraded"
130
+ ? "legacy_flow_pack_manifest"
131
+ : status === "invalid"
132
+ ? "flow_pack_manifest_schema_unknown"
133
+ : undefined;
134
+ return {
135
+ manifest_path: path.relative(projectRoot, manifestPath),
136
+ schema_id: schemaId,
137
+ pack_version: stringOrNull(manifest.pack_version),
138
+ canon_root: canonRoot,
139
+ canon_memory_bank_root: canonMemoryBankRoot,
140
+ canon_flow_root: canonFlowRoot,
141
+ source_commit: sourceCommit,
142
+ canon_version_at_source_commit: canonVersion,
143
+ status,
144
+ ...(reason ? { reason } : {}),
145
+ canonical_only_files: canonicalOnlyFiles
146
+ };
147
+ }
148
+ function readMemoryBankVersionFromIndex(file) {
149
+ const text = fs.readFileSync(file, "utf8");
150
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
151
+ if (!match?.[1])
152
+ return null;
153
+ try {
154
+ const parsed = parseYaml(match[1]);
155
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
156
+ const value = parsed.memory_bank_version;
157
+ return typeof value === "string" && value.length > 0 ? value : null;
158
+ }
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ return null;
164
+ }
165
+ function readMemoryBankCanonTextVersion(file) {
166
+ const text = fs.readFileSync(file, "utf8");
167
+ const ru = text.match(/Текущая версия канона Memory Bank:\s*`([^`]+)`/);
168
+ if (ru?.[1])
169
+ return ru[1];
170
+ const en = text.match(/Memory Bank canon version:\s*`?([0-9]+\.[0-9]+\.[0-9][^`\s]*)`?/i);
171
+ return en?.[1] ?? null;
172
+ }
173
+ function compareMemoryBankVersion(current, target, artifactStatus) {
174
+ if (artifactStatus === "missing")
175
+ return { status: "missing", confidence: "none", reason: "memory_bank_missing", current, target };
176
+ if (!current)
177
+ return { status: "unknown", confidence: "none", reason: "memory_bank_version_missing", current, target };
178
+ if (!target)
179
+ return { status: "unknown", confidence: "none", reason: "canon_version_unknown", current, target };
180
+ const currentSemver = parseSemver(current);
181
+ const targetSemver = parseSemver(target);
182
+ if (!currentSemver || !targetSemver)
183
+ return { status: "invalid", confidence: "none", reason: "invalid_semver", current, target };
184
+ const comparison = compareSemver(currentSemver, targetSemver);
185
+ if (comparison === 0)
186
+ return { status: "same", confidence: "semver", reason: "semver_equal", current, target };
187
+ return { status: comparison < 0 ? "behind" : "ahead", confidence: "semver", reason: comparison < 0 ? "semver_older" : "semver_newer", current, target };
188
+ }
189
+ function compareFlowPackCommit(current, target, canonRoot, artifactStatus) {
190
+ if (artifactStatus === "missing")
191
+ return { status: "missing", confidence: "none", reason: "flow_pack_manifest_missing", current, target };
192
+ if (artifactStatus === "invalid")
193
+ return { status: "invalid", confidence: "none", reason: "flow_pack_manifest_invalid", current, target };
194
+ if (!current)
195
+ return { status: "unknown", confidence: "none", reason: "flow_pack_source_commit_missing", current, target };
196
+ if (!target)
197
+ return { status: "unknown", confidence: "none", reason: "canon_commit_unknown", current, target };
198
+ if (current === target)
199
+ return { status: "same", confidence: "exact", reason: "commit_equal", current, target };
200
+ if (!canonRoot || !fs.existsSync(path.join(canonRoot, ".git"))) {
201
+ return { status: "unknown", confidence: "none", reason: "git_history_unavailable", current, target };
202
+ }
203
+ const projectAncestor = gitSuccess(canonRoot, ["merge-base", "--is-ancestor", current, target]);
204
+ if (projectAncestor)
205
+ return { status: "behind", confidence: "git_ancestry", reason: "source_commit_ancestor_of_canon", current, target };
206
+ const canonAncestor = gitSuccess(canonRoot, ["merge-base", "--is-ancestor", target, current]);
207
+ if (canonAncestor)
208
+ return { status: "ahead", confidence: "git_ancestry", reason: "source_commit_descendant_of_canon", current, target };
209
+ const currentExists = gitSuccess(canonRoot, ["cat-file", "-e", `${current}^{commit}`]);
210
+ const targetExists = gitSuccess(canonRoot, ["cat-file", "-e", `${target}^{commit}`]);
211
+ return {
212
+ status: currentExists && targetExists ? "diverged" : "unknown",
213
+ confidence: currentExists && targetExists ? "git_ancestry" : "none",
214
+ reason: currentExists && targetExists ? "git_histories_diverged" : "source_commit_not_in_resolved_canon",
215
+ current,
216
+ target
217
+ };
218
+ }
219
+ function overallDrift(memoryBank, flowPack, memoryStatus, flowStatus) {
220
+ if (memoryStatus === "missing")
221
+ return "not_applicable";
222
+ if (memoryBank.status === "invalid" || flowPack.status === "invalid")
223
+ return "invalid";
224
+ if (memoryBank.status === "missing" || flowPack.status === "missing")
225
+ return "missing";
226
+ if (memoryBank.status === "ahead" || flowPack.status === "ahead")
227
+ return "ahead";
228
+ if (memoryBank.status === "behind" || flowPack.status === "behind")
229
+ return "behind";
230
+ if (memoryBank.status === "diverged" || flowPack.status === "diverged")
231
+ return "diverged";
232
+ if (memoryBank.status === "same" && (flowPack.status === "same" || flowStatus === "degraded"))
233
+ return flowStatus === "degraded" ? "unknown" : "same";
234
+ return "unknown";
235
+ }
236
+ function nextAction(overall, memoryStatus, flowStatus) {
237
+ if (overall === "same")
238
+ return "no_action";
239
+ if (overall === "not_applicable" && memoryStatus === "missing")
240
+ return "run_mb_init_if_project_needs_memory_bank";
241
+ if (overall === "behind" || flowStatus === "degraded")
242
+ return "run_canonical_mb_upgrade";
243
+ if (overall === "missing")
244
+ return "run_canonical_mb_upgrade_or_repair_manifest";
245
+ if (overall === "invalid")
246
+ return "repair_memory_bank_markers_or_manifest";
247
+ return "inspect_status_reasons";
248
+ }
249
+ function nearestGitRoot(cwd) {
250
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
251
+ if (result.status !== 0)
252
+ return null;
253
+ const stdout = result.stdout.trim();
254
+ return stdout ? existingDirectoryRealpath(stdout) : null;
255
+ }
256
+ function existingDirectoryRealpath(root) {
257
+ const absolute = path.resolve(root);
258
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isDirectory())
259
+ return null;
260
+ return fs.realpathSync(absolute);
261
+ }
262
+ function stringOrNull(value) {
263
+ return typeof value === "string" && value.length > 0 ? value : null;
264
+ }
265
+ function parseSemver(version) {
266
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$/);
267
+ if (!match?.[1] || !match[2] || !match[3])
268
+ return null;
269
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
270
+ }
271
+ function compareSemver(left, right) {
272
+ for (let index = 0; index < 3; index += 1) {
273
+ const leftPart = left[index] ?? 0;
274
+ const rightPart = right[index] ?? 0;
275
+ const diff = leftPart - rightPart;
276
+ if (diff !== 0)
277
+ return diff;
278
+ }
279
+ return 0;
280
+ }
281
+ function gitSuccess(cwd, args) {
282
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
283
+ return result.status === 0;
284
+ }
@@ -194,6 +194,13 @@ function migrate(db) {
194
194
  FOREIGN KEY(project_id) REFERENCES projects(id)
195
195
  );
196
196
 
197
+ CREATE TABLE IF NOT EXISTS runtime_config (
198
+ key TEXT PRIMARY KEY,
199
+ value_json TEXT NOT NULL,
200
+ created_at TEXT NOT NULL,
201
+ updated_at TEXT NOT NULL
202
+ );
203
+
197
204
  CREATE TABLE IF NOT EXISTS flow_sessions (
198
205
  session_id TEXT NOT NULL,
199
206
  project_id TEXT NOT NULL,
@@ -236,6 +243,9 @@ function migrate(db) {
236
243
  runtime_path TEXT NOT NULL,
237
244
  run_dir TEXT NOT NULL,
238
245
  run_index_path TEXT NOT NULL,
246
+ run_home_path TEXT,
247
+ layout_version TEXT,
248
+ artifact_root_kind TEXT,
239
249
  index_json TEXT NOT NULL,
240
250
  created_at TEXT NOT NULL,
241
251
  updated_at TEXT NOT NULL,
@@ -311,6 +321,9 @@ function migrate(db) {
311
321
  ensureColumn(db, "flow_sessions", "next_action", "ALTER TABLE flow_sessions ADD COLUMN next_action TEXT");
312
322
  ensureColumn(db, "flow_sessions", "metadata_json", "ALTER TABLE flow_sessions ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'");
313
323
  ensureColumn(db, "flow_sessions", "run_id", "ALTER TABLE flow_sessions ADD COLUMN run_id TEXT");
324
+ ensureColumn(db, "flow_runs", "run_home_path", "ALTER TABLE flow_runs ADD COLUMN run_home_path TEXT");
325
+ ensureColumn(db, "flow_runs", "layout_version", "ALTER TABLE flow_runs ADD COLUMN layout_version TEXT");
326
+ ensureColumn(db, "flow_runs", "artifact_root_kind", "ALTER TABLE flow_runs ADD COLUMN artifact_root_kind TEXT");
314
327
  }
315
328
  function ensureColumn(db, table, column, sql) {
316
329
  const columns = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -33,12 +33,30 @@ export function runtimeRunDir(ddFlowHome, projectId, runId) {
33
33
  export function runtimeRunJsonPath(ddFlowHome, projectId, runId) {
34
34
  return path.join(runtimeRunDir(ddFlowHome, projectId, runId), "run.json");
35
35
  }
36
+ export function projectRunHome(ddFlowHome, projectId, runId) {
37
+ return path.join(projectHome(ddFlowHome, projectId), "runs", runId);
38
+ }
39
+ export function projectRunJsonPath(ddFlowHome, projectId, runId) {
40
+ return path.join(projectRunHome(ddFlowHome, projectId, runId), "run.json");
41
+ }
42
+ export function projectRunIndexPath(ddFlowHome, projectId, runId) {
43
+ return path.join(projectRunHome(ddFlowHome, projectId, runId), "run-index.json");
44
+ }
45
+ export function projectRunStageDir(ddFlowHome, projectId, runId, stageDir) {
46
+ return path.join(projectRunHome(ddFlowHome, projectId, runId), stageDir);
47
+ }
36
48
  export function userFacingRunDir(workspaceRoot, runId) {
37
49
  return path.join(workspaceRoot, ".tasks", "dd-flow-runs", runId);
38
50
  }
51
+ export function legacyWorkspaceRunDir(workspaceRoot, runId) {
52
+ return userFacingRunDir(workspaceRoot, runId);
53
+ }
39
54
  export function userFacingRunIndexPath(workspaceRoot, runId) {
40
55
  return path.join(userFacingRunDir(workspaceRoot, runId), "run-index.json");
41
56
  }
57
+ export function userFacingRunStageDir(workspaceRoot, runId, stageDir) {
58
+ return path.join(userFacingRunDir(workspaceRoot, runId), stageDir);
59
+ }
42
60
  export function stateJsonPath(projectRoot, protocolId) {
43
61
  return path.join(protocolDir(projectRoot, protocolId), "state.json");
44
62
  }
@@ -54,3 +72,6 @@ export function projectRuntimeRoot(ddFlowHome, projectId) {
54
72
  export function projectCheckoutRoot(ddFlowHome, projectId) {
55
73
  return path.join(projectHome(ddFlowHome, projectId), "checkouts");
56
74
  }
75
+ export function projectFeatureWorktreePath(ddFlowHome, projectId, runOrProtocolId, repoName) {
76
+ return path.join(projectCheckoutRoot(ddFlowHome, projectId), "worktrees", runOrProtocolId, repoName);
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
36
+ "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "lint": "eslint . --max-warnings=0",
39
39
  "test": "vitest run",