@deksden-com/dd-flow-cli 0.5.0 → 0.7.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 (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -3
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +24 -11
  5. package/dist/cli/run-cli.js +117 -28
  6. package/dist/domain/flow-contract.js +15 -4
  7. package/dist/domain/session-coverage.js +88 -0
  8. package/dist/runtime/context.js +8 -2
  9. package/dist/schemas/code-stage-report.schema.json +7 -2
  10. package/dist/schemas/engine-manifest.schema.json +22 -0
  11. package/dist/schemas/flow-contract.schema.json +15 -13
  12. package/dist/schemas/flow-run.schema.json +1 -0
  13. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  14. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  15. package/dist/schemas/run-engine-binding.schema.json +37 -0
  16. package/dist/schemas/stage-prompt.schema.json +9 -5
  17. package/dist/schemas/stage-start-response.schema.json +6 -5
  18. package/dist/services/canon.js +15 -1
  19. package/dist/services/cleanup.js +77 -0
  20. package/dist/services/cli-operation-classifier.js +52 -8
  21. package/dist/services/compatibility-preflight.js +1 -1
  22. package/dist/services/dashboard.js +2 -2
  23. package/dist/services/engines.js +408 -30
  24. package/dist/services/hooks.js +28 -15
  25. package/dist/services/lanes.js +0 -4
  26. package/dist/services/merge-queue.js +48 -0
  27. package/dist/services/merge-worker.js +3 -4
  28. package/dist/services/migrations.js +307 -44
  29. package/dist/services/plan-runtime.js +4 -4
  30. package/dist/services/plans.js +5 -3
  31. package/dist/services/protocols.js +23 -2
  32. package/dist/services/run-engine-bindings.js +157 -0
  33. package/dist/services/run-projection.js +18 -4
  34. package/dist/services/runs.js +54 -7
  35. package/dist/services/schema-validation.js +96 -0
  36. package/dist/services/sessions.js +33 -73
  37. package/dist/services/stage-lifecycle.js +298 -45
  38. package/dist/services/status.js +8 -3
  39. package/dist/storage/database.js +32 -11
  40. package/package.json +1 -1
@@ -1,10 +1,16 @@
1
1
  import { getDatabase } from "../storage/database.js";
2
2
  import { resolveDdFlowHome } from "../storage/paths.js";
3
- export function createContext(env) {
3
+ export function createRouterContext(env) {
4
+ return {
5
+ ddFlowHome: resolveDdFlowHome(env),
6
+ now: () => new Date().toISOString()
7
+ };
8
+ }
9
+ export function createContext(env, mode = "initialize") {
4
10
  const ddFlowHome = resolveDdFlowHome(env);
5
11
  return {
6
12
  ddFlowHome,
7
- db: getDatabase(ddFlowHome),
13
+ db: getDatabase(ddFlowHome, mode),
8
14
  env,
9
15
  now: () => new Date().toISOString()
10
16
  };
@@ -124,10 +124,15 @@
124
124
  "run": {
125
125
  "type": "object",
126
126
  "additionalProperties": true,
127
- "required": ["run_id", "run_state"],
127
+ "required": ["run_id"],
128
+ "anyOf": [
129
+ {"required": ["run_state"]},
130
+ {"required": ["run_index"]}
131
+ ],
128
132
  "properties": {
129
133
  "run_id": { "type": "string", "pattern": "^RUN-[0-9]{3}-[a-z0-9]+(?:-[a-z0-9]+)*$" },
130
- "run_state": { "type": "string", "minLength": 1 }
134
+ "run_state": { "type": "string", "minLength": 1 },
135
+ "run_index": { "type": "string", "minLength": 1 }
131
136
  }
132
137
  },
133
138
  "stage": {
@@ -44,6 +44,28 @@
44
44
  "items": { "type": "string", "minLength": 1 }
45
45
  }
46
46
  },
47
+ "schema_registry": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["schema_id", "entries"],
51
+ "properties": {
52
+ "schema_id": { "const": "dd-flow/engine-schema-registry@1" },
53
+ "entries": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["name", "id", "path", "checksum"],
59
+ "properties": {
60
+ "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
61
+ "id": { "type": "string", "minLength": 1 },
62
+ "path": { "type": "string", "pattern": "^dist/schemas/[a-z0-9][a-z0-9-]*\\.schema\\.json$" },
63
+ "checksum": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ },
47
69
  "integrity": {
48
70
  "type": "object",
49
71
  "additionalProperties": false,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/flow-contract@5",
3
+ "$id": "dd-flow/flow-contract@6",
4
4
  "title": "dd-flow canonical runtime contract",
5
- "description": "Structural contract for the SPC-004 v0.2 and SPC-005 runtime/PLAN cutover. Dynamic command and cross-repository checks belong to companion projects.",
5
+ "description": "Structural contract for the SPC-004/005/006 runtime, PLAN and deterministic stage-context cutover. Dynamic command and cross-repository checks belong to companion projects.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -25,21 +25,21 @@
25
25
  "policy"
26
26
  ],
27
27
  "properties": {
28
- "schema_id": {"const": "dd-flow/flow-contract@5"},
28
+ "schema_id": {"const": "dd-flow/flow-contract@6"},
29
29
  "id": {"const": "dd-flow-canonical-2026-08"},
30
30
  "contract_id": {"type": "string", "pattern": "^dd-flow-canonical-[0-9]{4}-[0-9]{2}$"},
31
- "version": {"const": 5},
31
+ "version": {"const": 6},
32
32
  "status": {"const": "active"},
33
33
  "source": {"const": ".memory-bank/dd-flow/common/runtime-contract.md"},
34
34
  "specifications": {
35
35
  "type": "array",
36
- "minItems": 2,
36
+ "minItems": 3,
37
37
  "items": {
38
38
  "type": "object",
39
39
  "additionalProperties": false,
40
40
  "required": ["id", "version", "sha256"],
41
41
  "properties": {
42
- "id": {"enum": ["SPC-004", "SPC-005"]},
42
+ "id": {"enum": ["SPC-004", "SPC-005", "SPC-006"]},
43
43
  "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
44
44
  "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}
45
45
  }
@@ -73,21 +73,23 @@
73
73
  "stageLifecycle": {
74
74
  "type": "object",
75
75
  "additionalProperties": false,
76
- "required": ["actions", "start_command", "finish_command", "prompt_sections", "current_write_alias", "archive_pattern"],
76
+ "required": ["actions", "start_command", "bootstrap_command", "finish_command", "prompt_sections", "current_write_alias", "archive_pattern"],
77
77
  "properties": {
78
78
  "actions": {
79
79
  "const": ["stage_start", "stage_finish"]
80
80
  },
81
81
  "start_command": {"const": "dd-flow stage start <RUN> --stage <stage> --json"},
82
- "finish_command": {"const": "dd-flow stage finish <RUN> --stage <stage> --status done --json"},
82
+ "bootstrap_command": {"const": "dd-flow stage start --bootstrap --stage specify --project-root <root> --subject <label> --intake-file <path> --json"},
83
+ "finish_command": {"const": "dd-flow stage finish <RUN> --stage <stage> --outcome <outcome> --json"},
83
84
  "prompt_sections": {
84
85
  "const": [
85
86
  "stage_identity",
86
- "runtime_context",
87
- "intake",
87
+ "authoritative_runtime_facts",
88
+ "preflight",
89
+ "task_intake",
88
90
  "applicable_instructions",
89
- "stage_cli",
90
- "file_boundaries",
91
+ "required_context",
92
+ "work_contract",
91
93
  "completion_contract"
92
94
  ]
93
95
  },
@@ -112,7 +114,7 @@
112
114
  "required": ["finish_outputs", "model_input", "mechanical_facts"],
113
115
  "properties": {
114
116
  "finish_outputs": {"const": ["stage-report.json", "stage-report.md", "stage-report.html", "summary.md"]},
115
- "model_input": {"const": "semantic-only"},
117
+ "model_input": {"const": "semantic-only; outcome flag"},
116
118
  "mechanical_facts": {"const": "cli-owned"}
117
119
  }
118
120
  },
@@ -123,6 +123,7 @@
123
123
  "expected": {"type": "array", "items": {"type": "string"}},
124
124
  "observed": {"type": "array", "items": {"type": "string"}},
125
125
  "missing": {"type": "array", "items": {"type": "string"}},
126
+ "diagnostics": {"type": "array", "items": {"type": "string"}},
126
127
  "reason": {"type": "string"}
127
128
  }
128
129
  },
@@ -70,12 +70,14 @@
70
70
  },
71
71
  "backup": {
72
72
  "type": "object",
73
- "required": ["status", "path", "created_at", "rollback_route"],
73
+ "required": ["status", "path", "created_at", "rollback_route", "size_bytes", "sha256"],
74
74
  "additionalProperties": true,
75
75
  "properties": {
76
76
  "status": { "enum": ["present", "planned", "missing"] },
77
77
  "path": { "type": ["string", "null"] },
78
78
  "created_at": { "type": ["string", "null"] },
79
+ "size_bytes": { "type": ["integer", "null"], "minimum": 1 },
80
+ "sha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
79
81
  "rollback_route": { "type": "string", "minLength": 1 }
80
82
  }
81
83
  },
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/merge-stage-report@2/engine-0.4.2",
4
+ "title": "Merge stage report emitted by dd-flow engine 0.4.2",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schema_id", "run", "stage", "project", "subject", "overall", "breadcrumbs", "implemented_goals", "acceptance_scenarios", "changed_files", "checks", "review", "review_gates", "defs"],
8
+ "properties": {
9
+ "schema_id": { "const": "dd-flow/merge-stage-report@2" },
10
+ "run": { "type": "object", "required": ["run_id", "run_index"], "properties": { "run_id": { "type": "string", "pattern": "^RUN-[0-9]{3}-[a-z0-9]+(?:-[a-z0-9]+)*$" }, "run_index": { "type": "string", "minLength": 1 } } },
11
+ "stage": { "type": "object", "required": ["name", "dir", "status"] },
12
+ "project": { "type": "object", "required": ["id", "title"] },
13
+ "subject": { "type": "object", "required": ["id", "title"] },
14
+ "overall": { "type": "object", "required": ["verdict", "summary", "next_action"], "properties": { "verdict": { "enum": ["accepted", "blocked", "failed"] }, "summary": { "type": "string", "minLength": 1 }, "next_action": { "type": "string", "minLength": 1 } } },
15
+ "breadcrumbs": { "type": "array" },
16
+ "implemented_goals": { "type": "array" },
17
+ "acceptance_scenarios": { "type": "array" },
18
+ "changed_files": { "type": "array" },
19
+ "checks": { "type": "array" },
20
+ "review": { "type": "object" },
21
+ "review_gates": { "type": "array" },
22
+ "defs": { "type": "array" }
23
+ }
24
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/run-engine-binding@1",
4
+ "title": "Immutable RUN engine binding",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_id", "run_id", "project_root", "bound_at", "source", "reason", "engine", "probe"],
8
+ "properties": {
9
+ "schema_id": {"const": "dd-flow/run-engine-binding@1"},
10
+ "run_id": {"type": "string", "pattern": "^RUN-[0-9]+-[a-z0-9][a-z0-9-]*$"},
11
+ "project_root": {"type": "string", "minLength": 1},
12
+ "bound_at": {"type": "string", "format": "date-time"},
13
+ "source": {"enum": ["run_creation", "legacy_recovery"]},
14
+ "reason": {"type": "string", "minLength": 1},
15
+ "engine": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["package_name", "package_version", "engine_version", "integrity_checksum", "snapshot_root"],
19
+ "properties": {
20
+ "package_name": {"type": "string", "minLength": 1},
21
+ "package_version": {"type": "string", "minLength": 1},
22
+ "engine_version": {"type": "string", "minLength": 1},
23
+ "integrity_checksum": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
24
+ "snapshot_root": {"type": "string", "minLength": 1}
25
+ }
26
+ },
27
+ "probe": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["status", "command"],
31
+ "properties": {
32
+ "status": {"enum": ["not_required", "passed"]},
33
+ "command": {"type": ["string", "null"]}
34
+ }
35
+ }
36
+ }
37
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/stage-prompt@1",
3
+ "$id": "dd-flow/stage-prompt@2",
4
4
  "title": "Generated stage prompt",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -16,16 +16,20 @@
16
16
  }
17
17
  }
