baxian 1.2.61 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +7 -4
  3. package/dist/agent/bootstrap-poller.d.ts +1 -1
  4. package/dist/agent/bootstrap-poller.d.ts.map +1 -1
  5. package/dist/agent/bootstrap-poller.js.map +1 -1
  6. package/dist/agent/bootstrap.d.ts +1 -1
  7. package/dist/agent/bootstrap.d.ts.map +1 -1
  8. package/dist/agent/bootstrap.js +6 -2
  9. package/dist/agent/bootstrap.js.map +1 -1
  10. package/dist/agent/branch.d.ts +1 -3
  11. package/dist/agent/branch.d.ts.map +1 -1
  12. package/dist/agent/branch.js +2 -2
  13. package/dist/agent/branch.js.map +1 -1
  14. package/dist/agent/dispatch-reconciler.d.ts +0 -1
  15. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  16. package/dist/agent/dispatch-reconciler.js +15 -49
  17. package/dist/agent/dispatch-reconciler.js.map +1 -1
  18. package/dist/agent/manager.d.ts +212 -90
  19. package/dist/agent/manager.d.ts.map +1 -1
  20. package/dist/agent/manager.js +2983 -1574
  21. package/dist/agent/manager.js.map +1 -1
  22. package/dist/agent/net-exec.d.ts +3 -0
  23. package/dist/agent/net-exec.d.ts.map +1 -1
  24. package/dist/agent/net-exec.js +6 -0
  25. package/dist/agent/net-exec.js.map +1 -1
  26. package/dist/agent/pane-streamer-manager.d.ts +6 -6
  27. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  28. package/dist/agent/pane-streamer-manager.js.map +1 -1
  29. package/dist/agent/pane-streamer.d.ts +2 -2
  30. package/dist/agent/pane-streamer.d.ts.map +1 -1
  31. package/dist/agent/pane-streamer.js +46 -36
  32. package/dist/agent/pane-streamer.js.map +1 -1
  33. package/dist/agent/phase-signal-watcher.d.ts +20 -2
  34. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  35. package/dist/agent/phase-signal-watcher.js +135 -17
  36. package/dist/agent/phase-signal-watcher.js.map +1 -1
  37. package/dist/agent/phase-signal.d.ts +9 -7
  38. package/dist/agent/phase-signal.d.ts.map +1 -1
  39. package/dist/agent/phase-signal.js +18 -9
  40. package/dist/agent/phase-signal.js.map +1 -1
  41. package/dist/agent/preflight.d.ts +4 -1
  42. package/dist/agent/preflight.d.ts.map +1 -1
  43. package/dist/agent/preflight.js +46 -45
  44. package/dist/agent/preflight.js.map +1 -1
  45. package/dist/agent/prompt.d.ts +8 -5
  46. package/dist/agent/prompt.d.ts.map +1 -1
  47. package/dist/agent/prompt.js +34 -30
  48. package/dist/agent/prompt.js.map +1 -1
  49. package/dist/agent/repo-store.d.ts +8 -1
  50. package/dist/agent/repo-store.d.ts.map +1 -1
  51. package/dist/agent/repo-store.js +70 -15
  52. package/dist/agent/repo-store.js.map +1 -1
  53. package/dist/agent/review-transport.d.ts +20 -1
  54. package/dist/agent/review-transport.d.ts.map +1 -1
  55. package/dist/agent/review-transport.js +68 -20
  56. package/dist/agent/review-transport.js.map +1 -1
  57. package/dist/agent/root-agent-runtime.d.ts +80 -0
  58. package/dist/agent/root-agent-runtime.d.ts.map +1 -0
  59. package/dist/agent/root-agent-runtime.js +635 -0
  60. package/dist/agent/root-agent-runtime.js.map +1 -0
  61. package/dist/agent/root-recovery-coordinator.d.ts +125 -0
  62. package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
  63. package/dist/agent/root-recovery-coordinator.js +1579 -0
  64. package/dist/agent/root-recovery-coordinator.js.map +1 -0
  65. package/dist/agent/runner.d.ts +1 -0
  66. package/dist/agent/runner.d.ts.map +1 -1
  67. package/dist/agent/runner.js +103 -7
  68. package/dist/agent/runner.js.map +1 -1
  69. package/dist/agent/tmux-probe-poller.d.ts +1 -0
  70. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  71. package/dist/agent/tmux-probe-poller.js +16 -10
  72. package/dist/agent/tmux-probe-poller.js.map +1 -1
  73. package/dist/agent/tmux.d.ts +3 -0
  74. package/dist/agent/tmux.d.ts.map +1 -1
  75. package/dist/agent/tmux.js +11 -5
  76. package/dist/agent/tmux.js.map +1 -1
  77. package/dist/api/agents.d.ts.map +1 -1
  78. package/dist/api/agents.js +42 -1
  79. package/dist/api/agents.js.map +1 -1
  80. package/dist/api/config.d.ts.map +1 -1
  81. package/dist/api/config.js +88 -17
  82. package/dist/api/config.js.map +1 -1
  83. package/dist/api/hosts.d.ts.map +1 -1
  84. package/dist/api/hosts.js +71 -15
  85. package/dist/api/hosts.js.map +1 -1
  86. package/dist/api/platform-guard.d.ts +10 -2
  87. package/dist/api/platform-guard.d.ts.map +1 -1
  88. package/dist/api/platform-guard.js +112 -10
  89. package/dist/api/platform-guard.js.map +1 -1
  90. package/dist/api/projects.d.ts.map +1 -1
  91. package/dist/api/projects.js +57 -16
  92. package/dist/api/projects.js.map +1 -1
  93. package/dist/api/tasks.d.ts.map +1 -1
  94. package/dist/api/tasks.js +21 -36
  95. package/dist/api/tasks.js.map +1 -1
  96. package/dist/app.d.ts +6 -4
  97. package/dist/app.d.ts.map +1 -1
  98. package/dist/app.js +6 -0
  99. package/dist/app.js.map +1 -1
  100. package/dist/cli.d.ts +1 -1
  101. package/dist/cli.d.ts.map +1 -1
  102. package/dist/cli.js +49 -7
  103. package/dist/cli.js.map +1 -1
  104. package/dist/config/hot-reload.d.ts +11 -2
  105. package/dist/config/hot-reload.d.ts.map +1 -1
  106. package/dist/config/hot-reload.js +43 -8
  107. package/dist/config/hot-reload.js.map +1 -1
  108. package/dist/config/loader.d.ts.map +1 -1
  109. package/dist/config/loader.js +23 -2
  110. package/dist/config/loader.js.map +1 -1
  111. package/dist/config/normalizer.d.ts.map +1 -1
  112. package/dist/config/normalizer.js +18 -0
  113. package/dist/config/normalizer.js.map +1 -1
  114. package/dist/config/validator.d.ts +3 -1
  115. package/dist/config/validator.d.ts.map +1 -1
  116. package/dist/config/validator.js +247 -54
  117. package/dist/config/validator.js.map +1 -1
  118. package/dist/event/handlers.d.ts.map +1 -1
  119. package/dist/event/handlers.js +615 -1069
  120. package/dist/event/handlers.js.map +1 -1
  121. package/dist/event/server-handlers.d.ts.map +1 -1
  122. package/dist/event/server-handlers.js +787 -487
  123. package/dist/event/server-handlers.js.map +1 -1
  124. package/dist/index.d.ts +4 -1
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +99 -79
  127. package/dist/index.js.map +1 -1
  128. package/dist/platform/command-renderer.d.ts +1 -0
  129. package/dist/platform/command-renderer.d.ts.map +1 -1
  130. package/dist/platform/command-renderer.js +16 -2
  131. package/dist/platform/command-renderer.js.map +1 -1
  132. package/dist/platform/comment-cursor.d.ts +9 -0
  133. package/dist/platform/comment-cursor.d.ts.map +1 -1
  134. package/dist/platform/comment-cursor.js +63 -5
  135. package/dist/platform/comment-cursor.js.map +1 -1
  136. package/dist/platform/driver-host.d.ts.map +1 -1
  137. package/dist/platform/driver-host.js +0 -2
  138. package/dist/platform/driver-host.js.map +1 -1
  139. package/dist/platform/driver-spec.d.ts.map +1 -1
  140. package/dist/platform/driver-spec.js +17 -6
  141. package/dist/platform/driver-spec.js.map +1 -1
  142. package/dist/platform/feedback.d.ts.map +1 -1
  143. package/dist/platform/feedback.js +0 -9
  144. package/dist/platform/feedback.js.map +1 -1
  145. package/dist/platform/git-driver.d.ts +3 -0
  146. package/dist/platform/git-driver.d.ts.map +1 -1
  147. package/dist/platform/git-driver.js +45 -3
  148. package/dist/platform/git-driver.js.map +1 -1
  149. package/dist/platform/platform-poller.d.ts +18 -5
  150. package/dist/platform/platform-poller.d.ts.map +1 -1
  151. package/dist/platform/platform-poller.js +283 -38
  152. package/dist/platform/platform-poller.js.map +1 -1
  153. package/dist/platform/plugins/github/baxian-plugin.json +1 -1
  154. package/dist/platform/plugins/github/driver.json +14 -3
  155. package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +7 -6
  156. package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
  157. package/dist/platform/pr-binding.d.ts.map +1 -1
  158. package/dist/platform/pr-binding.js +0 -2
  159. package/dist/platform/pr-binding.js.map +1 -1
  160. package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
  161. package/dist/platform/pr-conversation-cache.js.map +1 -0
  162. package/dist/platform/row-schema.d.ts.map +1 -1
  163. package/dist/platform/row-schema.js +1 -0
  164. package/dist/platform/row-schema.js.map +1 -1
  165. package/dist/platform/startup.d.ts +29 -1
  166. package/dist/platform/startup.d.ts.map +1 -1
  167. package/dist/platform/startup.js +82 -4
  168. package/dist/platform/startup.js.map +1 -1
  169. package/dist/platform/types.d.ts +2 -0
  170. package/dist/platform/types.d.ts.map +1 -1
  171. package/dist/platform/types.js +6 -4
  172. package/dist/platform/types.js.map +1 -1
  173. package/dist/platform/verdict-engine.d.ts.map +1 -1
  174. package/dist/platform/verdict-engine.js +0 -2
  175. package/dist/platform/verdict-engine.js.map +1 -1
  176. package/dist/shared/constants.d.ts +7 -1
  177. package/dist/shared/constants.d.ts.map +1 -1
  178. package/dist/shared/constants.js +5 -0
  179. package/dist/shared/constants.js.map +1 -1
  180. package/dist/shared/types.d.ts +98 -7
  181. package/dist/shared/types.d.ts.map +1 -1
  182. package/dist/shared/types.js +18 -0
  183. package/dist/shared/types.js.map +1 -1
  184. package/dist/skills/baxian-pr-feedback/SKILL.md +5 -15
  185. package/dist/skills/baxian-pr-recheck/SKILL.md +5 -44
  186. package/dist/skills/baxian-pr-review/SKILL.md +6 -43
  187. package/dist/skills/baxian-research/SKILL.md +1 -1
  188. package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
  189. package/dist/skills/baxian-server-review/SKILL.md +2 -2
  190. package/dist/skills/baxian-signals/SKILL.md +11 -7
  191. package/dist/skills/baxian-task-check/SKILL.md +3 -5
  192. package/dist/state/agent-store.js +0 -2
  193. package/dist/state/agent-store.js.map +1 -1
  194. package/dist/state/init.d.ts.map +1 -1
  195. package/dist/state/init.js +1 -0
  196. package/dist/state/init.js.map +1 -1
  197. package/dist/state/lock.d.ts +6 -0
  198. package/dist/state/lock.d.ts.map +1 -1
  199. package/dist/state/lock.js +8 -0
  200. package/dist/state/lock.js.map +1 -1
  201. package/dist/state/review-store.d.ts +21 -1
  202. package/dist/state/review-store.d.ts.map +1 -1
  203. package/dist/state/review-store.js +132 -0
  204. package/dist/state/review-store.js.map +1 -1
  205. package/dist/state/root-recovery-store.d.ts +107 -0
  206. package/dist/state/root-recovery-store.d.ts.map +1 -0
  207. package/dist/state/root-recovery-store.js +400 -0
  208. package/dist/state/root-recovery-store.js.map +1 -0
  209. package/dist/state/task-store.d.ts +6 -0
  210. package/dist/state/task-store.d.ts.map +1 -1
  211. package/dist/state/task-store.js +266 -21
  212. package/dist/state/task-store.js.map +1 -1
  213. package/dist/terminal/attach.d.ts +3 -3
  214. package/dist/terminal/attach.d.ts.map +1 -1
  215. package/dist/terminal/attach.js.map +1 -1
  216. package/dist/terminal/stream-ws.d.ts.map +1 -1
  217. package/dist/terminal/stream-ws.js.map +1 -1
  218. package/dist/web/assets/index-BXfgaFNm.js +18 -0
  219. package/dist/web/index.html +1 -1
  220. package/package.json +1 -1
  221. package/dist/github/mapper.d.ts +0 -9
  222. package/dist/github/mapper.d.ts.map +0 -1
  223. package/dist/github/mapper.js +0 -25
  224. package/dist/github/mapper.js.map +0 -1
  225. package/dist/github/poller.d.ts +0 -73
  226. package/dist/github/poller.d.ts.map +0 -1
  227. package/dist/github/poller.js +0 -556
  228. package/dist/github/poller.js.map +0 -1
  229. package/dist/github/pr-conversation-cache.d.ts.map +0 -1
  230. package/dist/github/pr-conversation-cache.js.map +0 -1
  231. package/dist/github/pr-conversation.d.ts +0 -7
  232. package/dist/github/pr-conversation.d.ts.map +0 -1
  233. package/dist/github/pr-conversation.js +0 -175
  234. package/dist/github/pr-conversation.js.map +0 -1
  235. package/dist/github/resolver.d.ts +0 -8
  236. package/dist/github/resolver.d.ts.map +0 -1
  237. package/dist/github/resolver.js +0 -28
  238. package/dist/github/resolver.js.map +0 -1
  239. package/dist/state/post-approve-store.d.ts +0 -23
  240. package/dist/state/post-approve-store.d.ts.map +0 -1
  241. package/dist/state/post-approve-store.js +0 -82
  242. package/dist/state/post-approve-store.js.map +0 -1
  243. package/dist/web/assets/index-CUJtd0bC.js +0 -16
  244. /package/dist/{github → platform}/pr-conversation-cache.d.ts +0 -0
  245. /package/dist/{github → platform}/pr-conversation-cache.js +0 -0
