@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81
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 +402 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1344 -376
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +575 -293
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +835 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +69 -28
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +701 -164
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -4,7 +4,7 @@ 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 { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
7
|
+
import { ensureLaneWorkspace, heartbeatLaneLock, laneRecord, prepareLaneLock, prepareLaneWaitTiming, prepareLaneWorkspace, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
8
8
|
import { stopMergeWorker, stoppedMergeWorkerState } from "./sessions.js";
|
|
9
9
|
import { getProtocolBranchContext, requireClaimableBranchBundle } from "./branch-context.js";
|
|
10
10
|
const mergeLockTtlSeconds = 300;
|
|
@@ -18,7 +18,10 @@ export function getMergeQueueStatus(context, input) {
|
|
|
18
18
|
export function getMergeBundleStatus(context, input) {
|
|
19
19
|
return getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
20
20
|
}
|
|
21
|
-
export function
|
|
21
|
+
export function prepareMergeClaim(context, input) {
|
|
22
|
+
requireMergeText(input.workerId, "worker-id");
|
|
23
|
+
if (input.protocolId !== undefined)
|
|
24
|
+
requireMergeText(input.protocolId, "protocol-id");
|
|
22
25
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
23
26
|
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
24
27
|
if (stopState.stopped) {
|
|
@@ -34,14 +37,22 @@ export function claimNextMergeJob(context, input) {
|
|
|
34
37
|
workerId: input.workerId,
|
|
35
38
|
workspacePath: input.workspacePath
|
|
36
39
|
});
|
|
40
|
+
const job = input.protocolId !== undefined
|
|
41
|
+
? queueJobByProtocol(context, project.id, input.protocolId)
|
|
42
|
+
: context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND status IN ('ready', 'requeued') ORDER BY created_at ASC, id ASC LIMIT 1", [project.id]);
|
|
43
|
+
if (input.protocolId !== undefined && (!job || !["ready", "requeued"].includes(job.status))) {
|
|
44
|
+
throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, { protocol_id: input.protocolId, status: job?.status ?? "missing", allowed: ["ready", "requeued"] });
|
|
45
|
+
}
|
|
46
|
+
const transition = job ? prepareClaimedProtocolTransition(context, project.id, job.protocol_id) : null;
|
|
47
|
+
return { project, job, transition };
|
|
48
|
+
}
|
|
49
|
+
export function claimNextMergeJob(context, input) {
|
|
50
|
+
const { project } = prepareMergeClaim(context, input);
|
|
37
51
|
const now = context.now();
|
|
38
52
|
let claimedProtocolId = null;
|
|
39
|
-
context.db.
|
|
53
|
+
context.db.beginWriteTransaction();
|
|
40
54
|
try {
|
|
41
|
-
const job = context
|
|
42
|
-
WHERE project_id = ? AND status IN ('ready', 'requeued')
|
|
43
|
-
ORDER BY created_at ASC, id ASC
|
|
44
|
-
LIMIT 1`, [project.id]);
|
|
55
|
+
const { job, transition } = prepareMergeClaim(context, input);
|
|
45
56
|
if (!job) {
|
|
46
57
|
context.db.exec("COMMIT");
|
|
47
58
|
return mergeQueueResult(null, { ok: true, outcome: "empty", claimed: false });
|
|
@@ -59,7 +70,7 @@ export function claimNextMergeJob(context, input) {
|
|
|
59
70
|
eventType: "merge_queue.claimed",
|
|
60
71
|
payload: { protocol_id: job.protocol_id, worker_id: input.workerId }
|
|
61
72
|
});
|
|
62
|
-
transitionClaimedProtocolToIntegration(context,
|
|
73
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
63
74
|
claimedProtocolId = job.protocol_id;
|
|
64
75
|
context.db.exec("COMMIT");
|
|
65
76
|
}
|
|
@@ -74,32 +85,14 @@ export function claimNextMergeJob(context, input) {
|
|
|
74
85
|
});
|
|
75
86
|
}
|
|
76
87
|
export function claimMergeJob(context, input) {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
if (stopState.stopped) {
|
|
80
|
-
throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim another merge job", 1, {
|
|
81
|
-
worker_id: input.workerId,
|
|
82
|
-
status: stopState.status,
|
|
83
|
-
reason: stopState.reason
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
requireLaneLockOwner(context, {
|
|
87
|
-
projectRoot: project.root,
|
|
88
|
-
lane: "merge",
|
|
89
|
-
workerId: input.workerId,
|
|
90
|
-
workspacePath: input.workspacePath
|
|
91
|
-
});
|
|
88
|
+
requireMergeText(input.protocolId, "protocol-id");
|
|
89
|
+
const { project } = prepareMergeClaim(context, input);
|
|
92
90
|
const now = context.now();
|
|
93
|
-
context.db.
|
|
91
|
+
context.db.beginWriteTransaction();
|
|
94
92
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
protocol_id: input.protocolId,
|
|
99
|
-
status: job?.status ?? "missing",
|
|
100
|
-
allowed: ["ready", "requeued"]
|
|
101
|
-
});
|
|
102
|
-
}
|
|
93
|
+
const prepared = prepareMergeClaim(context, input);
|
|
94
|
+
const job = prepared.job;
|
|
95
|
+
const transition = prepared.transition;
|
|
103
96
|
const update = context.db.run(`UPDATE merge_queue
|
|
104
97
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
105
98
|
WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
|
|
@@ -112,7 +105,7 @@ export function claimMergeJob(context, input) {
|
|
|
112
105
|
eventType: "merge_queue.claimed",
|
|
113
106
|
payload: { protocol_id: job.protocol_id, worker_id: input.workerId, targeted: true }
|
|
114
107
|
});
|
|
115
|
-
transitionClaimedProtocolToIntegration(context,
|
|
108
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
116
109
|
context.db.exec("COMMIT");
|
|
117
110
|
return mergeQueueResult(queueJobByProtocol(context, project.id, job.protocol_id), { ok: true, outcome: "claimed", claimed: true, targeted: true });
|
|
118
111
|
}
|
|
@@ -121,7 +114,8 @@ export function claimMergeJob(context, input) {
|
|
|
121
114
|
throw error;
|
|
122
115
|
}
|
|
123
116
|
}
|
|
124
|
-
export function
|
|
117
|
+
export function prepareMergeBundleClaim(context, input) {
|
|
118
|
+
requireMergeText(input.workerId, "worker-id");
|
|
125
119
|
const { project, protocolIds } = requireClaimableBranchBundle(context, {
|
|
126
120
|
projectRoot: input.projectRoot,
|
|
127
121
|
workspacePath: input.workspacePath
|
|
@@ -140,9 +134,16 @@ export function claimMergeBundle(context, input) {
|
|
|
140
134
|
workerId: input.workerId,
|
|
141
135
|
workspacePath: input.workspacePath
|
|
142
136
|
});
|
|
137
|
+
const transitions = protocolIds.map((protocolId) => prepareClaimedProtocolTransition(context, project.id, protocolId));
|
|
138
|
+
return { project, protocolIds, transitions };
|
|
139
|
+
}
|
|
140
|
+
export function claimMergeBundle(context, input) {
|
|
141
|
+
const { project, protocolIds } = prepareMergeBundleClaim(context, input);
|
|
143
142
|
const now = context.now();
|
|
144
|
-
context.db.
|
|
143
|
+
context.db.beginWriteTransaction();
|
|
145
144
|
try {
|
|
145
|
+
const transitions = protocolIds.map((protocolId) => prepareClaimedProtocolTransition(context, project.id, protocolId));
|
|
146
|
+
// Claim every queue row before publishing any filesystem state.
|
|
146
147
|
for (const protocolId of protocolIds) {
|
|
147
148
|
const update = context.db.run(`UPDATE merge_queue
|
|
148
149
|
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
@@ -153,7 +154,10 @@ export function claimMergeBundle(context, input) {
|
|
|
153
154
|
protocol_ids: protocolIds
|
|
154
155
|
});
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
}
|
|
158
|
+
for (const transition of transitions) {
|
|
159
|
+
const protocolId = transition.protocol.id;
|
|
160
|
+
transitionClaimedProtocolToIntegration(context, transition, input.workerId, now);
|
|
157
161
|
appendAudit(context, {
|
|
158
162
|
protocolId,
|
|
159
163
|
projectId: project.id,
|
|
@@ -182,22 +186,38 @@ export function claimMergeBundle(context, input) {
|
|
|
182
186
|
protocolIds
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
|
-
export
|
|
189
|
+
export function prepareMergeQueueWait(context, input) {
|
|
190
|
+
requireMergeText(input.workerId, "worker-id");
|
|
191
|
+
if (typeof input.acquireLock !== "boolean")
|
|
192
|
+
throw new AppError("validation", "acquire-lock must be boolean", 2);
|
|
193
|
+
const { timeoutSeconds, pollIntervalSeconds } = prepareLaneWaitTiming(input);
|
|
186
194
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
187
|
-
const timeoutSeconds = input.timeoutSeconds ?? 0;
|
|
188
|
-
const pollIntervalSeconds = input.pollIntervalSeconds ?? 10;
|
|
189
|
-
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
190
|
-
throw new AppError("validation", "--timeout must be zero or a positive number", 2);
|
|
191
|
-
}
|
|
192
|
-
if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds <= 0) {
|
|
193
|
-
throw new AppError("validation", "--poll-interval must be a positive number", 2);
|
|
194
|
-
}
|
|
195
195
|
if (input.acquireLock && pollIntervalSeconds >= mergeLockTtlSeconds) {
|
|
196
196
|
throw new AppError("validation", "--poll-interval must be lower than the merge lock TTL when --acquire-lock true", 2, {
|
|
197
197
|
lock_ttl_seconds: mergeLockTtlSeconds
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
const initialStopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
201
|
+
if (!initialStopState.stopped) {
|
|
202
|
+
const next = context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND status IN ('ready', 'requeued') ORDER BY created_at ASC, id ASC LIMIT 1", [project.id]);
|
|
203
|
+
if (next)
|
|
204
|
+
prepareClaimedProtocolTransition(context, project.id, next.protocol_id);
|
|
205
|
+
if (input.acquireLock) {
|
|
206
|
+
if (laneRecord(context, project.id, "merge")) {
|
|
207
|
+
prepareLaneLock(context, { ...input, lane: "merge", reason: "merge queue wait-next", ttlSeconds: mergeLockTtlSeconds }, "wait-acquire");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
prepareLaneWorkspace(context, { ...input, lane: "merge" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
requireLaneLockOwner(context, { ...input, lane: "merge" });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { project, timeoutSeconds, pollIntervalSeconds, initialStopState };
|
|
218
|
+
}
|
|
219
|
+
export async function waitNextMergeJob(context, input) {
|
|
220
|
+
const { project, timeoutSeconds, pollIntervalSeconds, initialStopState } = prepareMergeQueueWait(context, input);
|
|
201
221
|
if (initialStopState.stopped) {
|
|
202
222
|
return mergeQueueResult(null, {
|
|
203
223
|
ok: true,
|
|
@@ -325,7 +345,9 @@ export async function waitNextMergeJob(context, input) {
|
|
|
325
345
|
await delay(pollIntervalSeconds * 1000);
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
|
-
export function
|
|
348
|
+
export function prepareMergeJobCompletion(context, input) {
|
|
349
|
+
requireMergeText(input.summary, "summary");
|
|
350
|
+
requireMergeText(input.workerId, "worker-id");
|
|
329
351
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
330
352
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
331
353
|
const job = requireClaimedJob(context, protocol.project_id, input.protocolId, input.workerId);
|
|
@@ -335,14 +357,18 @@ export function completeMergeJob(context, input) {
|
|
|
335
357
|
workerId: input.workerId,
|
|
336
358
|
workspacePath: input.workspacePath
|
|
337
359
|
});
|
|
338
|
-
const now = context.now();
|
|
339
|
-
context.db.run(`UPDATE merge_queue
|
|
340
|
-
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
341
|
-
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
342
360
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
343
361
|
const flowContract = flowContractForState(state);
|
|
344
362
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
345
363
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
364
|
+
return { protocol, job, state, flowContract, targetStage, targetStatus };
|
|
365
|
+
}
|
|
366
|
+
export function completeMergeJob(context, input) {
|
|
367
|
+
const { protocol, job, state, flowContract, targetStage, targetStatus } = prepareMergeJobCompletion(context, input);
|
|
368
|
+
const now = context.now();
|
|
369
|
+
context.db.run(`UPDATE merge_queue
|
|
370
|
+
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
371
|
+
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
346
372
|
persistProtocolState(context, protocol, {
|
|
347
373
|
...state,
|
|
348
374
|
stage: targetStage,
|
|
@@ -365,7 +391,9 @@ export function completeMergeJob(context, input) {
|
|
|
365
391
|
stop_after_current
|
|
366
392
|
});
|
|
367
393
|
}
|
|
368
|
-
export function
|
|
394
|
+
export function prepareMergeBundleCompletion(context, input) {
|
|
395
|
+
requireMergeText(input.summary, "summary");
|
|
396
|
+
requireMergeText(input.workerId, "worker-id");
|
|
369
397
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
370
398
|
requireLaneLockOwner(context, {
|
|
371
399
|
projectRoot: project.root,
|
|
@@ -380,19 +408,22 @@ export function completeMergeBundle(context, input) {
|
|
|
380
408
|
workspace_path: input.workspacePath
|
|
381
409
|
});
|
|
382
410
|
}
|
|
411
|
+
const jobs = protocolIds.map((protocolId) => prepareMergeJobCompletion(context, { ...input, protocolId }));
|
|
412
|
+
return { project, protocolIds, jobs };
|
|
413
|
+
}
|
|
414
|
+
export function completeMergeBundle(context, input) {
|
|
415
|
+
prepareMergeBundleCompletion(context, input);
|
|
383
416
|
const now = context.now();
|
|
384
|
-
|
|
417
|
+
let prepared;
|
|
418
|
+
context.db.beginWriteTransaction();
|
|
385
419
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
420
|
+
prepared = prepareMergeBundleCompletion(context, input);
|
|
421
|
+
const { project, protocolIds, jobs } = prepared;
|
|
422
|
+
for (const { protocol, job, state, flowContract, targetStage, targetStatus } of jobs) {
|
|
423
|
+
const protocolId = protocol.id;
|
|
389
424
|
context.db.run(`UPDATE merge_queue
|
|
390
425
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
391
426
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
392
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
393
|
-
const flowContract = flowContractForState(state);
|
|
394
|
-
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
395
|
-
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
396
427
|
persistProtocolState(context, protocol, {
|
|
397
428
|
...state,
|
|
398
429
|
stage: targetStage,
|
|
@@ -418,6 +449,7 @@ export function completeMergeBundle(context, input) {
|
|
|
418
449
|
context.db.exec("ROLLBACK");
|
|
419
450
|
throw error;
|
|
420
451
|
}
|
|
452
|
+
const { project, protocolIds } = prepared;
|
|
421
453
|
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle complete after stop request");
|
|
422
454
|
return mergeBundleResult(context, {
|
|
423
455
|
ok: true,
|
|
@@ -430,7 +462,9 @@ export function completeMergeBundle(context, input) {
|
|
|
430
462
|
stop_after_current
|
|
431
463
|
});
|
|
432
464
|
}
|
|
433
|
-
export function
|
|
465
|
+
export function prepareMergeJobNote(context, input) {
|
|
466
|
+
requireMergeText(input.summary, "summary");
|
|
467
|
+
requireMergeText(input.workerId, "worker-id");
|
|
434
468
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
435
469
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
436
470
|
const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
|
|
@@ -449,6 +483,10 @@ export function noteMergeJob(context, input) {
|
|
|
449
483
|
claimed_by_session_id: job.claimed_by_session_id
|
|
450
484
|
});
|
|
451
485
|
}
|
|
486
|
+
return { protocol, job };
|
|
487
|
+
}
|
|
488
|
+
export function noteMergeJob(context, input) {
|
|
489
|
+
const { protocol, job } = prepareMergeJobNote(context, input);
|
|
452
490
|
const now = context.now();
|
|
453
491
|
context.db.run(`UPDATE merge_queue
|
|
454
492
|
SET last_reason = ?, updated_at = ?
|
|
@@ -461,7 +499,11 @@ export function noteMergeJob(context, input) {
|
|
|
461
499
|
});
|
|
462
500
|
return mergeQueueResult(queueJobByProtocol(context, protocol.project_id, protocol.id), { ok: true, outcome: "noted" });
|
|
463
501
|
}
|
|
464
|
-
export function
|
|
502
|
+
export function prepareMergeJobFailure(context, input) {
|
|
503
|
+
requireMergeText(input.reason, "reason");
|
|
504
|
+
requireMergeText(input.workerId, "worker-id");
|
|
505
|
+
if (typeof input.requeue !== "boolean")
|
|
506
|
+
throw new AppError("validation", "requeue must be boolean", 2);
|
|
465
507
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
466
508
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
467
509
|
const job = requireClaimedJob(context, protocol.project_id, input.protocolId, input.workerId);
|
|
@@ -471,13 +513,17 @@ export function failMergeJob(context, input) {
|
|
|
471
513
|
workerId: input.workerId,
|
|
472
514
|
workspacePath: input.workspacePath
|
|
473
515
|
});
|
|
516
|
+
const state = input.requeue ? readProtocolRuntimeState(context, protocol).state : null;
|
|
517
|
+
return { protocol, job, state };
|
|
518
|
+
}
|
|
519
|
+
export function failMergeJob(context, input) {
|
|
520
|
+
const { protocol, job, state } = prepareMergeJobFailure(context, input);
|
|
474
521
|
const now = context.now();
|
|
475
522
|
const status = input.requeue ? "requeued" : "failed";
|
|
476
523
|
context.db.run(`UPDATE merge_queue
|
|
477
524
|
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
478
525
|
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
479
|
-
if (
|
|
480
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
526
|
+
if (state) {
|
|
481
527
|
persistProtocolState(context, protocol, {
|
|
482
528
|
...state,
|
|
483
529
|
stage: "ready_for_merge",
|
|
@@ -500,7 +546,11 @@ export function failMergeJob(context, input) {
|
|
|
500
546
|
stop_after_current
|
|
501
547
|
});
|
|
502
548
|
}
|
|
503
|
-
export function
|
|
549
|
+
export function prepareMergeBundleFailure(context, input) {
|
|
550
|
+
requireMergeText(input.reason, "reason");
|
|
551
|
+
requireMergeText(input.workerId, "worker-id");
|
|
552
|
+
if (typeof input.requeue !== "boolean")
|
|
553
|
+
throw new AppError("validation", "requeue must be boolean", 2);
|
|
504
554
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
505
555
|
requireLaneLockOwner(context, {
|
|
506
556
|
projectRoot: project.root,
|
|
@@ -515,18 +565,24 @@ export function failMergeBundle(context, input) {
|
|
|
515
565
|
workspace_path: input.workspacePath
|
|
516
566
|
});
|
|
517
567
|
}
|
|
568
|
+
const jobs = protocolIds.map((protocolId) => prepareMergeJobFailure(context, { ...input, protocolId }));
|
|
569
|
+
return { project, protocolIds, jobs };
|
|
570
|
+
}
|
|
571
|
+
export function failMergeBundle(context, input) {
|
|
572
|
+
prepareMergeBundleFailure(context, input);
|
|
518
573
|
const now = context.now();
|
|
519
574
|
const status = input.requeue ? "requeued" : "failed";
|
|
520
|
-
|
|
575
|
+
let prepared;
|
|
576
|
+
context.db.beginWriteTransaction();
|
|
521
577
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
578
|
+
prepared = prepareMergeBundleFailure(context, input);
|
|
579
|
+
const { project, protocolIds, jobs } = prepared;
|
|
580
|
+
for (const { protocol, job, state } of jobs) {
|
|
581
|
+
const protocolId = protocol.id;
|
|
525
582
|
context.db.run(`UPDATE merge_queue
|
|
526
583
|
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
527
584
|
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
528
|
-
if (
|
|
529
|
-
const state = readProtocolRuntimeState(context, protocol).state;
|
|
585
|
+
if (state) {
|
|
530
586
|
persistProtocolState(context, protocol, {
|
|
531
587
|
...state,
|
|
532
588
|
stage: "ready_for_merge",
|
|
@@ -555,6 +611,7 @@ export function failMergeBundle(context, input) {
|
|
|
555
611
|
context.db.exec("ROLLBACK");
|
|
556
612
|
throw error;
|
|
557
613
|
}
|
|
614
|
+
const { project, protocolIds } = prepared;
|
|
558
615
|
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle fail after stop request");
|
|
559
616
|
return mergeBundleResult(context, {
|
|
560
617
|
ok: true,
|
|
@@ -566,47 +623,57 @@ export function failMergeBundle(context, input) {
|
|
|
566
623
|
stop_after_current
|
|
567
624
|
});
|
|
568
625
|
}
|
|
569
|
-
export function
|
|
626
|
+
export function prepareMergeQueueCancellation(context, input) {
|
|
627
|
+
requireMergeText(input.reason, "reason");
|
|
628
|
+
if (input.workerId !== undefined)
|
|
629
|
+
requireMergeText(input.workerId, "worker-id");
|
|
630
|
+
if (typeof input.force !== "boolean")
|
|
631
|
+
throw new AppError("validation", "force must be boolean", 2);
|
|
570
632
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
571
633
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
572
634
|
const reason = input.reason.trim();
|
|
573
|
-
|
|
574
|
-
|
|
635
|
+
const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
|
|
636
|
+
if (!job) {
|
|
637
|
+
throw new AppError("not_found", "Merge queue job is not found", 1, { protocol_id: protocol.id });
|
|
638
|
+
}
|
|
639
|
+
if (job.status === "cancelled") {
|
|
640
|
+
return { protocol, job, reason };
|
|
641
|
+
}
|
|
642
|
+
if (job.status === "claimed" && !input.force) {
|
|
643
|
+
if (!input.workerId) {
|
|
644
|
+
throw new AppError("merge_job_claimed", "Claimed jobs require --worker-id or --force", 1, { protocol_id: protocol.id });
|
|
645
|
+
}
|
|
646
|
+
if (job.claimed_by_session_id !== input.workerId) {
|
|
647
|
+
throw new AppError("merge_job_claimed_by_other_session", "Merge job is claimed by another worker", 1, {
|
|
648
|
+
protocol_id: protocol.id,
|
|
649
|
+
claimed_by_session_id: job.claimed_by_session_id
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
requireLaneLockOwner(context, {
|
|
653
|
+
projectRoot: protocol.project_root,
|
|
654
|
+
lane: "merge",
|
|
655
|
+
workerId: input.workerId,
|
|
656
|
+
workspacePath: input.workspacePath
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
if (["merged", "failed"].includes(job.status) && !input.force) {
|
|
660
|
+
throw new AppError("merge_job_terminal", `Merge job is ${job.status}; cancellation requires --force --reason`, 1, {
|
|
661
|
+
protocol_id: protocol.id,
|
|
662
|
+
status: job.status
|
|
663
|
+
});
|
|
575
664
|
}
|
|
665
|
+
return { protocol, job, reason };
|
|
666
|
+
}
|
|
667
|
+
export function cancelMergeQueueJob(context, input) {
|
|
668
|
+
const { protocol } = prepareMergeQueueCancellation(context, input);
|
|
576
669
|
const now = context.now();
|
|
577
|
-
context.db.
|
|
670
|
+
context.db.beginWriteTransaction();
|
|
578
671
|
try {
|
|
579
|
-
const job =
|
|
580
|
-
if (!job) {
|
|
581
|
-
throw new AppError("not_found", "Merge queue job is not found", 1, { protocol_id: protocol.id });
|
|
582
|
-
}
|
|
672
|
+
const { job, reason } = prepareMergeQueueCancellation(context, input);
|
|
583
673
|
if (job.status === "cancelled") {
|
|
584
674
|
context.db.exec("COMMIT");
|
|
585
675
|
return mergeQueueResult(job, { ok: true, outcome: "cancelled", cancelled: false, reason: "already_cancelled" });
|
|
586
676
|
}
|
|
587
|
-
if (job.status === "claimed" && !input.force) {
|
|
588
|
-
if (!input.workerId) {
|
|
589
|
-
throw new AppError("merge_job_claimed", "Claimed jobs require --worker-id or --force", 1, { protocol_id: protocol.id });
|
|
590
|
-
}
|
|
591
|
-
if (job.claimed_by_session_id !== input.workerId) {
|
|
592
|
-
throw new AppError("merge_job_claimed_by_other_session", "Merge job is claimed by another worker", 1, {
|
|
593
|
-
protocol_id: protocol.id,
|
|
594
|
-
claimed_by_session_id: job.claimed_by_session_id
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
requireLaneLockOwner(context, {
|
|
598
|
-
projectRoot: protocol.project_root,
|
|
599
|
-
lane: "merge",
|
|
600
|
-
workerId: input.workerId,
|
|
601
|
-
workspacePath: input.workspacePath
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
if (["merged", "failed"].includes(job.status) && !input.force) {
|
|
605
|
-
throw new AppError("merge_job_terminal", `Merge job is ${job.status}; cancellation requires --force --reason`, 1, {
|
|
606
|
-
protocol_id: protocol.id,
|
|
607
|
-
status: job.status
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
677
|
context.db.run(`UPDATE merge_queue
|
|
611
678
|
SET status = 'cancelled', last_reason = ?, updated_at = ?
|
|
612
679
|
WHERE id = ?`, [reason, now, job.id]);
|
|
@@ -629,6 +696,10 @@ export function cancelMergeQueueJob(context, input) {
|
|
|
629
696
|
export function queueForProject(context, projectId) {
|
|
630
697
|
return context.db.all(`SELECT * FROM merge_queue WHERE project_id = ? ORDER BY created_at ASC, id ASC`, [projectId]);
|
|
631
698
|
}
|
|
699
|
+
function requireMergeText(value, parameter) {
|
|
700
|
+
if (typeof value !== "string" || !value.trim())
|
|
701
|
+
throw new AppError("validation", `--${parameter} must be nonempty text`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter });
|
|
702
|
+
}
|
|
632
703
|
function queueJobByProtocol(context, projectId, protocolId) {
|
|
633
704
|
return context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
634
705
|
}
|
|
@@ -688,7 +759,7 @@ function requireClaimedJob(context, projectId, protocolId, sessionId) {
|
|
|
688
759
|
}
|
|
689
760
|
return job;
|
|
690
761
|
}
|
|
691
|
-
function
|
|
762
|
+
function prepareClaimedProtocolTransition(context, projectId, protocolId) {
|
|
692
763
|
const protocol = requireProtocol(context, protocolId, projectId);
|
|
693
764
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
694
765
|
const flowContract = flowContractForState(state);
|
|
@@ -707,6 +778,10 @@ function transitionClaimedProtocolToIntegration(context, projectId, protocolId,
|
|
|
707
778
|
diagnostics: blockingDiagnostics
|
|
708
779
|
});
|
|
709
780
|
}
|
|
781
|
+
return { protocol, state, flowContract };
|
|
782
|
+
}
|
|
783
|
+
function transitionClaimedProtocolToIntegration(context, prepared, workerId, now) {
|
|
784
|
+
const { protocol, state, flowContract } = prepared;
|
|
710
785
|
persistProtocolState(context, protocol, {
|
|
711
786
|
...state,
|
|
712
787
|
stage: "integration",
|
|
@@ -715,10 +790,10 @@ function transitionClaimedProtocolToIntegration(context, projectId, protocolId,
|
|
|
715
790
|
updated_at: now
|
|
716
791
|
});
|
|
717
792
|
appendAudit(context, {
|
|
718
|
-
protocolId,
|
|
793
|
+
protocolId: protocol.id,
|
|
719
794
|
projectId: protocol.project_id,
|
|
720
795
|
eventType: "protocol.integration_claimed",
|
|
721
|
-
payload: { protocol_id:
|
|
796
|
+
payload: { protocol_id: protocol.id, worker_id: workerId, flow_contract_id: flowContract.id }
|
|
722
797
|
});
|
|
723
798
|
}
|
|
724
799
|
function releaseLaneLockIfOwned(context, input) {
|