@deksden-com/dd-flow-cli 0.4.0 → 0.4.2

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 (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +25 -9
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +45 -29
  5. package/dist/cli/run-cli.js +229 -49
  6. package/dist/domain/entity-ids.js +4 -4
  7. package/dist/domain/flow-contract.js +502 -36
  8. package/dist/domain/validation.js +34 -0
  9. package/dist/protocol/local-files.js +8 -6
  10. package/dist/schemas/code-stage-report.schema.json +197 -2
  11. package/dist/schemas/flow-contract.schema.json +126 -0
  12. package/dist/schemas/flow-run-index-v3.schema.json +203 -0
  13. package/dist/schemas/flow-run-index.schema.json +22 -2
  14. package/dist/schemas/flow-run.schema.json +36 -0
  15. package/dist/schemas/merge-stage-report.schema.json +213 -2
  16. package/dist/schemas/plan-stage-report.schema.json +156 -2
  17. package/dist/schemas/release-impact.schema.json +16 -0
  18. package/dist/services/audit.js +3 -3
  19. package/dist/services/branch-context.js +17 -5
  20. package/dist/services/canon.js +0 -1
  21. package/dist/services/cleanup.js +6 -6
  22. package/dist/services/cli-operation-classifier.js +1 -1
  23. package/dist/services/compatibility-preflight.js +3 -75
  24. package/dist/services/dashboard.js +48 -11
  25. package/dist/services/engines.js +124 -13
  26. package/dist/services/hooks.js +6 -6
  27. package/dist/services/ids.js +40 -49
  28. package/dist/services/merge-queue.js +33 -26
  29. package/dist/services/merge-worker.js +2 -1
  30. package/dist/services/migrations.js +64 -0
  31. package/dist/services/plans.js +23 -16
  32. package/dist/services/projects.js +2 -2
  33. package/dist/services/prompts.js +322 -0
  34. package/dist/services/protocols.js +77 -46
  35. package/dist/services/run-projection.js +80 -0
  36. package/dist/services/runs.js +360 -22
  37. package/dist/services/schema-validation.js +35 -12
  38. package/dist/services/sessions.js +81 -3
  39. package/dist/services/status.js +32 -1
  40. package/dist/services/usage.js +233 -0
  41. package/dist/services/worktrees.js +24 -19
  42. package/dist/storage/database.js +223 -9
  43. package/package.json +1 -1
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/plan-stage-report@1",
3
+ "$id": "dd-flow/plan-stage-report@2",
4
4
  "title": "dd-flow plan stage report data",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
+ "allOf": [
8
+ {
9
+ "if": {
10
+ "properties": { "schema_id": { "const": "dd-flow/plan-stage-report@2" } },
11
+ "required": ["schema_id"]
12
+ },
13
+ "then": { "required": ["flow_flags"] }
14
+ }
15
+ ],
7
16
  "required": [
8
17
  "schema_id",
9
18
  "generated_at",
@@ -17,7 +26,7 @@
17
26
  ],
18
27
  "properties": {
19
28
  "schema_id": {
20
- "const": "dd-flow/plan-stage-report@1"
29
+ "enum": ["dd-flow/plan-stage-report@1", "dd-flow/plan-stage-report@2"]
21
30
  },
22
31
  "generated_at": {
23
32
  "type": "string",
@@ -88,6 +97,7 @@
88
97
  "reason": {"type": "string"}
89
98
  }
90
99
  },
100
+ "flow_flags": { "$ref": "#/definitions/flowFlags" },
91
101
  "graph": {
92
102
  "type": "object",
93
103
  "additionalProperties": false,
@@ -153,6 +163,50 @@
153
163
  }
154
164
  }
155
165
  },