@@ -1,7 +1,15 @@
1
- import { isSpecStagePhase } from '../shared/index.js';
2
- import { ReviewExchangeError } from '../agent/review-transport.js';
1
+ import { isSpecStagePhase, taskGenerationGuard } from '../shared/index.js';
2
+ import { ReviewExchangeError, } from '../agent/review-transport.js';
3
+ import { settlePermitOf } from '../agent/phase-signal-watcher.js';
4
+ import { computeBackoffMs } from '../timing/backoff.js';
5
+ import { reviewFindingsDigest, serverResponseFailureSignature, } from '../state/review-store.js';
3
6
  const LEGACY_DIFF_LARGE_THRESHOLD = 2000;
4
7
  const FILE_HEADER_RE = /^diff --git a\/(.+?) b\//;
8
+ const SERVER_HANDLER_ATTEMPTS = 3;
9
+ const SERVER_HANDLER_BACKOFF = { baseMs: 10, maxMs: 20, factor: 2, jitter: 0 };
10
+ const SERVER_SIGNAL_KINDS = new Set([
11
+ 'code-done', 'code-reviewed', 'code-fixed', 'code-ready', 'spec-done', 'spec-reviewed', 'spec-fixed',
12
+ ]);
5
13
  function splitLegacyDiffByFile(diff) {
6
14
  if (diff.trim() === '')
7
15
  return [];
@@ -96,10 +104,14 @@ function aggregateBatchFindings(batches, round) {
96
104
  function coverageGaps(findings, responseIds) {
97
105
  const findingIds = new Set(findings.findings.map(f => f.id));
98
106
  return {
99
- missing: [...findingIds].filter(id => !responseIds.has(id)),
100
- unknown: [...responseIds].filter(id => !findingIds.has(id)),
107
+ missing: [...findingIds].filter(id => !responseIds.has(id)).sort(),
108
+ unknown: [...responseIds].filter(id => !findingIds.has(id)).sort(),
101
109
  };
102
110
  }
111
+ function waitForHandlerRetry(attempt) {
112
+ const ms = computeBackoffMs(attempt, SERVER_HANDLER_BACKOFF);
113
+ return new Promise(resolve => setTimeout(resolve, ms));
114
+ }
103
115
  async function emitIntervention(bus, task, data) {
104
116
  try {
105
117
  await bus.emit({
@@ -129,7 +141,9 @@ async function gate(bus, manager, event, expect) {
129
141
  const token = event.data?.token;
130
142
  const phaseOk = expect.phase === 'any'
131
143
  || (expect.phase === 'spec' ? task.phase !== 'code' : !isSpecStagePhase(task.phase));
132
- if (task.status !== expect.status || !phaseOk || !token || token !== task.signalToken) {
144
+ if (token && token !== task.signalToken)
145
+ return null;
146
+ if (task.status !== expect.status || !phaseOk || !token) {
133
147
  await emitIntervention(bus, task, {
134
148
  phase: `${event.type}-stale`,
135
149
  taskStatus: task.status,
@@ -137,7 +151,7 @@ async function gate(bus, manager, event, expect) {
137
151
  });
138
152
  return null;
139
153
  }
140
- return { task };
154
+ return { task, expectedTask: taskGenerationGuard(task) };
141
155
  }
142
156
  export function registerServerEventHandlers(bus, manager) {
143
157
  const configured = manager.getReviewStore();
@@ -166,25 +180,225 @@ export function registerServerEventHandlers(bus, manager) {
166
180
  .catch(err => console.error(`[ServerHandler] max_rounds clear agentId(${task.id}) failed:`, err));
167
181
  }
168
182
  }
169
- async function putVerdictRound(task, agentId, kind, data) {
183
+ async function putVerdictRound(task, agentId, kind, data, opts = {}) {
184
+ let lastError;
185
+ for (let attempt = 1; attempt <= SERVER_HANDLER_ATTEMPTS; attempt++) {
186
+ try {
187
+ const stored = opts.expectedTask
188
+ ? await manager.putReviewRoundIfCurrent(task.id, data, opts.expectedTask)
189
+ : (await reviewStore.putRound(task.id, data.phase, data), true);
190
+ if (!stored)
191
+ return false;
192
+ return true;
193
+ }
194
+ catch (err) {
195
+ lastError = err;
196
+ try {
197
+ const stored = await reviewStore.getRound(task.id, data.phase, data.round);
198
+ if (stored && JSON.stringify(stored) === JSON.stringify(data))
199
+ return true;
200
+ }
201
+ catch (readBackError) {
202
+ lastError = readBackError;
203
+ }
204
+ if (attempt < SERVER_HANDLER_ATTEMPTS)
205
+ await waitForHandlerRetry(attempt);
206
+ }
207
+ }
208
+ return holdConsumedSignal(task, agentId, kind, 'verdict-store-failed', `server-${data.phase}-verdict-store-failed`, {
209
+ round: data.round,
210
+ error: lastError instanceof Error ? lastError.message : String(lastError),
211
+ }, opts.onSideEffect);
212
+ }
213
+ async function holdConsumedSignal(task, agentId, kind, reason, failurePhase, data = {}, onSideEffect) {
214
+ if (!SERVER_SIGNAL_KINDS.has(kind)) {
215
+ throw new Error(`unsupported consumed server signal kind: ${kind}`);
216
+ }
217
+ const phase = kind.startsWith('spec-') ? 'spec' : 'code';
218
+ const round = phase === 'spec' ? (task.specReviewRound ?? 0) : task.reviewRound;
219
+ const held = await manager.holdConsumedServerSignal(task, agentId, kind, { phase, round, reason, failurePhase });
220
+ if (!held)
221
+ return false;
222
+ onSideEffect?.();
223
+ await emitIntervention(bus, held, {
224
+ phase: failurePhase,
225
+ ...data,
226
+ successorToken: held.signalToken,
227
+ });
228
+ return false;
229
+ }
230
+ async function readResponseWithRetry(task, dev) {
231
+ let result;
232
+ for (let attempt = 1; attempt <= SERVER_HANDLER_ATTEMPTS; attempt++) {
233
+ result = await transport().readResponseWithRaw(task, dev);
234
+ if (result.kind !== 'unknown' || attempt === SERVER_HANDLER_ATTEMPTS)
235
+ return result;
236
+ await waitForHandlerRetry(attempt);
237
+ }
238
+ return result;
239
+ }
240
+ async function runConsumedSignal(task, agentId, kind, handler) {
170
241
  try {
171
- await reviewStore.putRound(task.id, data.phase, data);
172
- return true;
242
+ await handler();
173
243
  }
174
244
  catch (err) {
175
- await emitIntervention(bus, task, {
176
- phase: `server-${data.phase}-verdict-store-failed`,
177
- round: data.round,
178
- error: err instanceof Error ? err.message : String(err),
179
- });
180
- await manager.setupPhaseSignal(task.id, agentId, kind, { skipSnapshot: true });
245
+ await holdConsumedSignal(task, agentId, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-handler-failed`, { error: err instanceof Error ? err.message : String(err) });
246
+ }
247
+ }
248
+ async function deleteFindingsOrHold(task, qa, kind, expectedTask) {
249
+ try {
250
+ return await manager.deleteReviewExchangeIfCurrent(task.id, qa, 'findings', expectedTask);
251
+ }
252
+ catch (err) {
253
+ await holdConsumedSignal(task, qa.id, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-findings-cleanup-failed`, { error: err instanceof Error ? err.message : String(err) });
181
254
  return false;
182
255
  }
183
256
  }
184
- async function rearmOrHold(task, agentId, kind) {
185
- const armed = await manager.setupPhaseSignal(task.id, agentId, kind, { skipSnapshot: true });
186
- if (!armed)
187
- await manager.holdAgentForUnarmedSignal(task.id, agentId, kind);
257
+ async function deleteResponseOrHold(task, dev, kind, opts) {
258
+ try {
259
+ const deleted = await manager.deleteReviewExchangeIfCurrent(task.id, dev, 'response', opts.expectedTask ?? taskGenerationGuard(task));
260
+ if (deleted)
261
+ opts.onSideEffect?.();
262
+ return deleted;
263
+ }
264
+ catch (err) {
265
+ return holdConsumedSignal(task, dev.id, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-response-cleanup-failed`, { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
266
+ }
267
+ }
268
+ async function rejectServerResponse(task, dev, roundData, failure, onSideEffect) {
269
+ const phase = roundData.phase;
270
+ const signalKind = `${phase}-fixed`;
271
+ const sourceToken = task.signalToken;
272
+ if (!sourceToken)
273
+ return false;
274
+ const findingsDigest = reviewFindingsDigest(roundData.findings);
275
+ const missingFindingIds = [...(failure.missingFindingIds ?? [])].sort();
276
+ const unknownFindingIds = [...(failure.unknownFindingIds ?? [])].sort();
277
+ const schemaViolationCodes = [...(failure.schemaViolationCodes ?? [])].sort();
278
+ const failureSignature = serverResponseFailureSignature({
279
+ phase,
280
+ round: roundData.round,
281
+ findingsDigest,
282
+ reason: failure.reason,
283
+ missingFindingIds,
284
+ unknownFindingIds,
285
+ schemaViolationCodes,
286
+ });
287
+ const claimed = await manager.claimServerSignalRecovery(task, dev.id, signalKind, {
288
+ mode: 'classify-response',
289
+ phase,
290
+ round: roundData.round,
291
+ findingsDigest,
292
+ failureSignature,
293
+ reason: failure.reason,
294
+ failurePhase: failure.failurePhase,
295
+ missingFindingIds,
296
+ unknownFindingIds,
297
+ schemaViolationCodes,
298
+ ...(failure.responseDigest ? { responseDigest: failure.responseDigest } : {}),
299
+ });
300
+ if (!claimed?.signalToken)
301
+ return false;
302
+ onSideEffect?.();
303
+ let recorded;
304
+ try {
305
+ recorded = await reviewStore.recordServerResponseFailure(task.id, phase, roundData.round, {
306
+ signalKind,
307
+ sourceToken,
308
+ successorToken: claimed.signalToken,
309
+ failureSignature,
310
+ reason: failure.reason,
311
+ missingFindingIds,
312
+ unknownFindingIds,
313
+ schemaViolationCodes,
314
+ createdAt: new Date().toISOString(),
315
+ ...(failure.responseDigest ? { responseDigest: failure.responseDigest } : {}),
316
+ ...(failure.rawResponse !== undefined ? { rawResponse: failure.rawResponse } : {}),
317
+ });
318
+ }
319
+ catch (err) {
320
+ const message = err instanceof Error ? err.message : String(err);
321
+ const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, `server-${phase}-response-audit-store-failed`, `${message}. The response file was preserved and the old token is retired.`);
322
+ if (!held)
323
+ return false;
324
+ await emitIntervention(bus, held, {
325
+ phase: `server-${phase}-response-audit-store-failed`,
326
+ round: roundData.round,
327
+ error: message,
328
+ successorToken: claimed.signalToken,
329
+ });
330
+ return false;
331
+ }
332
+ const nextMode = recorded.disposition === 'auto-correct' ? 'correct-response' : 'hold';
333
+ const finalized = await manager.setServerSignalRecoveryMode(task.id, claimed.signalToken, nextMode);
334
+ if (!finalized)
335
+ return false;
336
+ if (recorded.disposition !== 'auto-correct') {
337
+ const holdPhase = recorded.disposition === 'hold-repeated-signature'
338
+ ? 'server-feedback-response-repeated'
339
+ : 'server-feedback-correction-limit';
340
+ const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, holdPhase, `The ${phase} response failed with ${failure.reason}; automatic correction stopped `
341
+ + `(${recorded.disposition}). Rewrite it deliberately, then Restart or Cancel the task.`);
342
+ if (!held)
343
+ return false;
344
+ await emitIntervention(bus, held, {
345
+ phase: holdPhase,
346
+ reviewPhase: phase,
347
+ round: roundData.round,
348
+ reason: failure.reason,
349
+ disposition: recorded.disposition,
350
+ missingFindingIds,
351
+ unknownFindingIds,
352
+ findingsDigest,
353
+ successorToken: claimed.signalToken,
354
+ });
355
+ return false;
356
+ }
357
+ const beforeCleanup = await manager.getTask(task.id);
358
+ const currentRecovery = beforeCleanup?.serverSignalRecovery;
359
+ const currentRound = phase === 'spec' ? beforeCleanup?.specReviewRound : beforeCleanup?.reviewRound;
360
+ if (!beforeCleanup
361
+ || beforeCleanup.status !== task.status
362
+ || beforeCleanup.phase !== task.phase
363
+ || beforeCleanup.signalToken !== claimed.signalToken
364
+ || currentRound !== roundData.round
365
+ || currentRecovery?.mode !== 'correct-response'
366
+ || currentRecovery.sourceToken !== sourceToken
367
+ || currentRecovery.failureSignature !== failureSignature) {
368
+ return false;
369
+ }
370
+ try {
371
+ const deleted = await manager.deleteReviewExchangeIfCurrent(task.id, dev, 'response', taskGenerationGuard(beforeCleanup), currentRecovery);
372
+ if (!deleted)
373
+ return false;
374
+ }
375
+ catch (err) {
376
+ const message = err instanceof Error ? err.message : String(err);
377
+ const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, `server-${phase}-response-cleanup-failed`, `${message}. The rejected response is archived, but the live file could not be removed safely.`);
378
+ if (!held)
379
+ return false;
380
+ await emitIntervention(bus, held, {
381
+ phase: `server-${phase}-response-cleanup-failed`,
382
+ round: roundData.round,
383
+ error: message,
384
+ successorToken: claimed.signalToken,
385
+ });
386
+ return false;
387
+ }
388
+ await emitIntervention(bus, finalized, {
389
+ phase: failure.failurePhase,
390
+ round: roundData.round,
391
+ reason: failure.reason,
392
+ missingFindingIds,
393
+ unknownFindingIds,
394
+ schemaViolationCodes,
395
+ findingsDigest,
396
+ responseDigest: failure.responseDigest,
397
+ successorToken: claimed.signalToken,
398
+ ...failure.details,
399
+ });
400
+ await manager.dispatchServerFeedbackCorrection(task.id, JSON.stringify(roundData.findings));
401
+ return false;
188
402
  }
189
403
  function resolveActiveQa(task) {
190
404
  if (!task.qaAgentId)
@@ -192,28 +406,30 @@ export function registerServerEventHandlers(bus, manager) {
192
406
  const recorded = manager.getAgentConfig(task.qaAgentId);
193
407
  return recorded?.role === 'qa' ? recorded : undefined;
194
408
  }
195
- async function pauseForUnavailableQa(task, entryKind, phase) {
409
+ async function pauseForUnavailableQa(task, entryKind, phase, onSideEffect) {
196
410
  if (!task.qaAgentId || resolveActiveQa(task))
197
411
  return false;
198
- await emitIntervention(bus, task, { phase, qaAgentId: task.qaAgentId });
199
- if (task.agentId)
200
- await rearmOrHold(task, task.agentId, entryKind);
412
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', phase, { qaAgentId: task.qaAgentId }, onSideEffect);
201
413
  return true;
202
414
  }
203
- async function autoApproveCode(task, entryKind = 'code-done') {
204
- const afterDone = manager.resolveAfterDone(task);
205
- if (task.afterDone === undefined) {
206
- await manager.updateTask(task.id, { afterDone });
207
- }
415
+ async function autoApproveCode(task, expectedTask, entryKind = 'code-done', onSideEffect) {
416
+ const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
417
+ onSideEffect?.();
208
418
  if (afterDone === null) {
209
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['in_progress', 'fixing'] });
210
- if (!ready)
211
- await emitIntervention(bus, task, { phase: 'server-code-auto-approve-transition-failed' });
419
+ const ready = await manager.transitionTaskStatus(task.id, 'ready', {
420
+ fromStatus: ['in_progress', 'fixing'],
421
+ ...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
422
+ });
423
+ if (ready)
424
+ onSideEffect?.();
425
+ if (!ready) {
426
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
427
+ }
212
428
  return;
213
429
  }
214
430
  const dev = manager.getAgentConfig(task.agentId);
215
431
  if (!dev) {
216
- await emitIntervention(bus, task, { phase: 'server-code-auto-approve-no-dev-agent' });
432
+ await holdConsumedSignal(task, task.agentId, entryKind, 'fix-no-dev-agent', 'server-code-auto-approve-no-dev-agent', {}, onSideEffect);
217
433
  return;
218
434
  }
219
435
  await manager.refreshWorkdirCacheFor(task.agentId);
@@ -222,73 +438,75 @@ export function registerServerEventHandlers(bus, manager) {
222
438
  reviewHeadAnchorSha = await transport().readHeadSha(dev);
223
439
  }
224
440
  catch (err) {
225
- await emitIntervention(bus, task, {
226
- phase: 'server-code-auto-approve-head-capture-failed',
227
- error: err instanceof Error ? err.message : String(err),
228
- });
229
- await manager.setupPhaseSignal(task.id, task.agentId, entryKind, { skipSnapshot: true });
441
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-head-capture-failed', { error: err instanceof Error ? err.message : String(err) }, onSideEffect);
230
442
  return;
231
443
  }
232
- const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['in_progress', 'fixing'] }, { reviewHeadAnchorSha });
444
+ const approved = await manager.transitionTaskStatus(task.id, 'approved', {
445
+ fromStatus: ['in_progress', 'fixing'],
446
+ ...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
447
+ }, { reviewHeadAnchorSha });
448
+ if (approved)
449
+ onSideEffect?.();
233
450
  if (!approved) {
234
- await emitIntervention(bus, task, { phase: 'server-code-auto-approve-transition-failed' });
451
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
235
452
  return;
236
453
  }
237
- await manager.dispatchServerAfterDone(task.id, afterDone);
454
+ const dispatched = await manager.dispatchServerAfterDone(task.id, afterDone);
455
+ if (!dispatched) {
456
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-publish-dispatch-failed', {}, onSideEffect);
457
+ }
238
458
  }
239
459
  function specApprovalIsHuman(task) {
240
460
  return task.researchAgentId !== undefined
241
461
  || manager.getProjectConfig(task.projectId)?.specApproval === 'human';
242
462
  }
243
- async function advanceApprovedSpec(task, failurePhase) {
463
+ async function advanceApprovedSpec(task, failurePhase, expectedTask) {
244
464
  try {
245
465
  const result = specApprovalIsHuman(task)
246
- ? await manager.parkTaskAtSpecReady(task.id)
247
- : await manager.transitionToCodePhase(task.id);
466
+ ? await manager.parkTaskAtSpecReady(task.id, {
467
+ ...(expectedTask !== undefined ? { expectedTask } : {}),
468
+ })
469
+ : await manager.transitionToCodePhase(task.id, expectedTask);
248
470
  if (!result) {
249
- await emitIntervention(bus, task, { phase: failurePhase });
471
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase);
250
472
  }
251
473
  }
252
474
  catch (err) {
253
- await emitIntervention(bus, task, {
254
- phase: failurePhase,
255
- error: err instanceof Error ? err.message : String(err),
256
- });
475
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase, { error: err instanceof Error ? err.message : String(err) });
257
476
  }
258
477
  }
259
- async function autoApproveSpec(task) {
478
+ async function autoApproveSpec(task, expectedTask, onSideEffect) {
260
479
  try {
261
- const result = await manager.transitionToCodePhase(task.id);
480
+ const result = await manager.transitionToCodePhase(task.id, expectedTask);
481
+ if (result)
482
+ onSideEffect?.();
262
483
  if (!result) {
263
- await emitIntervention(bus, task, { phase: 'server-spec-auto-approve-transition-failed' });
484
+ await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-auto-approve-transition-failed', {}, onSideEffect);
264
485
  return false;
265
486
  }
266
487
  return true;
267
488
  }
268
489
  catch (err) {
269
- await emitIntervention(bus, task, {
270
- phase: 'server-spec-auto-approve-transition-failed',
271
- error: err instanceof Error ? err.message : String(err),
272
- });
490
+ await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-auto-approve-transition-failed', { error: err instanceof Error ? err.message : String(err) }, onSideEffect);
273
491
  return false;
274
492
  }
275
493
  }
276
494
  async function prepareAndDispatchCodeReview(task, opts) {
277
495
  const dev = manager.getAgentConfig(task.agentId);
278
496
  if (!dev) {
279
- await emitIntervention(bus, task, { phase: 'server-code-review-no-dev-agent' });
280
- if (task.agentId) {
281
- const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
282
- await rearmOrHold(task, task.agentId, kind);
283
- }
284
- return false;
497
+ const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
498
+ return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-code-review-no-dev-agent', {}, opts.onSideEffect);
285
499
  }
286
500
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
287
- const nextRound = task.reviewRound + 1;
501
+ const nextRound = opts.bumpRound === false ? task.reviewRound : task.reviewRound + 1;
288
502
  if (nextRound > cap) {
289
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['in_progress', 'fixing'] });
503
+ const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
504
+ fromStatus: ['in_progress', 'fixing'],
505
+ ...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
506
+ });
290
507
  if (!capResult)
291
508
  return false;
509
+ opts.onSideEffect?.();
292
510
  const paused = capResult.task;
293
511
  if (paused.qaAgentId)
294
512
  await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
@@ -303,49 +521,50 @@ export function registerServerEventHandlers(bus, manager) {
303
521
  });
304
522
  return false;
305
523
  }
306
- await manager.refreshWorkdirCacheFor(task.agentId);
307
524
  let content;
308
525
  let reviewHeadAnchorSha;
309
- try {
310
- content = await transport().readContent(task, dev, 'code');
311
- if (!content.headSha)
312
- throw new ReviewExchangeError('head-failed', 'review head missing from captured content');
313
- reviewHeadAnchorSha = content.headSha;
314
- }
315
- catch (err) {
316
- await emitIntervention(bus, task, {
317
- phase: 'server-code-content-read-failed',
318
- error: err instanceof Error ? err.message : String(err),
319
- });
320
- const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
321
- await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
322
- return false;
323
- }
324
- const rearmEntry = async () => {
325
- const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
326
- await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
327
- };
328
- try {
329
- const violation = await manager.findLineageViolation(task.id, content.baseSha);
330
- if (violation) {
331
- await emitIntervention(bus, task, {
332
- phase: 'server-code-lineage-violation',
333
- offendingTaskId: violation.taskId,
334
- offendingBranch: violation.branch,
335
- offendingSha: violation.sha,
336
- note: 'The task branch embeds another active task\'s commits — reviewing it would leak foreign work into this task. Have the dev rebase onto origin/HEAD and re-emit the signal.',
337
- });
338
- await rearmEntry();
339
- return false;
526
+ if (opts.bumpRound === false) {
527
+ const stored = await reviewStore.getRound(task.id, 'code', nextRound);
528
+ if (!stored || stored.phase !== 'code' || !stored.headSha) {
529
+ throw new Error(`Task ${task.id} has no persisted code review round ${nextRound} to redispatch`);
340
530
  }
531
+ content = {
532
+ content: stored.content,
533
+ ...(stored.diffstat ? { diffstat: stored.diffstat } : {}),
534
+ ...(stored.baseSha ? { baseSha: stored.baseSha } : {}),
535
+ headSha: stored.headSha,
536
+ ...(stored.headTree ? { headTree: stored.headTree } : {}),
537
+ };
538
+ reviewHeadAnchorSha = stored.headSha;
341
539
  }
342
- catch (err) {
343
- await emitIntervention(bus, task, {
344
- phase: 'server-code-lineage-check-failed',
345
- error: err instanceof Error ? err.message : String(err),
346
- });
347
- await rearmEntry();
348
- return false;
540
+ else {
541
+ await manager.refreshWorkdirCacheFor(task.agentId);
542
+ try {
543
+ content = await transport().readContent(task, dev, 'code');
544
+ if (!content.headSha)
545
+ throw new ReviewExchangeError('head-failed', 'review head missing from captured content');
546
+ reviewHeadAnchorSha = content.headSha;
547
+ }
548
+ catch (err) {
549
+ const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
550
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
551
+ }
552
+ try {
553
+ const violation = await manager.findLineageViolation(task.id, content.baseSha);
554
+ if (violation) {
555
+ const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
556
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-violation', {
557
+ offendingTaskId: violation.taskId,
558
+ offendingBranch: violation.branch,
559
+ offendingSha: violation.sha,
560
+ note: 'The task branch embeds another active task\'s commits — reviewing it would leak foreign work into this task. Have the dev rebase onto origin/HEAD and re-emit the signal.',
561
+ }, opts.onSideEffect);
562
+ }
563
+ }
564
+ catch (err) {
565
+ const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
566
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-check-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
567
+ }
349
568
  }
350
569
  const round = {
351
570
  round: nextRound,
@@ -357,21 +576,7 @@ export function registerServerEventHandlers(bus, manager) {
357
576
  ...(content.headTree ? { headTree: content.headTree } : {}),
358
577
  startedAt: new Date().toISOString(),
359
578
  };
360
- const putEntryRound = async (data) => {
361
- try {
362
- await reviewStore.putRound(task.id, 'code', data);
363
- return true;
364
- }
365
- catch (err) {
366
- await emitIntervention(bus, task, {
367
- phase: 'server-code-round-store-failed',
368
- error: err instanceof Error ? err.message : String(err),
369
- });
370
- const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
371
- await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
372
- return false;
373
- }
374
- };
579
+ const putEntryRound = (data) => putVerdictRound(task, task.agentId, task.status === 'fixing' ? 'code-fixed' : 'code-done', data, opts);
375
580
  const resolveRecheckInterdiff = async () => {
376
581
  if (!opts.recheck)
377
582
  return undefined;
@@ -385,12 +590,13 @@ export function registerServerEventHandlers(bus, manager) {
385
590
  return undefined;
386
591
  }
387
592
  };
388
- if (!(await putEntryRound(round)))
593
+ if (opts.bumpRound !== false && !(await putEntryRound(round)))
389
594
  return false;
390
595
  const interdiff = await resolveRecheckInterdiff();
391
596
  const dispatched = await manager.dispatchServerReviewToQa(task.id, {
392
597
  phase: 'code',
393
598
  recheck: opts.recheck,
599
+ ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
394
600
  content: content.content,
395
601
  reviewHeadAnchorSha,
396
602
  ...(content.baseSha ? { baseSha: content.baseSha } : {}),
@@ -399,8 +605,15 @@ export function registerServerEventHandlers(bus, manager) {
399
605
  ...(content.diffstat ? { diffstat: content.diffstat } : {}),
400
606
  ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
401
607
  ...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
608
+ callerOwnsConsumedSignalFailure: true,
609
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
610
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
611
+ ...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
402
612
  });
403
- return dispatched !== null;
613
+ if (dispatched)
614
+ return true;
615
+ const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
616
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-review-dispatch-failed', {}, opts.onSideEffect);
404
617
  }
405
618
  async function dispatchLegacyBatch(taskId, batches, index, diffstat, opts) {
406
619
  const text = batches[index].map(f => f.text).join('\n');
@@ -413,6 +626,9 @@ export function registerServerEventHandlers(bus, manager) {
413
626
  batch: { index, total: batches.length },
414
627
  ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
415
628
  ...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
629
+ callerOwnsConsumedSignalFailure: true,
630
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
631
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
416
632
  });
417
633
  return dispatched !== null;
418
634
  }
@@ -423,137 +639,135 @@ export function registerServerEventHandlers(bus, manager) {
423
639
  const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'code', requireServerMode: true });
424
640
  if (!gated)
425
641
  return;
426
- const { task } = gated;
427
- if (await pauseForUnavailableQa(task, 'code-done', 'server-code-qa-unavailable'))
428
- return;
429
- if (!task.qaAgentId) {
430
- await autoApproveCode(task);
431
- return;
432
- }
433
- await prepareAndDispatchCodeReview(task, { recheck: false });
642
+ const { task, expectedTask } = gated;
643
+ await runConsumedSignal(task, task.agentId, 'code-done', async () => {
644
+ if (await pauseForUnavailableQa(task, 'code-done', 'server-code-qa-unavailable'))
645
+ return;
646
+ if (!task.qaAgentId) {
647
+ await autoApproveCode(task, expectedTask);
648
+ return;
649
+ }
650
+ await prepareAndDispatchCodeReview(task, { recheck: false, expectedTask, settlePermit: settlePermitOf(event) });
651
+ });
434
652
  });
435
653
  bus.on('server.code.review.submitted', async (event) => {
436
654
  const gated = await gate(bus, manager, event, { status: 'review', phase: 'code', requireServerMode: true });
437
655
  if (!gated)
438
656
  return;
439
- const { task } = gated;
440
- const qa = manager.getAgentConfig(task.qaAgentId ?? '');
441
- if (!qa) {
442
- await emitIntervention(bus, task, { phase: 'server-code-review-no-qa-agent' });
443
- if (task.qaAgentId) {
444
- await rearmOrHold(task, task.qaAgentId, 'code-reviewed');
445
- }
446
- return;
447
- }
448
- const round = Math.max(task.reviewRound, 1);
449
- const roundData = await reviewStore.getRound(task.id, 'code', round);
450
- if (!roundData) {
451
- await emitIntervention(bus, task, { phase: 'server-code-review-round-missing', round });
452
- await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
453
- return;
454
- }
455
- await manager.refreshWorkdirCacheFor(qa.id);
456
- let findings;
457
- try {
458
- findings = await transport().readFindings(task, qa);
459
- }
460
- catch (err) {
461
- const reason = err instanceof ReviewExchangeError ? err.reason : 'unknown';
462
- await emitIntervention(bus, task, {
463
- phase: 'server-code-findings-invalid',
464
- reason,
465
- error: err instanceof Error ? err.message : String(err),
466
- });
467
- await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
468
- return;
469
- }
470
- let current = roundData;
471
- if (findings === null) {
472
- const alreadyStored = task.batchTotal !== undefined
473
- ? (roundData.batchFindings?.length ?? 0) > (task.batchIndex ?? 0)
474
- : roundData.findings !== undefined;
475
- if (!alreadyStored) {
476
- await emitIntervention(bus, task, { phase: 'server-code-findings-missing', round });
477
- await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
657
+ const { task, expectedTask } = gated;
658
+ await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', async () => {
659
+ const qa = manager.getAgentConfig(task.qaAgentId ?? '');
660
+ if (!qa) {
661
+ await holdConsumedSignal(task, task.qaAgentId ?? '', 'code-reviewed', 'handler-failed', 'server-code-review-no-qa-agent');
478
662
  return;
479
663
  }
480
- }
481
- else if (findings.round !== round) {
482
- await emitIntervention(bus, task, {
483
- phase: 'server-code-findings-round-mismatch',
484
- round,
485
- payloadRound: findings.round,
486
- });
487
- await transport().deleteFindings(qa);
488
- await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
489
- return;
490
- }
491
- else if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
492
- const batchFindings = [...(roundData.batchFindings ?? [])];
493
- batchFindings[task.batchIndex] = findings;
494
- current = { ...roundData, batchFindings };
495
- if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current)))
664
+ const round = Math.max(task.reviewRound, 1);
665
+ const roundData = await reviewStore.getRound(task.id, 'code', round);
666
+ if (!roundData) {
667
+ await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-review-round-missing', { round });
496
668
  return;
497
- await transport().deleteFindings(qa);
498
- }
499
- else {
500
- current = { ...roundData, findings, completedAt: new Date().toISOString() };
501
- if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current)))
669
+ }
670
+ await manager.refreshWorkdirCacheFor(qa.id);
671
+ let findings;
672
+ try {
673
+ findings = await transport().readFindings(task, qa);
674
+ }
675
+ catch (err) {
676
+ const reason = err instanceof ReviewExchangeError ? err.reason : 'unknown';
677
+ await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-invalid', { reason, error: err instanceof Error ? err.message : String(err) });
502
678
  return;
503
- await transport().deleteFindings(qa);
504
- }
505
- const isRecheck = round > 1;
506
- if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
507
- const nextIndex = task.batchIndex + 1;
508
- if (nextIndex < task.batchTotal) {
509
- const prev = isRecheck ? await reviewStore.getRound(task.id, 'code', round - 1) : null;
510
- await dispatchLegacyBatch(task.id, rebuildLegacyBatches(current), nextIndex, current.diffstat, {
511
- recheck: isRecheck,
512
- ...(prev?.findings ? { priorFindingsJson: JSON.stringify(prev.findings) } : {}),
513
- ...(prev?.response ? { priorResponseJson: JSON.stringify(prev.response) } : {}),
514
- });
679
+ }
680
+ let current = roundData;
681
+ if (findings === null) {
682
+ const alreadyStored = task.batchTotal !== undefined
683
+ ? (roundData.batchFindings?.length ?? 0) > (task.batchIndex ?? 0)
684
+ : roundData.findings !== undefined;
685
+ if (!alreadyStored) {
686
+ await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-missing', { round });
687
+ return;
688
+ }
689
+ }
690
+ else if (findings.round !== round) {
691
+ await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-round-mismatch', { round, payloadRound: findings.round });
515
692
  return;
516
693
  }
517
- const aggregated = current.findings
518
- ?? aggregateBatchFindings((current.batchFindings ?? []).filter(Boolean), round);
519
- if (!current.findings) {
520
- const stored = await putVerdictRound(task, qa.id, 'code-reviewed', {
521
- ...current,
522
- findings: aggregated,
523
- completedAt: new Date().toISOString(),
524
- });
525
- if (!stored)
694
+ else if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
695
+ const batchFindings = [...(roundData.batchFindings ?? [])];
696
+ batchFindings[task.batchIndex] = findings;
697
+ current = { ...roundData, batchFindings };
698
+ if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
699
+ return;
700
+ if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
526
701
  return;
527
702
  }
528
- await manager.updateTask(task.id, { batchIndex: undefined, batchTotal: undefined });
529
- await routeCodeVerdict({ ...task, batchIndex: undefined, batchTotal: undefined }, aggregated);
530
- return;
531
- }
532
- await routeCodeVerdict(task, current.findings);
703
+ else {
704
+ current = { ...roundData, findings, completedAt: new Date().toISOString() };
705
+ if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
706
+ return;
707
+ if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
708
+ return;
709
+ }
710
+ const isRecheck = round > 1;
711
+ if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
712
+ const nextIndex = task.batchIndex + 1;
713
+ if (nextIndex < task.batchTotal) {
714
+ const prev = isRecheck ? await reviewStore.getRound(task.id, 'code', round - 1) : null;
715
+ const dispatched = await dispatchLegacyBatch(task.id, rebuildLegacyBatches(current), nextIndex, current.diffstat, {
716
+ recheck: isRecheck,
717
+ ...(prev?.findings ? { priorFindingsJson: JSON.stringify(prev.findings) } : {}),
718
+ ...(prev?.response ? { priorResponseJson: JSON.stringify(prev.response) } : {}),
719
+ expectedTask,
720
+ settlePermit: settlePermitOf(event),
721
+ });
722
+ if (!dispatched) {
723
+ await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-review-continuation-dispatch-failed');
724
+ }
725
+ return;
726
+ }
727
+ const aggregated = current.findings
728
+ ?? aggregateBatchFindings((current.batchFindings ?? []).filter(Boolean), round);
729
+ if (!current.findings) {
730
+ const stored = await putVerdictRound(task, qa.id, 'code-reviewed', {
731
+ ...current,
732
+ findings: aggregated,
733
+ completedAt: new Date().toISOString(),
734
+ }, { expectedTask });
735
+ if (!stored)
736
+ return;
737
+ }
738
+ const cleared = await manager.updateTaskIfStatus(task.id, expectedTask.status, { batchIndex: undefined, batchTotal: undefined }, expectedTask);
739
+ if (!cleared)
740
+ return;
741
+ await routeCodeVerdict({ ...task, batchIndex: undefined, batchTotal: undefined }, aggregated, expectedTask);
742
+ return;
743
+ }
744
+ await routeCodeVerdict(task, current.findings, expectedTask);
745
+ });
533
746
  });
534
- async function routeCodeVerdict(task, findings) {
747
+ async function routeCodeVerdict(task, findings, expectedTask) {
535
748
  if (findings.verdict === 'approve') {
536
- const afterDone = manager.resolveAfterDone(task);
537
- if (task.afterDone === undefined) {
538
- await manager.updateTask(task.id, { afterDone });
539
- }
749
+ const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
540
750
  if (afterDone === null) {
541
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'] });
542
- if (!ready)
543
- await emitIntervention(bus, task, { phase: 'server-code-ready-transition-failed' });
751
+ const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'], expectTask: expectedTask });
752
+ if (!ready) {
753
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-ready-transition-failed');
754
+ }
544
755
  return;
545
756
  }
546
- const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'] });
757
+ const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'], expectTask: expectedTask });
547
758
  if (!approved) {
548
- await emitIntervention(bus, task, { phase: 'server-code-approved-transition-failed' });
759
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-approved-transition-failed');
549
760
  return;
550
761
  }
551
- await manager.dispatchServerAfterDone(task.id, afterDone);
762
+ const dispatched = await manager.dispatchServerAfterDone(task.id, afterDone);
763
+ if (!dispatched) {
764
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-publish-dispatch-failed');
765
+ }
552
766
  return;
553
767
  }
554
768
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
555
769
  if (task.reviewRound >= cap) {
556
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'] });
770
+ const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'], expectTask: expectedTask });
557
771
  if (!capResult)
558
772
  return;
559
773
  const paused = capResult.task;
@@ -571,173 +785,217 @@ export function registerServerEventHandlers(bus, manager) {
571
785
  });
572
786
  return;
573
787
  }
574
- await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings));
788
+ const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings), { callerOwnsConsumedSignalFailure: true, expectedTask });
789
+ if (!dispatched) {
790
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-fix-dispatch-failed');
791
+ }
575
792
  }
576
- async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson) {
577
- if (await pauseForUnavailableQa(task, 'code-fixed', 'server-code-qa-unavailable-after-fix')) {
793
+ async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson, opts = {}) {
794
+ if (await pauseForUnavailableQa(task, 'code-fixed', 'server-code-qa-unavailable-after-fix', opts.onSideEffect)) {
578
795
  return false;
579
796
  }
580
797
  if (!task.qaAgentId) {
581
- await autoApproveCode(task, 'code-fixed');
798
+ await autoApproveCode(task, opts.expectedTask, 'code-fixed', opts.onSideEffect);
582
799
  return true;
583
800
  }
584
- return prepareAndDispatchCodeReview(task, { recheck: true, priorFindingsJson, priorResponseJson });
801
+ return prepareAndDispatchCodeReview(task, {
802
+ recheck: true,
803
+ priorFindingsJson,
804
+ priorResponseJson,
805
+ ...opts,
806
+ });
585
807
  }
586
- async function runCodeFixSubmission(task) {
808
+ async function runCodeFixSubmission(task, opts = {}) {
587
809
  const dev = manager.getAgentConfig(task.agentId);
588
810
  if (!dev) {
589
- await emitIntervention(bus, task, { phase: 'server-code-fix-no-dev-agent' });
590
- if (task.agentId) {
591
- await rearmOrHold(task, task.agentId, 'code-fixed');
592
- }
593
- return false;
811
+ return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-no-dev-agent', 'server-code-fix-no-dev-agent', {}, opts.onSideEffect);
594
812
  }
595
813
  const round = Math.max(task.reviewRound, 1);
596
814
  const roundData = await reviewStore.getRound(task.id, 'code', round);
597
815
  if (!roundData?.findings) {
598
- await emitIntervention(bus, task, { phase: 'server-code-fix-findings-missing', round });
599
- await manager.setupPhaseSignal(task.id, task.agentId, 'code-fixed', { skipSnapshot: true });
600
- return false;
816
+ return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-findings-missing', 'server-code-fix-findings-missing', { round }, opts.onSideEffect);
601
817
  }
602
818
  await manager.refreshWorkdirCacheFor(task.agentId);
603
- let response;
819
+ let readResult;
604
820
  try {
605
- response = await transport().readResponse(task, dev);
821
+ readResult = await readResponseWithRetry(task, dev);
606
822
  }
607
823
  catch (err) {
608
- await emitIntervention(bus, task, {
609
- phase: 'server-code-response-invalid',
824
+ return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', {
825
+ reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
610
826
  error: err instanceof Error ? err.message : String(err),
611
- });
612
- await manager.setupPhaseSignal(task.id, dev.id, 'code-fixed', { skipSnapshot: true });
613
- return false;
614
- }
615
- if (response === null) {
827
+ }, opts.onSideEffect);
828
+ }
829
+ if (readResult.kind === 'unknown') {
830
+ return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message }, opts.onSideEffect);
831
+ }
832
+ if (readResult.kind === 'invalid') {
833
+ return rejectServerResponse(task, dev, roundData, {
834
+ reason: 'response-invalid',
835
+ failurePhase: 'server-code-response-invalid',
836
+ rawResponse: readResult.raw,
837
+ responseDigest: readResult.responseDigest,
838
+ schemaViolationCodes: readResult.schemaViolationCodes,
839
+ details: { error: readResult.error.message },
840
+ }, opts.onSideEffect);
841
+ }
842
+ if (readResult.kind === 'absent') {
616
843
  if (roundData.response === undefined) {
617
- await emitIntervention(bus, task, { phase: 'server-code-response-missing', round });
618
- await manager.setupPhaseSignal(task.id, dev.id, 'code-fixed', { skipSnapshot: true });
619
- return false;
844
+ return rejectServerResponse(task, dev, roundData, {
845
+ reason: 'response-missing',
846
+ failurePhase: 'server-code-response-missing',
847
+ }, opts.onSideEffect);
620
848
  }
621
- return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response));
849
+ return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response), opts);
622
850
  }
851
+ const { response } = readResult;
623
852
  if (response.round !== round) {
624
- await emitIntervention(bus, task, {
625
- phase: 'server-code-response-round-mismatch',
626
- round,
627
- payloadRound: response.round,
628
- });
629
- await transport().deleteResponse(dev);
630
- await manager.setupPhaseSignal(task.id, dev.id, 'code-fixed', { skipSnapshot: true });
631
- return false;
853
+ return rejectServerResponse(task, dev, roundData, {
854
+ reason: 'round-mismatch',
855
+ failurePhase: 'server-code-response-round-mismatch',
856
+ rawResponse: readResult.raw,
857
+ responseDigest: readResult.responseDigest,
858
+ details: { payloadRound: response.round },
859
+ }, opts.onSideEffect);
860
+ }
861
+ if (response.token !== task.signalToken) {
862
+ return rejectServerResponse(task, dev, roundData, {
863
+ reason: 'token-mismatch',
864
+ failurePhase: 'server-code-response-token-mismatch',
865
+ rawResponse: readResult.raw,
866
+ responseDigest: readResult.responseDigest,
867
+ }, opts.onSideEffect);
868
+ }
869
+ const findingsDigest = reviewFindingsDigest(roundData.findings);
870
+ if (response.findingsDigest !== findingsDigest) {
871
+ return rejectServerResponse(task, dev, roundData, {
872
+ reason: 'findings-digest-mismatch',
873
+ failurePhase: 'server-code-response-findings-digest-mismatch',
874
+ rawResponse: readResult.raw,
875
+ responseDigest: readResult.responseDigest,
876
+ }, opts.onSideEffect);
632
877
  }
633
878
  const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
634
879
  if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
635
- await emitIntervention(bus, task, {
636
- phase: 'server-code-response-coverage-gap',
637
- round,
880
+ return rejectServerResponse(task, dev, roundData, {
881
+ reason: 'coverage-gap',
882
+ failurePhase: 'server-code-response-coverage-gap',
883
+ rawResponse: readResult.raw,
884
+ responseDigest: readResult.responseDigest,
638
885
  missingFindingIds: gaps.missing,
639
886
  unknownFindingIds: gaps.unknown,
640
- });
641
- await manager.setupPhaseSignal(task.id, dev.id, 'code-fixed', { skipSnapshot: true });
642
- return false;
887
+ }, opts.onSideEffect);
643
888
  }
644
- if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response })))
889
+ if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response }, opts)))
645
890
  return false;
646
- await transport().deleteResponse(dev);
647
- return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(response));
891
+ if (!(await deleteResponseOrHold(task, dev, 'code-fixed', opts)))
892
+ return false;
893
+ return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(response), opts);
648
894
  }
649
895
  bus.on('server.code.fix.submitted', async (event) => {
650
896
  const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'code', requireServerMode: true });
651
897
  if (!gated)
652
898
  return;
653
- await runCodeFixSubmission(gated.task);
899
+ await runConsumedSignal(gated.task, gated.task.agentId, 'code-fixed', async () => {
900
+ await runCodeFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
901
+ });
654
902
  });
655
903
  bus.on('server.code.published', async (event) => {
656
904
  const gated = await gate(bus, manager, event, { status: 'approved', phase: 'code', requireServerMode: true });
657
905
  if (!gated)
658
906
  return;
659
907
  const { task } = gated;
660
- const prNumber = typeof event.data?.prNumber === 'number' ? event.data.prNumber : undefined;
661
- if (prNumber === undefined && manager.resolveAfterDone(task) === 'pr' && task.prNumber === undefined) {
662
- await emitIntervention(bus, task, { phase: 'server-code-published-missing-pr-number' });
663
- await manager.setupPhaseSignal(task.id, task.agentId, 'code-ready', { skipSnapshot: true });
664
- return;
665
- }
666
- if (prNumber !== undefined) {
667
- const verified = await manager.verifyPaneSignalPrNumber(task.id, prNumber);
668
- if (!verified) {
669
- await emitIntervention(bus, task, {
670
- phase: 'server-code-published-pr-number-unverified',
671
- prNumber,
908
+ await runConsumedSignal(task, task.agentId, 'code-ready', async () => {
909
+ const prNumber = typeof event.data?.prNumber === 'number' ? event.data.prNumber : task.prNumber;
910
+ if (prNumber === undefined && manager.resolveAfterDone(task) === 'pr') {
911
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-missing-pr-number');
912
+ return;
913
+ }
914
+ const dev = manager.getAgentConfig(task.agentId);
915
+ if (!dev) {
916
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'fix-no-dev-agent', 'server-code-published-no-dev-agent');
917
+ return;
918
+ }
919
+ await manager.refreshWorkdirCacheFor(task.agentId);
920
+ let publishedHead;
921
+ try {
922
+ publishedHead = await transport().readHeadSha(dev);
923
+ }
924
+ catch (err) {
925
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
926
+ return;
927
+ }
928
+ if (publishedHead !== task.reviewHeadAnchorSha) {
929
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-mismatch', {
930
+ publishedHead,
931
+ reviewedHead: task.reviewHeadAnchorSha ?? null,
672
932
  });
673
- await manager.setupPhaseSignal(task.id, task.agentId, 'code-ready', { skipSnapshot: true });
674
933
  return;
675
934
  }
676
- await manager.updateTask(task.id, { prNumber });
677
- }
678
- const dev = manager.getAgentConfig(task.agentId);
679
- if (!dev) {
680
- await emitIntervention(bus, task, { phase: 'server-code-published-no-dev-agent' });
681
- return;
682
- }
683
- await manager.refreshWorkdirCacheFor(task.agentId);
684
- let publishedHead;
685
- try {
686
- publishedHead = await transport().readHeadSha(dev);
687
- }
688
- catch (err) {
689
- await emitIntervention(bus, task, {
690
- phase: 'server-code-published-head-capture-failed',
691
- error: err instanceof Error ? err.message : String(err),
692
- });
693
- await manager.setupPhaseSignal(task.id, task.agentId, 'code-ready', { skipSnapshot: true });
694
- return;
695
- }
696
- if (publishedHead !== task.reviewHeadAnchorSha) {
697
- await emitIntervention(bus, task, {
698
- phase: 'server-code-published-head-mismatch',
699
- publishedHead,
700
- reviewedHead: task.reviewHeadAnchorSha ?? null,
701
- });
702
- return;
703
- }
704
- try {
705
- await manager.updateTask(task.id, { latestHeadSha: publishedHead });
706
- }
707
- catch (err) {
708
- await emitIntervention(bus, task, {
709
- phase: 'server-code-published-head-capture-failed',
710
- error: err instanceof Error ? err.message : String(err),
711
- });
712
- await manager.setupPhaseSignal(task.id, task.agentId, 'code-ready', { skipSnapshot: true });
713
- return;
714
- }
715
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['approved'] });
716
- if (!ready)
717
- await emitIntervention(bus, task, { phase: 'server-code-published-transition-failed' });
935
+ let verifiedBaseBranch;
936
+ if (prNumber !== undefined) {
937
+ const verified = await manager.verifyPaneSignalPrNumber(task.id, prNumber);
938
+ if (!verified) {
939
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-pr-number-unverified', { prNumber });
940
+ return;
941
+ }
942
+ if (verified.headSha !== publishedHead) {
943
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-pr-head-mismatch', {
944
+ prNumber,
945
+ remoteHead: verified.headSha,
946
+ publishedHead,
947
+ reviewedHead: task.reviewHeadAnchorSha,
948
+ });
949
+ return;
950
+ }
951
+ verifiedBaseBranch = verified.targetBranch;
952
+ }
953
+ let ready;
954
+ try {
955
+ ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['approved'], expectTask: gated.expectedTask }, {
956
+ ...(prNumber !== undefined ? { prNumber } : {}),
957
+ ...(prNumber !== undefined
958
+ && task.baseBranch === undefined
959
+ && verifiedBaseBranch !== undefined
960
+ ? { baseBranch: verifiedBaseBranch }
961
+ : {}),
962
+ latestHeadSha: publishedHead,
963
+ });
964
+ }
965
+ catch (err) {
966
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
967
+ return;
968
+ }
969
+ if (!ready) {
970
+ await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-transition-failed');
971
+ }
972
+ });
718
973
  });
719
974
  bus.on('server.spec.ready', async (event) => {
720
975
  const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'spec', requireServerMode: false });
721
976
  if (!gated)
722
977
  return;
723
- await dispatchSpecReview(gated.task);
978
+ await runConsumedSignal(gated.task, gated.task.agentId, 'spec-done', async () => {
979
+ await dispatchSpecReview(gated.task, undefined, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
980
+ });
724
981
  });
725
- async function dispatchSpecReview(task, prior) {
982
+ async function dispatchSpecReview(task, prior, opts = {}) {
726
983
  const dev = manager.getAgentConfig(task.agentId);
727
984
  if (!dev) {
728
- await emitIntervention(bus, task, { phase: 'server-spec-review-no-dev-agent' });
729
- if (task.agentId) {
730
- const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
731
- await rearmOrHold(task, task.agentId, kind);
732
- }
733
- return false;
985
+ const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
986
+ return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-spec-review-no-dev-agent', {}, opts.onSideEffect);
734
987
  }
735
988
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
736
- const nextRound = (task.specReviewRound ?? 0) + 1;
989
+ const currentRound = task.specReviewRound ?? 0;
990
+ const nextRound = opts.bumpRound === false ? currentRound : currentRound + 1;
737
991
  if (nextRound > cap) {
738
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['in_progress', 'fixing'] });
992
+ const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
993
+ fromStatus: ['in_progress', 'fixing'],
994
+ ...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
995
+ });
739
996
  if (!capResult)
740
997
  return false;
998
+ opts.onSideEffect?.();
741
999
  const paused = capResult.task;
742
1000
  if (paused.qaAgentId)
743
1001
  await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
@@ -745,39 +1003,43 @@ export function registerServerEventHandlers(bus, manager) {
745
1003
  await releaseAtCap(paused, paused.agentId, 'agentId');
746
1004
  return false;
747
1005
  }
748
- await manager.refreshWorkdirCacheFor(task.agentId);
749
1006
  let content;
750
- try {
751
- content = await transport().readContent(task, dev, 'spec');
752
- }
753
- catch (err) {
754
- await emitIntervention(bus, task, {
755
- phase: 'server-spec-content-read-failed',
756
- error: err instanceof Error ? err.message : String(err),
757
- });
758
- const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
759
- await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
760
- return false;
761
- }
762
- try {
763
- if (!content.documents)
764
- throw new Error('spec transport returned no documents');
765
- await reviewStore.putRound(task.id, 'spec', {
766
- round: nextRound,
767
- phase: 'spec',
768
- content: content.content,
769
- documents: content.documents,
770
- startedAt: new Date().toISOString(),
771
- });
1007
+ if (opts.bumpRound === false) {
1008
+ const stored = await reviewStore.getRound(task.id, 'spec', nextRound);
1009
+ if (!stored || stored.phase !== 'spec') {
1010
+ throw new Error(`Task ${task.id} has no persisted spec review round ${nextRound} to redispatch`);
1011
+ }
1012
+ content = { content: stored.content, documents: stored.documents };
772
1013
  }
773
- catch (err) {
774
- await emitIntervention(bus, task, {
775
- phase: 'server-spec-round-store-failed',
776
- error: err instanceof Error ? err.message : String(err),
777
- });
778
- const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
779
- await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
780
- return false;
1014
+ else {
1015
+ await manager.refreshWorkdirCacheFor(task.agentId);
1016
+ try {
1017
+ content = await transport().readContent(task, dev, 'spec');
1018
+ }
1019
+ catch (err) {
1020
+ const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
1021
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-spec-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
1022
+ }
1023
+ try {
1024
+ if (!content.documents)
1025
+ throw new Error('spec transport returned no documents');
1026
+ const roundData = {
1027
+ round: nextRound,
1028
+ phase: 'spec',
1029
+ content: content.content,
1030
+ documents: content.documents,
1031
+ startedAt: new Date().toISOString(),
1032
+ };
1033
+ const stored = opts.expectedTask
1034
+ ? await manager.putReviewRoundIfCurrent(task.id, roundData, opts.expectedTask)
1035
+ : (await reviewStore.putRound(task.id, 'spec', roundData), true);
1036
+ if (!stored)
1037
+ return false;
1038
+ }
1039
+ catch (err) {
1040
+ const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
1041
+ return holdConsumedSignal(task, task.agentId, kind, 'verdict-store-failed', 'server-spec-round-store-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
1042
+ }
781
1043
  }
782
1044
  if (!resolveActiveQa(task)) {
783
1045
  if (task.qaAgentId) {
@@ -787,175 +1049,208 @@ export function registerServerEventHandlers(bus, manager) {
787
1049
  });
788
1050
  }
789
1051
  if (specApprovalIsHuman(task) || task.qaAgentId) {
790
- const parked = await manager.parkTaskAtSpecReady(task.id, { specReviewRound: nextRound });
791
- if (!parked)
792
- await emitIntervention(bus, task, { phase: 'server-spec-park-transition-failed' });
1052
+ const parked = await manager.parkTaskAtSpecReady(task.id, {
1053
+ specReviewRound: nextRound,
1054
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
1055
+ });
1056
+ if (parked)
1057
+ opts.onSideEffect?.();
1058
+ if (!parked) {
1059
+ await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-park-transition-failed', {}, opts.onSideEffect);
1060
+ }
793
1061
  return !!parked;
794
1062
  }
795
- return autoApproveSpec(task);
1063
+ return autoApproveSpec(task, opts.expectedTask, opts.onSideEffect);
796
1064
  }
797
1065
  const dispatched = await manager.dispatchServerReviewToQa(task.id, {
798
1066
  phase: 'spec',
1067
+ ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
799
1068
  content: content.content,
800
1069
  ...(prior ? { priorFindingsJson: prior.findingsJson } : {}),
801
1070
  ...(prior?.responseJson ? { priorResponseJson: prior.responseJson } : {}),
1071
+ callerOwnsConsumedSignalFailure: true,
1072
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
1073
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
1074
+ ...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
802
1075
  });
803
- return dispatched !== null;
1076
+ if (dispatched)
1077
+ return true;
1078
+ return holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-review-dispatch-failed', {}, opts.onSideEffect);
804
1079
  }
805
1080
  bus.on('server.spec.review.submitted', async (event) => {
806
1081
  const gated = await gate(bus, manager, event, { status: 'review', phase: 'spec', requireServerMode: false });
807
1082
  if (!gated)
808
1083
  return;
809
- const { task } = gated;
810
- const qa = manager.getAgentConfig(task.qaAgentId ?? '');
811
- if (!qa) {
812
- await emitIntervention(bus, task, { phase: 'server-spec-review-no-qa-agent' });
813
- if (task.qaAgentId) {
814
- await rearmOrHold(task, task.qaAgentId, 'spec-reviewed');
1084
+ const { task, expectedTask } = gated;
1085
+ await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', async () => {
1086
+ const qa = manager.getAgentConfig(task.qaAgentId ?? '');
1087
+ if (!qa) {
1088
+ await holdConsumedSignal(task, task.qaAgentId ?? '', 'spec-reviewed', 'handler-failed', 'server-spec-review-no-qa-agent');
1089
+ return;
815
1090
  }
816
- return;
817
- }
818
- const round = task.specReviewRound ?? 1;
819
- const roundData = await reviewStore.getRound(task.id, 'spec', round);
820
- if (!roundData) {
821
- await emitIntervention(bus, task, { phase: 'server-spec-review-round-missing', round });
822
- await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
823
- return;
824
- }
825
- await manager.refreshWorkdirCacheFor(qa.id);
826
- let findings;
827
- try {
828
- findings = await transport().readFindings(task, qa);
829
- }
830
- catch (err) {
831
- await emitIntervention(bus, task, {
832
- phase: 'server-spec-findings-invalid',
833
- error: err instanceof Error ? err.message : String(err),
834
- });
835
- await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
836
- return;
837
- }
838
- let effective = findings;
839
- if (effective === null) {
840
- if (roundData.findings === undefined) {
841
- await emitIntervention(bus, task, { phase: 'server-spec-findings-missing', round });
842
- await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
1091
+ const round = task.specReviewRound ?? 1;
1092
+ const roundData = await reviewStore.getRound(task.id, 'spec', round);
1093
+ if (!roundData) {
1094
+ await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-review-round-missing', { round });
843
1095
  return;
844
1096
  }
845
- effective = roundData.findings;
846
- }
847
- else if (effective.round !== round) {
848
- await emitIntervention(bus, task, {
849
- phase: 'server-spec-findings-round-mismatch',
850
- round,
851
- payloadRound: effective.round,
852
- });
853
- await transport().deleteFindings(qa);
854
- await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
855
- return;
856
- }
857
- else {
858
- const stored = await putVerdictRound(task, qa.id, 'spec-reviewed', {
859
- ...roundData,
860
- findings: effective,
861
- completedAt: new Date().toISOString(),
862
- });
863
- if (!stored)
1097
+ await manager.refreshWorkdirCacheFor(qa.id);
1098
+ let findings;
1099
+ try {
1100
+ findings = await transport().readFindings(task, qa);
1101
+ }
1102
+ catch (err) {
1103
+ await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-invalid', { error: err instanceof Error ? err.message : String(err) });
864
1104
  return;
865
- await transport().deleteFindings(qa);
866
- }
867
- if (effective.verdict === 'approve') {
868
- await advanceApprovedSpec(task, 'server-spec-approve-transition-failed');
869
- return;
870
- }
871
- const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
872
- if ((task.specReviewRound ?? 0) >= cap) {
873
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'] });
874
- if (!capResult)
1105
+ }
1106
+ let effective = findings;
1107
+ if (effective === null) {
1108
+ if (roundData.findings === undefined) {
1109
+ await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-missing', { round });
1110
+ return;
1111
+ }
1112
+ effective = roundData.findings;
1113
+ }
1114
+ else if (effective.round !== round) {
1115
+ await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-round-mismatch', { round, payloadRound: effective.round });
875
1116
  return;
876
- const paused = capResult.task;
877
- if (paused.qaAgentId)
878
- await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
879
- if (paused.agentId)
880
- await releaseAtCap(paused, paused.agentId, 'agentId');
881
- return;
882
- }
883
- await manager.dispatchServerFixToDev(task.id, JSON.stringify(effective));
1117
+ }
1118
+ else {
1119
+ const stored = await putVerdictRound(task, qa.id, 'spec-reviewed', {
1120
+ ...roundData,
1121
+ findings: effective,
1122
+ completedAt: new Date().toISOString(),
1123
+ }, { expectedTask });
1124
+ if (!stored)
1125
+ return;
1126
+ if (!(await deleteFindingsOrHold(task, qa, 'spec-reviewed', expectedTask)))
1127
+ return;
1128
+ }
1129
+ if (effective.verdict === 'approve') {
1130
+ await advanceApprovedSpec(task, 'server-spec-approve-transition-failed', expectedTask);
1131
+ return;
1132
+ }
1133
+ const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
1134
+ if ((task.specReviewRound ?? 0) >= cap) {
1135
+ const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
1136
+ fromStatus: ['review'],
1137
+ expectTask: expectedTask,
1138
+ });
1139
+ if (!capResult)
1140
+ return;
1141
+ const paused = capResult.task;
1142
+ if (paused.qaAgentId)
1143
+ await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
1144
+ if (paused.agentId)
1145
+ await releaseAtCap(paused, paused.agentId, 'agentId');
1146
+ return;
1147
+ }
1148
+ const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(effective), { callerOwnsConsumedSignalFailure: true, expectedTask });
1149
+ if (!dispatched) {
1150
+ await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', 'server-spec-fix-dispatch-failed');
1151
+ }
1152
+ });
884
1153
  });
885
- async function runSpecFixSubmission(task) {
1154
+ async function runSpecFixSubmission(task, opts = {}) {
886
1155
  const dev = manager.getAgentConfig(task.agentId);
887
1156
  if (!dev) {
888
- await emitIntervention(bus, task, { phase: 'server-spec-fix-no-dev-agent' });
889
- if (task.agentId) {
890
- await rearmOrHold(task, task.agentId, 'spec-fixed');
891
- }
892
- return false;
1157
+ return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-no-dev-agent', 'server-spec-fix-no-dev-agent', {}, opts.onSideEffect);
893
1158
  }
894
1159
  const round = task.specReviewRound ?? 1;
895
1160
  const roundData = await reviewStore.getRound(task.id, 'spec', round);
896
1161
  if (!roundData?.findings) {
897
- await emitIntervention(bus, task, { phase: 'server-spec-fix-findings-missing', round });
898
- await manager.setupPhaseSignal(task.id, task.agentId, 'spec-fixed', { skipSnapshot: true });
899
- return false;
1162
+ return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-findings-missing', 'server-spec-fix-findings-missing', { round }, opts.onSideEffect);
900
1163
  }
901
1164
  await manager.refreshWorkdirCacheFor(task.agentId);
902
- let response;
1165
+ let readResult;
903
1166
  try {
904
- response = await transport().readResponse(task, dev);
1167
+ readResult = await readResponseWithRetry(task, dev);
905
1168
  }
906
1169
  catch (err) {
907
- await emitIntervention(bus, task, {
908
- phase: 'server-spec-response-invalid',
1170
+ return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', {
1171
+ reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
909
1172
  error: err instanceof Error ? err.message : String(err),
910
- });
911
- await manager.setupPhaseSignal(task.id, dev.id, 'spec-fixed', { skipSnapshot: true });
912
- return false;
913
- }
914
- if (response === null) {
1173
+ }, opts.onSideEffect);
1174
+ }
1175
+ if (readResult.kind === 'unknown') {
1176
+ return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message }, opts.onSideEffect);
1177
+ }
1178
+ if (readResult.kind === 'invalid') {
1179
+ return rejectServerResponse(task, dev, roundData, {
1180
+ reason: 'response-invalid',
1181
+ failurePhase: 'server-spec-response-invalid',
1182
+ rawResponse: readResult.raw,
1183
+ responseDigest: readResult.responseDigest,
1184
+ schemaViolationCodes: readResult.schemaViolationCodes,
1185
+ details: { error: readResult.error.message },
1186
+ }, opts.onSideEffect);
1187
+ }
1188
+ if (readResult.kind === 'absent') {
915
1189
  if (roundData.response === undefined) {
916
- await emitIntervention(bus, task, { phase: 'server-spec-response-missing', round });
917
- await manager.setupPhaseSignal(task.id, dev.id, 'spec-fixed', { skipSnapshot: true });
918
- return false;
1190
+ return rejectServerResponse(task, dev, roundData, {
1191
+ reason: 'response-missing',
1192
+ failurePhase: 'server-spec-response-missing',
1193
+ }, opts.onSideEffect);
919
1194
  }
920
1195
  return dispatchSpecReview(task, {
921
1196
  findingsJson: JSON.stringify(roundData.findings),
922
1197
  responseJson: JSON.stringify(roundData.response),
923
- });
1198
+ }, opts);
924
1199
  }
1200
+ const { response } = readResult;
925
1201
  if (response.round !== round) {
926
- await emitIntervention(bus, task, {
927
- phase: 'server-spec-response-round-mismatch',
928
- round,
929
- payloadRound: response.round,
930
- });
931
- await transport().deleteResponse(dev);
932
- await manager.setupPhaseSignal(task.id, dev.id, 'spec-fixed', { skipSnapshot: true });
933
- return false;
1202
+ return rejectServerResponse(task, dev, roundData, {
1203
+ reason: 'round-mismatch',
1204
+ failurePhase: 'server-spec-response-round-mismatch',
1205
+ rawResponse: readResult.raw,
1206
+ responseDigest: readResult.responseDigest,
1207
+ details: { payloadRound: response.round },
1208
+ }, opts.onSideEffect);
1209
+ }
1210
+ if (response.token !== task.signalToken) {
1211
+ return rejectServerResponse(task, dev, roundData, {
1212
+ reason: 'token-mismatch',
1213
+ failurePhase: 'server-spec-response-token-mismatch',
1214
+ rawResponse: readResult.raw,
1215
+ responseDigest: readResult.responseDigest,
1216
+ }, opts.onSideEffect);
1217
+ }
1218
+ const findingsDigest = reviewFindingsDigest(roundData.findings);
1219
+ if (response.findingsDigest !== findingsDigest) {
1220
+ return rejectServerResponse(task, dev, roundData, {
1221
+ reason: 'findings-digest-mismatch',
1222
+ failurePhase: 'server-spec-response-findings-digest-mismatch',
1223
+ rawResponse: readResult.raw,
1224
+ responseDigest: readResult.responseDigest,
1225
+ }, opts.onSideEffect);
934
1226
  }
935
1227
  const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
936
1228
  if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
937
- await emitIntervention(bus, task, {
938
- phase: 'server-spec-response-coverage-gap',
939
- round,
1229
+ return rejectServerResponse(task, dev, roundData, {
1230
+ reason: 'coverage-gap',
1231
+ failurePhase: 'server-spec-response-coverage-gap',
1232
+ rawResponse: readResult.raw,
1233
+ responseDigest: readResult.responseDigest,
940
1234
  missingFindingIds: gaps.missing,
941
1235
  unknownFindingIds: gaps.unknown,
942
- });
943
- await manager.setupPhaseSignal(task.id, dev.id, 'spec-fixed', { skipSnapshot: true });
944
- return false;
1236
+ }, opts.onSideEffect);
945
1237
  }
946
- if (!(await putVerdictRound(task, dev.id, 'spec-fixed', { ...roundData, response })))
1238
+ if (!(await putVerdictRound(task, dev.id, 'spec-fixed', { ...roundData, response }, opts)))
1239
+ return false;
1240
+ if (!(await deleteResponseOrHold(task, dev, 'spec-fixed', opts)))
947
1241
  return false;
948
- await transport().deleteResponse(dev);
949
1242
  return dispatchSpecReview(task, {
950
1243
  findingsJson: JSON.stringify(roundData.findings),
951
1244
  responseJson: JSON.stringify(response),
952
- });
1245
+ }, opts);
953
1246
  }
954
1247
  bus.on('server.spec.fix.submitted', async (event) => {
955
1248
  const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'spec', requireServerMode: false });
956
1249
  if (!gated)
957
1250
  return;
958
- await runSpecFixSubmission(gated.task);
1251
+ await runConsumedSignal(gated.task, gated.task.agentId, 'spec-fixed', async () => {
1252
+ await runSpecFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
1253
+ });
959
1254
  });
960
1255
  async function latestVerdictRound(taskId, phase, fromRound) {
961
1256
  for (let r = fromRound; r >= 1; r--) {
@@ -968,26 +1263,31 @@ export function registerServerEventHandlers(bus, manager) {
968
1263
  // 手工发起复用自然入口的完整协议:fixing 走 fix-submission(读/校验 dev 的 response 后派 recheck,
969
1264
  // 缺 response 拒绝);其余状态按最近结论轮携带 prior 上下文,否则 QA 无从核对上一轮 findings
970
1265
  manager.setServerReviewDriver({
971
- dispatchCodeReview: async (task) => {
1266
+ dispatchCodeReview: async (task, opts = {}) => {
972
1267
  if (task.status === 'fixing')
973
- return runCodeFixSubmission(task);
1268
+ return runCodeFixSubmission(task, opts);
974
1269
  const prior = await latestVerdictRound(task.id, 'code', task.reviewRound);
975
1270
  return prepareAndDispatchCodeReview(task, {
976
1271
  recheck: prior !== null,
1272
+ ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
1273
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
1274
+ ...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
1275
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
977
1276
  ...(prior?.findings ? { priorFindingsJson: JSON.stringify(prior.findings) } : {}),
978
1277
  ...(prior?.response ? { priorResponseJson: JSON.stringify(prior.response) } : {}),
979
1278
  });
980
1279
  },
981
- dispatchSpecReview: async (task) => {
1280
+ dispatchSpecReview: async (task, opts = {}) => {
982
1281
  if (task.status === 'fixing')
983
- return runSpecFixSubmission(task);
1282
+ return runSpecFixSubmission(task, opts);
984
1283
  const prior = await latestVerdictRound(task.id, 'spec', task.specReviewRound ?? 0);
985
- return dispatchSpecReview(task, prior?.findings
1284
+ const priorContext = prior?.findings
986
1285
  ? {
987
1286
  findingsJson: JSON.stringify(prior.findings),
988
1287
  ...(prior.response ? { responseJson: JSON.stringify(prior.response) } : {}),
989
1288
  }
990
- : undefined);
1289
+ : undefined;
1290
+ return dispatchSpecReview(task, priorContext, opts);
991
1291
  },
992
1292
  });
993
1293
  }