@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
|
@@ -32,7 +32,7 @@ export function registerProtocol(context, input) {
|
|
|
32
32
|
workspacePath,
|
|
33
33
|
now
|
|
34
34
|
});
|
|
35
|
-
const existing = findProtocol(context, protocolId);
|
|
35
|
+
const existing = findProtocol(context, protocolId, project.id);
|
|
36
36
|
const params = [
|
|
37
37
|
protocolId,
|
|
38
38
|
input.handshakeId,
|
|
@@ -55,7 +55,7 @@ export function registerProtocol(context, input) {
|
|
|
55
55
|
route_json, workspace_json, blockers_json, active_def_json, state_path, plan_path,
|
|
56
56
|
created_at, updated_at)
|
|
57
57
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
58
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
58
|
+
ON CONFLICT(project_id, id) DO UPDATE SET
|
|
59
59
|
handshake_id = excluded.handshake_id,
|
|
60
60
|
project_id = excluded.project_id,
|
|
61
61
|
project_root = excluded.project_root,
|
|
@@ -81,15 +81,16 @@ export function registerProtocol(context, input) {
|
|
|
81
81
|
runtime_dir: runtimeDir
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
-
return { ok: true, protocol: protocolStatusPayload(requireProtocol(context, protocolId)), state };
|
|
84
|
+
return { ok: true, protocol: protocolStatusPayload(requireProtocol(context, protocolId, project.id)), state };
|
|
85
85
|
}
|
|
86
86
|
export function getProtocolStatus(context, input) {
|
|
87
|
-
const
|
|
87
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
88
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
88
89
|
const runtime = readProtocolRuntimeState(context, protocol);
|
|
89
90
|
const state = runtime.state;
|
|
90
91
|
const plan = readPlanFile(protocol.plan_path);
|
|
91
92
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
92
|
-
const queue = context.db.get("SELECT * FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
93
|
+
const queue = context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
93
94
|
const lifecycle = normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null });
|
|
94
95
|
return {
|
|
95
96
|
ok: true,
|
|
@@ -100,15 +101,15 @@ export function getProtocolStatus(context, input) {
|
|
|
100
101
|
state,
|
|
101
102
|
plan: plan ? summarizePlan(plan) : state.plan,
|
|
102
103
|
merge_queue: queue,
|
|
103
|
-
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
104
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context,
|
|
104
105
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null }),
|
|
105
|
-
worktree: context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]),
|
|
106
|
+
worktree: context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]),
|
|
106
107
|
hook_status: hookStatusForProject(context, protocol.project_id),
|
|
107
108
|
codex_home_profiles: codexHomeProfilesForProject(context, protocol.project_id),
|
|
108
109
|
flow_sessions: activeFlowSessionBindingsForProject(context, protocol.project_id).filter((session) => session.protocol_id === protocol.id),
|
|
109
110
|
codex_session_bindings: activeCodexSessionBindingsForProject(context, protocol.project_id).filter((binding) => binding.protocol_id === protocol.id),
|
|
110
111
|
codex_hook_events: codexHookEventsForProject(context, protocol.project_id).filter((event) => event.protocol_id === protocol.id),
|
|
111
|
-
audit: getAuditEvents(context, protocol.id)
|
|
112
|
+
audit: getAuditEvents(context, protocol.project_id, protocol.id)
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
export function getReadyProtocols(context, input) {
|
|
@@ -128,7 +129,7 @@ export function getReadyProtocols(context, input) {
|
|
|
128
129
|
}
|
|
129
130
|
export function getProtocolBlockers(context, input) {
|
|
130
131
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
131
|
-
const document = requireProtocolDocument(project.root, input.protocolId);
|
|
132
|
+
const document = requireProtocolDocument(context, project.root, input.protocolId);
|
|
132
133
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, project.id, project.root, id));
|
|
133
134
|
return {
|
|
134
135
|
ok: true,
|
|
@@ -143,13 +144,13 @@ export function getProtocolBlockers(context, input) {
|
|
|
143
144
|
}
|
|
144
145
|
export function implementProtocol(context, input) {
|
|
145
146
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
146
|
-
const document = requireProtocolDocument(project.root, input.protocolId);
|
|
147
|
-
const protocol = findProtocol(context, document.id);
|
|
147
|
+
const document = requireProtocolDocument(context, project.root, input.protocolId);
|
|
148
|
+
const protocol = findProtocol(context, document.id, project.id);
|
|
148
149
|
const runtime = protocol ? readProtocolRuntimeState(context, protocol) : null;
|
|
149
150
|
const state = runtime?.state ?? null;
|
|
150
151
|
const terminal = protocolTerminalStatus(context, project.id, project.root, document.id, state);
|
|
151
152
|
const activeSessions = activeFlowSessionBindingsForProject(context, project.id).filter((session) => session.protocol_id === document.id);
|
|
152
|
-
const queue = context.db.get("SELECT status, claimed_by_session_id FROM merge_queue WHERE protocol_id = ?", [document.id]);
|
|
153
|
+
const queue = context.db.get("SELECT status, claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [project.id, document.id]);
|
|
153
154
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, project.id, project.root, id));
|
|
154
155
|
const unresolved = blockers.filter((blocker) => !blocker.resolved);
|
|
155
156
|
const diagnostics = [
|
|
@@ -205,7 +206,7 @@ export function implementProtocol(context, input) {
|
|
|
205
206
|
blockers,
|
|
206
207
|
active_sessions: activeSessions,
|
|
207
208
|
merge_queue: queue ?? null,
|
|
208
|
-
branch_context: protocol ? getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context : null,
|
|
209
|
+
branch_context: protocol ? getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context : null,
|
|
209
210
|
lifecycle,
|
|
210
211
|
terminal,
|
|
211
212
|
related_context: document.related_context,
|
|
@@ -242,7 +243,8 @@ export function implementProtocol(context, input) {
|
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
export function transitionProtocol(context, input) {
|
|
245
|
-
const
|
|
246
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
247
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
246
248
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
247
249
|
const flowContract = flowContractForState(state);
|
|
248
250
|
const from = requireStage(state.stage, flowContract);
|
|
@@ -276,10 +278,11 @@ export function transitionProtocol(context, input) {
|
|
|
276
278
|
payload: { from, to, payload, flow_contract_id: flowContract.id, flow_contract_version: flowContract.version }
|
|
277
279
|
});
|
|
278
280
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, nextState);
|
|
279
|
-
const queue = context.db.get("SELECT * FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
281
|
+
const queue = context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
280
282
|
return {
|
|
281
283
|
ok: true,
|
|
282
284
|
protocol_id: protocol.id,
|
|
285
|
+
project_root: project.root,
|
|
283
286
|
from,
|
|
284
287
|
to,
|
|
285
288
|
forced: input.force,
|
|
@@ -289,13 +292,15 @@ export function transitionProtocol(context, input) {
|
|
|
289
292
|
};
|
|
290
293
|
}
|
|
291
294
|
export function readyForMerge(context, input) {
|
|
292
|
-
const
|
|
295
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
296
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
293
297
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
294
298
|
const flowContract = flowContractForState(state);
|
|
295
299
|
const stage = requireStage(state.stage, flowContract);
|
|
296
300
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
297
|
-
const
|
|
298
|
-
|
|
301
|
+
const branchContext = getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context;
|
|
302
|
+
const existingQueueJob = context.db.get("SELECT status FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [
|
|
303
|
+
protocol.project_id, protocol.id
|
|
299
304
|
]);
|
|
300
305
|
if (existingQueueJob && ["claimed", "merged", "cancelled", "failed"].includes(existingQueueJob.status)) {
|
|
301
306
|
appendAudit(context, {
|
|
@@ -307,9 +312,10 @@ export function readyForMerge(context, input) {
|
|
|
307
312
|
return {
|
|
308
313
|
ok: true,
|
|
309
314
|
protocol_id: protocol.id,
|
|
315
|
+
project_root: project.root,
|
|
310
316
|
queue_status: existingQueueJob.status,
|
|
311
317
|
state,
|
|
312
|
-
branch_context:
|
|
318
|
+
branch_context: branchContext,
|
|
313
319
|
lifecycle: normalizeProtocolLifecycle({ state, queueStatus: existingQueueJob.status }),
|
|
314
320
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: existingQueueJob.status })
|
|
315
321
|
};
|
|
@@ -345,7 +351,7 @@ export function readyForMerge(context, input) {
|
|
|
345
351
|
context.db.run(`INSERT INTO merge_queue
|
|
346
352
|
(protocol_id, project_id, status, claimed_by_session_id, claimed_at, completed_at, created_at, updated_at)
|
|
347
353
|
VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
348
|
-
ON CONFLICT(protocol_id) DO UPDATE SET
|
|
354
|
+
ON CONFLICT(project_id, protocol_id) DO UPDATE SET
|
|
349
355
|
status = excluded.status,
|
|
350
356
|
claimed_by_session_id = NULL,
|
|
351
357
|
claimed_at = NULL,
|
|
@@ -360,15 +366,17 @@ export function readyForMerge(context, input) {
|
|
|
360
366
|
return {
|
|
361
367
|
ok: true,
|
|
362
368
|
protocol_id: protocol.id,
|
|
369
|
+
project_root: project.root,
|
|
363
370
|
queue_status: "ready",
|
|
364
371
|
state: nextState,
|
|
365
|
-
branch_context:
|
|
372
|
+
branch_context: branchContext,
|
|
366
373
|
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: "ready" }),
|
|
367
374
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
368
375
|
};
|
|
369
376
|
}
|
|
370
377
|
export function syncProtocolFromRun(context, input) {
|
|
371
|
-
const
|
|
378
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
379
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
372
380
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
373
381
|
const run = resolveLinkedRun(context, protocol, input.runId);
|
|
374
382
|
if (run.subject_type !== "protocol" || run.subject_id !== protocol.id) {
|
|
@@ -405,7 +413,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
405
413
|
context.db.run(`INSERT INTO merge_queue
|
|
406
414
|
(protocol_id, project_id, status, claimed_by_session_id, claimed_at, completed_at, created_at, updated_at)
|
|
407
415
|
VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
408
|
-
ON CONFLICT(protocol_id) DO UPDATE SET
|
|
416
|
+
ON CONFLICT(project_id, protocol_id) DO UPDATE SET
|
|
409
417
|
status = CASE WHEN merge_queue.status IN ('merged', 'cancelled', 'failed') THEN merge_queue.status ELSE excluded.status END,
|
|
410
418
|
updated_at = excluded.updated_at`, [protocol.id, protocol.project_id, "ready", now, now]);
|
|
411
419
|
}
|
|
@@ -415,7 +423,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
415
423
|
last_reason = COALESCE(last_reason, ?),
|
|
416
424
|
completed_at = COALESCE(completed_at, ?),
|
|
417
425
|
updated_at = ?
|
|
418
|
-
WHERE protocol_id = ?`, [`synced from ${run.id}`, now, now, protocol.id]);
|
|
426
|
+
WHERE project_id = ? AND protocol_id = ?`, [`synced from ${run.id}`, now, now, protocol.project_id, protocol.id]);
|
|
419
427
|
}
|
|
420
428
|
appendAudit(context, {
|
|
421
429
|
protocolId: protocol.id,
|
|
@@ -440,6 +448,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
440
448
|
return {
|
|
441
449
|
ok: true,
|
|
442
450
|
protocol_id: protocol.id,
|
|
451
|
+
project_root: project.root,
|
|
443
452
|
run_id: run.id,
|
|
444
453
|
from: state.stage,
|
|
445
454
|
to: targetStage,
|
|
@@ -457,14 +466,15 @@ export function syncProtocolFromRun(context, input) {
|
|
|
457
466
|
};
|
|
458
467
|
}
|
|
459
468
|
export function cancelProtocol(context, input) {
|
|
460
|
-
const
|
|
469
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
470
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
461
471
|
const reason = input.reason.trim();
|
|
462
472
|
if (!reason) {
|
|
463
473
|
throw new AppError("validation", "protocol cancel requires --reason", 2);
|
|
464
474
|
}
|
|
465
475
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
466
476
|
const now = context.now();
|
|
467
|
-
const worktree = context.db.get("SELECT worktree_path, status FROM worktree_records WHERE protocol_id = ?", [protocol.id]);
|
|
477
|
+
const worktree = context.db.get("SELECT worktree_path, status FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
468
478
|
const worktreeRemoval = input.worktree === "remove" ? removeProtocolWorktreeBeforeCancel(protocol, state, worktree, input.force) : undefined;
|
|
469
479
|
context.db.exec("BEGIN IMMEDIATE");
|
|
470
480
|
try {
|
|
@@ -497,7 +507,7 @@ export function cancelProtocol(context, input) {
|
|
|
497
507
|
const nextWorktreeStatus = worktreeRemoval?.status ?? "removed";
|
|
498
508
|
const result = worktree
|
|
499
509
|
? context.db.run(`UPDATE worktree_records SET status = ?, updated_at = ?, closed_at = ?
|
|
500
|
-
WHERE protocol_id = ? AND status = 'active'`, [nextWorktreeStatus, now, now, protocol.id])
|
|
510
|
+
WHERE project_id = ? AND protocol_id = ? AND status = 'active'`, [nextWorktreeStatus, now, now, protocol.project_id, protocol.id])
|
|
501
511
|
: { changes: 0 };
|
|
502
512
|
worktreeOutcome = {
|
|
503
513
|
ok: true,
|
|
@@ -532,12 +542,13 @@ export function cancelProtocol(context, input) {
|
|
|
532
542
|
return {
|
|
533
543
|
ok: true,
|
|
534
544
|
protocol_id: protocol.id,
|
|
545
|
+
project_root: project.root,
|
|
535
546
|
state: nextState,
|
|
536
547
|
queue: cancelledQueue,
|
|
537
548
|
closed_sessions: closedSessions,
|
|
538
549
|
lock_release: lockRelease,
|
|
539
550
|
worktree_outcome: worktreeOutcome,
|
|
540
|
-
worktree: context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]) ?? null
|
|
551
|
+
worktree: context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]) ?? null
|
|
541
552
|
};
|
|
542
553
|
}
|
|
543
554
|
catch (error) {
|
|
@@ -545,15 +556,23 @@ export function cancelProtocol(context, input) {
|
|
|
545
556
|
throw error;
|
|
546
557
|
}
|
|
547
558
|
}
|
|
548
|
-
export function requireProtocol(context, protocolId) {
|
|
549
|
-
const
|
|
550
|
-
|
|
559
|
+
export function requireProtocol(context, protocolId, projectId) {
|
|
560
|
+
const matches = projectId
|
|
561
|
+
? context.db.all("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId])
|
|
562
|
+
: context.db.all("SELECT * FROM protocols WHERE id = ? ORDER BY project_id", [protocolId]);
|
|
563
|
+
if (matches.length === 0) {
|
|
551
564
|
throw new AppError("not_found", `Protocol is not registered: ${protocolId}`, 1);
|
|
552
565
|
}
|
|
553
|
-
|
|
566
|
+
if (matches.length > 1) {
|
|
567
|
+
throw new AppError("ambiguous_protocol", `Protocol id is ambiguous without project context: ${protocolId}`, 1, {
|
|
568
|
+
protocol_id: protocolId,
|
|
569
|
+
candidates: matches.map((protocol) => ({ project_id: protocol.project_id, project_root: protocol.project_root }))
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return matches[0];
|
|
554
573
|
}
|
|
555
574
|
function cancelQueueForProtocol(context, protocol, reason, force) {
|
|
556
|
-
const job = context.db.get("SELECT id, status, claimed_by_session_id FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
575
|
+
const job = context.db.get("SELECT id, status, claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
557
576
|
if (!job) {
|
|
558
577
|
return { ok: true, skipped: true, reason: "no_queue_job" };
|
|
559
578
|
}
|
|
@@ -678,7 +697,7 @@ function removeLocalFeatureBranch(projectRoot, branch, force) {
|
|
|
678
697
|
function releaseRelatedMergeLocks(context, protocol, reason) {
|
|
679
698
|
const workers = context.db.all(`SELECT DISTINCT worker_id FROM flow_sessions
|
|
680
699
|
WHERE project_id = ? AND protocol_id = ? AND worker_id IS NOT NULL`, [protocol.project_id, protocol.id]).map((row) => row.worker_id).filter((workerId) => Boolean(workerId));
|
|
681
|
-
const job = context.db.get("SELECT claimed_by_session_id FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
700
|
+
const job = context.db.get("SELECT claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
682
701
|
if (job?.claimed_by_session_id) {
|
|
683
702
|
workers.push(job.claimed_by_session_id);
|
|
684
703
|
}
|
|
@@ -720,7 +739,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
720
739
|
recommended_action: "legacy protocol state_path was moved to stable runtime storage"
|
|
721
740
|
});
|
|
722
741
|
const stablePlanPath = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
723
|
-
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE id = ?", [stableStatePath, stablePlanPath, protocol.id]);
|
|
742
|
+
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [stableStatePath, stablePlanPath, protocol.project_id, protocol.id]);
|
|
724
743
|
protocol.state_path = stableStatePath;
|
|
725
744
|
protocol.plan_path = stablePlanPath;
|
|
726
745
|
return { state: readStateFile(stableStatePath, protocol.id), diagnostics };
|
|
@@ -733,7 +752,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
733
752
|
recommended_action: "runtime state reconstructed from SQLite; run cleanup scan/apply if this persists"
|
|
734
753
|
});
|
|
735
754
|
}
|
|
736
|
-
const plan = context.db.get("SELECT plan_json FROM plans WHERE protocol_id = ?", [protocol.id]);
|
|
755
|
+
const plan = context.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
737
756
|
const planSummary = plan
|
|
738
757
|
? planSummaryForState(JSON.parse(plan.plan_json))
|
|
739
758
|
: { plan_id: null, total: 0, done: 0, blocked: 0 };
|
|
@@ -757,7 +776,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
757
776
|
const stablePlanPath = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
758
777
|
ensureDir(path.dirname(stableStatePath));
|
|
759
778
|
writeState(stableStatePath, state);
|
|
760
|
-
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE id = ?", [stableStatePath, stablePlanPath, protocol.id]);
|
|
779
|
+
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [stableStatePath, stablePlanPath, protocol.project_id, protocol.id]);
|
|
761
780
|
const repairedFrom = protocol.state_path;
|
|
762
781
|
protocol.state_path = stableStatePath;
|
|
763
782
|
protocol.plan_path = stablePlanPath;
|
|
@@ -802,7 +821,7 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
802
821
|
context.db.run(`UPDATE protocols SET
|
|
803
822
|
status = ?, stage = ?, next_action = ?, route_json = ?, workspace_json = ?,
|
|
804
823
|
blockers_json = ?, active_def_json = ?, updated_at = ?
|
|
805
|
-
WHERE id = ?`, [
|
|
824
|
+
WHERE project_id = ? AND id = ?`, [
|
|
806
825
|
state.status,
|
|
807
826
|
state.stage,
|
|
808
827
|
state.next_action,
|
|
@@ -811,6 +830,7 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
811
830
|
JSON.stringify(state.blockers),
|
|
812
831
|
JSON.stringify(state.active_def),
|
|
813
832
|
state.updated_at,
|
|
833
|
+
protocol.project_id,
|
|
814
834
|
protocol.id
|
|
815
835
|
]);
|
|
816
836
|
if (state.status === "closed" || state.stage === "closed") {
|
|
@@ -822,8 +842,12 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
822
842
|
AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [now, now, protocol.project_id, protocol.id]);
|
|
823
843
|
}
|
|
824
844
|
}
|
|
825
|
-
function findProtocol(context, protocolId) {
|
|
826
|
-
|
|
845
|
+
function findProtocol(context, protocolId, projectId) {
|
|
846
|
+
if (projectId) {
|
|
847
|
+
return context.db.get("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
848
|
+
}
|
|
849
|
+
const matches = context.db.all("SELECT * FROM protocols WHERE id = ? ORDER BY project_id", [protocolId]);
|
|
850
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
827
851
|
}
|
|
828
852
|
function protocolDocumentsForProject(projectRoot) {
|
|
829
853
|
const protocolRoot = path.join(projectRoot, ".memory-bank", "protocol");
|
|
@@ -836,12 +860,19 @@ function protocolDocumentsForProject(projectRoot) {
|
|
|
836
860
|
.filter((document) => Boolean(document))
|
|
837
861
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
838
862
|
}
|
|
839
|
-
function requireProtocolDocument(projectRoot, protocolId) {
|
|
840
|
-
const
|
|
863
|
+
function requireProtocolDocument(context, projectRoot, protocolId) {
|
|
864
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
|
|
865
|
+
const protocol = findProtocol(context, protocolId, project.id);
|
|
866
|
+
const workspace = protocol ? JSON.parse(protocol.workspace_json).worktree_path : null;
|
|
867
|
+
const workspaceFile = typeof workspace === "string" ? path.join(workspace, ".memory-bank", "protocol", `${protocolId}.md`) : null;
|
|
868
|
+
const file = workspaceFile && fs.existsSync(workspaceFile)
|
|
869
|
+
? workspaceFile
|
|
870
|
+
: path.join(projectRoot, ".memory-bank", "protocol", `${protocolId}.md`);
|
|
841
871
|
if (!fs.existsSync(file)) {
|
|
842
872
|
throw new AppError("protocol_file_not_found", `Protocol markdown is not found: ${protocolId}`, 1, {
|
|
843
873
|
protocol_id: protocolId,
|
|
844
|
-
expected_path: file
|
|
874
|
+
expected_path: file,
|
|
875
|
+
workspace_path: workspace
|
|
845
876
|
});
|
|
846
877
|
}
|
|
847
878
|
const document = readProtocolDocument(file);
|
|
@@ -917,9 +948,9 @@ function protocolDocumentDiagnostics(document) {
|
|
|
917
948
|
return diagnostics;
|
|
918
949
|
}
|
|
919
950
|
function protocolSetMemberStatus(context, projectId, document) {
|
|
920
|
-
const runtime = findProtocol(context, document.id);
|
|
951
|
+
const runtime = findProtocol(context, document.id, projectId);
|
|
921
952
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
922
|
-
const queue = context.db.get("SELECT status, claimed_by_session_id FROM merge_queue WHERE protocol_id = ?", [document.id]);
|
|
953
|
+
const queue = context.db.get("SELECT status, claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [projectId, document.id]);
|
|
923
954
|
const activeSessions = activeFlowSessionBindingsForProject(context, projectId).filter((session) => session.protocol_id === document.id);
|
|
924
955
|
const projectRoot = projectRootFromProtocolFile(document.path);
|
|
925
956
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, projectId, projectRoot, id));
|
|
@@ -983,7 +1014,7 @@ export function protocolSetBoardForProtocol(context, projectId, projectRoot, pro
|
|
|
983
1014
|
return protocolSetBoardsForProject(context, projectId, projectRoot).find((board) => board.protocol_set === document.protocol_set) ?? null;
|
|
984
1015
|
}
|
|
985
1016
|
function blockerStatus(context, projectId, projectRoot, blockerId) {
|
|
986
|
-
const runtime = findProtocol(context, blockerId);
|
|
1017
|
+
const runtime = findProtocol(context, blockerId, projectId);
|
|
987
1018
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
988
1019
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, blockerId, state);
|
|
989
1020
|
const lifecycle = normalizeProtocolLifecycle({ state, rawStage: runtime?.stage, rawStatus: runtime?.status });
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function refreshRunSessionProjection(context, projectId, runId) {
|
|
4
|
+
const run = context.db.get("SELECT id, runtime_path, run_index_path, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
5
|
+
if (!run)
|
|
6
|
+
return;
|
|
7
|
+
const index = JSON.parse(run.index_json);
|
|
8
|
+
const sessions = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
|
|
9
|
+
created_at, updated_at, stopped_at, coverage_units_json
|
|
10
|
+
FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]).map(sessionProjection);
|
|
11
|
+
if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions))
|
|
12
|
+
return;
|
|
13
|
+
index.sessions = sessions;
|
|
14
|
+
index.updated_at = context.now();
|
|
15
|
+
const runtimeRevision = typeof index.runtime_revision === "number" ? index.runtime_revision + 1 : 1;
|
|
16
|
+
index.runtime_revision = runtimeRevision;
|
|
17
|
+
const runtime = readJson(run.runtime_path);
|
|
18
|
+
if (runtime) {
|
|
19
|
+
runtime.sessions = sessions;
|
|
20
|
+
runtime.updated_at = index.updated_at;
|
|
21
|
+
runtime.runtime_revision = runtimeRevision;
|
|
22
|
+
}
|
|
23
|
+
writeJson(run.run_index_path, index);
|
|
24
|
+
writeJson(run.runtime_path, runtime ?? index);
|
|
25
|
+
context.db.run("UPDATE flow_runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ?", [JSON.stringify(index), index.updated_at, projectId, runId]);
|
|
26
|
+
}
|
|
27
|
+
function sessionProjection(row) {
|
|
28
|
+
return {
|
|
29
|
+
session_id: row.session_id,
|
|
30
|
+
parent_session_id: row.parent_session_id,
|
|
31
|
+
role: row.role,
|
|
32
|
+
session_kind: row.session_kind,
|
|
33
|
+
worker_id: row.worker_id,
|
|
34
|
+
current_stage: row.current_stage,
|
|
35
|
+
status: row.status,
|
|
36
|
+
created_at: row.created_at,
|
|
37
|
+
updated_at: row.updated_at,
|
|
38
|
+
stopped_at: row.stopped_at,
|
|
39
|
+
coverage_units: parseCoverageUnits(row.coverage_units_json)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function parseCoverageUnits(value) {
|
|
43
|
+
if (!value)
|
|
44
|
+
return [];
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(value);
|
|
47
|
+
if (!Array.isArray(parsed))
|
|
48
|
+
return [];
|
|
49
|
+
return parsed.flatMap((item) => {
|
|
50
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
51
|
+
return [];
|
|
52
|
+
const object = item;
|
|
53
|
+
if (typeof object.unit_id !== "string" || object.unit_id.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
return [{
|
|
56
|
+
unit_id: object.unit_id,
|
|
57
|
+
group_id: typeof object.group_id === "string" ? object.group_id : null,
|
|
58
|
+
job_id: typeof object.job_id === "string" ? object.job_id : null,
|
|
59
|
+
kind: typeof object.kind === "string" ? object.kind : null
|
|
60
|
+
}];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readJson(file) {
|
|
68
|
+
try {
|
|
69
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeJson(file, value) {
|
|
76
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
77
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
78
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
79
|
+
fs.renameSync(temporary, file);
|
|
80
|
+
}
|