166
+ "review_gates": {
167
+ "type": "array",
168
+ "items": { "$ref": "#/definitions/reviewGate" }
169
+ },
170
+ "design_aspect_traceability": {
171
+ "type": "object",
172
+ "additionalProperties": true,
173
+ "properties": {
174
+ "source": {"type": "string"},
175
+ "verdict": {"type": "string"},
176
+ "summary": {"type": "string"},
177
+ "design_aspects": {
178
+ "type": "array",
179
+ "items": {"type": "string"}
180
+ },
181
+ "requirements": {
182
+ "type": "array",
183
+ "items": {"type": "string"}
184
+ },
185
+ "verification_seeds": {
186
+ "type": "array",
187
+ "items": {"type": "string"}
188
+ },
189
+ "gaps": {
190
+ "type": "array",
191
+ "items": {"type": "string"}
192
+ }
193
+ }
194
+ },
195
+ "testing_system": {
196
+ "type": "object",
197
+ "additionalProperties": true,
198
+ "properties": {
199
+ "applicability": {"type": "string"},
200
+ "applicability_reason": {"type": "string"},
201
+ "stages": {"type": "array"},
202
+ "test_levels": {"type": "object"},
203
+ "datasets": {"type": "array"},
204
+ "scenario_links": {"type": "array"},
205
+ "design_decision_coverage": {"type": "array"},
206
+ "gaps": {"type": "array"}
207
+ }
208
+ },
209
+ "aspect_map": { "$ref": "#/definitions/aspectMap" },
156
210
  "sdlc_contours": {
157
211
  "type": "object",
158
212
  "additionalProperties": true,
@@ -183,6 +237,7 @@
183
237
  "next_delivery_action": {"type": "string"}
184
238
  }
185
239
  },
240
+ "policy_context": { "$ref": "#/definitions/policyContext" },
186
241
  "links": {
187
242
  "type": "object",
188
243
  "additionalProperties": { "$ref": "#/definitions/link" },
@@ -250,6 +305,105 @@
250
305
  "degraded_reason": { "type": "string" },
251
306
  "source": { "type": "string" }
252
307
  }
308
+ },
309
+ "reviewGate": {
310
+ "type": "object",
311
+ "additionalProperties": true,
312
+ "required": ["id", "label", "stage", "applicability", "verdict", "summary"],
313
+ "properties": {
314
+ "id": { "type": "string", "minLength": 1 },
315
+ "label": { "type": "string", "minLength": 1 },
316
+ "stage": { "type": "string", "minLength": 1 },
317
+ "applicability": { "type": "string", "minLength": 1 },
318
+ "verdict": { "type": "string", "minLength": 1 },
319
+ "summary": { "type": "string", "minLength": 1 },
320
+ "evidence": {
321
+ "type": "array",
322
+ "items": { "type": "string", "minLength": 1 }
323
+ }
324
+ }
325
+ },
326
+ "aspectMap": {
327
+ "type": "object",
328
+ "additionalProperties": true,
329
+ "required": ["path", "schema_id", "status"],
330
+ "properties": {
331
+ "path": { "type": "string", "minLength": 1 },
332
+ "schema_id": { "type": "string", "minLength": 1 },
333
+ "status": { "type": "string", "minLength": 1 },
334
+ "summary": { "type": "string" },
335
+ "counts": {
336
+ "type": "object",
337
+ "additionalProperties": true,
338
+ "properties": {
339
+ "applicable": { "type": "integer", "minimum": 0 },
340
+ "not_applicable": { "type": "integer", "minimum": 0 },
341
+ "unknown": { "type": "integer", "minimum": 0 }
342
+ }
343
+ }
344
+ }
345
+ },
346
+ "policyContext": {
347
+ "type": "object",
348
+ "additionalProperties": true,
349
+ "required": ["sources", "git", "checks", "delivery", "gaps"],
350
+ "properties": {
351
+ "sources": { "type": "object", "additionalProperties": true },
352
+ "git": {
353
+ "type": "object",
354
+ "additionalProperties": true,
355
+ "properties": {
356
+ "workspace_route": { "type": "string" },
357
+ "delivery_strategy": {
358
+ "type": "string",
359
+ "enum": ["direct_commit", "direct_commit_push", "feature_merge", "pull_request", "merge_queue", "squash_merge", "rebase_ff", "release_branch", "external_handoff", "local_only", "no_git"]
360
+ },
361
+ "fixation_required": { "type": "boolean" },
362
+ "result_evidence_required": { "type": "array", "items": { "type": "string" } }
363
+ }
364
+ },
365
+ "checks": { "type": "object", "additionalProperties": true },
366
+ "delivery": { "type": "object", "additionalProperties": true },
367
+ "gaps": { "type": "object", "additionalProperties": true }
368
+ }
369
+ },
370
+ "flowFlags": {
371
+ "type": "object",
372
+ "additionalProperties": false,
373
+ "required": ["flow_kind", "snapshot_revision", "resolution_status", "values", "snapshot_checksum"],
374
+ "properties": {
375
+ "contract": { "type": "object", "additionalProperties": true },
376
+ "flow_kind": { "type": "string", "minLength": 1 },
377
+ "preset": { "type": "object", "additionalProperties": true },
378
+ "snapshot_revision": { "type": "integer", "minimum": 1 },
379
+ "resolution_status": { "type": "string", "enum": ["valid", "legacy_incomplete", "reconciliation_required"] },
380
+ "values": { "type": "object", "additionalProperties": { "$ref": "#/definitions/flagValue" } },
381
+ "snapshot_checksum": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
382
+ "floors_applied": { "type": "array", "items": { "type": "string" } },
383
+ "resolved_at": { "type": "string", "format": "date-time" }
384
+ }
385
+ },
386
+ "flagValue": {
387
+ "type": "object",
388
+ "additionalProperties": false,
389
+ "required": ["value", "value_type", "owner", "source", "rationale", "resolved_at"],
390
+ "properties": {
391
+ "value": {},
392
+ "value_type": { "type": "string", "minLength": 1 },
393
+ "owner": { "type": "string", "minLength": 1 },
394
+ "source": {
395
+ "type": "object",
396
+ "additionalProperties": false,
397
+ "required": ["kind", "ref"],
398
+ "properties": {
399
+ "kind": { "type": "string", "minLength": 1 },
400
+ "ref": { "type": "string", "minLength": 1 },
401
+ "revision": { "type": ["integer", "null"] }
402
+ }
403
+ },
404
+ "rationale": { "type": "string", "minLength": 1 },
405
+ "resolved_at": { "type": "string", "format": "date-time" }
406
+ }
253
407
  }
