@deksden-com/dd-flow-cli 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +25 -9
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +58 -32
- package/dist/cli/run-cli.js +282 -50
- 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 +266 -0
- 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 +8 -77
- package/dist/services/dashboard.js +48 -11
- package/dist/services/engines.js +123 -13
- package/dist/services/hooks.js +6 -6
- package/dist/services/ids.js +40 -49
- package/dist/services/merge-queue.js +240 -20
- package/dist/services/merge-worker.js +12 -4
- 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 +78 -42
- 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
|
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
8
|
+
export function getProtocolBranchContext(context, input) {
|
|
9
|
+
const resolved = resolveBranchContextInput(context, input);
|
|
10
|
+
const rows = context.db.all(`SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
|
|
11
|
+
FROM protocols
|
|
12
|
+
WHERE project_id = ?
|
|
13
|
+
ORDER BY updated_at ASC, id ASC`, [resolved.project.id]);
|
|
14
|
+
const queues = new Map(context.db
|
|
15
|
+
.all(`SELECT protocol_id, status, claimed_by_session_id, claimed_at, attempts_count, updated_at
|
|
16
|
+
FROM merge_queue
|
|
17
|
+
WHERE project_id = ?`, [resolved.project.id])
|
|
18
|
+
.map((row) => [row.protocol_id, row]));
|
|
19
|
+
const worktrees = new Map(context.db
|
|
20
|
+
.all(`SELECT protocol_id, integration_branch, feature_branch, base_ref, worktree_path, status
|
|
21
|
+
FROM worktree_records
|
|
22
|
+
WHERE project_id = ?`, [resolved.project.id])
|
|
23
|
+
.map((row) => [row.protocol_id, row]));
|
|
24
|
+
const sessions = context.db.all(`SELECT protocol_id, worker_id, flow_kind, status, current_stage, next_action
|
|
25
|
+
FROM flow_sessions
|
|
26
|
+
WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [resolved.project.id]);
|
|
27
|
+
const sessionsByProtocol = new Map();
|
|
28
|
+
for (const session of sessions) {
|
|
29
|
+
if (!session.protocol_id)
|
|
30
|
+
continue;
|
|
31
|
+
sessionsByProtocol.set(session.protocol_id, [...(sessionsByProtocol.get(session.protocol_id) ?? []), session]);
|
|
32
|
+
}
|
|
33
|
+
const candidates = rows.map((row) => summarizeProtocol(row, queues.get(row.id), worktrees.get(row.id), sessionsByProtocol.get(row.id) ?? []));
|
|
34
|
+
const protocols = candidates.filter((candidate) => protocolMatchesBranch(candidate, resolved));
|
|
35
|
+
const eligible = protocols.filter((item) => item.bundle_eligible).map((item) => item.id);
|
|
36
|
+
const notReady = protocols.filter((item) => !item.bundle_eligible && !terminalStage(item.stage) && item.queue_status !== "claimed").map((item) => item.id);
|
|
37
|
+
const claimed = protocols.filter((item) => item.queue_status === "claimed").map((item) => item.id);
|
|
38
|
+
const blocked = protocols.filter((item) => item.stage === "blocked" || item.bundle_blockers.length > 0).map((item) => item.id);
|
|
39
|
+
const diagnostics = branchDiagnostics(resolved);
|
|
40
|
+
const diagnosticBlockers = diagnostics.filter((item) => item.severity === "error").map((item) => String(item.code));
|
|
41
|
+
const claimable = eligible.length > 0 && notReady.length === 0 && claimed.length === 0 && blocked.length === 0 && diagnosticBlockers.length === 0;
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
branch_context: {
|
|
45
|
+
current_protocol_id: resolved.protocol?.id ?? null,
|
|
46
|
+
project_id: resolved.project.id,
|
|
47
|
+
project_root: resolved.project.root,
|
|
48
|
+
integration_branch: resolved.integrationBranch,
|
|
49
|
+
feature_branch: resolved.featureBranch,
|
|
50
|
+
worktree_path: resolved.workspacePath,
|
|
51
|
+
grouping_key: resolved.groupingKey,
|
|
52
|
+
git: resolved.git,
|
|
53
|
+
diagnostics,
|
|
54
|
+
protocols,
|
|
55
|
+
merge_bundle: {
|
|
56
|
+
eligible_protocols: eligible,
|
|
57
|
+
not_ready_protocols: notReady,
|
|
58
|
+
claimed_protocols: claimed,
|
|
59
|
+
blocked_protocols: blocked,
|
|
60
|
+
claimable,
|
|
61
|
+
reason: claimable ? "claimable" : bundleReason(eligible, notReady, claimed, blocked, diagnosticBlockers)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function requireClaimableBranchBundle(context, input) {
|
|
67
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
68
|
+
const contextBody = branchContext.branch_context;
|
|
69
|
+
const bundle = contextBody.merge_bundle;
|
|
70
|
+
if (bundle.claimable !== true) {
|
|
71
|
+
throw new AppError("merge_bundle_not_claimable", "Branch bundle is not claimable", 1, { branch_context: contextBody });
|
|
72
|
+
}
|
|
73
|
+
const protocolIds = Array.isArray(bundle.eligible_protocols) ? bundle.eligible_protocols.map(String) : [];
|
|
74
|
+
if (protocolIds.length === 0) {
|
|
75
|
+
throw new AppError("merge_bundle_empty", "Branch bundle has no ready protocols", 1, { branch_context: contextBody });
|
|
76
|
+
}
|
|
77
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
78
|
+
return { project, branchContext, protocolIds };
|
|
79
|
+
}
|
|
80
|
+
function resolveBranchContextInput(context, input) {
|
|
81
|
+
if (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];
|
|
97
|
+
if (!protocol) {
|
|
98
|
+
throw new AppError("not_found", `Protocol is not registered: ${input.protocolId}`, 1, { protocol_id: input.protocolId });
|
|
99
|
+
}
|
|
100
|
+
const project = scopedProject ?? requireProjectByRoot(context, protocol.project_root);
|
|
101
|
+
const workspace = parseRecord(protocol.workspace_json);
|
|
102
|
+
const worktree = context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
103
|
+
const workspacePath = normalizePath(input.workspacePath ?? stringValue(worktree?.worktree_path) ?? stringValue(workspace.worktree_path));
|
|
104
|
+
const git = gitFacts(workspacePath ?? project.root);
|
|
105
|
+
const featureBranch = stringValue(worktree?.feature_branch) ?? stringValue(workspace.feature_branch) ?? stringValue(git.actual_branch);
|
|
106
|
+
return {
|
|
107
|
+
project,
|
|
108
|
+
protocol,
|
|
109
|
+
featureBranch,
|
|
110
|
+
workspacePath,
|
|
111
|
+
integrationBranch: stringValue(worktree?.integration_branch) ?? stringValue(workspace.integration_branch),
|
|
112
|
+
groupingKey: groupingKey(project.id, featureBranch, workspacePath),
|
|
113
|
+
git: { ...git, matches_runtime: branchMatches(featureBranch, stringValue(git.actual_branch)) }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (!input.projectRoot) {
|
|
117
|
+
throw new AppError("usage", "branch context requires <protocol-id> or --project-root", 2);
|
|
118
|
+
}
|
|
119
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
120
|
+
const workspacePath = normalizePath(input.workspacePath ?? project.root);
|
|
121
|
+
const git = gitFacts(workspacePath ?? project.root);
|
|
122
|
+
const featureBranch = stringValue(git.actual_branch);
|
|
123
|
+
return {
|
|
124
|
+
project,
|
|
125
|
+
protocol: null,
|
|
126
|
+
featureBranch,
|
|
127
|
+
workspacePath,
|
|
128
|
+
integrationBranch: null,
|
|
129
|
+
groupingKey: groupingKey(project.id, featureBranch, workspacePath),
|
|
130
|
+
git: { ...git, matches_runtime: true }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function summarizeProtocol(row, queue, worktree, activeSessions) {
|
|
134
|
+
const workspace = parseRecord(row.workspace_json);
|
|
135
|
+
const featureBranch = stringValue(worktree?.feature_branch) ?? stringValue(workspace.feature_branch);
|
|
136
|
+
const worktreePath = normalizePath(stringValue(worktree?.worktree_path) ?? stringValue(workspace.worktree_path));
|
|
137
|
+
const activeDef = parseJsonArray(row.active_def_json);
|
|
138
|
+
const queueStatus = queue?.status ?? null;
|
|
139
|
+
const bundleBlockers = bundleBlockersFor(row.stage, queueStatus, activeDef.length);
|
|
140
|
+
return {
|
|
141
|
+
id: row.id,
|
|
142
|
+
stage: row.stage,
|
|
143
|
+
status: row.status,
|
|
144
|
+
lifecycle: normalizeProtocolLifecycle({ rawStage: row.stage, rawStatus: row.status, queueStatus }),
|
|
145
|
+
queue_status: queueStatus,
|
|
146
|
+
queue_claimed_by: queue?.claimed_by_session_id ?? null,
|
|
147
|
+
active_sessions: activeSessions,
|
|
148
|
+
integration_branch: stringValue(worktree?.integration_branch) ?? stringValue(workspace.integration_branch),
|
|
149
|
+
feature_branch: featureBranch,
|
|
150
|
+
worktree_path: worktreePath,
|
|
151
|
+
bundle_eligible: ["ready_for_merge", "queued_for_merge"].includes(row.stage) && ["ready", "requeued"].includes(queueStatus ?? "") && activeDef.length === 0,
|
|
152
|
+
bundle_blockers: bundleBlockers,
|
|
153
|
+
updated_at: row.updated_at
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function protocolMatchesBranch(item, resolved) {
|
|
157
|
+
if (resolved.featureBranch && item.feature_branch) {
|
|
158
|
+
return item.feature_branch === resolved.featureBranch;
|
|
159
|
+
}
|
|
160
|
+
if (resolved.workspacePath && item.worktree_path) {
|
|
161
|
+
return item.worktree_path === resolved.workspacePath;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
function gitFacts(workspacePath) {
|
|
166
|
+
if (!fs.existsSync(workspacePath)) {
|
|
167
|
+
return { status: "missing_workspace", actual_branch: null, dirty: null, path: workspacePath };
|
|
168
|
+
}
|
|
169
|
+
const branch = spawnSync("git", ["-C", workspacePath, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
170
|
+
if (branch.status !== 0) {
|
|
171
|
+
return { status: "not_git_checkout", actual_branch: null, dirty: null, path: workspacePath };
|
|
172
|
+
}
|
|
173
|
+
const status = spawnSync("git", ["-C", workspacePath, "status", "--porcelain"], { encoding: "utf8" });
|
|
174
|
+
return {
|
|
175
|
+
status: "ok",
|
|
176
|
+
actual_branch: branch.stdout.trim() || null,
|
|
177
|
+
dirty: status.status === 0 ? status.stdout.trim().length > 0 : null,
|
|
178
|
+
path: workspacePath
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function branchDiagnostics(resolved) {
|
|
182
|
+
const diagnostics = [];
|
|
183
|
+
if (resolved.git.status !== "ok") {
|
|
184
|
+
diagnostics.push({ code: "git_workspace_degraded", severity: "warning", summary: `Git workspace status is ${String(resolved.git.status)}` });
|
|
185
|
+
}
|
|
186
|
+
if (resolved.featureBranch && resolved.git.actual_branch && resolved.featureBranch !== resolved.git.actual_branch) {
|
|
187
|
+
diagnostics.push({
|
|
188
|
+
code: "branch_runtime_mismatch",
|
|
189
|
+
severity: "error",
|
|
190
|
+
summary: `Runtime branch ${resolved.featureBranch} does not match Git branch ${String(resolved.git.actual_branch)}.`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return diagnostics;
|
|
194
|
+
}
|
|
195
|
+
function bundleBlockersFor(stage, queueStatus, activeDefCount) {
|
|
196
|
+
const blockers = [];
|
|
197
|
+
if (activeDefCount > 0)
|
|
198
|
+
blockers.push("active_def");
|
|
199
|
+
if (stage === "blocked")
|
|
200
|
+
blockers.push("protocol_blocked");
|
|
201
|
+
if (queueStatus === "claimed")
|
|
202
|
+
blockers.push("already_claimed");
|
|
203
|
+
return blockers;
|
|
204
|
+
}
|
|
205
|
+
function bundleReason(eligible, notReady, claimed, blocked, diagnostics) {
|
|
206
|
+
if (diagnostics.length > 0)
|
|
207
|
+
return diagnostics[0] ?? "diagnostic_blocker";
|
|
208
|
+
if (claimed.length > 0)
|
|
209
|
+
return "branch_has_claimed_protocols";
|
|
210
|
+
if (blocked.length > 0)
|
|
211
|
+
return "branch_has_blocked_protocols";
|
|
212
|
+
if (notReady.length > 0)
|
|
213
|
+
return "branch_has_not_ready_protocols";
|
|
214
|
+
if (eligible.length === 0)
|
|
215
|
+
return "no_ready_protocols";
|
|
216
|
+
return "unknown";
|
|
217
|
+
}
|
|
218
|
+
function terminalStage(stage) {
|
|
219
|
+
return ["closed", "cancelled"].includes(stage);
|
|
220
|
+
}
|
|
221
|
+
function groupingKey(projectId, featureBranch, workspacePath) {
|
|
222
|
+
if (featureBranch)
|
|
223
|
+
return `${projectId}:branch:${featureBranch}`;
|
|
224
|
+
if (workspacePath)
|
|
225
|
+
return `${projectId}:worktree:${workspacePath}`;
|
|
226
|
+
return `${projectId}:unknown`;
|
|
227
|
+
}
|
|
228
|
+
function branchMatches(expected, actual) {
|
|
229
|
+
if (!expected || !actual)
|
|
230
|
+
return null;
|
|
231
|
+
return expected === actual;
|
|
232
|
+
}
|
|
233
|
+
function normalizePath(value) {
|
|
234
|
+
if (!value)
|
|
235
|
+
return null;
|
|
236
|
+
const absolute = path.resolve(value);
|
|
237
|
+
if (fs.existsSync(absolute))
|
|
238
|
+
return fs.realpathSync(absolute);
|
|
239
|
+
const parent = path.dirname(absolute);
|
|
240
|
+
if (fs.existsSync(parent))
|
|
241
|
+
return path.join(fs.realpathSync(parent), path.basename(absolute));
|
|
242
|
+
return absolute;
|
|
243
|
+
}
|
|
244
|
+
function parseRecord(value) {
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(value);
|
|
247
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function parseJsonArray(value) {
|
|
254
|
+
if (typeof value !== "string")
|
|
255
|
+
return [];
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(value);
|
|
258
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function stringValue(value) {
|
|
265
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
266
|
+
}
|
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")
|