@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80

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.
Files changed (136) hide show
  1. package/CHANGELOG.md +396 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1342 -375
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +553 -285
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +828 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +342 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +65 -23
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +707 -156
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/errors.js +12 -0
  131. package/dist/storage/database.js +444 -32
  132. package/dist/storage/paths.js +17 -4
  133. package/dist/storage/writer-contract.js +77 -0
  134. package/dist/storage/writer-migration.js +102 -0
  135. package/package.json +11 -4
  136. 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 claimNextMergeJob(context, input) {
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.exec("BEGIN IMMEDIATE");
53
+ context.db.beginWriteTransaction();
40
54
  try {
41
- const job = context.db.get(`SELECT * FROM merge_queue
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, project.id, job.protocol_id, input.workerId, now);
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
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
78
- const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
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.exec("BEGIN IMMEDIATE");
91
+ context.db.beginWriteTransaction();
94
92
  try {
95
- const job = queueJobByProtocol(context, project.id, input.protocolId);
96
- if (!job || !["ready", "requeued"].includes(job.status)) {
97
- throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, {
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, project.id, job.protocol_id, input.workerId, now);
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 claimMergeBundle(context, input) {
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.exec("BEGIN IMMEDIATE");
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
- transitionClaimedProtocolToIntegration(context, project.id, protocolId, input.workerId, now);
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 async function waitNextMergeJob(context, input) {
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 completeMergeJob(context, input) {
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 completeMergeBundle(context, input) {
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
- context.db.exec("BEGIN IMMEDIATE");
417
+ let prepared;
418
+ context.db.beginWriteTransaction();
385
419
  try {
386
- for (const protocolId of protocolIds) {
387
- const protocol = requireProtocol(context, protocolId, project.id);
388
- const job = requireClaimedJob(context, project.id, protocolId, input.workerId);
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 noteMergeJob(context, input) {
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 failMergeJob(context, input) {
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 (input.requeue) {
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 failMergeBundle(context, input) {
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
- context.db.exec("BEGIN IMMEDIATE");
575
+ let prepared;
576
+ context.db.beginWriteTransaction();
521
577
  try {
522
- for (const protocolId of protocolIds) {
523
- const protocol = requireProtocol(context, protocolId, project.id);
524
- const job = requireClaimedJob(context, project.id, protocolId, input.workerId);
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 (input.requeue) {
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 cancelMergeQueueJob(context, input) {
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
- if (!reason) {
574
- throw new AppError("validation", "merge-queue cancel requires --reason", 2);
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.exec("BEGIN IMMEDIATE");
670
+ context.db.beginWriteTransaction();
578
671
  try {
579
- const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
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 transitionClaimedProtocolToIntegration(context, projectId, protocolId, workerId, now) {
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: protocolId, worker_id: workerId, flow_contract_id: flowContract.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) {