18
18
  ],
19
- "required": ["schema_id", "run_id", "stage", "generated_at", "prompt_path", "sections", "aliases", "write_boundary", "source_fragments"],
19
+ "required": ["schema_id", "run_id", "stage", "generated_at", "prompt_path", "sections", "aliases", "write_boundary", "source_fragments", "authoritative_facts", "preflight", "required_context", "worker_prompt_markdown"],
20
20
  "properties": {
21
- "schema_id": {"const": "dd-flow/stage-prompt@1"},
21
+ "schema_id": {"const": "dd-flow/stage-prompt@2"},
22
22
  "run_id": {"type": "string", "minLength": 1},
23
23
  "stage": {"type": "string", "minLength": 1},
24
24
  "generated_at": {"type": "string", "format": "date-time"},
25
25
  "prompt_path": {"type": "string", "minLength": 1},
26
- "sections": {"type": "array", "minItems": 7, "maxItems": 7, "items": {"enum": ["stage_identity", "runtime_context", "intake", "applicable_instructions", "stage_cli", "file_boundaries", "completion_contract"]}},
26
+ "sections": {"const": ["stage_identity", "authoritative_runtime_facts", "preflight", "task_intake", "applicable_instructions", "required_context", "work_contract", "completion_contract"]},
27
27
  "aliases": {"type": "object", "required": ["project", "workspace", "run", "stage", "protocol", "intake"], "additionalProperties": {"type": "string"}},
28
28
  "write_boundary": {"type": "object", "required": ["current", "archive", "archive_writable"], "properties": {"current": {"const": "@stage"}, "archive": {"pattern": "try-[0-9]{3}"}, "archive_writable": {"const": false}}, "additionalProperties": false},
29
- "source_fragments": {"type": "array", "items": {"type": "string", "minLength": 1}}
29
+ "source_fragments": {"type": "array", "items": {"type": "string", "minLength": 1}},
30
+ "authoritative_facts": {"type": "object", "required": ["git", "runtime"], "additionalProperties": true},
31
+ "preflight": {"type": "object", "required": ["compatibility", "permissions", "session_binding"], "additionalProperties": true},
32
+ "required_context": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["path", "reason", "stop_condition"], "properties": {"path": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "stop_condition": {"type": "string", "minLength": 1}}}},
33
+ "worker_prompt_markdown": {"type": "string", "minLength": 1}
30
34
  }
