@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +76 -5
  3. package/dist/build-info.json +10 -6
  4. package/dist/cli/help.js +165 -20
  5. package/dist/cli/run-cli.js +427 -17
  6. package/dist/schemas/compatibility.schema.json +105 -0
  7. package/dist/schemas/engine-manifest.schema.json +61 -0
  8. package/dist/schemas/flow-guidance.schema.json +90 -0
  9. package/dist/schemas/flow-run-index.schema.json +30 -4
  10. package/dist/schemas/global-dashboard-data.schema.json +126 -0
  11. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  12. package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
  13. package/dist/schemas/plan-stage-report.schema.json +83 -0
  14. package/dist/schemas/project-dashboard-data.schema.json +122 -0
  15. package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
  16. package/dist/schemas/project-summary.schema.json +73 -0
  17. package/dist/schemas/protocol-dashboard-data.schema.json +112 -0
  18. package/dist/schemas/status-report.schema.json +38 -2
  19. package/dist/schemas/version-report.schema.json +22 -0
  20. package/dist/services/build-info.js +26 -3
  21. package/dist/services/canon.js +93 -22
  22. package/dist/services/cleanup.js +45 -1
  23. package/dist/services/cli-operation-classifier.js +104 -0
  24. package/dist/services/compatibility-preflight.js +124 -0
  25. package/dist/services/config.js +31 -0
  26. package/dist/services/dashboard-targets.js +95 -0
  27. package/dist/services/dashboard.js +972 -10
  28. package/dist/services/engines.js +532 -0
  29. package/dist/services/flow-guidance.js +221 -0
  30. package/dist/services/hooks.js +1 -1
  31. package/dist/services/ids.js +106 -0
  32. package/dist/services/lanes.js +333 -1
  33. package/dist/services/merge-queue.js +106 -16
  34. package/dist/services/merge-worker.js +67 -4
  35. package/dist/services/migrations.js +231 -0
  36. package/dist/services/project-summary.js +122 -0
  37. package/dist/services/projects.js +44 -3
  38. package/dist/services/protocol-lifecycle.js +144 -0
  39. package/dist/services/protocols.js +660 -7
  40. package/dist/services/runs.js +98 -21
  41. package/dist/services/schema-validation.js +84 -4
  42. package/dist/services/sessions.js +21 -4
  43. package/dist/services/status.js +199 -1
  44. package/dist/services/version-status.js +59 -9
  45. package/dist/storage/database.js +31 -0
  46. package/dist/storage/paths.js +33 -0
  47. 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 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,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
+ }