@deksden-com/dd-flow-cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +76 -5
- package/dist/build-info.json +10 -6
- package/dist/cli/help.js +165 -20
- package/dist/cli/run-cli.js +427 -17
- package/dist/schemas/compatibility.schema.json +105 -0
- package/dist/schemas/engine-manifest.schema.json +61 -0
- package/dist/schemas/flow-guidance.schema.json +90 -0
- package/dist/schemas/flow-run-index.schema.json +30 -4
- package/dist/schemas/global-dashboard-data.schema.json +126 -0
- package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
- package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
- package/dist/schemas/plan-stage-report.schema.json +83 -0
- package/dist/schemas/project-dashboard-data.schema.json +122 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
- package/dist/schemas/project-summary.schema.json +73 -0
- package/dist/schemas/protocol-dashboard-data.schema.json +112 -0
- package/dist/schemas/status-report.schema.json +38 -2
- package/dist/schemas/version-report.schema.json +22 -0
- package/dist/services/build-info.js +26 -3
- package/dist/services/canon.js +93 -22
- package/dist/services/cleanup.js +45 -1
- package/dist/services/cli-operation-classifier.js +104 -0
- package/dist/services/compatibility-preflight.js +124 -0
- package/dist/services/config.js +31 -0
- package/dist/services/dashboard-targets.js +95 -0
- package/dist/services/dashboard.js +972 -10
- package/dist/services/engines.js +532 -0
- package/dist/services/flow-guidance.js +221 -0
- package/dist/services/hooks.js +1 -1
- package/dist/services/ids.js +106 -0
- package/dist/services/lanes.js +333 -1
- package/dist/services/merge-queue.js +106 -16
- package/dist/services/merge-worker.js +67 -4
- package/dist/services/migrations.js +231 -0
- package/dist/services/project-summary.js +122 -0
- package/dist/services/projects.js +44 -3
- package/dist/services/protocol-lifecycle.js +144 -0
- package/dist/services/protocols.js +660 -7
- package/dist/services/runs.js +98 -21
- package/dist/services/schema-validation.js +84 -4
- package/dist/services/sessions.js +21 -4
- package/dist/services/status.js +199 -1
- package/dist/services/version-status.js +59 -9
- package/dist/storage/database.js +31 -0
- package/dist/storage/paths.js +33 -0
- package/package.json +3 -2
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "dd-flow/protocol-dashboard-data@1",
|
|
4
|
+
"title": "dd-flow protocol dashboard data",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"required": ["schema_id", "generated_at", "target_language", "page", "project", "protocol", "resource", "primary_run_id", "run_history", "stage_pipeline", "metrics", "warnings"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_id": { "const": "dd-flow/protocol-dashboard-data@1" },
|
|
10
|
+
"generated_at": { "type": "string", "format": "date-time" },
|
|
11
|
+
"target_language": { "type": "string", "minLength": 2 },
|
|
12
|
+
"page": { "$ref": "#/definitions/page" },
|
|
13
|
+
"project": { "type": "object", "additionalProperties": true, "required": ["id", "title", "root"] },
|
|
14
|
+
"protocol": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": true,
|
|
17
|
+
"required": ["id", "short_id", "lifecycle", "raw_status", "raw_stage", "display_status", "action_level", "updated_at"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": { "type": "string", "minLength": 1 },
|
|
20
|
+
"short_id": { "type": "string", "minLength": 1 },
|
|
21
|
+
"raw_status": { "type": "string", "minLength": 1 },
|
|
22
|
+
"raw_stage": { "type": "string", "minLength": 1 },
|
|
23
|
+
"lifecycle": { "$ref": "#/definitions/lifecycle" },
|
|
24
|
+
"lifecycle_stage": { "type": "string" },
|
|
25
|
+
"lifecycle_status": { "type": "string" },
|
|
26
|
+
"display_status": { "type": "string", "minLength": 1 },
|
|
27
|
+
"action_level": { "type": "string", "enum": ["none", "watch", "actionable", "blocked"] },
|
|
28
|
+
"updated_at": { "type": "string", "minLength": 1 }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"resource": { "type": "object", "additionalProperties": true },
|
|
32
|
+
"queue_item": { "type": ["object", "null"], "additionalProperties": true },
|
|
33
|
+
"claim": { "type": ["object", "null"], "additionalProperties": true },
|
|
34
|
+
"links": { "type": "object", "additionalProperties": { "$ref": "#/definitions/link" } },
|
|
35
|
+
"primary_run_id": { "type": ["string", "null"] },
|
|
36
|
+
"primary_run_reason": { "type": "string" },
|
|
37
|
+
"run_history": { "type": "array", "items": { "$ref": "#/definitions/run" } },
|
|
38
|
+
"review_runs": { "type": "array", "items": { "$ref": "#/definitions/run" } },
|
|
39
|
+
"stage_pipeline": { "type": "array" },
|
|
40
|
+
"metrics": { "type": "array", "items": { "$ref": "#/definitions/metric" } },
|
|
41
|
+
"warnings": { "type": "array" }
|
|
42
|
+
},
|
|
43
|
+
"definitions": {
|
|
44
|
+
"page": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": true,
|
|
47
|
+
"required": ["kind", "screen_id", "title", "html_path", "json_path"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"kind": { "const": "protocol" },
|
|
50
|
+
"screen_id": { "const": "SCR-DD-FLOW-PROTOCOL-PAGE" },
|
|
51
|
+
"title": { "type": "string", "minLength": 1 },
|
|
52
|
+
"html_path": { "type": "string", "minLength": 1 },
|
|
53
|
+
"json_path": { "type": "string", "minLength": 1 }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"metric": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": true,
|
|
59
|
+
"required": ["id", "label", "value", "status", "source"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"id": { "type": "string", "minLength": 1 },
|
|
62
|
+
"label": { "type": "string", "minLength": 1 },
|
|
63
|
+
"value": { "type": ["number", "null"] },
|
|
64
|
+
"status": { "type": "string", "enum": ["known", "unknown", "degraded", "not_applicable"] },
|
|
65
|
+
"source": { "type": "string", "minLength": 1 }
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"link": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": true,
|
|
71
|
+
"required": ["label", "href", "path_kind", "status"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"label": { "type": "string", "minLength": 1 },
|
|
74
|
+
"href": { "type": "string" },
|
|
75
|
+
"path_kind": { "type": "string", "enum": ["absolute", "relative"] },
|
|
76
|
+
"status": { "type": "string", "enum": ["available", "pending", "missing", "not_applicable"] },
|
|
77
|
+
"degraded_reason": { "type": "string" }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"run": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": true,
|
|
83
|
+
"required": ["id", "short_id", "flow_kind", "display_flow_kind", "status", "display_status", "stages"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"id": { "type": "string", "minLength": 1 },
|
|
86
|
+
"short_id": { "type": "string", "minLength": 1 },
|
|
87
|
+
"flow_kind": { "type": "string", "minLength": 1 },
|
|
88
|
+
"display_flow_kind": { "type": "string", "minLength": 1 },
|
|
89
|
+
"status": { "type": "string", "minLength": 1 },
|
|
90
|
+
"display_status": { "type": "string", "minLength": 1 },
|
|
91
|
+
"stages": { "type": "array" }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"lifecycle": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": true,
|
|
97
|
+
"required": ["flow", "stage", "status", "terminal", "raw_stage", "raw_status"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"flow": { "type": "string", "minLength": 1 },
|
|
100
|
+
"stage": { "type": "string", "minLength": 1 },
|
|
101
|
+
"substage": { "type": ["string", "null"] },
|
|
102
|
+
"status": { "type": "string", "minLength": 1 },
|
|
103
|
+
"terminal": { "type": "boolean" },
|
|
104
|
+
"legacy_stage": { "type": ["string", "null"] },
|
|
105
|
+
"legacy_status": { "type": ["string", "null"] },
|
|
106
|
+
"raw_stage": { "type": "string", "minLength": 1 },
|
|
107
|
+
"raw_status": { "type": "string", "minLength": 1 },
|
|
108
|
+
"queue_status": { "type": ["string", "null"] }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -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,
|
|
@@ -101,13 +135,14 @@
|
|
|
101
135
|
"flowPack": {
|
|
102
136
|
"type": ["object", "null"],
|
|
103
137
|
"additionalProperties": true,
|
|
104
|
-
"required": ["manifest_path", "schema_id", "pack_version", "source_commit", "canon_version_at_source_commit", "status", "canonical_only_files"],
|
|
138
|
+
"required": ["manifest_path", "schema_id", "pack_version", "source_commit", "canon_version_at_source_commit", "flow_contract", "status", "canonical_only_files"],
|
|
105
139
|
"properties": {
|
|
106
140
|
"manifest_path": { "type": ["string", "null"] },
|
|
107
141
|
"schema_id": { "type": ["string", "null"] },
|
|
108
142
|
"pack_version": { "type": ["string", "null"] },
|
|
109
143
|
"source_commit": { "type": ["string", "null"] },
|
|
110
144
|
"canon_version_at_source_commit": { "type": ["string", "null"] },
|
|
145
|
+
"flow_contract": { "type": ["string", "null"] },
|
|
111
146
|
"status": { "type": "string", "enum": ["present", "missing", "invalid", "degraded"] },
|
|
112
147
|
"reason": { "type": "string" },
|
|
113
148
|
"canonical_only_files": {
|
|
@@ -119,10 +154,11 @@
|
|
|
119
154
|
"drift": {
|
|
120
155
|
"type": ["object", "null"],
|
|
121
156
|
"additionalProperties": true,
|
|
122
|
-
"required": ["memory_bank_version", "flow_pack_commit", "overall", "next_action"],
|
|
157
|
+
"required": ["memory_bank_version", "flow_pack_commit", "flow_contract", "overall", "next_action"],
|
|
123
158
|
"properties": {
|
|
124
159
|
"memory_bank_version": { "$ref": "#/definitions/comparison" },
|
|
125
160
|
"flow_pack_commit": { "$ref": "#/definitions/comparison" },
|
|
161
|
+
"flow_contract": { "$ref": "#/definitions/comparison" },
|
|
126
162
|
"overall": {
|
|
127
163
|
"type": "string",
|
|
128
164
|
"enum": ["same", "behind", "ahead", "diverged", "missing", "invalid", "unknown", "not_applicable"]
|
|
@@ -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
|
|
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
|
|
37
|
-
package_name:
|
|
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) {
|
package/dist/services/canon.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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;
|
package/dist/services/cleanup.js
CHANGED
|
@@ -7,12 +7,13 @@ 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",
|
|
14
14
|
"close_flow_session",
|
|
15
15
|
"expire_lane_lock",
|
|
16
|
+
"expire_lane_waiter",
|
|
16
17
|
"close_worktree_record"
|
|
17
18
|
]);
|
|
18
19
|
export function cleanupScan(context, input) {
|
|
@@ -39,6 +40,19 @@ export function cleanupScan(context, input) {
|
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
const fullProtocol = requireProtocol(context, protocol.id);
|
|
44
|
+
const state = readProtocolRuntimeState(context, fullProtocol).state;
|
|
45
|
+
const diagnostics = protocolRunDiagnostics(context, fullProtocol, state).diagnostics;
|
|
46
|
+
for (const diagnostic of diagnostics.filter((entry) => entry.code === "protocol_run_stage_mismatch")) {
|
|
47
|
+
findings.push({
|
|
48
|
+
code: "protocol_run_stage_mismatch",
|
|
49
|
+
severity: diagnostic.severity === "error" ? "warning" : "info",
|
|
50
|
+
protocol_id: protocol.id,
|
|
51
|
+
protocol_stage: state.stage,
|
|
52
|
+
latest_run: diagnostic.latest_run,
|
|
53
|
+
recommended_commands: diagnostic.recommended_commands
|
|
54
|
+
});
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
57
|
const orphanQueueJobs = context.db.all(`SELECT mq.protocol_id, mq.status FROM merge_queue mq
|
|
44
58
|
LEFT JOIN protocols p ON p.id = mq.protocol_id
|
|
@@ -85,6 +99,18 @@ export function cleanupScan(context, input) {
|
|
|
85
99
|
action: { kind: "expire_lane_lock", project_id: project.id, lock_id: lock.id }
|
|
86
100
|
});
|
|
87
101
|
}
|
|
102
|
+
const expiredWaiters = context.db.all(`SELECT id, lane, worker_id, expires_at FROM lane_waiters
|
|
103
|
+
WHERE project_id = ? AND status = 'queued' AND expires_at IS NOT NULL AND expires_at <= ?
|
|
104
|
+
ORDER BY id ASC`, [project.id, now]);
|
|
105
|
+
for (const waiter of expiredWaiters) {
|
|
106
|
+
pushFinding(findings, actions, "expired_lane_waiter", "warning", {
|
|
107
|
+
waiter_id: waiter.id,
|
|
108
|
+
lane: waiter.lane,
|
|
109
|
+
worker_id: waiter.worker_id,
|
|
110
|
+
expires_at: waiter.expires_at,
|
|
111
|
+
action: { kind: "expire_lane_waiter", project_id: project.id, waiter_id: waiter.id }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
88
114
|
const staleWorktrees = context.db.all(`SELECT wr.protocol_id, wr.worktree_path FROM worktree_records wr
|
|
89
115
|
JOIN protocols p ON p.id = wr.protocol_id
|
|
90
116
|
WHERE wr.project_id = ?
|
|
@@ -272,6 +298,24 @@ function applyAction(context, action, reason, force, now) {
|
|
|
272
298
|
lock_id: action.lock_id
|
|
273
299
|
};
|
|
274
300
|
}
|
|
301
|
+
if (action.kind === "expire_lane_waiter" && typeof action.waiter_id === "number") {
|
|
302
|
+
const result = context.db.run("UPDATE lane_waiters SET status = 'expired', reason = ?, updated_at = ? WHERE project_id = ? AND id = ? AND status = 'queued'", [reason, now, action.project_id, action.waiter_id]);
|
|
303
|
+
if (result.changes === 1) {
|
|
304
|
+
appendAudit(context, {
|
|
305
|
+
projectId: action.project_id,
|
|
306
|
+
eventType: "lane_waiter.expired",
|
|
307
|
+
reason,
|
|
308
|
+
forced: force,
|
|
309
|
+
payload: { waiter_id: action.waiter_id, source: "cleanup.apply" }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
kind: action.kind,
|
|
314
|
+
changed: result.changes === 1,
|
|
315
|
+
...(result.changes === 1 ? {} : { skipped: true, reason: "already_inactive" }),
|
|
316
|
+
waiter_id: action.waiter_id
|
|
317
|
+
};
|
|
318
|
+
}
|
|
275
319
|
if (action.kind === "close_worktree_record" && action.protocol_id) {
|
|
276
320
|
const worktreePath = action.worktree_path ?? context.db.get("SELECT worktree_path FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [action.project_id, action.protocol_id])?.worktree_path;
|
|
277
321
|
const status = worktreePath && fs.existsSync(worktreePath) ? "kept" : "removed";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function classifyCliOperation(args, env = process.env) {
|
|
2
|
+
const positional = positionalArgs(args);
|
|
3
|
+
const [family = null, command = null, action = null] = positional;
|
|
4
|
+
const operation = [family, command, action].filter(Boolean).join(".") || "help";
|
|
5
|
+
if (isMbUpgradeMode(args, env)) {
|
|
6
|
+
return { mode: "mb_upgrade", operation, family, command, action };
|
|
7
|
+
}
|
|
8
|
+
if (isRouterNative(args)) {
|
|
9
|
+
return { mode: "router_native", operation, family, command, action };
|
|
10
|
+
}
|
|
11
|
+
if (isReadOnlyDiagnostic(family, command, action)) {
|
|
12
|
+
return { mode: "read_only_diagnostics", operation, family, command, action };
|
|
13
|
+
}
|
|
14
|
+
return { mode: "normal_write", operation, family, command, action };
|
|
15
|
+
}
|
|
16
|
+
function positionalArgs(args) {
|
|
17
|
+
const positional = [];
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
const value = args[index];
|
|
20
|
+
if (!value)
|
|
21
|
+
continue;
|
|
22
|
+
if (value.startsWith("--")) {
|
|
23
|
+
if (!value.includes("=") && args[index + 1] && !args[index + 1]?.startsWith("--"))
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
positional.push(value);
|
|
28
|
+
}
|
|
29
|
+
return positional;
|
|
30
|
+
}
|
|
31
|
+
export function isReadOnlyCliOperation(args, env = process.env) {
|
|
32
|
+
return classifyCliOperation(args, env).mode === "read_only_diagnostics";
|
|
33
|
+
}
|
|
34
|
+
export function isMbUpgradeCliOperation(args, env = process.env) {
|
|
35
|
+
return classifyCliOperation(args, env).mode === "mb_upgrade";
|
|
36
|
+
}
|
|
37
|
+
function isMbUpgradeMode(args, env) {
|
|
38
|
+
if (env.DD_FLOW_COMPATIBILITY_MODE === "mb-upgrade" || env.DD_FLOW_MB_UPGRADE_MODE === "1") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (arg === "--compatibility-mode" && args[index + 1] === "mb-upgrade")
|
|
44
|
+
return true;
|
|
45
|
+
if (arg === "--mb-upgrade-mode")
|
|
46
|
+
return true;
|
|
47
|
+
if (arg?.startsWith("--compatibility-mode="))
|
|
48
|
+
return arg.slice("--compatibility-mode=".length) === "mb-upgrade";
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function isRouterNative(args) {
|
|
53
|
+
const first = args[0];
|
|
54
|
+
if (!first || first === "--version")
|
|
55
|
+
return true;
|
|
56
|
+
if (first === "--help" || first === "-h")
|
|
57
|
+
return true;
|
|
58
|
+
if (args.includes("--help") || args.includes("-h"))
|
|
59
|
+
return true;
|
|
60
|
+
return ["engine", "version", "schema"].includes(first);
|
|
61
|
+
}
|
|
62
|
+
function isReadOnlyDiagnostic(family, command, action) {
|
|
63
|
+
if (!family)
|
|
64
|
+
return true;
|
|
65
|
+
if (family === "status" || family === "version" || family === "id" || family === "integration")
|
|
66
|
+
return true;
|
|
67
|
+
if (family === "canon")
|
|
68
|
+
return command === "status" || command === "resolve";
|
|
69
|
+
if (family === "project")
|
|
70
|
+
return command === "status" || command === "resolve";
|
|
71
|
+
if (family === "protocol")
|
|
72
|
+
return ["status", "ready", "blockers", "implement"].includes(command ?? "");
|
|
73
|
+
if (family === "plan")
|
|
74
|
+
return command === "status";
|
|
75
|
+
if (family === "run")
|
|
76
|
+
return command === "status" || command === "list";
|
|
77
|
+
if (family === "lane") {
|
|
78
|
+
return (command === "status" ||
|
|
79
|
+
command === "waiters" ||
|
|
80
|
+
(command === "lock" && action === "status") ||
|
|
81
|
+
(command === "workspace" && action === "check"));
|
|
82
|
+
}
|
|
83
|
+
if (family === "merge")
|
|
84
|
+
return command === "status";
|
|
85
|
+
if (family === "merge-worker")
|
|
86
|
+
return command === "status";
|
|
87
|
+
if (family === "merge-queue")
|
|
88
|
+
return command === "status";
|
|
89
|
+
if (family === "migration")
|
|
90
|
+
return ["plan", "report", "verify"].includes(command ?? "");
|
|
91
|
+
if (family === "session")
|
|
92
|
+
return command === "status";
|
|
93
|
+
if (family === "dashboard")
|
|
94
|
+
return command === "data" || command === "open";
|
|
95
|
+
if (family === "memory")
|
|
96
|
+
return command === "permissions" && action === "preflight";
|
|
97
|
+
if (family === "codex" && command === "hooks")
|
|
98
|
+
return action === "print" || action === "status";
|
|
99
|
+
if (family === "codex" && command === "home")
|
|
100
|
+
return ["plan", "status", "print-env"].includes(action ?? "");
|
|
101
|
+
if (family === "worktree")
|
|
102
|
+
return command === "plan" || command === "status";
|
|
103
|
+
return false;
|
|
104
|
+
}
|