31
35
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/stage-start-response@1",
3
+ "$id": "dd-flow/stage-start-response@2",
4
4
  "title": "Stage start response",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -10,9 +10,9 @@
10
10
  "then": {"required": ["plan_ref", "aspect_map_ref"]}
11
11
  }
12
12
  ],
13
- "required": ["schema_id", "run_id", "stage", "attempt_number", "stage_root", "prompt_path", "aliases", "next_command"],
13
+ "required": ["schema_id", "run_id", "stage", "attempt_number", "stage_root", "prompt_path", "aliases", "next_command", "worker_prompt_markdown"],
14
14
  "properties": {
15
- "schema_id": {"const": "dd-flow/stage-start-response@1"},
15
+ "schema_id": {"const": "dd-flow/stage-start-response@2"},
16
16
  "run_id": {"type": "string", "minLength": 1},
17
17
  "stage": {"type": "string", "minLength": 1},
18
18
  "attempt_number": {"type": "integer", "minimum": 1},
@@ -23,7 +23,8 @@
23
23
  "resolved_context": {"type": "object", "additionalProperties": true},
24
24
  "plan_ref": {"type": "string", "pattern": "^\\.memory-bank/protocol/[^/]+/plan\\.json$"},
25
25
  "aspect_map_ref": {"type": "string", "minLength": 1},
26
- "next_command": {"const": "dd-flow stage finish"},
27
- "permission_probe": {"type": "object", "additionalProperties": true}
26
+ "next_command": {"type": "string", "pattern": "^dd-flow stage finish "},
27
+ "permission_probe": {"type": "object", "additionalProperties": true},
28
+ "worker_prompt_markdown": {"type": "string", "minLength": 1}
28
29
  }
