@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
|
@@ -15,6 +15,7 @@ import { ensureRuntimeProtocolFiles, readPlanFile, readStateFile, writeState } f
|
|
|
15
15
|
import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
|
|
16
16
|
import { buildProtocolFlowGuidance } from "./flow-guidance.js";
|
|
17
17
|
import { lifecycleIsTerminalFailure, lifecycleIsTerminalSuccess, normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
18
|
+
import { getProtocolBranchContext } from "./branch-context.js";
|
|
18
19
|
export function registerProtocol(context, input) {
|
|
19
20
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
20
21
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -31,7 +32,7 @@ export function registerProtocol(context, input) {
|
|
|
31
32
|
workspacePath,
|
|
32
33
|
now
|
|
33
34
|
});
|
|
34
|
-
const existing = findProtocol(context, protocolId);
|
|
35
|
+
const existing = findProtocol(context, protocolId, project.id);
|
|
35
36
|
const params = [
|
|
36
37
|
protocolId,
|
|
37
38
|
input.handshakeId,
|
|
@@ -54,7 +55,7 @@ export function registerProtocol(context, input) {
|
|
|
54
55
|
route_json, workspace_json, blockers_json, active_def_json, state_path, plan_path,
|
|
55
56
|
created_at, updated_at)
|
|
56
57
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
57
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
58
|
+
ON CONFLICT(project_id, id) DO UPDATE SET
|
|
58
59
|
handshake_id = excluded.handshake_id,
|
|
59
60
|
project_id = excluded.project_id,
|
|
60
61
|
project_root = excluded.project_root,
|
|
@@ -80,15 +81,16 @@ export function registerProtocol(context, input) {
|
|
|
80
81
|
runtime_dir: runtimeDir
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
|
-
return { ok: true, protocol: protocolStatusPayload(requireProtocol(context, protocolId)), state };
|
|
84
|
+
return { ok: true, protocol: protocolStatusPayload(requireProtocol(context, protocolId, project.id)), state };
|
|
84
85
|
}
|
|
85
86
|
export function getProtocolStatus(context, input) {
|
|
86
|
-
const
|
|
87
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
88
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
87
89
|
const runtime = readProtocolRuntimeState(context, protocol);
|
|
88
90
|
const state = runtime.state;
|
|
89
91
|
const plan = readPlanFile(protocol.plan_path);
|
|
90
92
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
91
|
-
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]);
|
|
92
94
|
const lifecycle = normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null });
|
|
93
95
|
return {
|
|
94
96
|
ok: true,
|
|
@@ -99,14 +101,15 @@ export function getProtocolStatus(context, input) {
|
|
|
99
101
|
state,
|
|
100
102
|
plan: plan ? summarizePlan(plan) : state.plan,
|
|
101
103
|
merge_queue: queue,
|
|
104
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context,
|
|
102
105
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null }),
|
|
103
|
-
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]),
|
|
104
107
|
hook_status: hookStatusForProject(context, protocol.project_id),
|
|
105
108
|
codex_home_profiles: codexHomeProfilesForProject(context, protocol.project_id),
|
|
106
109
|
flow_sessions: activeFlowSessionBindingsForProject(context, protocol.project_id).filter((session) => session.protocol_id === protocol.id),
|
|
107
110
|
codex_session_bindings: activeCodexSessionBindingsForProject(context, protocol.project_id).filter((binding) => binding.protocol_id === protocol.id),
|
|
108
111
|
codex_hook_events: codexHookEventsForProject(context, protocol.project_id).filter((event) => event.protocol_id === protocol.id),
|
|
109
|
-
audit: getAuditEvents(context, protocol.id)
|
|
112
|
+
audit: getAuditEvents(context, protocol.project_id, protocol.id)
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
export function getReadyProtocols(context, input) {
|
|
@@ -126,7 +129,7 @@ export function getReadyProtocols(context, input) {
|
|
|
126
129
|
}
|
|
127
130
|
export function getProtocolBlockers(context, input) {
|
|
128
131
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
129
|
-
const document = requireProtocolDocument(project.root, input.protocolId);
|
|
132
|
+
const document = requireProtocolDocument(context, project.root, input.protocolId);
|
|
130
133
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, project.id, project.root, id));
|
|
131
134
|
return {
|
|
132
135
|
ok: true,
|
|
@@ -141,13 +144,13 @@ export function getProtocolBlockers(context, input) {
|
|
|
141
144
|
}
|
|
142
145
|
export function implementProtocol(context, input) {
|
|
143
146
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
144
|
-
const document = requireProtocolDocument(project.root, input.protocolId);
|
|
145
|
-
const protocol = findProtocol(context, document.id);
|
|
147
|
+
const document = requireProtocolDocument(context, project.root, input.protocolId);
|
|
148
|
+
const protocol = findProtocol(context, document.id, project.id);
|
|
146
149
|
const runtime = protocol ? readProtocolRuntimeState(context, protocol) : null;
|
|
147
150
|
const state = runtime?.state ?? null;
|
|
148
151
|
const terminal = protocolTerminalStatus(context, project.id, project.root, document.id, state);
|
|
149
152
|
const activeSessions = activeFlowSessionBindingsForProject(context, project.id).filter((session) => session.protocol_id === document.id);
|
|
150
|
-
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]);
|
|
151
154
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, project.id, project.root, id));
|
|
152
155
|
const unresolved = blockers.filter((blocker) => !blocker.resolved);
|
|
153
156
|
const diagnostics = [
|
|
@@ -203,6 +206,7 @@ export function implementProtocol(context, input) {
|
|
|
203
206
|
blockers,
|
|
204
207
|
active_sessions: activeSessions,
|
|
205
208
|
merge_queue: queue ?? null,
|
|
209
|
+
branch_context: protocol ? getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context : null,
|
|
206
210
|
lifecycle,
|
|
207
211
|
terminal,
|
|
208
212
|
related_context: document.related_context,
|
|
@@ -239,7 +243,8 @@ export function implementProtocol(context, input) {
|
|
|
239
243
|
};
|
|
240
244
|
}
|
|
241
245
|
export function transitionProtocol(context, input) {
|
|
242
|
-
const
|
|
246
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
247
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
243
248
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
244
249
|
const flowContract = flowContractForState(state);
|
|
245
250
|
const from = requireStage(state.stage, flowContract);
|
|
@@ -273,10 +278,11 @@ export function transitionProtocol(context, input) {
|
|
|
273
278
|
payload: { from, to, payload, flow_contract_id: flowContract.id, flow_contract_version: flowContract.version }
|
|
274
279
|
});
|
|
275
280
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, nextState);
|
|
276
|
-
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]);
|
|
277
282
|
return {
|
|
278
283
|
ok: true,
|
|
279
284
|
protocol_id: protocol.id,
|
|
285
|
+
project_root: project.root,
|
|
280
286
|
from,
|
|
281
287
|
to,
|
|
282
288
|
forced: input.force,
|
|
@@ -286,13 +292,15 @@ export function transitionProtocol(context, input) {
|
|
|
286
292
|
};
|
|
287
293
|
}
|
|
288
294
|
export function readyForMerge(context, input) {
|
|
289
|
-
const
|
|
295
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
296
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
290
297
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
291
298
|
const flowContract = flowContractForState(state);
|
|
292
299
|
const stage = requireStage(state.stage, flowContract);
|
|
293
300
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
294
|
-
const
|
|
295
|
-
|
|
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
|
|
296
304
|
]);
|
|
297
305
|
if (existingQueueJob && ["claimed", "merged", "cancelled", "failed"].includes(existingQueueJob.status)) {
|
|
298
306
|
appendAudit(context, {
|
|
@@ -304,8 +312,10 @@ export function readyForMerge(context, input) {
|
|
|
304
312
|
return {
|
|
305
313
|
ok: true,
|
|
306
314
|
protocol_id: protocol.id,
|
|
315
|
+
project_root: project.root,
|
|
307
316
|
queue_status: existingQueueJob.status,
|
|
308
317
|
state,
|
|
318
|
+
branch_context: branchContext,
|
|
309
319
|
lifecycle: normalizeProtocolLifecycle({ state, queueStatus: existingQueueJob.status }),
|
|
310
320
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: existingQueueJob.status })
|
|
311
321
|
};
|
|
@@ -341,7 +351,7 @@ export function readyForMerge(context, input) {
|
|
|
341
351
|
context.db.run(`INSERT INTO merge_queue
|
|
342
352
|
(protocol_id, project_id, status, claimed_by_session_id, claimed_at, completed_at, created_at, updated_at)
|
|
343
353
|
VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
344
|
-
ON CONFLICT(protocol_id) DO UPDATE SET
|
|
354
|
+
ON CONFLICT(project_id, protocol_id) DO UPDATE SET
|
|
345
355
|
status = excluded.status,
|
|
346
356
|
claimed_by_session_id = NULL,
|
|
347
357
|
claimed_at = NULL,
|
|
@@ -356,14 +366,17 @@ export function readyForMerge(context, input) {
|
|
|
356
366
|
return {
|
|
357
367
|
ok: true,
|
|
358
368
|
protocol_id: protocol.id,
|
|
369
|
+
project_root: project.root,
|
|
359
370
|
queue_status: "ready",
|
|
360
371
|
state: nextState,
|
|
372
|
+
branch_context: branchContext,
|
|
361
373
|
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: "ready" }),
|
|
362
374
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
363
375
|
};
|
|
364
376
|
}
|
|
365
377
|
export function syncProtocolFromRun(context, input) {
|
|
366
|
-
const
|
|
378
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
379
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
367
380
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
368
381
|
const run = resolveLinkedRun(context, protocol, input.runId);
|
|
369
382
|
if (run.subject_type !== "protocol" || run.subject_id !== protocol.id) {
|
|
@@ -400,7 +413,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
400
413
|
context.db.run(`INSERT INTO merge_queue
|
|
401
414
|
(protocol_id, project_id, status, claimed_by_session_id, claimed_at, completed_at, created_at, updated_at)
|
|
402
415
|
VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
403
|
-
ON CONFLICT(protocol_id) DO UPDATE SET
|
|
416
|
+
ON CONFLICT(project_id, protocol_id) DO UPDATE SET
|
|
404
417
|
status = CASE WHEN merge_queue.status IN ('merged', 'cancelled', 'failed') THEN merge_queue.status ELSE excluded.status END,
|
|
405
418
|
updated_at = excluded.updated_at`, [protocol.id, protocol.project_id, "ready", now, now]);
|
|
406
419
|
}
|
|
@@ -410,7 +423,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
410
423
|
last_reason = COALESCE(last_reason, ?),
|
|
411
424
|
completed_at = COALESCE(completed_at, ?),
|
|
412
425
|
updated_at = ?
|
|
413
|
-
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]);
|
|
414
427
|
}
|
|
415
428
|
appendAudit(context, {
|
|
416
429
|
protocolId: protocol.id,
|
|
@@ -435,6 +448,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
435
448
|
return {
|
|
436
449
|
ok: true,
|
|
437
450
|
protocol_id: protocol.id,
|
|
451
|
+
project_root: project.root,
|
|
438
452
|
run_id: run.id,
|
|
439
453
|
from: state.stage,
|
|
440
454
|
to: targetStage,
|
|
@@ -452,14 +466,15 @@ export function syncProtocolFromRun(context, input) {
|
|
|
452
466
|
};
|
|
453
467
|
}
|
|
454
468
|
export function cancelProtocol(context, input) {
|
|
455
|
-
const
|
|
469
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
470
|
+
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
456
471
|
const reason = input.reason.trim();
|
|
457
472
|
if (!reason) {
|
|
458
473
|
throw new AppError("validation", "protocol cancel requires --reason", 2);
|
|
459
474
|
}
|
|
460
475
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
461
476
|
const now = context.now();
|
|
462
|
-
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]);
|
|
463
478
|
const worktreeRemoval = input.worktree === "remove" ? removeProtocolWorktreeBeforeCancel(protocol, state, worktree, input.force) : undefined;
|
|
464
479
|
context.db.exec("BEGIN IMMEDIATE");
|
|
465
480
|
try {
|
|
@@ -492,7 +507,7 @@ export function cancelProtocol(context, input) {
|
|
|
492
507
|
const nextWorktreeStatus = worktreeRemoval?.status ?? "removed";
|
|
493
508
|
const result = worktree
|
|
494
509
|
? context.db.run(`UPDATE worktree_records SET status = ?, updated_at = ?, closed_at = ?
|
|
495
|
-
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])
|
|
496
511
|
: { changes: 0 };
|
|
497
512
|
worktreeOutcome = {
|
|
498
513
|
ok: true,
|
|
@@ -527,12 +542,13 @@ export function cancelProtocol(context, input) {
|
|
|
527
542
|
return {
|
|
528
543
|
ok: true,
|
|
529
544
|
protocol_id: protocol.id,
|
|
545
|
+
project_root: project.root,
|
|
530
546
|
state: nextState,
|
|
531
547
|
queue: cancelledQueue,
|
|
532
548
|
closed_sessions: closedSessions,
|
|
533
549
|
lock_release: lockRelease,
|
|
534
550
|
worktree_outcome: worktreeOutcome,
|
|
535
|
-
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
|
|
536
552
|
};
|
|
537
553
|
}
|
|
538
554
|
catch (error) {
|
|
@@ -540,15 +556,23 @@ export function cancelProtocol(context, input) {
|
|
|
540
556
|
throw error;
|
|
541
557
|
}
|
|
542
558
|
}
|
|
543
|
-
export function requireProtocol(context, protocolId) {
|
|
544
|
-
const
|
|
545
|
-
|
|
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) {
|
|
546
564
|
throw new AppError("not_found", `Protocol is not registered: ${protocolId}`, 1);
|
|
547
565
|
}
|
|
548
|
-
|
|
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];
|
|
549
573
|
}
|
|
550
574
|
function cancelQueueForProtocol(context, protocol, reason, force) {
|
|
551
|
-
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]);
|
|
552
576
|
if (!job) {
|
|
553
577
|
return { ok: true, skipped: true, reason: "no_queue_job" };
|
|
554
578
|
}
|
|
@@ -673,7 +697,7 @@ function removeLocalFeatureBranch(projectRoot, branch, force) {
|
|
|
673
697
|
function releaseRelatedMergeLocks(context, protocol, reason) {
|
|
674
698
|
const workers = context.db.all(`SELECT DISTINCT worker_id FROM flow_sessions
|
|
675
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));
|
|
676
|
-
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]);
|
|
677
701
|
if (job?.claimed_by_session_id) {
|
|
678
702
|
workers.push(job.claimed_by_session_id);
|
|
679
703
|
}
|
|
@@ -715,7 +739,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
715
739
|
recommended_action: "legacy protocol state_path was moved to stable runtime storage"
|
|
716
740
|
});
|
|
717
741
|
const stablePlanPath = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
718
|
-
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]);
|
|
719
743
|
protocol.state_path = stableStatePath;
|
|
720
744
|
protocol.plan_path = stablePlanPath;
|
|
721
745
|
return { state: readStateFile(stableStatePath, protocol.id), diagnostics };
|
|
@@ -728,7 +752,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
728
752
|
recommended_action: "runtime state reconstructed from SQLite; run cleanup scan/apply if this persists"
|
|
729
753
|
});
|
|
730
754
|
}
|
|
731
|
-
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]);
|
|
732
756
|
const planSummary = plan
|
|
733
757
|
? planSummaryForState(JSON.parse(plan.plan_json))
|
|
734
758
|
: { plan_id: null, total: 0, done: 0, blocked: 0 };
|
|
@@ -752,7 +776,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
752
776
|
const stablePlanPath = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
753
777
|
ensureDir(path.dirname(stableStatePath));
|
|
754
778
|
writeState(stableStatePath, state);
|
|
755
|
-
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]);
|
|
756
780
|
const repairedFrom = protocol.state_path;
|
|
757
781
|
protocol.state_path = stableStatePath;
|
|
758
782
|
protocol.plan_path = stablePlanPath;
|
|
@@ -797,7 +821,7 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
797
821
|
context.db.run(`UPDATE protocols SET
|
|
798
822
|
status = ?, stage = ?, next_action = ?, route_json = ?, workspace_json = ?,
|
|
799
823
|
blockers_json = ?, active_def_json = ?, updated_at = ?
|
|
800
|
-
WHERE id = ?`, [
|
|
824
|
+
WHERE project_id = ? AND id = ?`, [
|
|
801
825
|
state.status,
|
|
802
826
|
state.stage,
|
|
803
827
|
state.next_action,
|
|
@@ -806,6 +830,7 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
806
830
|
JSON.stringify(state.blockers),
|
|
807
831
|
JSON.stringify(state.active_def),
|
|
808
832
|
state.updated_at,
|
|
833
|
+
protocol.project_id,
|
|
809
834
|
protocol.id
|
|
810
835
|
]);
|
|
811
836
|
if (state.status === "closed" || state.stage === "closed") {
|
|
@@ -817,8 +842,12 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
817
842
|
AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [now, now, protocol.project_id, protocol.id]);
|
|
818
843
|
}
|
|
819
844
|
}
|
|
820
|
-
function findProtocol(context, protocolId) {
|
|
821
|
-
|
|
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;
|
|
822
851
|
}
|
|
823
852
|
function protocolDocumentsForProject(projectRoot) {
|
|
824
853
|
const protocolRoot = path.join(projectRoot, ".memory-bank", "protocol");
|
|
@@ -831,12 +860,19 @@ function protocolDocumentsForProject(projectRoot) {
|
|
|
831
860
|
.filter((document) => Boolean(document))
|
|
832
861
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
833
862
|
}
|
|
834
|
-
function requireProtocolDocument(projectRoot, protocolId) {
|
|
835
|
-
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`);
|
|
836
871
|
if (!fs.existsSync(file)) {
|
|
837
872
|
throw new AppError("protocol_file_not_found", `Protocol markdown is not found: ${protocolId}`, 1, {
|
|
838
873
|
protocol_id: protocolId,
|
|
839
|
-
expected_path: file
|
|
874
|
+
expected_path: file,
|
|
875
|
+
workspace_path: workspace
|
|
840
876
|
});
|
|
841
877
|
}
|
|
842
878
|
const document = readProtocolDocument(file);
|
|
@@ -912,9 +948,9 @@ function protocolDocumentDiagnostics(document) {
|
|
|
912
948
|
return diagnostics;
|
|
913
949
|
}
|
|
914
950
|
function protocolSetMemberStatus(context, projectId, document) {
|
|
915
|
-
const runtime = findProtocol(context, document.id);
|
|
951
|
+
const runtime = findProtocol(context, document.id, projectId);
|
|
916
952
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
917
|
-
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]);
|
|
918
954
|
const activeSessions = activeFlowSessionBindingsForProject(context, projectId).filter((session) => session.protocol_id === document.id);
|
|
919
955
|
const projectRoot = projectRootFromProtocolFile(document.path);
|
|
920
956
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, projectId, projectRoot, id));
|
|
@@ -978,7 +1014,7 @@ export function protocolSetBoardForProtocol(context, projectId, projectRoot, pro
|
|
|
978
1014
|
return protocolSetBoardsForProject(context, projectId, projectRoot).find((board) => board.protocol_set === document.protocol_set) ?? null;
|
|
979
1015
|
}
|
|
980
1016
|
function blockerStatus(context, projectId, projectRoot, blockerId) {
|
|
981
|
-
const runtime = findProtocol(context, blockerId);
|
|
1017
|
+
const runtime = findProtocol(context, blockerId, projectId);
|
|
982
1018
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
983
1019
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, blockerId, state);
|
|
984
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
|
+
}
|