@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
|
@@ -4,8 +4,9 @@ import { appendAudit } from "./audit.js";
|
|
|
4
4
|
import { requireProjectByRoot } from "./projects.js";
|
|
5
5
|
import { persistProtocolState, protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
8
8
|
import { stopMergeWorker, stoppedMergeWorkerState } from "./sessions.js";
|
|
9
|
+
import { getProtocolBranchContext, requireClaimableBranchBundle } from "./branch-context.js";
|
|
9
10
|
const mergeLockTtlSeconds = 300;
|
|
10
11
|
export function getMergeQueueStatus(context, input) {
|
|
11
12
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -14,6 +15,9 @@ export function getMergeQueueStatus(context, input) {
|
|
|
14
15
|
queue: queueForProject(context, project.id)
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
export function getMergeBundleStatus(context, input) {
|
|
19
|
+
return getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
20
|
+
}
|
|
17
21
|
export function claimNextMergeJob(context, input) {
|
|
18
22
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
19
23
|
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
@@ -40,14 +44,14 @@ export function claimNextMergeJob(context, input) {
|
|
|
40
44
|
LIMIT 1`, [project.id]);
|
|
41
45
|
if (!job) {
|
|
42
46
|
context.db.exec("COMMIT");
|
|
43
|
-
return { ok: true,
|
|
47
|
+
return mergeQueueResult(null, { ok: true, outcome: "empty", claimed: false });
|
|
44
48
|
}
|
|
45
49
|
const update = context.db.run(`UPDATE merge_queue
|
|
46
50
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
47
51
|
WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
|
|
48
52
|
if (update.changes !== 1) {
|
|
49
53
|
context.db.exec("COMMIT");
|
|
50
|
-
return { ok: true,
|
|
54
|
+
return mergeQueueResult(null, { ok: true, outcome: "empty", claimed: false });
|
|
51
55
|
}
|
|
52
56
|
appendAudit(context, {
|
|
53
57
|
protocolId: job.protocol_id,
|
|
@@ -63,7 +67,72 @@ export function claimNextMergeJob(context, input) {
|
|
|
63
67
|
context.db.exec("ROLLBACK");
|
|
64
68
|
throw error;
|
|
65
69
|
}
|
|
66
|
-
return
|
|
70
|
+
return mergeQueueResult(claimedProtocolId ? queueJobByProtocol(context, claimedProtocolId) : null, {
|
|
71
|
+
ok: true,
|
|
72
|
+
outcome: claimedProtocolId ? "claimed" : "empty",
|
|
73
|
+
claimed: Boolean(claimedProtocolId)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function claimMergeBundle(context, input) {
|
|
77
|
+
const { project, protocolIds } = requireClaimableBranchBundle(context, {
|
|
78
|
+
projectRoot: input.projectRoot,
|
|
79
|
+
workspacePath: input.workspacePath
|
|
80
|
+
});
|
|
81
|
+
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
82
|
+
if (stopState.stopped) {
|
|
83
|
+
throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim a merge bundle", 1, {
|
|
84
|
+
worker_id: input.workerId,
|
|
85
|
+
status: stopState.status,
|
|
86
|
+
reason: stopState.reason
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
requireLaneLockOwner(context, {
|
|
90
|
+
projectRoot: project.root,
|
|
91
|
+
lane: "merge",
|
|
92
|
+
workerId: input.workerId,
|
|
93
|
+
workspacePath: input.workspacePath
|
|
94
|
+
});
|
|
95
|
+
const now = context.now();
|
|
96
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
97
|
+
try {
|
|
98
|
+
for (const protocolId of protocolIds) {
|
|
99
|
+
const update = context.db.run(`UPDATE merge_queue
|
|
100
|
+
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
101
|
+
WHERE protocol_id = ? AND project_id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, protocolId, project.id]);
|
|
102
|
+
if (update.changes !== 1) {
|
|
103
|
+
throw new AppError("merge_bundle_claim_race", "Branch bundle changed while claiming", 1, {
|
|
104
|
+
protocol_id: protocolId,
|
|
105
|
+
protocol_ids: protocolIds
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
transitionClaimedProtocolToIntegration(context, protocolId, input.workerId, now);
|
|
109
|
+
appendAudit(context, {
|
|
110
|
+
protocolId,
|
|
111
|
+
projectId: project.id,
|
|
112
|
+
eventType: "merge_bundle.claimed_protocol",
|
|
113
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, protocol_ids: protocolIds }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
appendAudit(context, {
|
|
117
|
+
projectId: project.id,
|
|
118
|
+
eventType: "merge_bundle.claimed",
|
|
119
|
+
payload: { project_id: project.id, worker_id: input.workerId, protocol_ids: protocolIds, workspace_path: input.workspacePath }
|
|
120
|
+
});
|
|
121
|
+
context.db.exec("COMMIT");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
context.db.exec("ROLLBACK");
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return mergeBundleResult(context, {
|
|
128
|
+
ok: true,
|
|
129
|
+
outcome: "claimed",
|
|
130
|
+
claimed: true,
|
|
131
|
+
projectRoot: project.root,
|
|
132
|
+
workspacePath: input.workspacePath,
|
|
133
|
+
workerId: input.workerId,
|
|
134
|
+
protocolIds
|
|
135
|
+
});
|
|
67
136
|
}
|
|
68
137
|
export async function waitNextMergeJob(context, input) {
|
|
69
138
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -82,19 +151,43 @@ export async function waitNextMergeJob(context, input) {
|
|
|
82
151
|
}
|
|
83
152
|
const initialStopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
84
153
|
if (initialStopState.stopped) {
|
|
85
|
-
return
|
|
154
|
+
return mergeQueueResult(null, {
|
|
155
|
+
ok: true,
|
|
156
|
+
outcome: "stopped",
|
|
157
|
+
wait_requested: input.acquireLock,
|
|
158
|
+
waited: false,
|
|
159
|
+
timed_out: false,
|
|
160
|
+
stopped: true,
|
|
161
|
+
reason: initialStopState.reason
|
|
162
|
+
});
|
|
86
163
|
}
|
|
87
164
|
let releaseLockOnTimeout = false;
|
|
88
165
|
if (input.acquireLock) {
|
|
89
166
|
ensureLaneWorkspace(context, { projectRoot: project.root, lane: "merge", workspacePath: input.workspacePath });
|
|
90
|
-
const lockResult =
|
|
167
|
+
const lockResult = (await waitAcquireLaneLock(context, {
|
|
91
168
|
projectRoot: project.root,
|
|
92
169
|
lane: "merge",
|
|
93
170
|
workerId: input.workerId,
|
|
94
171
|
workspacePath: input.workspacePath,
|
|
95
172
|
ttlSeconds: mergeLockTtlSeconds,
|
|
173
|
+
timeoutSeconds,
|
|
174
|
+
pollIntervalSeconds,
|
|
96
175
|
reason: "merge queue wait-next"
|
|
97
|
-
});
|
|
176
|
+
}));
|
|
177
|
+
if (!lockResult.acquired) {
|
|
178
|
+
const outcome = lockResult.timed_out ? "wait_timeout" : "blocked";
|
|
179
|
+
return mergeQueueResult(null, {
|
|
180
|
+
ok: true,
|
|
181
|
+
outcome,
|
|
182
|
+
wait_requested: true,
|
|
183
|
+
waited: true,
|
|
184
|
+
timed_out: Boolean(lockResult.timed_out),
|
|
185
|
+
lane_waiter: lockResult.waiter ?? null,
|
|
186
|
+
lane_lock: lockResult.lock ?? null,
|
|
187
|
+
lane_waiter_status: lockResult.status ?? null,
|
|
188
|
+
position: lockResult.position ?? null
|
|
189
|
+
});
|
|
190
|
+
}
|
|
98
191
|
releaseLockOnTimeout = !lockResult.reused;
|
|
99
192
|
}
|
|
100
193
|
else {
|
|
@@ -124,7 +217,15 @@ export async function waitNextMergeJob(context, input) {
|
|
|
124
217
|
...(stopState.reason ? { reason: stopState.reason } : {}),
|
|
125
218
|
payload: { project_id: project.id, worker_id: input.workerId }
|
|
126
219
|
});
|
|
127
|
-
return
|
|
220
|
+
return mergeQueueResult(null, {
|
|
221
|
+
ok: true,
|
|
222
|
+
outcome: "stopped",
|
|
223
|
+
wait_requested: input.acquireLock,
|
|
224
|
+
waited: true,
|
|
225
|
+
timed_out: false,
|
|
226
|
+
stopped: true,
|
|
227
|
+
reason: stopState.reason
|
|
228
|
+
});
|
|
128
229
|
}
|
|
129
230
|
if (input.acquireLock) {
|
|
130
231
|
heartbeatLaneLock(context, {
|
|
@@ -141,7 +242,14 @@ export async function waitNextMergeJob(context, input) {
|
|
|
141
242
|
workspacePath: input.workspacePath
|
|
142
243
|
});
|
|
143
244
|
if (claimed.job) {
|
|
144
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
...claimed,
|
|
247
|
+
outcome: "claimed",
|
|
248
|
+
claimed: true,
|
|
249
|
+
wait_requested: input.acquireLock,
|
|
250
|
+
waited: true,
|
|
251
|
+
timed_out: false
|
|
252
|
+
};
|
|
145
253
|
}
|
|
146
254
|
if (timeoutSeconds > 0 && Date.now() - start >= timeoutSeconds * 1000) {
|
|
147
255
|
appendAudit(context, {
|
|
@@ -158,7 +266,13 @@ export async function waitNextMergeJob(context, input) {
|
|
|
158
266
|
reason: "merge queue wait-next timeout"
|
|
159
267
|
});
|
|
160
268
|
}
|
|
161
|
-
return
|
|
269
|
+
return mergeQueueResult(null, {
|
|
270
|
+
ok: true,
|
|
271
|
+
outcome: "wait_timeout",
|
|
272
|
+
wait_requested: input.acquireLock,
|
|
273
|
+
waited: true,
|
|
274
|
+
timed_out: true
|
|
275
|
+
});
|
|
162
276
|
}
|
|
163
277
|
await delay(pollIntervalSeconds * 1000);
|
|
164
278
|
}
|
|
@@ -194,7 +308,78 @@ export function completeMergeJob(context, input) {
|
|
|
194
308
|
payload: { protocol_id: protocol.id, worker_id: input.workerId, summary: input.summary, flow_contract_id: flowContract.id }
|
|
195
309
|
});
|
|
196
310
|
const stop_after_current = stopAfterCurrentIfRequested(context, protocol.project_id, protocol.project_root, input.workerId, "merge complete after stop request");
|
|
197
|
-
return
|
|
311
|
+
return mergeQueueResult(queueJobByProtocol(context, protocol.id), {
|
|
312
|
+
ok: true,
|
|
313
|
+
outcome: "merged",
|
|
314
|
+
merged: true,
|
|
315
|
+
next_stage: targetStage,
|
|
316
|
+
stop_after_current
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
export function completeMergeBundle(context, input) {
|
|
320
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
321
|
+
requireLaneLockOwner(context, {
|
|
322
|
+
projectRoot: project.root,
|
|
323
|
+
lane: "merge",
|
|
324
|
+
workerId: input.workerId,
|
|
325
|
+
workspacePath: input.workspacePath
|
|
326
|
+
});
|
|
327
|
+
const protocolIds = claimedBundleProtocolIds(context, { projectRoot: project.root, workspacePath: input.workspacePath, workerId: input.workerId });
|
|
328
|
+
if (protocolIds.length === 0) {
|
|
329
|
+
throw new AppError("merge_bundle_not_claimed", "No claimed protocols found for this branch bundle and worker", 1, {
|
|
330
|
+
worker_id: input.workerId,
|
|
331
|
+
workspace_path: input.workspacePath
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const now = context.now();
|
|
335
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
336
|
+
try {
|
|
337
|
+
for (const protocolId of protocolIds) {
|
|
338
|
+
const protocol = requireProtocol(context, protocolId);
|
|
339
|
+
const job = requireClaimedJob(context, protocolId, input.workerId);
|
|
340
|
+
context.db.run(`UPDATE merge_queue
|
|
341
|
+
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
342
|
+
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
343
|
+
const state = readProtocolRuntimeState(context, protocol).state;
|
|
344
|
+
const flowContract = loadProjectFlowContract(protocol.project_root);
|
|
345
|
+
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
346
|
+
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
347
|
+
persistProtocolState(context, protocol, {
|
|
348
|
+
...state,
|
|
349
|
+
stage: targetStage,
|
|
350
|
+
status: targetStatus,
|
|
351
|
+
next_action: flowContract.merge_queue.complete.next_action,
|
|
352
|
+
updated_at: now
|
|
353
|
+
});
|
|
354
|
+
appendAudit(context, {
|
|
355
|
+
protocolId,
|
|
356
|
+
projectId: project.id,
|
|
357
|
+
eventType: "merge_bundle.completed_protocol",
|
|
358
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, summary: input.summary, protocol_ids: protocolIds }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
appendAudit(context, {
|
|
362
|
+
projectId: project.id,
|
|
363
|
+
eventType: "merge_bundle.completed",
|
|
364
|
+
payload: { project_id: project.id, worker_id: input.workerId, summary: input.summary, protocol_ids: protocolIds }
|
|
365
|
+
});
|
|
366
|
+
context.db.exec("COMMIT");
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
context.db.exec("ROLLBACK");
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle complete after stop request");
|
|
373
|
+
return mergeBundleResult(context, {
|
|
374
|
+
ok: true,
|
|
375
|
+
outcome: "merged",
|
|
376
|
+
merged: true,
|
|
377
|
+
projectRoot: project.root,
|
|
378
|
+
workspacePath: input.workspacePath,
|
|
379
|
+
workerId: input.workerId,
|
|
380
|
+
protocolIds,
|
|
381
|
+
stop_after_current
|
|
382
|
+
});
|
|
198
383
|
}
|
|
199
384
|
export function noteMergeJob(context, input) {
|
|
200
385
|
const protocol = requireProtocol(context, input.protocolId);
|
|
@@ -224,7 +409,7 @@ export function noteMergeJob(context, input) {
|
|
|
224
409
|
eventType: "merge_queue.note",
|
|
225
410
|
payload: { protocol_id: protocol.id, worker_id: input.workerId, summary: input.summary }
|
|
226
411
|
});
|
|
227
|
-
return { ok: true,
|
|
412
|
+
return mergeQueueResult(queueJobByProtocol(context, protocol.id), { ok: true, outcome: "noted" });
|
|
228
413
|
}
|
|
229
414
|
export function failMergeJob(context, input) {
|
|
230
415
|
const protocol = requireProtocol(context, input.protocolId);
|
|
@@ -258,7 +443,77 @@ export function failMergeJob(context, input) {
|
|
|
258
443
|
payload: { protocol_id: protocol.id, worker_id: input.workerId, requeue: input.requeue }
|
|
259
444
|
});
|
|
260
445
|
const stop_after_current = stopAfterCurrentIfRequested(context, protocol.project_id, protocol.project_root, input.workerId, "merge fail after stop request");
|
|
261
|
-
return
|
|
446
|
+
return mergeQueueResult(queueJobByProtocol(context, protocol.id), {
|
|
447
|
+
ok: true,
|
|
448
|
+
outcome: input.requeue ? "requeued" : "failed",
|
|
449
|
+
stop_after_current
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
export function failMergeBundle(context, input) {
|
|
453
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
454
|
+
requireLaneLockOwner(context, {
|
|
455
|
+
projectRoot: project.root,
|
|
456
|
+
lane: "merge",
|
|
457
|
+
workerId: input.workerId,
|
|
458
|
+
workspacePath: input.workspacePath
|
|
459
|
+
});
|
|
460
|
+
const protocolIds = claimedBundleProtocolIds(context, { projectRoot: project.root, workspacePath: input.workspacePath, workerId: input.workerId });
|
|
461
|
+
if (protocolIds.length === 0) {
|
|
462
|
+
throw new AppError("merge_bundle_not_claimed", "No claimed protocols found for this branch bundle and worker", 1, {
|
|
463
|
+
worker_id: input.workerId,
|
|
464
|
+
workspace_path: input.workspacePath
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const now = context.now();
|
|
468
|
+
const status = input.requeue ? "requeued" : "failed";
|
|
469
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
470
|
+
try {
|
|
471
|
+
for (const protocolId of protocolIds) {
|
|
472
|
+
const protocol = requireProtocol(context, protocolId);
|
|
473
|
+
const job = requireClaimedJob(context, protocolId, input.workerId);
|
|
474
|
+
context.db.run(`UPDATE merge_queue
|
|
475
|
+
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
476
|
+
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
477
|
+
if (input.requeue) {
|
|
478
|
+
const state = readProtocolRuntimeState(context, protocol).state;
|
|
479
|
+
persistProtocolState(context, protocol, {
|
|
480
|
+
...state,
|
|
481
|
+
stage: "ready_for_merge",
|
|
482
|
+
status: "running",
|
|
483
|
+
next_action: "merge_requeued",
|
|
484
|
+
updated_at: now
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
appendAudit(context, {
|
|
488
|
+
protocolId,
|
|
489
|
+
projectId: project.id,
|
|
490
|
+
eventType: "merge_bundle.failed_protocol",
|
|
491
|
+
reason: input.reason,
|
|
492
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, requeue: input.requeue, protocol_ids: protocolIds }
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
appendAudit(context, {
|
|
496
|
+
projectId: project.id,
|
|
497
|
+
eventType: "merge_bundle.failed",
|
|
498
|
+
reason: input.reason,
|
|
499
|
+
payload: { project_id: project.id, worker_id: input.workerId, requeue: input.requeue, protocol_ids: protocolIds }
|
|
500
|
+
});
|
|
501
|
+
context.db.exec("COMMIT");
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
context.db.exec("ROLLBACK");
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle fail after stop request");
|
|
508
|
+
return mergeBundleResult(context, {
|
|
509
|
+
ok: true,
|
|
510
|
+
outcome: input.requeue ? "requeued" : "failed",
|
|
511
|
+
projectRoot: project.root,
|
|
512
|
+
workspacePath: input.workspacePath,
|
|
513
|
+
workerId: input.workerId,
|
|
514
|
+
protocolIds,
|
|
515
|
+
stop_after_current
|
|
516
|
+
});
|
|
262
517
|
}
|
|
263
518
|
export function cancelMergeQueueJob(context, input) {
|
|
264
519
|
const protocol = requireProtocol(context, input.protocolId);
|
|
@@ -275,7 +530,7 @@ export function cancelMergeQueueJob(context, input) {
|
|
|
275
530
|
}
|
|
276
531
|
if (job.status === "cancelled") {
|
|
277
532
|
context.db.exec("COMMIT");
|
|
278
|
-
return { ok: true,
|
|
533
|
+
return mergeQueueResult(job, { ok: true, outcome: "cancelled", cancelled: false, reason: "already_cancelled" });
|
|
279
534
|
}
|
|
280
535
|
if (job.status === "claimed" && !input.force) {
|
|
281
536
|
if (!input.workerId) {
|
|
@@ -317,7 +572,7 @@ export function cancelMergeQueueJob(context, input) {
|
|
|
317
572
|
context.db.exec("ROLLBACK");
|
|
318
573
|
throw error;
|
|
319
574
|
}
|
|
320
|
-
return { ok: true,
|
|
575
|
+
return mergeQueueResult(queueJobByProtocol(context, protocol.id), { ok: true, outcome: "cancelled", cancelled: true });
|
|
321
576
|
}
|
|
322
577
|
export function queueForProject(context, projectId) {
|
|
323
578
|
return context.db.all(`SELECT * FROM merge_queue WHERE project_id = ? ORDER BY created_at ASC, id ASC`, [projectId]);
|
|
@@ -325,6 +580,46 @@ export function queueForProject(context, projectId) {
|
|
|
325
580
|
function queueJobByProtocol(context, protocolId) {
|
|
326
581
|
return context.db.get("SELECT * FROM merge_queue WHERE protocol_id = ?", [protocolId]);
|
|
327
582
|
}
|
|
583
|
+
function mergeQueueResult(job, extra) {
|
|
584
|
+
const queueItem = job ?? null;
|
|
585
|
+
return {
|
|
586
|
+
...extra,
|
|
587
|
+
queue_item: queueItem,
|
|
588
|
+
protocol: queueItem
|
|
589
|
+
? {
|
|
590
|
+
id: queueItem.protocol_id,
|
|
591
|
+
queue_status: queueItem.status,
|
|
592
|
+
project_id: queueItem.project_id
|
|
593
|
+
}
|
|
594
|
+
: null,
|
|
595
|
+
claim: queueItem?.claimed_by_session_id
|
|
596
|
+
? {
|
|
597
|
+
protocol_id: queueItem.protocol_id,
|
|
598
|
+
worker_id: queueItem.claimed_by_session_id,
|
|
599
|
+
claimed_at: queueItem.claimed_at,
|
|
600
|
+
status: queueItem.status
|
|
601
|
+
}
|
|
602
|
+
: null,
|
|
603
|
+
job: queueItem
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function mergeBundleResult(context, input) {
|
|
607
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath }).branch_context;
|
|
608
|
+
return {
|
|
609
|
+
...input,
|
|
610
|
+
protocol_ids: input.protocolIds,
|
|
611
|
+
branch_context: branchContext,
|
|
612
|
+
queue_items: input.protocolIds.map((protocolId) => queueJobByProtocol(context, protocolId) ?? null)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function claimedBundleProtocolIds(context, input) {
|
|
616
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath }).branch_context;
|
|
617
|
+
const protocols = Array.isArray(branchContext.protocols) ? branchContext.protocols : [];
|
|
618
|
+
return protocols
|
|
619
|
+
.filter((protocol) => protocol.queue_status === "claimed" && protocol.queue_claimed_by === input.workerId)
|
|
620
|
+
.map((protocol) => String(protocol.id))
|
|
621
|
+
.filter(Boolean);
|
|
622
|
+
}
|
|
328
623
|
function requireClaimedJob(context, protocolId, sessionId) {
|
|
329
624
|
const job = queueJobByProtocol(context, protocolId);
|
|
330
625
|
if (!job || job.status !== "claimed") {
|
|
@@ -7,13 +7,17 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
7
7
|
import { buildStaticFlowGuidance } from "./flow-guidance.js";
|
|
8
8
|
import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
9
9
|
import { registerFlowSession, stopMergeWorker } from "./sessions.js";
|
|
10
|
+
import { getProtocolBranchContext } from "./branch-context.js";
|
|
10
11
|
export function getMergeWorkerStatus(context, input) {
|
|
11
12
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
12
13
|
return {
|
|
13
14
|
ok: true,
|
|
14
15
|
project: { id: project.id, root: project.root },
|
|
15
16
|
merge_worker: detectMergeWorkerState(context, project.id),
|
|
16
|
-
queue: queueForProject(context, project.id).map((job) => withJobGuidance(context, { ...job }))
|
|
17
|
+
queue: queueForProject(context, project.id).map((job) => withJobGuidance(context, { ...job })),
|
|
18
|
+
branch_context: input.workspacePath
|
|
19
|
+
? getProtocolBranchContext(context, { projectRoot: project.root, workspacePath: input.workspacePath }).branch_context
|
|
20
|
+
: null
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
export function startMergeWorker(context, input) {
|
|
@@ -21,7 +25,17 @@ export function startMergeWorker(context, input) {
|
|
|
21
25
|
const workspacePath = resolveProjectRoot(input.workspacePath ?? project.root);
|
|
22
26
|
const detection = detectMergeWorkerState(context, project.id);
|
|
23
27
|
if (detection.state !== "clear") {
|
|
24
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
started: false,
|
|
31
|
+
claimed: false,
|
|
32
|
+
mode: "status_only",
|
|
33
|
+
outcome: "status_only",
|
|
34
|
+
reason: "merge_worker_already_active",
|
|
35
|
+
merge_worker: detection,
|
|
36
|
+
queue: queueForProject(context, project.id),
|
|
37
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
40
|
ensureLaneWorkspace(context, { projectRoot: project.root, lane: "merge", workspacePath, branch: input.branch });
|
|
27
41
|
const registered = registerFlowSession(context, {
|
|
@@ -82,7 +96,16 @@ export function oneShotMergeClaim(context, input) {
|
|
|
82
96
|
const workspacePath = resolveProjectRoot(input.workspacePath ?? project.root);
|
|
83
97
|
const detection = detectMergeWorkerState(context, project.id);
|
|
84
98
|
if (detection.state !== "clear") {
|
|
85
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
claimed: false,
|
|
102
|
+
mode: "status_only",
|
|
103
|
+
outcome: "status_only",
|
|
104
|
+
reason: "active_merge_worker_or_lock",
|
|
105
|
+
merge_worker: detection,
|
|
106
|
+
queue: queueForProject(context, project.id),
|
|
107
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context
|
|
108
|
+
};
|
|
86
109
|
}
|
|
87
110
|
ensureLaneWorkspace(context, { projectRoot: project.root, lane: "merge", workspacePath });
|
|
88
111
|
const lock = acquireLaneLock(context, {
|
|
@@ -107,8 +130,26 @@ export function oneShotMergeClaim(context, input) {
|
|
|
107
130
|
ok: true,
|
|
108
131
|
claimed: Boolean(claimed.job),
|
|
109
132
|
mode: "one_shot",
|
|
133
|
+
outcome: claimed.job ? "claimed" : "empty",
|
|
134
|
+
queue_item: claimed.job ? withJobGuidance(context, { ...claimed.job }) : null,
|
|
135
|
+
protocol: claimed.job
|
|
136
|
+
? {
|
|
137
|
+
id: String(claimed.job.protocol_id ?? ""),
|
|
138
|
+
queue_status: String(claimed.job.status ?? ""),
|
|
139
|
+
project_id: String(claimed.job.project_id ?? "")
|
|
140
|
+
}
|
|
141
|
+
: null,
|
|
142
|
+
claim: claimed.job?.claimed_by_session_id
|
|
143
|
+
? {
|
|
144
|
+
protocol_id: String(claimed.job.protocol_id ?? ""),
|
|
145
|
+
worker_id: String(claimed.job.claimed_by_session_id ?? ""),
|
|
146
|
+
claimed_at: claimed.job.claimed_at ?? null,
|
|
147
|
+
status: claimed.job.status ?? null
|
|
148
|
+
}
|
|
149
|
+
: null,
|
|
110
150
|
job: claimed.job ? withJobGuidance(context, { ...claimed.job }) : null,
|
|
111
151
|
lock: claimed.job ? lock.lock : null,
|
|
152
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context,
|
|
112
153
|
flow_guidance: claimed.job ? withJobGuidance(context, { ...claimed.job }).flow_guidance : undefined
|
|
113
154
|
};
|
|
114
155
|
}
|