29
30
  }
@@ -36,7 +36,21 @@ export function getCanonStatus(context, input = {}) {
36
36
  };
37
37
  }
38
38
  export function resolveCanonRoot(context, input = {}) {
39
- const candidates = canonCandidates(context, input);
39
+ return resolveCanonCandidates(canonCandidates(context, input));
40
+ }
41
+ /** Read only: resolve an explicit/environment/nearby canon without opening DD_FLOW_HOME. */
42
+ export function resolveCanonRootBeforeContext(env, input = {}) {
43
+ const candidates = [];
44
+ if (input.explicitRoot)
45
+ candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
46
+ else if (env.DD_MEMORYBANK)
47
+ candidates.push(validateCanonCandidate("env", env.DD_MEMORYBANK));
48
+ else
49
+ for (const known of knownCanonRoots())
50
+ candidates.push(validateCanonCandidate("known", known));
51
+ return resolveCanonCandidates(uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`));
52
+ }
53
+ function resolveCanonCandidates(candidates) {
40
54
  const validCandidates = candidates.filter((candidate) => candidate.valid && candidate.realpath);
41
55
  const uniqueValid = uniqueBy(validCandidates, (candidate) => candidate.realpath ?? candidate.root);
42
56
  const blockers = candidates.flatMap((candidate) => candidate.blockers);
@@ -12,6 +12,7 @@ const actionKinds = new Set([
12
12
  "repair_protocol_state",
13
13
  "cancel_queue_job",
14
14
  "close_flow_session",
15
+ "discard_stale_run",
15
16
  "expire_lane_lock",
16
17
  "expire_lane_waiter",
17
18
  "close_worktree_record"
@@ -86,6 +87,36 @@ export function cleanupScan(context, input) {
86
87
  });
87
88
  }
88
89
  }
90
+ const staleRuns = context.db.all(`SELECT id, runtime_path, run_dir, workspace_root FROM flow_runs
91
+ WHERE project_id = ? AND status = 'running'
92
+ ORDER BY updated_at ASC, id ASC`, [project.id]);
93
+ for (const run of staleRuns) {
94
+ const missingAuthority = !fs.existsSync(run.runtime_path);
95
+ const missingRunHome = !fs.existsSync(run.run_dir);
96
+ const missingWorkspace = !fs.existsSync(run.workspace_root);
97
+ if (!missingAuthority)
98
+ continue;
99
+ const details = {
100
+ code: "stale_run_missing_authority",
101
+ severity: "warning",
102
+ run_id: run.id,
103
+ runtime_path: run.runtime_path,
104
+ missing_run_home: missingRunHome,
105
+ missing_workspace: missingWorkspace
106
+ };
107
+ if (missingRunHome && missingWorkspace) {
108
+ pushFinding(findings, actions, details.code, details.severity, {
109
+ run_id: run.id,
110
+ runtime_path: run.runtime_path,
111
+ missing_run_home: missingRunHome,
112
+ missing_workspace: missingWorkspace,
113
+ action: { kind: "discard_stale_run", project_id: project.id, run_id: run.id }
114
+ });
115
+ }
116
+ else {
117
+ findings.push({ ...details, action: null, reason: "manual_reconciliation_required" });
118
+ }
119
+ }
89
120
  const now = context.now();
90
121
  const expiredLocks = context.db.all(`SELECT id, lane, worker_id, expires_at FROM lane_locks
91
122
  WHERE project_id = ? AND status = 'active' AND expires_at <= ?
@@ -289,6 +320,41 @@ function applyAction(context, action, reason, force, now) {
289
320
  });
290
321
  return { kind: action.kind, changed: true, session_id: action.session_id };
291
322
  }
