@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.
- package/CHANGELOG.md +22 -0
- package/README.md +25 -9
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +45 -29
- package/dist/cli/run-cli.js +229 -49
- package/dist/domain/entity-ids.js +4 -4
- package/dist/domain/flow-contract.js +502 -36
- package/dist/domain/validation.js +34 -0
- package/dist/protocol/local-files.js +8 -6
- package/dist/schemas/code-stage-report.schema.json +197 -2
- package/dist/schemas/flow-contract.schema.json +126 -0
- package/dist/schemas/flow-run-index-v3.schema.json +203 -0
- package/dist/schemas/flow-run-index.schema.json +22 -2
- package/dist/schemas/flow-run.schema.json +36 -0
- package/dist/schemas/merge-stage-report.schema.json +213 -2
- package/dist/schemas/plan-stage-report.schema.json +156 -2
- package/dist/schemas/release-impact.schema.json +16 -0
- package/dist/services/audit.js +3 -3
- package/dist/services/branch-context.js +17 -5
- package/dist/services/canon.js +0 -1
- package/dist/services/cleanup.js +6 -6
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/compatibility-preflight.js +3 -75
- package/dist/services/dashboard.js +48 -11
- package/dist/services/engines.js +124 -13
- package/dist/services/hooks.js +6 -6
- package/dist/services/ids.js +40 -49
- package/dist/services/merge-queue.js +33 -26
- package/dist/services/merge-worker.js +2 -1
- package/dist/services/migrations.js +64 -0
- package/dist/services/plans.js +23 -16
- package/dist/services/projects.js +2 -2
- package/dist/services/prompts.js +322 -0
- package/dist/services/protocols.js +77 -46
- package/dist/services/run-projection.js +80 -0
- package/dist/services/runs.js +360 -22
- package/dist/services/schema-validation.js +35 -12
- package/dist/services/sessions.js +81 -3
- package/dist/services/status.js +32 -1
- package/dist/services/usage.js +233 -0
- package/dist/services/worktrees.js +24 -19
- package/dist/storage/database.js +223 -9
- 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@
|
|
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
|
-
"
|
|
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
|
+
}
|
package/dist/services/audit.js
CHANGED
|
@@ -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
|
|
83
|
-
|
|
84
|
-
|
|
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);
|
package/dist/services/canon.js
CHANGED
package/dist/services/cleanup.js
CHANGED
|
@@ -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
|
-
|
|
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);
|