@deksden-com/dd-flow-cli 0.3.0 → 0.3.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 +43 -0
- package/README.md +74 -5
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +103 -15
- package/dist/cli/run-cli.js +307 -27
- 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/cleanup.js +31 -0
- package/dist/services/cli-operation-classifier.js +104 -0
- package/dist/services/compatibility-preflight.js +124 -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 +97 -15
- package/dist/services/merge-worker.js +36 -2
- 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 +29 -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,7 @@ 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";
|
|
17
18
|
export function registerProtocol(context, input) {
|
|
18
19
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
19
20
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -88,10 +89,12 @@ export function getProtocolStatus(context, input) {
|
|
|
88
89
|
const plan = readPlanFile(protocol.plan_path);
|
|
89
90
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
90
91
|
const queue = context.db.get("SELECT * FROM merge_queue WHERE protocol_id = ?", [protocol.id]);
|
|
92
|
+
const lifecycle = normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null });
|
|
91
93
|
return {
|
|
92
94
|
ok: true,
|
|
93
|
-
protocol: protocolStatusPayload(protocol),
|
|
94
|
-
|
|
95
|
+
protocol: { ...protocolStatusPayload(protocol), lifecycle },
|
|
96
|
+
lifecycle,
|
|
97
|
+
diagnostics: [...runtime.diagnostics, ...runDiagnostics.diagnostics, ...lifecycle.diagnostics],
|
|
95
98
|
latest_run: runDiagnostics.latest_run,
|
|
96
99
|
state,
|
|
97
100
|
plan: plan ? summarizePlan(plan) : state.plan,
|
|
@@ -173,6 +176,9 @@ export function implementProtocol(context, input) {
|
|
|
173
176
|
if (input.force && (!input.reason || input.reason.trim().length === 0)) {
|
|
174
177
|
throw new AppError("validation", "Forced protocol implement preflight requires --reason", 2);
|
|
175
178
|
}
|
|
179
|
+
const lifecycle = protocol
|
|
180
|
+
? normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null })
|
|
181
|
+
: normalizeProtocolLifecycle({ rawStage: "unregistered", rawStatus: "missing" });
|
|
176
182
|
const currentStage = state?.stage ?? "unregistered";
|
|
177
183
|
const recommendedPrompt = promptForImplementationStage(currentStage);
|
|
178
184
|
return {
|
|
@@ -197,6 +203,7 @@ export function implementProtocol(context, input) {
|
|
|
197
203
|
blockers,
|
|
198
204
|
active_sessions: activeSessions,
|
|
199
205
|
merge_queue: queue ?? null,
|
|
206
|
+
lifecycle,
|
|
200
207
|
terminal,
|
|
201
208
|
related_context: document.related_context,
|
|
202
209
|
coding_standards_sources: document.coding_standards_sources,
|
|
@@ -274,6 +281,7 @@ export function transitionProtocol(context, input) {
|
|
|
274
281
|
to,
|
|
275
282
|
forced: input.force,
|
|
276
283
|
state: nextState,
|
|
284
|
+
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: queue?.status ?? null }),
|
|
277
285
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null })
|
|
278
286
|
};
|
|
279
287
|
}
|
|
@@ -298,6 +306,7 @@ export function readyForMerge(context, input) {
|
|
|
298
306
|
protocol_id: protocol.id,
|
|
299
307
|
queue_status: existingQueueJob.status,
|
|
300
308
|
state,
|
|
309
|
+
lifecycle: normalizeProtocolLifecycle({ state, queueStatus: existingQueueJob.status }),
|
|
301
310
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: existingQueueJob.status })
|
|
302
311
|
};
|
|
303
312
|
}
|
|
@@ -349,6 +358,7 @@ export function readyForMerge(context, input) {
|
|
|
349
358
|
protocol_id: protocol.id,
|
|
350
359
|
queue_status: "ready",
|
|
351
360
|
state: nextState,
|
|
361
|
+
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: "ready" }),
|
|
352
362
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
353
363
|
};
|
|
354
364
|
}
|
|
@@ -909,11 +919,17 @@ function protocolSetMemberStatus(context, projectId, document) {
|
|
|
909
919
|
const projectRoot = projectRootFromProtocolFile(document.path);
|
|
910
920
|
const blockers = document.blocked_by_protocols.map((id) => blockerStatus(context, projectId, projectRoot, id));
|
|
911
921
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, document.id, state);
|
|
922
|
+
const lifecycle = normalizeProtocolLifecycle({
|
|
923
|
+
state,
|
|
924
|
+
rawStage: runtime?.stage ?? "unregistered",
|
|
925
|
+
rawStatus: runtime?.status ?? "missing",
|
|
926
|
+
queueStatus: queue?.status ?? null
|
|
927
|
+
});
|
|
912
928
|
const setStatus = terminal.success
|
|
913
929
|
? "done"
|
|
914
930
|
: queue?.status === "claimed" || activeSessions.length > 0
|
|
915
931
|
? "claimed"
|
|
916
|
-
: state && ["
|
|
932
|
+
: state && ["specify", "plan", "code", "merge"].includes(lifecycle.stage)
|
|
917
933
|
? "running"
|
|
918
934
|
: blockers.some((blocker) => !blocker.resolved)
|
|
919
935
|
? "blocked"
|
|
@@ -926,8 +942,9 @@ function protocolSetMemberStatus(context, projectId, document) {
|
|
|
926
942
|
blocked_by_protocols: document.blocked_by_protocols,
|
|
927
943
|
blockers,
|
|
928
944
|
runtime: runtime
|
|
929
|
-
? { registered: true, status: runtime.status, stage: runtime.stage, next_action: runtime.next_action, updated_at: runtime.updated_at }
|
|
945
|
+
? { registered: true, status: runtime.status, stage: runtime.stage, lifecycle, next_action: runtime.next_action, updated_at: runtime.updated_at }
|
|
930
946
|
: { registered: false, status: "missing", stage: "unregistered", next_action: "register_protocol" },
|
|
947
|
+
lifecycle,
|
|
931
948
|
active_sessions: activeSessions,
|
|
932
949
|
merge_queue: queue ?? null,
|
|
933
950
|
terminal
|
|
@@ -964,22 +981,25 @@ function blockerStatus(context, projectId, projectRoot, blockerId) {
|
|
|
964
981
|
const runtime = findProtocol(context, blockerId);
|
|
965
982
|
const state = runtime ? readProtocolRuntimeState(context, runtime).state : null;
|
|
966
983
|
const terminal = protocolTerminalStatus(context, projectId, projectRoot, blockerId, state);
|
|
984
|
+
const lifecycle = normalizeProtocolLifecycle({ state, rawStage: runtime?.stage, rawStatus: runtime?.status });
|
|
967
985
|
return {
|
|
968
986
|
id: blockerId,
|
|
969
987
|
resolved: terminal.success,
|
|
970
988
|
source: terminal.source,
|
|
971
989
|
status: runtime?.status ?? terminal.status ?? "unknown",
|
|
972
990
|
stage: runtime?.stage ?? terminal.stage ?? "unknown",
|
|
973
|
-
lifecycle
|
|
991
|
+
lifecycle,
|
|
992
|
+
document_lifecycle: terminal.lifecycle,
|
|
974
993
|
reason: terminal.reason
|
|
975
994
|
};
|
|
976
995
|
}
|
|
977
996
|
function protocolTerminalStatus(context, projectId, projectRoot, protocolId, state) {
|
|
978
997
|
if (state) {
|
|
979
|
-
|
|
998
|
+
const lifecycle = normalizeProtocolLifecycle({ state });
|
|
999
|
+
if (lifecycleIsTerminalSuccess(lifecycle)) {
|
|
980
1000
|
return { terminal: true, success: true, source: "runtime_state", status: state.status, stage: state.stage, reason: "runtime protocol is closed" };
|
|
981
1001
|
}
|
|
982
|
-
if (
|
|
1002
|
+
if (lifecycleIsTerminalFailure(lifecycle)) {
|
|
983
1003
|
return { terminal: true, success: false, source: "runtime_state", status: state.status, stage: state.stage, reason: "runtime protocol is cancelled" };
|
|
984
1004
|
}
|
|
985
1005
|
}
|
|
@@ -1089,6 +1109,7 @@ function objectOrExisting(value, existing) {
|
|
|
1089
1109
|
return value && typeof value === "object" && !Array.isArray(value) ? value : existing;
|
|
1090
1110
|
}
|
|
1091
1111
|
function protocolStatusPayload(protocol) {
|
|
1112
|
+
const lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
1092
1113
|
return {
|
|
1093
1114
|
id: protocol.id,
|
|
1094
1115
|
handshake_id: protocol.handshake_id,
|
|
@@ -1096,6 +1117,7 @@ function protocolStatusPayload(protocol) {
|
|
|
1096
1117
|
project_root: protocol.project_root,
|
|
1097
1118
|
status: protocol.status,
|
|
1098
1119
|
stage: protocol.stage,
|
|
1120
|
+
lifecycle,
|
|
1099
1121
|
next_action: protocol.next_action,
|
|
1100
1122
|
route: JSON.parse(protocol.route_json),
|
|
1101
1123
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|