323
+ if (action.kind === "discard_stale_run" && action.run_id) {
324
+ const run = context.db.get("SELECT status, runtime_path, run_dir, workspace_root, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);
325
+ if (!run) {
326
+ return { kind: action.kind, changed: false, skipped: true, reason: "run_not_found", run_id: action.run_id };
327
+ }
328
+ if (run.status === "discarded") {
329
+ const index = discardedRunIndex(run.index_json, reason, now);
330
+ context.db.run("UPDATE flow_runs SET verdict = 'discarded', next_action = ?, index_json = ?, updated_at = ?, completed_at = COALESCE(completed_at, ?) WHERE project_id = ? AND id = ?", [reason, JSON.stringify(index), now, now, action.project_id, action.run_id]);
331
+ if (fs.existsSync(run.runtime_path))
332
+ fs.writeFileSync(run.runtime_path, `${JSON.stringify(index, null, 2)}\n`);
333
+ return { kind: action.kind, changed: true, run_id: action.run_id };
334
+ }
335
+ if (run.status !== "running") {
336
+ return { kind: action.kind, changed: false, skipped: true, reason: `already_${run.status}`, run_id: action.run_id };
337
+ }
338
+ if (fs.existsSync(run.runtime_path) || fs.existsSync(run.run_dir) || fs.existsSync(run.workspace_root)) {
339
+ return { kind: action.kind, changed: false, skipped: true, reason: "runtime_or_workspace_reappeared", run_id: action.run_id };
340
+ }
341
+ const index = discardedRunIndex(run.index_json, reason, now);
342
+ context.db.run(`UPDATE flow_runs SET status = 'discarded', verdict = 'discarded', next_action = ?, index_json = ?, updated_at = ?, completed_at = ?
343
+ WHERE project_id = ? AND id = ? AND status = 'running'`, [reason, JSON.stringify(index), now, now, action.project_id, action.run_id]);
344
+ fs.mkdirSync(run.run_dir, { recursive: true });
345
+ fs.writeFileSync(run.runtime_path, `${JSON.stringify(index, null, 2)}\n`);
346
+ context.db.run(`UPDATE flow_sessions SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
347
+ WHERE project_id = ? AND run_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, action.run_id]);
348
+ context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE project_id = ? AND run_id = ? AND ended_at IS NULL", [now, action.project_id, action.run_id]);
349
+ appendAudit(context, {
350
+ projectId: action.project_id,
351
+ eventType: "flow_run.discarded",
352
+ reason,
353
+ forced: force,
354
+ payload: { run_id: action.run_id, source: "cleanup.apply", authority_missing: true }
355
+ });
356
+ return { kind: action.kind, changed: true, run_id: action.run_id };
357
+ }
292
358
  if (action.kind === "expire_lane_lock" && typeof action.lock_id === "number") {
293
359
  const result = context.db.run("UPDATE lane_locks SET status = 'expired', reason = ?, updated_at = ? WHERE project_id = ? AND id = ? AND status = 'active'", [reason, now, action.project_id, action.lock_id]);
294
360
  return {
@@ -330,6 +396,17 @@ function applyAction(context, action, reason, force, now) {
330
396
  }
331
397
  return { kind: action.kind, changed: false, skipped: true, reason: "invalid_action" };
332
398
  }
399
+ function discardedRunIndex(indexJson, reason, now) {
400
+ const index = JSON.parse(indexJson);
401
+ index.schema_id = "dd-flow/flow-run@2";
402
+ index.status = "discarded";
403
+ index.verdict = "discarded";
404
+ index.next_action = reason;
405
+ index.updated_at = now;
406
+ index.completed_at ??= now;
407
+ index.finished_at ??= now;
408
+ return index;
409
+ }
333
410
  function dirtyGit(worktreePath) {
334
411
  if (!fs.existsSync(worktreePath)) {
335
412
  return false;
@@ -34,21 +34,63 @@ export function isReadOnlyCliOperation(args, env = process.env) {
34
34
  export function isMbUpgradeCliOperation(args, env = process.env) {
35
35
  return classifyCliOperation(args, env).mode === "mb_upgrade";
36
36
  }
37
- function isMbUpgradeMode(args, env) {
38
- if (env.DD_FLOW_COMPATIBILITY_MODE === "mb-upgrade" || env.DD_FLOW_MB_UPGRADE_MODE === "1") {
37
+ export function requiresExplicitUpgradeAuthorization(args) {
38
+ const positional = positionalArgs(args);
39
+ const [family, command] = positional;
40
+ if (family === "migration" && command === "apply")
39
41
  return true;
40
- }
42
+ if (family === "run" && command === "start" && optionValue(args, "flow-kind") === "mb-upgrade")
43
+ return true;
44
+ return false;
45
+ }
46
+ function isMbUpgradeMode(args, env) {
47
+ const marked = env.DD_FLOW_COMPATIBILITY_MODE === "mb-upgrade" || env.DD_FLOW_MB_UPGRADE_MODE === "1";
41
48
  for (let index = 0; index < args.length; index += 1) {
42
49
  const arg = args[index];
43
50
  if (arg === "--compatibility-mode" && args[index + 1] === "mb-upgrade")
44
- return true;
51
+ return isUpgradeAllowlisted(args);
45
52
  if (arg === "--mb-upgrade-mode")
46
- return true;
47
- if (arg?.startsWith("--compatibility-mode="))
48
- return arg.slice("--compatibility-mode=".length) === "mb-upgrade";
53
+ return isUpgradeAllowlisted(args);
54
+ if (arg?.startsWith("--compatibility-mode=") && arg.slice("--compatibility-mode=".length) === "mb-upgrade") {
55
+ return isUpgradeAllowlisted(args);
56
+ }
49
57
  }
58
+ return marked && isUpgradeAllowlisted(args);
59
+ }
60
+ function isUpgradeAllowlisted(args) {
61
+ const positional = positionalArgs(args);
62
+ const [family, command] = positional;
63
+ if (family === "status")
64
+ return true;
65
+ if (family === "engine" && command === "resolve")
66
+ return true;
67
+ if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
68
+ return true;
69
+ if (family === "memory" && command === "permissions" && positional[2] === "preflight")
70
+ return true;
71
+ if (family === "run" && command === "start")
72
+ return optionValue(args, "flow-kind") === "mb-upgrade";
73
+ if (family === "stage" && ["start", "finish"].includes(command ?? ""))
74
+ return hasRunBinding(args);
75
+ if (family === "session" && ["status", "register", "stop", "usage"].includes(command ?? ""))
76
+ return hasRunBinding(args);
77
+ if (family === "dashboard" && ["data", "render", "refresh", "open"].includes(command ?? ""))
78
+ return true;
50
79
  return false;
51
80
  }
81
+ function hasRunBinding(args) {
82
+ return Boolean(optionValue(args, "run") || positionalArgs(args).some((value) => value.startsWith("RUN-")));
83
+ }
84
+ function optionValue(args, name) {
85
+ for (let index = args.length - 1; index >= 0; index -= 1) {
86
+ const arg = args[index];
87
+ if (arg === `--${name}`)
88
+ return args[index + 1] ?? null;
89
+ if (arg?.startsWith(`--${name}=`))
90
+ return arg.slice(name.length + 3);
91
+ }
92
+ return null;
93
+ }
52
94
  function isRouterNative(args) {
53
95
  const first = args[0];
54
96
  if (!first || first === "--version")
@@ -57,7 +99,9 @@ function isRouterNative(args) {
57
99
  return true;
58
100
  if (args.includes("--help") || args.includes("-h"))
59
101
  return true;
60
- return ["engine", "version", "schema"].includes(first);
102
+ if (["engine", "version", "schema"].includes(first))
103
+ return true;
104
+ return first === "codex" && args[1] === "hook" && args[2] === "handle";
61
105
  }
62
106
  function isReadOnlyDiagnostic(family, command, action) {
63
107
  if (!family)
@@ -11,7 +11,7 @@ export function preflightCliCompatibility(context, args, env = process.env, reso
11
11
  if (!projectRoot) {
12
12
  return { ok: true, classification, compatibility: null };
13
13
  }
14
- const selection = selectEngine(context, { projectRoot });
14
+ const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true }, classification);
15
15
  const compatibility = compatibilityReport(selection, classification);
16
16
  if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
17
17
  return { ok: true, classification, compatibility };
@@ -893,7 +893,7 @@ function planSummaryForProtocol(context, projectId, protocolId) {
893
893
  return { plan_id: "none", total: 0, done: 0, blocked: 0 };
894
894
  }
895
895
  try {
896
- const summary = planSummary(context, { projectId, protocolId, planPath: protocol.plan_path });
896
+ const summary = planSummary(context, { projectId, protocolId, planPath: protocol.plan_path }, { bind: false });
897
897
  return { plan_id: summary.plan_id, total: summary.total, done: summary.done, blocked: summary.blocked };
898
898
  }
899
899
  catch {
@@ -909,7 +909,7 @@ function activePlanItems(context, projectId, protocols) {
909
909
  }
910
910
  let plan;
911
911
  try {
912
- plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: record.plan_path }).plan;
912
+ plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: record.plan_path }, { bind: false }).plan;
913
913
  }
914
914
  catch {
915
915
  continue;