@deksden-com/dd-flow-cli 0.3.0 → 0.4.0
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 +55 -0
- package/README.md +74 -5
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +116 -18
- package/dist/cli/run-cli.js +361 -29
- package/dist/schemas/compatibility.schema.json +81 -2
- package/dist/schemas/engine-manifest.schema.json +61 -0
- package/dist/schemas/flow-guidance.schema.json +17 -0
- package/dist/schemas/global-dashboard-data.schema.json +60 -2
- package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
- package/dist/schemas/project-dashboard-data.schema.json +26 -2
- package/dist/schemas/project-summary.schema.json +73 -0
- package/dist/schemas/protocol-dashboard-data.schema.json +25 -2
- package/dist/schemas/status-report.schema.json +4 -2
- package/dist/services/branch-context.js +254 -0
- package/dist/services/cleanup.js +31 -0
- package/dist/services/cli-operation-classifier.js +104 -0
- package/dist/services/compatibility-preflight.js +127 -0
- package/dist/services/config.js +6 -0
- package/dist/services/dashboard-targets.js +95 -0
- package/dist/services/dashboard.js +376 -59
- package/dist/services/engines.js +532 -0
- package/dist/services/flow-guidance.js +8 -1
- package/dist/services/hooks.js +1 -1
- package/dist/services/lanes.js +333 -1
- package/dist/services/merge-queue.js +310 -15
- package/dist/services/merge-worker.js +44 -3
- package/dist/services/migrations.js +231 -0
- package/dist/services/project-summary.js +122 -0
- package/dist/services/projects.js +41 -6
- package/dist/services/protocol-lifecycle.js +144 -0
- package/dist/services/protocols.js +34 -7
- package/dist/services/sessions.js +21 -4
- package/dist/services/status.js +10 -0
- package/dist/services/version-status.js +39 -15
- package/dist/storage/database.js +25 -0
- package/dist/storage/paths.js +12 -0
- package/package.json +3 -2
|
@@ -14,6 +14,8 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
14
14
|
import { ensureRuntimeProtocolFiles, readPlanFile, readStateFile, writeState } from "../protocol/local-files.js";
|
|
15
15
|
import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
|
|
16
16
|
import { buildProtocolFlowGuidance } from "./flow-guidance.js";
|
|
17
|
+
import { lifecycleIsTerminalFailure, lifecycleIsTerminalSuccess, normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
18
|
+
import { getProtocolBranchContext } from "./branch-context.js";
|
|
17
19
|
export function registerProtocol(context, input) {
|
|
18
20
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
19
21
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -88,14 +90,17 @@ export function getProtocolStatus(context, input) {
|
|
|
88
90
|
const plan = readPlanFile(protocol.plan_path);
|
|
89
91
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
90
92
|
const queue = context.db.get("SELECT * FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
93
|
+
const lifecycle = normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null });
|
|
91
94
|
return {
|
|
92
95
|
ok: true,
|
|
93
|
-
protocol: protocolStatusPayload(protocol),
|
|
94
|
-
|
|
96
|
+
protocol: { ...protocolStatusPayload(protocol), lifecycle },
|
|
97
|
+
lifecycle,
|
|
98
|
+
diagnostics: [...runtime.diagnostics, ...runDiagnostics.diagnostics, ...lifecycle.diagnostics],
|
|
95
99
|
latest_run: runDiagnostics.latest_run,
|
|
96
100
|
state,
|
|
97
101
|
plan: plan ? summarizePlan(plan) : state.plan,
|
|
98
102
|
merge_queue: queue,
|
|
103
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
99
104
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null }),
|
|
100
105
|
worktree: context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]),
|
|
101
106
|
hook_status: hookStatusForProject(context, protocol.project_id),
|
|
@@ -173,6 +178,9 @@ export function implementProtocol(context, input) {
|
|
|
173
178
|
if (input.force && (!input.reason || input.reason.trim().length === 0)) {
|
|
174
179
|
throw new AppError("validation", "Forced protocol implement preflight requires --reason", 2);
|
|
175
180
|
}
|
|
181
|
+
const lifecycle = protocol
|
|
182
|
+
? normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null })
|
|
183
|
+
: normalizeProtocolLifecycle({ rawStage: "unregistered", rawStatus: "missing" });
|
|
176
184
|
const currentStage = state?.stage ?? "unregistered";
|
|
177
185
|
const recommendedPrompt = promptForImplementationStage(currentStage);
|
|
178
186
|
return {
|
|
@@ -197,6 +205,8 @@ export function implementProtocol(context, input) {
|
|
|
197
205
|
blockers,
|
|
198
206
|
active_sessions: activeSessions,
|
|
199
207
|
merge_queue: queue ?? null,
|
|
208
|
+
branch_context: protocol ? getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context : null,
|
|
209
|
+
lifecycle,
|
|
200
210
|
terminal,
|
|
201
211
|
related_context: document.related_context,
|
|
202
212
|
coding_standards_sources: document.coding_standards_sources,
|
|
@@ -274,6 +284,7 @@ export function transitionProtocol(context, input) {
|
|
|
274
284
|
to,
|
|
275
285
|
forced: input.force,
|
|
276
286
|
state: nextState,
|
|
287
|
+
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: queue?.status ?? null }),
|
|
277
288
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null })
|
|
278
289
|
};
|
|
279
290
|
}
|
|
@@ -298,6 +309,8 @@ export function readyForMerge(context, input) {
|
|
|
298
309
|
protocol_id: protocol.id,
|
|
299
310
|
queue_status: existingQueueJob.status,
|
|
300
311
|
state,
|
|
312
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
313
|
+
lifecycle: normalizeProtocolLifecycle({ state, queueStatus: existingQueueJob.status }),
|
|
301
314
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: existingQueueJob.status })
|
|
302
315
|
};
|
|
303
316
|
}
|
|
@@ -349,6 +362,8 @@ export function readyForMerge(context, input) {
|
|
|
349
362
|
protocol_id: protocol.id,
|
|
350
363
|
queue_status: "ready",
|
|
351
364
|
state: nextState,
|
|
365
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
366
|
+
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: "ready" }),
|
|
352
367
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
353
368
|
};
|
|
354
369
|
}
|
|
@@ -909,11 +924,17 @@ function protocolSetMemberStatus(context, projectId, document) {
|
|
|
909
924
|
const projectRoot = projectRootFromProtocolFile(document.path);
|
|
910
925
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, projectId, projectRoot, id));
|
|
911
926
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, document.id, state);
|
|
927
|
+
const lifecycle = normalizeProtocolLifecycle({
|
|
928
|
+
state,
|
|
929
|
+
rawStage: runtime?.stage ?? "unregistered",
|
|
930
|
+
rawStatus: runtime?.status ?? "missing",
|
|
931
|
+
queueStatus: queue?.status ?? null
|
|
932
|
+
});
|
|
912
933
|
const setStatus = terminal.success
|
|
913
934
|
? "done"
|
|
914
935
|
: queue?.status === "claimed" || activeSessions.length > 0
|
|
915
936
|
? "claimed"
|
|
916
|
-
: state && ["
|
|
937
|
+
: state && ["specify", "plan", "code", "merge"].includes(lifecycle.stage)
|
|
917
938
|
? "running"
|
|
918
939
|
: blockers.some((blocker) => !blocker.resolved)
|
|
919
940
|
? "blocked"
|
|
@@ -926,8 +947,9 @@ function protocolSetMemberStatus(context, projectId, document) {
|
|
|
926
947
|
blocked_by_protocols: document.blocked_by_protocols,
|
|
927
948
|
blockers,
|
|
928
949
|
runtime: runtime
|
|
929
|
-
? { registered: true, status: runtime.status, stage: runtime.stage, next_action: runtime.next_action, updated_at: runtime.updated_at }
|
|
950
|
+
? { registered: true, status: runtime.status, stage: runtime.stage, lifecycle, next_action: runtime.next_action, updated_at: runtime.updated_at }
|
|
930
951
|
: { registered: false, status: "missing", stage: "unregistered", next_action: "register_protocol" },
|
|
952
|
+
lifecycle,
|
|
931
953
|
active_sessions: activeSessions,
|
|
932
954
|
merge_queue: queue ?? null,
|
|
933
955
|
terminal
|
|
@@ -964,22 +986,25 @@ function blockerStatus(context, projectId, projectRoot, blockerId) {
|
|
|
964
986
|
const runtime = findProtocol(context, blockerId);
|
|
965
987
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
966
988
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, blockerId, state);
|
|
989
|
+
const lifecycle = normalizeProtocolLifecycle({ state, rawStage: runtime?.stage, rawStatus: runtime?.status });
|
|
967
990
|
return {
|
|
968
991
|
id: blockerId,
|
|
969
992
|
resolved: terminal.success,
|
|
970
993
|
source: terminal.source,
|
|
971
994
|
status: runtime?.status ?? terminal.status ?? "unknown",
|
|
972
995
|
stage: runtime?.stage ?? terminal.stage ?? "unknown",
|
|
973
|
-
lifecycle
|
|
996
|
+
lifecycle,
|
|
997
|
+
document_lifecycle: terminal.lifecycle,
|
|
974
998
|
reason: terminal.reason
|
|
975
999
|
};
|
|
976
1000
|
}
|
|
977
1001
|
function protocolTerminalStatus(context, projectId, projectRoot, protocolId, state) {
|
|
978
1002
|
if (state) {
|
|
979
|
-
|
|
1003
|
+
const lifecycle = normalizeProtocolLifecycle({ state });
|
|
1004
|
+
if (lifecycleIsTerminalSuccess(lifecycle)) {
|
|
980
1005
|
return { terminal: true, success: true, source: "runtime_state", status: state.status, stage: state.stage, reason: "runtime protocol is closed" };
|
|
981
1006
|
}
|
|
982
|
-
if (
|
|
1007
|
+
if (lifecycleIsTerminalFailure(lifecycle)) {
|
|
983
1008
|
return { terminal: true, success: false, source: "runtime_state", status: state.status, stage: state.stage, reason: "runtime protocol is cancelled" };
|
|
984
1009
|
}
|
|
985
1010
|
}
|
|
@@ -1089,6 +1114,7 @@ function objectOrExisting(value, existing) {
|
|
|
1089
1114
|
return value && typeof value === "object" && !Array.isArray(value) ? value : existing;
|
|
1090
1115
|
}
|
|
1091
1116
|
function protocolStatusPayload(protocol) {
|
|
1117
|
+
const lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
1092
1118
|
return {
|
|
1093
1119
|
id: protocol.id,
|
|
1094
1120
|
handshake_id: protocol.handshake_id,
|
|
@@ -1096,6 +1122,7 @@ function protocolStatusPayload(protocol) {
|
|
|
1096
1122
|
project_root: protocol.project_root,
|
|
1097
1123
|
status: protocol.status,
|
|
1098
1124
|
stage: protocol.stage,
|
|
1125
|
+
lifecycle,
|
|
1099
1126
|
next_action: protocol.next_action,
|
|
1100
1127
|
route: JSON.parse(protocol.route_json),
|
|
1101
1128
|
workspace: JSON.parse(protocol.workspace_json),
|
|
@@ -5,6 +5,7 @@ import { AppError } from "../shared/errors.js";
|
|
|
5
5
|
import { parseJsonObject } from "../shared/json.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
7
|
import { appendAudit } from "./audit.js";
|
|
8
|
+
import { cancelLaneWaitersForWorker } from "./lanes.js";
|
|
8
9
|
export function registerFlowSession(context, input) {
|
|
9
10
|
const payload = decodeFlowSessionPayload(input);
|
|
10
11
|
const project = requireProjectByRoot(context, resolveProjectRoot(payload.project_root));
|
|
@@ -44,16 +45,21 @@ export function stopMergeWorker(context, input) {
|
|
|
44
45
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
45
46
|
const sessions = flowSessionsForProject(context, project.id, { workerId: input.workerId }).filter((session) => session.flow_kind === "merge_worker" && ["active", "pending", "stopping"].includes(session.status));
|
|
46
47
|
let lock_release = { ok: true, released: false, reason: "no_active_session" };
|
|
48
|
+
let waiter_cancellation = { ok: true, cancelled: 0 };
|
|
47
49
|
for (const session of sessions) {
|
|
48
|
-
|
|
50
|
+
const stopped = markSessionStopped(context, project, session, input.reason);
|
|
51
|
+
if (isStopResult(stopped)) {
|
|
52
|
+
lock_release = stopped.lock_release ?? lock_release;
|
|
53
|
+
waiter_cancellation = stopped.waiter_cancellation ?? waiter_cancellation;
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
appendAudit(context, {
|
|
51
57
|
projectId: project.id,
|
|
52
58
|
eventType: "flow_session.merge_worker_stopped",
|
|
53
59
|
reason: input.reason,
|
|
54
|
-
payload: { project_id: project.id, worker_id: input.workerId, stopped_sessions: sessions.length, lock_release }
|
|
60
|
+
payload: { project_id: project.id, worker_id: input.workerId, stopped_sessions: sessions.length, lock_release, waiter_cancellation }
|
|
55
61
|
});
|
|
56
|
-
return { ok: true, stopped_sessions: sessions.length, lock_release };
|
|
62
|
+
return { ok: true, stopped_sessions: sessions.length, lock_release, waiter_cancellation };
|
|
57
63
|
}
|
|
58
64
|
export function stoppedMergeWorkerState(context, projectId, workerId) {
|
|
59
65
|
const latest = context.db.get(`SELECT status, stop_reason FROM flow_sessions
|
|
@@ -268,6 +274,14 @@ function markSessionStopped(context, project, session, reason) {
|
|
|
268
274
|
SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
|
|
269
275
|
WHERE project_id = ? AND session_id = ?`, [reason, now, now, project.id, session.session_id]);
|
|
270
276
|
let lockRelease = undefined;
|
|
277
|
+
let waiterCancellation = undefined;
|
|
278
|
+
if (session.worker_id) {
|
|
279
|
+
waiterCancellation = cancelLaneWaitersForWorker(context, {
|
|
280
|
+
projectRoot: project.root,
|
|
281
|
+
workerId: session.worker_id,
|
|
282
|
+
reason
|
|
283
|
+
});
|
|
284
|
+
}
|
|
271
285
|
if ((session.flow_kind === "merge_worker" || session.flow_kind === "merge_job") && session.worker_id) {
|
|
272
286
|
lockRelease = releaseMergeLockIfOwned(context, project.root, session.worker_id, reason);
|
|
273
287
|
}
|
|
@@ -278,7 +292,10 @@ function markSessionStopped(context, project, session, reason) {
|
|
|
278
292
|
payload: { project_id: project.id, session_id: session.session_id, flow_kind: session.flow_kind },
|
|
279
293
|
...(session.protocol_id ? { protocolId: session.protocol_id } : {})
|
|
280
294
|
});
|
|
281
|
-
return lockRelease;
|
|
295
|
+
return { lock_release: lockRelease, waiter_cancellation: waiterCancellation };
|
|
296
|
+
}
|
|
297
|
+
function isStopResult(value) {
|
|
298
|
+
return Boolean(value && typeof value === "object" && ("lock_release" in value || "waiter_cancellation" in value));
|
|
282
299
|
}
|
|
283
300
|
function releaseMergeLockIfOwned(context, projectRoot, workerId, reason) {
|
|
284
301
|
const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
|
package/dist/services/status.js
CHANGED
|
@@ -5,6 +5,9 @@ import { getCanonStatus } from "./canon.js";
|
|
|
5
5
|
import { getCliBuildInfo } from "./build-info.js";
|
|
6
6
|
import { getProjectVersionStatus, resolveStatusProjectRoot } from "./version-status.js";
|
|
7
7
|
import { findProjectByRoot } from "./projects.js";
|
|
8
|
+
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
9
|
+
import { compatibilityReport } from "./compatibility-preflight.js";
|
|
10
|
+
import { selectEngine } from "./engines.js";
|
|
8
11
|
export function getRuntimeStatus(context, input = {}) {
|
|
9
12
|
const cwd = process.cwd();
|
|
10
13
|
const projectRootResolution = resolveStatusProjectRoot({ requestedRoot: input.projectRoot, cwd });
|
|
@@ -21,6 +24,7 @@ export function getRuntimeStatus(context, input = {}) {
|
|
|
21
24
|
const cli = getCliBuildInfo();
|
|
22
25
|
const compatibilityManifest = resolvedCanonForProject ? readCompatibilityManifest(canon) : null;
|
|
23
26
|
const cliCompatibility = cliCompatibilityVerdict(cli, compatibilityManifest);
|
|
27
|
+
const engineSelection = projectRoot ? selectEngine(context, { projectRoot }) : null;
|
|
24
28
|
const registry = input.checkRegistry ? checkNpmRegistry(context, cli.package_name) : undefined;
|
|
25
29
|
return {
|
|
26
30
|
ok: true,
|
|
@@ -43,6 +47,12 @@ export function getRuntimeStatus(context, input = {}) {
|
|
|
43
47
|
flow_pack: projectVersionStatus?.flow_pack ?? null,
|
|
44
48
|
drift: projectVersionStatus?.drift ?? null
|
|
45
49
|
},
|
|
50
|
+
engine: engineSelection
|
|
51
|
+
? {
|
|
52
|
+
selection: engineSelection,
|
|
53
|
+
compatibility: compatibilityReport(engineSelection, classifyCliOperation(["status"]))
|
|
54
|
+
}
|
|
55
|
+
: null,
|
|
46
56
|
canon: {
|
|
47
57
|
...(asRecord(canonStatus) ?? {}),
|
|
48
58
|
resolved: canon ?? null,
|
|
@@ -22,7 +22,8 @@ export function getProjectVersionStatus(input) {
|
|
|
22
22
|
const flowPack = readFlowPackMetadata(input.projectRoot, memoryBank.root);
|
|
23
23
|
const memoryBankDrift = compareMemoryBankVersion(memoryBank.version, input.canon?.metadata.version ?? null, memoryBank.status);
|
|
24
24
|
const flowPackDrift = compareFlowPackCommit(flowPack.source_commit, input.canon?.metadata.commit ?? null, input.canon?.root ?? null, flowPack.status);
|
|
25
|
-
const
|
|
25
|
+
const flowContractDrift = compareFlowContract(flowPack.flow_contract, input.canon?.metadata.flow_contract ?? null, flowPack.status);
|
|
26
|
+
const overall = overallDrift(memoryBankDrift, flowPackDrift, flowContractDrift, memoryBank.status, flowPack.status);
|
|
26
27
|
return {
|
|
27
28
|
root: input.projectRoot,
|
|
28
29
|
root_source: input.rootSource,
|
|
@@ -31,8 +32,9 @@ export function getProjectVersionStatus(input) {
|
|
|
31
32
|
drift: {
|
|
32
33
|
memory_bank_version: memoryBankDrift,
|
|
33
34
|
flow_pack_commit: flowPackDrift,
|
|
35
|
+
flow_contract: flowContractDrift,
|
|
34
36
|
overall,
|
|
35
|
-
next_action: nextAction(overall, memoryBank.status, flowPack.status)
|
|
37
|
+
next_action: nextAction(overall, memoryBank.status, flowPack.status, flowContractDrift.status)
|
|
36
38
|
}
|
|
37
39
|
};
|
|
38
40
|
}
|
|
@@ -67,6 +69,7 @@ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
|
|
|
67
69
|
canon_flow_root: null,
|
|
68
70
|
source_commit: null,
|
|
69
71
|
canon_version_at_source_commit: null,
|
|
72
|
+
flow_contract: null,
|
|
70
73
|
status: "missing",
|
|
71
74
|
reason: "no_memory_bank",
|
|
72
75
|
canonical_only_files: []
|
|
@@ -85,6 +88,7 @@ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
|
|
|
85
88
|
canon_flow_root: null,
|
|
86
89
|
source_commit: null,
|
|
87
90
|
canon_version_at_source_commit: null,
|
|
91
|
+
flow_contract: null,
|
|
88
92
|
status: "missing",
|
|
89
93
|
reason: "flow_pack_manifest_missing",
|
|
90
94
|
canonical_only_files: canonicalOnlyFiles
|
|
@@ -107,6 +111,7 @@ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
|
|
|
107
111
|
canon_flow_root: null,
|
|
108
112
|
source_commit: null,
|
|
109
113
|
canon_version_at_source_commit: null,
|
|
114
|
+
flow_contract: null,
|
|
110
115
|
status: "invalid",
|
|
111
116
|
reason: "flow_pack_manifest_invalid_json",
|
|
112
117
|
canonical_only_files: canonicalOnlyFiles
|
|
@@ -118,19 +123,22 @@ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
|
|
|
118
123
|
const canonRoot = stringOrNull(manifest.canon_root);
|
|
119
124
|
const canonMemoryBankRoot = stringOrNull(manifest.canon_memory_bank_root);
|
|
120
125
|
const canonFlowRoot = stringOrNull(manifest.canon_flow_root);
|
|
121
|
-
const
|
|
126
|
+
const flowContract = stringOrNull(manifest.flow_contract);
|
|
127
|
+
const status = schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion && canonMemoryBankRoot && canonFlowRoot && flowContract
|
|
122
128
|
? "present"
|
|
123
129
|
: schemaId === "dd-flow/project-flow-pack-manifest@1" ||
|
|
124
|
-
(schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion && (!canonMemoryBankRoot || !canonFlowRoot))
|
|
130
|
+
(schemaId === "dd-flow/project-flow-pack-manifest@2" && canonVersion && (!canonMemoryBankRoot || !canonFlowRoot || !flowContract))
|
|
125
131
|
? "degraded"
|
|
126
132
|
: "invalid";
|
|
127
|
-
const reason = status === "degraded" && schemaId === "dd-flow/project-flow-pack-manifest@2"
|
|
133
|
+
const reason = status === "degraded" && schemaId === "dd-flow/project-flow-pack-manifest@2" && (!canonMemoryBankRoot || !canonFlowRoot)
|
|
128
134
|
? "flow_pack_manifest_missing_explicit_canon_roots"
|
|
129
|
-
: status === "degraded"
|
|
130
|
-
? "
|
|
131
|
-
: status === "
|
|
132
|
-
? "
|
|
133
|
-
:
|
|
135
|
+
: status === "degraded" && schemaId === "dd-flow/project-flow-pack-manifest@2" && !flowContract
|
|
136
|
+
? "flow_pack_manifest_missing_flow_contract"
|
|
137
|
+
: status === "degraded"
|
|
138
|
+
? "legacy_flow_pack_manifest"
|
|
139
|
+
: status === "invalid"
|
|
140
|
+
? "flow_pack_manifest_schema_unknown"
|
|
141
|
+
: undefined;
|
|
134
142
|
return {
|
|
135
143
|
manifest_path: path.relative(projectRoot, manifestPath),
|
|
136
144
|
schema_id: schemaId,
|
|
@@ -140,6 +148,7 @@ function readFlowPackMetadata(projectRoot, memoryBankRoot) {
|
|
|
140
148
|
canon_flow_root: canonFlowRoot,
|
|
141
149
|
source_commit: sourceCommit,
|
|
142
150
|
canon_version_at_source_commit: canonVersion,
|
|
151
|
+
flow_contract: flowContract,
|
|
143
152
|
status,
|
|
144
153
|
...(reason ? { reason } : {}),
|
|
145
154
|
canonical_only_files: canonicalOnlyFiles
|
|
@@ -216,28 +225,43 @@ function compareFlowPackCommit(current, target, canonRoot, artifactStatus) {
|
|
|
216
225
|
target
|
|
217
226
|
};
|
|
218
227
|
}
|
|
219
|
-
function
|
|
228
|
+
function compareFlowContract(current, target, artifactStatus) {
|
|
229
|
+
if (artifactStatus === "missing")
|
|
230
|
+
return { status: "missing", confidence: "none", reason: "flow_pack_manifest_missing", current, target };
|
|
231
|
+
if (artifactStatus === "invalid")
|
|
232
|
+
return { status: "invalid", confidence: "none", reason: "flow_pack_manifest_invalid", current, target };
|
|
233
|
+
if (!current)
|
|
234
|
+
return { status: "unknown", confidence: "none", reason: "flow_pack_flow_contract_missing", current, target };
|
|
235
|
+
if (!target)
|
|
236
|
+
return { status: "unknown", confidence: "none", reason: "canon_flow_contract_unknown", current, target };
|
|
237
|
+
if (current === target)
|
|
238
|
+
return { status: "same", confidence: "exact", reason: "flow_contract_equal", current, target };
|
|
239
|
+
return { status: "diverged", confidence: "declared", reason: "flow_contract_mismatch", current, target };
|
|
240
|
+
}
|
|
241
|
+
function overallDrift(memoryBank, flowPack, flowContract, memoryStatus, flowStatus) {
|
|
220
242
|
if (memoryStatus === "missing")
|
|
221
243
|
return "not_applicable";
|
|
222
|
-
if (memoryBank.status === "invalid" || flowPack.status === "invalid")
|
|
244
|
+
if (memoryBank.status === "invalid" || flowPack.status === "invalid" || flowContract.status === "invalid")
|
|
223
245
|
return "invalid";
|
|
224
|
-
if (memoryBank.status === "missing" || flowPack.status === "missing")
|
|
246
|
+
if (memoryBank.status === "missing" || flowPack.status === "missing" || flowContract.status === "missing")
|
|
225
247
|
return "missing";
|
|
226
248
|
if (memoryBank.status === "ahead" || flowPack.status === "ahead")
|
|
227
249
|
return "ahead";
|
|
228
250
|
if (memoryBank.status === "behind" || flowPack.status === "behind")
|
|
229
251
|
return "behind";
|
|
230
|
-
if (memoryBank.status === "diverged" || flowPack.status === "diverged")
|
|
252
|
+
if (memoryBank.status === "diverged" || flowPack.status === "diverged" || flowContract.status === "diverged")
|
|
231
253
|
return "diverged";
|
|
232
254
|
if (memoryBank.status === "same" && (flowPack.status === "same" || flowStatus === "degraded"))
|
|
233
255
|
return flowStatus === "degraded" ? "unknown" : "same";
|
|
234
256
|
return "unknown";
|
|
235
257
|
}
|
|
236
|
-
function nextAction(overall, memoryStatus, flowStatus) {
|
|
258
|
+
function nextAction(overall, memoryStatus, flowStatus, flowContractStatus) {
|
|
237
259
|
if (overall === "same")
|
|
238
260
|
return "no_action";
|
|
239
261
|
if (overall === "not_applicable" && memoryStatus === "missing")
|
|
240
262
|
return "run_mb_init_if_project_needs_memory_bank";
|
|
263
|
+
if (flowContractStatus === "diverged" || flowContractStatus === "unknown")
|
|
264
|
+
return "run_canonical_mb_upgrade_before_lifecycle_mutations";
|
|
241
265
|
if (overall === "behind" || flowStatus === "degraded")
|
|
242
266
|
return "run_canonical_mb_upgrade";
|
|
243
267
|
if (overall === "missing")
|
package/dist/storage/database.js
CHANGED
|
@@ -134,10 +134,34 @@ function migrate(db) {
|
|
|
134
134
|
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
135
135
|
);
|
|
136
136
|
|
|
137
|
+
CREATE TABLE IF NOT EXISTS lane_waiters (
|
|
138
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
+
project_id TEXT NOT NULL,
|
|
140
|
+
lane TEXT NOT NULL,
|
|
141
|
+
worker_id TEXT NOT NULL,
|
|
142
|
+
status TEXT NOT NULL,
|
|
143
|
+
queued_at TEXT NOT NULL,
|
|
144
|
+
acquired_at TEXT,
|
|
145
|
+
cancelled_at TEXT,
|
|
146
|
+
expires_at TEXT,
|
|
147
|
+
reason TEXT,
|
|
148
|
+
metadata_json TEXT NOT NULL,
|
|
149
|
+
created_at TEXT NOT NULL,
|
|
150
|
+
updated_at TEXT NOT NULL,
|
|
151
|
+
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
152
|
+
);
|
|
153
|
+
|
|
137
154
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_lane_locks_one_active
|
|
138
155
|
ON lane_locks(project_id, lane)
|
|
139
156
|
WHERE status = 'active';
|
|
140
157
|
|
|
158
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_lane_waiters_one_queued_worker
|
|
159
|
+
ON lane_waiters(project_id, lane, worker_id)
|
|
160
|
+
WHERE status = 'queued';
|
|
161
|
+
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_lane_waiters_fifo
|
|
163
|
+
ON lane_waiters(project_id, lane, status, queued_at, id);
|
|
164
|
+
|
|
141
165
|
CREATE TABLE IF NOT EXISTS hook_installations (
|
|
142
166
|
project_id TEXT NOT NULL,
|
|
143
167
|
scope TEXT NOT NULL,
|
|
@@ -324,6 +348,7 @@ function migrate(db) {
|
|
|
324
348
|
ensureColumn(db, "flow_runs", "run_home_path", "ALTER TABLE flow_runs ADD COLUMN run_home_path TEXT");
|
|
325
349
|
ensureColumn(db, "flow_runs", "layout_version", "ALTER TABLE flow_runs ADD COLUMN layout_version TEXT");
|
|
326
350
|
ensureColumn(db, "flow_runs", "artifact_root_kind", "ALTER TABLE flow_runs ADD COLUMN artifact_root_kind TEXT");
|
|
351
|
+
ensureColumn(db, "lane_waiters", "expires_at", "ALTER TABLE lane_waiters ADD COLUMN expires_at TEXT");
|
|
327
352
|
}
|
|
328
353
|
function ensureColumn(db, table, column, sql) {
|
|
329
354
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
package/dist/storage/paths.js
CHANGED
|
@@ -75,3 +75,15 @@ export function projectCheckoutRoot(ddFlowHome, projectId) {
|
|
|
75
75
|
export function projectFeatureWorktreePath(ddFlowHome, projectId, runOrProtocolId, repoName) {
|
|
76
76
|
return path.join(projectCheckoutRoot(ddFlowHome, projectId), "worktrees", runOrProtocolId, repoName);
|
|
77
77
|
}
|
|
78
|
+
export function engineStoreRoot(ddFlowHome) {
|
|
79
|
+
return path.join(ddFlowHome, "engines");
|
|
80
|
+
}
|
|
81
|
+
export function packageEngineRoot(ddFlowHome, packageName) {
|
|
82
|
+
return path.join(engineStoreRoot(ddFlowHome), safePathSegment(packageName));
|
|
83
|
+
}
|
|
84
|
+
export function engineVersionRoot(ddFlowHome, packageName, version) {
|
|
85
|
+
return path.join(packageEngineRoot(ddFlowHome, packageName), safePathSegment(version));
|
|
86
|
+
}
|
|
87
|
+
function safePathSegment(value) {
|
|
88
|
+
return value.replace(/[^a-zA-Z0-9_.@-]+/g, "_");
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=26.0.0",
|