254
408
  }
255
409
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/release-impact@1",
4
+ "title": "Canonical release impact",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_id", "from_version", "to_version", "minimum_upgrade_mode", "domains", "path_migration", "runtime_migration", "required_checks"],
8
+ "properties": {
9
+ "schema_id": {"const": "dd-flow/release-impact@1"},
10
+ "from_version": {"type": "string"}, "to_version": {"type": "string"},
11
+ "minimum_upgrade_mode": {"enum": ["patch", "targeted_minor", "full_migration"]},
12
+ "domains": {"type": "array", "items": {"type": "string"}},
13
+ "path_migration": {"type": "boolean"}, "runtime_migration": {"type": "boolean"},
14
+ "required_checks": {"type": "array", "items": {"type": "string"}, "minItems": 1}, "notes": {"type": "string"}
15
+ }
16
+ }
@@ -11,9 +11,9 @@ export function appendAudit(context, input) {
11
11
  context.now()
12
12
  ]);
13
13
  }
14
- export function getAuditEvents(context, protocolId) {
14
+ export function getAuditEvents(context, projectId, protocolId) {
15
15
  return context.db.all(`SELECT id, protocol_id, project_id, event_type, forced, reason, payload_json, created_at
16
16
  FROM audit_events
17
- WHERE protocol_id = ?
18
- ORDER BY id ASC`, [protocolId]);
17
+ WHERE project_id = ? AND protocol_id = ?
18
+ ORDER BY id ASC`, [projectId, protocolId]);
19
19
  }
@@ -79,15 +79,27 @@ export function requireClaimableBranchBundle(context, input) {
79
79
  }
80
80
  function resolveBranchContextInput(context, input) {
81
81
  if (input.protocolId) {
82
- const protocol = context.db.get(`SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
83
- FROM protocols
84
- WHERE id = ?`, [input.protocolId]);
82
+ const scopedProject = input.projectRoot
83
+ ? requireProjectByRoot(context, resolveProjectRoot(input.projectRoot))
84
+ : null;
85
+ const matches = context.db.all(scopedProject
86
+ ? `SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
87
+ FROM protocols WHERE project_id = ? AND id = ? ORDER BY project_id`
88
+ : `SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
89
+ FROM protocols WHERE id = ? ORDER BY project_id`, scopedProject ? [scopedProject.id, input.protocolId] : [input.protocolId]);
90
+ if (matches.length > 1) {
91
+ throw new AppError("ambiguous_protocol", `Protocol id is ambiguous without project context: ${input.protocolId}`, 1, {
92
+ protocol_id: input.protocolId,
93
+ candidates: matches.map((item) => ({ project_id: item.project_id, project_root: item.project_root }))
94
+ });
95
+ }
96
+ const protocol = matches[0];
85
97
  if (!protocol) {
86
98
  throw new AppError("not_found", `Protocol is not registered: ${input.protocolId}`, 1, { protocol_id: input.protocolId });
87
99
  }
88
- const project = requireProjectByRoot(context, protocol.project_root);
100
+ const project = scopedProject ?? requireProjectByRoot(context, protocol.project_root);
89
101
  const workspace = parseRecord(protocol.workspace_json);
90
- const worktree = context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]);
102
+ const worktree = context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
91
103
  const workspacePath = normalizePath(input.workspacePath ?? stringValue(worktree?.worktree_path) ?? stringValue(workspace.worktree_path));
92
104
  const git = gitFacts(workspacePath ?? project.root);
93
105
  const featureBranch = stringValue(worktree?.feature_branch) ?? stringValue(workspace.feature_branch) ?? stringValue(git.actual_branch);
@@ -217,7 +217,6 @@ function assessLayout(root, layout, memorybankRoot) {
217
217
  "protocol/index.md",
218
218
  "dd-flow/mb-init.md",
219
219
  "dd-flow/mb-upgrade.md",
220
- "dd-flow/mb-upgrade-review.md",
221
220
  "dd-flow/mb-distill.md"
222
221
  ];
223
222
  for (const relativePath of requiredFiles) {
@@ -31,7 +31,7 @@ export function cleanupScan(context, input) {
31
31
  });
32
32
  }
33
33
  if (terminal) {
34
- const queue = context.db.get("SELECT status FROM merge_queue WHERE protocol_id = ? AND status IN ('ready', 'requeued', 'claimed')", [protocol.id]);
34
+ const queue = context.db.get("SELECT status FROM merge_queue WHERE project_id = ? AND protocol_id = ? AND status IN ('ready', 'requeued', 'claimed')", [project.id, protocol.id]);
35
35
  if (queue) {
36
36
  pushFinding(findings, actions, "active_queue_for_terminal_protocol", "warning", {
37
37
  protocol_id: protocol.id,
@@ -40,7 +40,7 @@ export function cleanupScan(context, input) {
40
40
  });
41
41
  }
42
42
  }
43
- const fullProtocol = requireProtocol(context, protocol.id);
43
+ const fullProtocol = requireProtocol(context, protocol.id, project.id);
44
44
  const state = readProtocolRuntimeState(context, fullProtocol).state;
45
45
  const diagnostics = protocolRunDiagnostics(context, fullProtocol, state).diagnostics;
46
46
  for (const diagnostic of diagnostics.filter((entry) => entry.code === "protocol_run_stage_mismatch")) {
@@ -55,7 +55,7 @@ export function cleanupScan(context, input) {
55
55
  }
56
56
  }
57
57
  const orphanQueueJobs = context.db.all(`SELECT mq.protocol_id, mq.status FROM merge_queue mq
58
- LEFT JOIN protocols p ON p.id = mq.protocol_id
58
+ LEFT JOIN protocols p ON p.project_id = mq.project_id AND p.id = mq.protocol_id
59
59
  WHERE mq.project_id = ?
60
60
  AND mq.status IN ('ready', 'requeued')
61
61
  AND p.id IS NULL
@@ -72,7 +72,7 @@ export function cleanupScan(context, input) {
72
72
  ORDER BY updated_at ASC, session_id ASC`, [project.id]);
73
73
  for (const session of staleSessions) {
74
74
  const protocol = session.protocol_id
75
- ? context.db.get("SELECT status, stage FROM protocols WHERE id = ?", [session.protocol_id])
75
+ ? context.db.get("SELECT status, stage FROM protocols WHERE project_id = ? AND id = ?", [project.id, session.protocol_id])
76
76
  : undefined;
77
77
  const missingWorkspace = !fs.existsSync(session.workspace_path);
78
78
  const terminalProtocol = Boolean(protocol && (["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage)));
@@ -112,7 +112,7 @@ export function cleanupScan(context, input) {
112
112
  });
113
113
  }
114
114
  const staleWorktrees = context.db.all(`SELECT wr.protocol_id, wr.worktree_path FROM worktree_records wr
115
- JOIN protocols p ON p.id = wr.protocol_id
115
+ JOIN protocols p ON p.project_id = wr.project_id AND p.id = wr.protocol_id
116
116
  WHERE wr.project_id = ?
117
117
  AND wr.status = 'active'
118
118
  AND (p.status IN ('closed', 'cancelled') OR p.stage IN ('closed', 'cancelled') OR wr.worktree_path = '')
@@ -242,7 +242,7 @@ function validateNoDestructiveSurprises(context, plan, force) {
242
242
  }
243
243
  function applyAction(context, action, reason, force, now) {
244
244
  if (action.kind === "repair_protocol_state" && action.protocol_id) {
245
- const protocol = requireProtocol(context, action.protocol_id);
245
+ const protocol = requireProtocol(context, action.protocol_id, action.project_id);
246
246
  const beforePath = protocol.state_path;
247
247
  const existed = fs.existsSync(beforePath);
248
248
  readProtocolRuntimeState(context, protocol);
@@ -69,7 +69,7 @@ function isReadOnlyDiagnostic(family, command, action) {
69
69
  if (family === "project")
70
70
  return command === "status" || command === "resolve";
71
71
  if (family === "protocol")
72
- return ["status", "ready", "blockers", "implement"].includes(command ?? "");
72
+ return ["status", "branch-status", "ready", "blockers", "implement"].includes(command ?? "");
73
73
  if (family === "plan")
74
74
  return command === "status";
75
75
  if (family === "run")
@@ -1,14 +1,13 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { getCliBuildInfo } from "./build-info.js";
3
3
  import { classifyCliOperation } from "./cli-operation-classifier.js";
4
- import { selectEngine } from "./engines.js";
5
- import { requireProtocol } from "./protocols.js";
6
- export function preflightCliCompatibility(context, args, env = process.env) {
4
+ import { resolveOperationProjectRoot, selectEngine } from "./engines.js";
5
+ export function preflightCliCompatibility(context, args, env = process.env, resolvedProjectRoot) {
7
6
  const classification = classifyCliOperation(args, env);
8
7
  if (classification.mode === "router_native") {
9
8
  return { ok: true, classification, compatibility: null };
10
9
  }
11
- const projectRoot = resolveOperationProjectRoot(context, args);
10
+ const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
12
11
  if (!projectRoot) {
13
12
  return { ok: true, classification, compatibility: null };
14
13
  }
@@ -54,74 +53,3 @@ function remediationForSelection(selection) {
54
53
  diagnostics: selection.diagnostics
55
54
  };
56
55
  }
57
- function resolveOperationProjectRoot(context, args) {
58
- const [family, command, ...rest] = args;
59
- const parsed = parseLightArgs(rest);
60
- const explicitRoot = option(parsed, "project-root") ?? option(parsed, "root");
61
- if (explicitRoot)
62
- return explicitRoot;
63
- if (family === "protocol") {
64
- const protocolId = positional(parsed, 0);
65
- if (protocolId && command && ["ready-for-merge", "cancel", "status"].includes(command)) {
66
- return requireProtocol(context, protocolId).project_root;
67
- }
68
- }
69
- if (family === "transition") {
70
- const transitionParsed = parseLightArgs([command ?? "", ...rest]);
71
- const protocolId = positional(transitionParsed, 0);
72
- return protocolId ? requireProtocol(context, protocolId).project_root : null;
73
- }
74
- if (family === "plan" && (command === "set" || command === "status")) {
75
- const protocolId = positional(parsed, 0);
76
- return protocolId ? requireProtocol(context, protocolId).project_root : null;
77
- }
78
- if (family === "plan" && command === "item") {
79
- const protocolId = positional(parsed, 1);
80
- return protocolId ? requireProtocol(context, protocolId).project_root : null;
81
- }
82
- if (family === "merge-queue" && ["complete", "fail", "cancel", "note"].includes(command ?? "")) {
83
- const protocolId = positional(parsed, 0);
84
- return protocolId ? requireProtocol(context, protocolId).project_root : null;
85
- }
86
- if (family === "worktree") {
87
- const protocolId = option(parsed, "protocol-id");
88
- return protocolId ? requireProtocol(context, protocolId).project_root : null;
89
- }
90
- return null;
91
- }
92
- function parseLightArgs(args) {
93
- const positional = [];
94
- const options = new Map();
95
- for (let index = 0; index < args.length; index += 1) {
96
- const value = args[index];
97
- if (value?.startsWith("--")) {
98
- const equal = value.indexOf("=");
99
- if (equal > 2) {
100
- const key = value.slice(2, equal);
101
- options.set(key, [...(options.get(key) ?? []), value.slice(equal + 1)]);
102
- continue;
103
- }
104
- const key = value.slice(2);
105
- const next = args[index + 1];
106
- if (!next || next.startsWith("--")) {
107
- options.set(key, [...(options.get(key) ?? []), ""]);
108
- }
109
- else {
110
- options.set(key, [...(options.get(key) ?? []), next]);
111
- index += 1;
112
- }
113
- }
114
- else if (value) {
115
- positional.push(value);
116
- }
117
- }
118
- return { positional, options };
119
- }
120
- function option(parsed, key) {
121
- const values = parsed.options.get(key);
122
- const value = values?.[values.length - 1];
123
- return value ? value : null;
124
- }
125
- function positional(parsed, index) {
126
- return parsed.positional[index] ?? null;
127
- }
@@ -10,6 +10,7 @@ import { publishProjectSummary } from "./project-summary.js";
10
10
  import { requireProjectByRoot } from "./projects.js";
11
11
  import { protocolRunDiagnostics, protocolSetBoardForProtocol, protocolSetBoardsForProject, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
12
12
  import { activeFlowSessionsForProject } from "./sessions.js";
13
+ import { usageForRun } from "./usage.js";
13
14
  import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
14
15
  export function getCmuxStatus(context, input) {
15
16
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -617,7 +618,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
617
618
  if (!protocol) {
618
619
  throw new AppError("not_found", `Protocol is not registered: ${protocolId}`, 1, { protocol_id: protocolId });
619
620
  }
620
- const runtimeProtocol = requireProtocol(context, protocol.id);
621
+ const runtimeProtocol = requireProtocol(context, protocol.id, project.id);
621
622
  const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
622
623
  const lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
623
624
  const queueItem = queueForProject(context, project.id).find((item) => item.protocol_id === protocolId) ?? null;
@@ -626,6 +627,9 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
626
627
  FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
627
628
  ORDER BY updated_at DESC, id DESC`, [project.id, protocolId]);
628
629
  const primaryRun = runs.find((run) => run.status === "running") ?? runs[0];
630
+ const observability = primaryRun
631
+ ? usageForRun(context, { projectId: project.id, runId: primaryRun.id, groupBy: "role" })
632
+ : { status: "not_observable", reason: "no_protocol_run" };
629
633
  const warnings = [...runDiagnostics.diagnostics];
630
634
  const protocolSetBoard = protocolSetBoardForProtocol(context, project.id, project.root, protocolId);
631
635
  const reviewRuns = recentReviewRunsForProtocol(context, project.id, protocolId, 8);
@@ -643,6 +647,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
643
647
  run_index_path: run.run_index_path,
644
648
  updated_at: run.updated_at,
645
649
  completed_at: run.completed_at,
650
+ flow_flags: flowFlagsProjection(index),
646
651
  stages: Array.isArray(index?.stage_runs) ? index.stage_runs.map((stage) => stageLink(project.root, run, stage)) : []
647
652
  };
648
653
  });
@@ -696,6 +701,7 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
696
701
  },
697
702
  primary_run_id: primaryRun?.id ?? null,
698
703
  primary_run_reason: primaryRun ? (primaryRun.status === "running" ? "latest running run" : "latest updated run") : "no runs registered",
704
+ observability,
699
705
  protocol_set_board: protocolSetBoard,
700
706
  review_runs: reviewRuns,
701
707
  run_history: runHistory,
@@ -730,7 +736,7 @@ function renderProjectDashboardMarkdown(context, project, output) {
730
736
  const config = readProjectConfig(context, project.id);
731
737
  const globalOutput = globalDashboardMarkdownPath(context, config);
732
738
  const latestProtocol = protocols.find((protocol) => isActiveProtocolStatus(protocol.status)) ?? protocols[0];
733
- const latestProtocolSummary = latestProtocol ? planSummaryForProtocol(context, latestProtocol.id) : null;
739
+ const latestProtocolSummary = latestProtocol ? planSummaryForProtocol(context, project.id, latestProtocol.id) : null;
734
740
  const lines = [
735
741
  "# 🧭 dd-flow Dashboard",
736
742
  "",
@@ -750,14 +756,14 @@ function renderProjectDashboardMarkdown(context, project, output) {
750
756
  if (activeProtocols.length || sessions.length) {
751
757
  lines.push("| --- | --- | --- | --- |");
752
758
  for (const protocol of activeProtocols.slice(0, 5)) {
753
- const summary = planSummaryForProtocol(context, protocol.id);
759
+ const summary = planSummaryForProtocol(context, project.id, protocol.id);
754
760
  lines.push(`| ${statusIcon(protocol.status)} ${cell(shortId(protocol.id))} | ${cell(`${protocol.stage}/${protocol.status} · ${summary.done}/${summary.total}`)} | ${cell(protocol.next_action ?? "none")} | ${cell(workspaceForProtocol(worktrees, protocol.id) ?? "-")} |`);
755
761
  }
756
762
  for (const session of sessions.slice(0, 6)) {
757
763
  lines.push(`| ${statusIcon(session.status)} ${cell(session.flow_kind)} | ${cell(session.worker_id ?? shortId(session.session_id))} | ${cell(session.next_action ?? "none")} | ${cell(compactPath(session.workspace_path))} |`);
758
764
  }
759
765
  }
760
- const activeItems = activePlanItems(context, activeProtocols);
766
+ const activeItems = activePlanItems(context, project.id, activeProtocols);
761
767
  lines.push("", "## ✅ Plan", activeItems.length ? "| protocol | item | status | summary |" : "No active plan items.");
762
768
  if (activeItems.length) {
763
769
  lines.push("| --- | --- | --- | --- |");
@@ -793,7 +799,7 @@ function renderProjectDashboardMarkdown(context, project, output) {
793
799
  if (recentProtocols.length) {
794
800
  lines.push("| --- | --- | --- | --- |");
795
801
  for (const protocol of recentProtocols) {
796
- const summary = planSummaryForProtocol(context, protocol.id);
802
+ const summary = planSummaryForProtocol(context, project.id, protocol.id);
797
803
  lines.push(`| ${cell(shortId(protocol.id))} | ${statusIcon(protocol.status)} ${cell(protocol.stage)}/${cell(protocol.status)} | ${cell(`${summary.done}/${summary.total}`)} | ${cell(protocol.updated_at)} |`);
798
804
  }
799
805
  }
@@ -874,8 +880,8 @@ function protocolsForProject(context, projectId) {
874
880
  return context.db.all(`SELECT id, status, stage, next_action, blockers_json, active_def_json, updated_at
875
881
  FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
876
882
  }
877
- function planSummaryForProtocol(context, protocolId) {
878
- const row = context.db.get("SELECT plan_json FROM plans WHERE protocol_id = ?", [protocolId]);
883
+ function planSummaryForProtocol(context, projectId, protocolId) {
884
+ const row = context.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
879
885
  if (!row) {
880
886
  return { plan_id: "none", total: 0, done: 0, blocked: 0 };
881
887
  }
@@ -888,10 +894,10 @@ function planSummaryForProtocol(context, protocolId) {
888
894
  blocked: items.filter((item) => item.status === "blocked").length
889
895
  };
890
896
  }
891
- function activePlanItems(context, protocols) {
897
+ function activePlanItems(context, projectId, protocols) {
892
898
  const items = [];
893
899
  for (const protocol of protocols) {
894
- const row = context.db.get("SELECT plan_json FROM plans WHERE protocol_id = ?", [protocol.id]);
900
+ const row = context.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [projectId, protocol.id]);
895
901
  if (!row) {
896
902
  continue;
897
903
  }
@@ -1031,7 +1037,7 @@ function link(label, href, status, reason) {
1031
1037
  };
1032
1038
  }
1033
1039
  function buildProtocolCard(context, project, protocol, generatePage) {
1034
- const summary = planSummaryForProtocol(context, protocol.id);
1040
+ const summary = planSummaryForProtocol(context, project.id, protocol.id);
1035
1041
  const activeDefCount = jsonArray(protocol.active_def_json).length;
1036
1042
  const blockerCount = jsonArray(protocol.blockers_json).length;
1037
1043
  const latestRun = context.db.get(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
@@ -1040,7 +1046,7 @@ function buildProtocolCard(context, project, protocol, generatePage) {
1040
1046
  let diagnostics = [];
1041
1047
  let lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
1042
1048
  try {
1043
- const runtimeProtocol = requireProtocol(context, protocol.id);
1049
+ const runtimeProtocol = requireProtocol(context, protocol.id, project.id);
1044
1050
  const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
1045
1051
  lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
1046
1052
  diagnostics = [...protocolRunDiagnostics(context, runtimeProtocol, runtimeState).diagnostics, ...lifecycle.diagnostics];
@@ -1116,11 +1122,42 @@ function reviewRunSummary(run) {
1116
1122
  verdict: run.verdict,
1117
1123
  next_action: run.next_action,
1118
1124
  run_index_path: run.run_index_path,
1125
+ flow_flags: flowFlagsProjection(index),
1119
1126
  report: reviewStage ? stageLink("", run, reviewStage).report : link("Run index", run.run_index_path, fs.existsSync(run.run_index_path) ? "available" : "missing"),
1120
1127
  updated_at: run.updated_at,
1121
1128
  completed_at: run.completed_at
1122
1129
  };
1123
1130
  }
1131
+ function flowFlagsProjection(index) {
1132
+ const flags = index?.flow_flags;
1133
+ if (!flags || typeof flags !== "object" || Array.isArray(flags))
1134
+ return null;
1135
+ const object = flags;
1136
+ const values = object.values;
1137
+ const projectedValues = {};
1138
+ if (values && typeof values === "object" && !Array.isArray(values)) {
1139
+ for (const [key, raw] of Object.entries(values)) {
1140
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1141
+ continue;
1142
+ const value = raw;
1143
+ const source = value.source;
1144
+ projectedValues[key] = {
1145
+ value: value.value ?? null,
1146
+ source: source && typeof source === "object" && !Array.isArray(source)
1147
+ ? { kind: source.kind ?? null, ref: source.ref ?? null }
1148
+ : null
1149
+ };
1150
+ }
1151
+ }
1152
+ return {
1153
+ snapshot_revision: object.snapshot_revision ?? null,
1154
+ snapshot_checksum: object.snapshot_checksum ?? null,
1155
+ resolution_status: object.resolution_status ?? "legacy_incomplete",
1156
+ preset: object.preset ?? null,
1157
+ floors_applied: Array.isArray(object.floors_applied) ? object.floors_applied : [],
1158
+ values: projectedValues
1159
+ };
1160
+ }
1124
1161
  function safeParseIndex(text) {
1125
1162
  try {
1126
1163
  const parsed = JSON.parse(text);