baxian 2.0.0 → 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 (133) hide show
  1. package/dist/agent/branch.d.ts +0 -1
  2. package/dist/agent/branch.d.ts.map +1 -1
  3. package/dist/agent/branch.js.map +1 -1
  4. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  5. package/dist/agent/dispatch-reconciler.js +10 -5
  6. package/dist/agent/dispatch-reconciler.js.map +1 -1
  7. package/dist/agent/manager.d.ts +42 -13
  8. package/dist/agent/manager.d.ts.map +1 -1
  9. package/dist/agent/manager.js +568 -165
  10. package/dist/agent/manager.js.map +1 -1
  11. package/dist/agent/net-exec.d.ts +3 -0
  12. package/dist/agent/net-exec.d.ts.map +1 -1
  13. package/dist/agent/net-exec.js +6 -0
  14. package/dist/agent/net-exec.js.map +1 -1
  15. package/dist/agent/pane-streamer-manager.d.ts +6 -6
  16. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  17. package/dist/agent/pane-streamer-manager.js.map +1 -1
  18. package/dist/agent/pane-streamer.d.ts +2 -2
  19. package/dist/agent/pane-streamer.d.ts.map +1 -1
  20. package/dist/agent/pane-streamer.js +46 -36
  21. package/dist/agent/pane-streamer.js.map +1 -1
  22. package/dist/agent/phase-signal-watcher.d.ts +15 -1
  23. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  24. package/dist/agent/phase-signal-watcher.js +113 -6
  25. package/dist/agent/phase-signal-watcher.js.map +1 -1
  26. package/dist/agent/phase-signal.d.ts +7 -0
  27. package/dist/agent/phase-signal.d.ts.map +1 -1
  28. package/dist/agent/phase-signal.js +14 -5
  29. package/dist/agent/phase-signal.js.map +1 -1
  30. package/dist/agent/prompt.d.ts +0 -5
  31. package/dist/agent/prompt.d.ts.map +1 -1
  32. package/dist/agent/prompt.js +5 -18
  33. package/dist/agent/prompt.js.map +1 -1
  34. package/dist/agent/repo-store.d.ts +4 -1
  35. package/dist/agent/repo-store.d.ts.map +1 -1
  36. package/dist/agent/repo-store.js +33 -7
  37. package/dist/agent/repo-store.js.map +1 -1
  38. package/dist/agent/root-agent-runtime.d.ts +80 -0
  39. package/dist/agent/root-agent-runtime.d.ts.map +1 -0
  40. package/dist/agent/root-agent-runtime.js +635 -0
  41. package/dist/agent/root-agent-runtime.js.map +1 -0
  42. package/dist/agent/root-recovery-coordinator.d.ts +125 -0
  43. package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
  44. package/dist/agent/root-recovery-coordinator.js +1579 -0
  45. package/dist/agent/root-recovery-coordinator.js.map +1 -0
  46. package/dist/agent/runner.d.ts +1 -0
  47. package/dist/agent/runner.d.ts.map +1 -1
  48. package/dist/agent/runner.js +103 -7
  49. package/dist/agent/runner.js.map +1 -1
  50. package/dist/agent/tmux-probe-poller.d.ts +1 -0
  51. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  52. package/dist/agent/tmux-probe-poller.js +16 -10
  53. package/dist/agent/tmux-probe-poller.js.map +1 -1
  54. package/dist/agent/tmux.d.ts +3 -0
  55. package/dist/agent/tmux.d.ts.map +1 -1
  56. package/dist/agent/tmux.js +11 -5
  57. package/dist/agent/tmux.js.map +1 -1
  58. package/dist/api/agents.d.ts.map +1 -1
  59. package/dist/api/agents.js +42 -1
  60. package/dist/api/agents.js.map +1 -1
  61. package/dist/api/config.d.ts.map +1 -1
  62. package/dist/api/config.js +77 -7
  63. package/dist/api/config.js.map +1 -1
  64. package/dist/api/hosts.d.ts.map +1 -1
  65. package/dist/api/hosts.js +68 -13
  66. package/dist/api/hosts.js.map +1 -1
  67. package/dist/api/projects.d.ts.map +1 -1
  68. package/dist/api/projects.js +23 -7
  69. package/dist/api/projects.js.map +1 -1
  70. package/dist/app.d.ts +2 -0
  71. package/dist/app.d.ts.map +1 -1
  72. package/dist/app.js +6 -0
  73. package/dist/app.js.map +1 -1
  74. package/dist/cli.d.ts +1 -1
  75. package/dist/cli.d.ts.map +1 -1
  76. package/dist/cli.js +49 -7
  77. package/dist/cli.js.map +1 -1
  78. package/dist/config/loader.d.ts.map +1 -1
  79. package/dist/config/loader.js +22 -1
  80. package/dist/config/loader.js.map +1 -1
  81. package/dist/config/normalizer.d.ts.map +1 -1
  82. package/dist/config/normalizer.js +18 -0
  83. package/dist/config/normalizer.js.map +1 -1
  84. package/dist/config/validator.d.ts.map +1 -1
  85. package/dist/config/validator.js +194 -23
  86. package/dist/config/validator.js.map +1 -1
  87. package/dist/event/handlers.d.ts.map +1 -1
  88. package/dist/event/handlers.js +0 -3
  89. package/dist/event/handlers.js.map +1 -1
  90. package/dist/event/server-handlers.d.ts.map +1 -1
  91. package/dist/event/server-handlers.js +201 -128
  92. package/dist/event/server-handlers.js.map +1 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +45 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/platform/platform-poller.d.ts.map +1 -1
  97. package/dist/platform/platform-poller.js +1 -5
  98. package/dist/platform/platform-poller.js.map +1 -1
  99. package/dist/shared/constants.d.ts +7 -1
  100. package/dist/shared/constants.d.ts.map +1 -1
  101. package/dist/shared/constants.js +5 -0
  102. package/dist/shared/constants.js.map +1 -1
  103. package/dist/shared/types.d.ts +25 -2
  104. package/dist/shared/types.d.ts.map +1 -1
  105. package/dist/shared/types.js +18 -0
  106. package/dist/shared/types.js.map +1 -1
  107. package/dist/skills/baxian-pr-feedback/SKILL.md +2 -4
  108. package/dist/skills/baxian-pr-recheck/SKILL.md +2 -0
  109. package/dist/skills/baxian-pr-review/SKILL.md +2 -0
  110. package/dist/skills/baxian-research/SKILL.md +1 -1
  111. package/dist/skills/baxian-server-feedback/SKILL.md +1 -1
  112. package/dist/skills/baxian-server-review/SKILL.md +2 -2
  113. package/dist/skills/baxian-signals/SKILL.md +6 -0
  114. package/dist/state/init.d.ts.map +1 -1
  115. package/dist/state/init.js +1 -0
  116. package/dist/state/init.js.map +1 -1
  117. package/dist/state/lock.d.ts +6 -0
  118. package/dist/state/lock.d.ts.map +1 -1
  119. package/dist/state/lock.js +8 -0
  120. package/dist/state/lock.js.map +1 -1
  121. package/dist/state/root-recovery-store.d.ts +107 -0
  122. package/dist/state/root-recovery-store.d.ts.map +1 -0
  123. package/dist/state/root-recovery-store.js +400 -0
  124. package/dist/state/root-recovery-store.js.map +1 -0
  125. package/dist/state/task-store.d.ts.map +1 -1
  126. package/dist/state/task-store.js +7 -3
  127. package/dist/state/task-store.js.map +1 -1
  128. package/dist/terminal/attach.d.ts +3 -3
  129. package/dist/terminal/attach.d.ts.map +1 -1
  130. package/dist/terminal/attach.js.map +1 -1
  131. package/dist/terminal/stream-ws.d.ts.map +1 -1
  132. package/dist/terminal/stream-ws.js.map +1 -1
  133. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { isSpecStagePhase } from '../shared/index.js';
1
+ import { isSpecStagePhase, taskGenerationGuard } from '../shared/index.js';
2
2
  import { ReviewExchangeError, } from '../agent/review-transport.js';
3
+ import { settlePermitOf } from '../agent/phase-signal-watcher.js';
3
4
  import { computeBackoffMs } from '../timing/backoff.js';
4
5
  import { reviewFindingsDigest, serverResponseFailureSignature, } from '../state/review-store.js';
5
6
  const LEGACY_DIFF_LARGE_THRESHOLD = 2000;
@@ -150,7 +151,7 @@ async function gate(bus, manager, event, expect) {
150
151
  });
151
152
  return null;
152
153
  }
153
- return { task };
154
+ return { task, expectedTask: taskGenerationGuard(task) };
154
155
  }
155
156
  export function registerServerEventHandlers(bus, manager) {
156
157
  const configured = manager.getReviewStore();
@@ -179,11 +180,15 @@ export function registerServerEventHandlers(bus, manager) {
179
180
  .catch(err => console.error(`[ServerHandler] max_rounds clear agentId(${task.id}) failed:`, err));
180
181
  }
181
182
  }
182
- async function putVerdictRound(task, agentId, kind, data) {
183
+ async function putVerdictRound(task, agentId, kind, data, opts = {}) {
183
184
  let lastError;
184
185
  for (let attempt = 1; attempt <= SERVER_HANDLER_ATTEMPTS; attempt++) {
185
186
  try {
186
- await reviewStore.putRound(task.id, data.phase, data);
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;
187
192
  return true;
188
193
  }
189
194
  catch (err) {
@@ -203,9 +208,9 @@ export function registerServerEventHandlers(bus, manager) {
203
208
  return holdConsumedSignal(task, agentId, kind, 'verdict-store-failed', `server-${data.phase}-verdict-store-failed`, {
204
209
  round: data.round,
205
210
  error: lastError instanceof Error ? lastError.message : String(lastError),
206
- });
211
+ }, opts.onSideEffect);
207
212
  }
208
- async function holdConsumedSignal(task, agentId, kind, reason, failurePhase, data = {}) {
213
+ async function holdConsumedSignal(task, agentId, kind, reason, failurePhase, data = {}, onSideEffect) {
209
214
  if (!SERVER_SIGNAL_KINDS.has(kind)) {
210
215
  throw new Error(`unsupported consumed server signal kind: ${kind}`);
211
216
  }
@@ -214,6 +219,7 @@ export function registerServerEventHandlers(bus, manager) {
214
219
  const held = await manager.holdConsumedServerSignal(task, agentId, kind, { phase, round, reason, failurePhase });
215
220
  if (!held)
216
221
  return false;
222
+ onSideEffect?.();
217
223
  await emitIntervention(bus, held, {
218
224
  phase: failurePhase,
219
225
  ...data,
@@ -239,17 +245,27 @@ export function registerServerEventHandlers(bus, manager) {
239
245
  await holdConsumedSignal(task, agentId, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-handler-failed`, { error: err instanceof Error ? err.message : String(err) });
240
246
  }
241
247
  }
242
- async function deleteFindingsOrHold(task, qa, kind) {
248
+ async function deleteFindingsOrHold(task, qa, kind, expectedTask) {
243
249
  try {
244
- await transport().deleteFindings(qa);
245
- return true;
250
+ return await manager.deleteReviewExchangeIfCurrent(task.id, qa, 'findings', expectedTask);
246
251
  }
247
252
  catch (err) {
248
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) });
249
254
  return false;
250
255
  }
251
256
  }
252
- async function rejectServerResponse(task, dev, roundData, failure) {
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) {
253
269
  const phase = roundData.phase;
254
270
  const signalKind = `${phase}-fixed`;
255
271
  const sourceToken = task.signalToken;
@@ -283,6 +299,7 @@ export function registerServerEventHandlers(bus, manager) {
283
299
  });
284
300
  if (!claimed?.signalToken)
285
301
  return false;
302
+ onSideEffect?.();
286
303
  let recorded;
287
304
  try {
288
305
  recorded = await reviewStore.recordServerResponseFailure(task.id, phase, roundData.round, {
@@ -351,7 +368,9 @@ export function registerServerEventHandlers(bus, manager) {
351
368
  return false;
352
369
  }
353
370
  try {
354
- await transport().deleteResponse(dev);
371
+ const deleted = await manager.deleteReviewExchangeIfCurrent(task.id, dev, 'response', taskGenerationGuard(beforeCleanup), currentRecovery);
372
+ if (!deleted)
373
+ return false;
355
374
  }
356
375
  catch (err) {
357
376
  const message = err instanceof Error ? err.message : String(err);
@@ -387,24 +406,30 @@ export function registerServerEventHandlers(bus, manager) {
387
406
  const recorded = manager.getAgentConfig(task.qaAgentId);
388
407
  return recorded?.role === 'qa' ? recorded : undefined;
389
408
  }
390
- async function pauseForUnavailableQa(task, entryKind, phase) {
409
+ async function pauseForUnavailableQa(task, entryKind, phase, onSideEffect) {
391
410
  if (!task.qaAgentId || resolveActiveQa(task))
392
411
  return false;
393
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', phase, { qaAgentId: task.qaAgentId });
412
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', phase, { qaAgentId: task.qaAgentId }, onSideEffect);
394
413
  return true;
395
414
  }
396
- async function autoApproveCode(task, entryKind = 'code-done') {
397
- const afterDone = await manager.commitServerAfterDone(task.id);
415
+ async function autoApproveCode(task, expectedTask, entryKind = 'code-done', onSideEffect) {
416
+ const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
417
+ onSideEffect?.();
398
418
  if (afterDone === null) {
399
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['in_progress', 'fixing'] });
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?.();
400
425
  if (!ready) {
401
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed');
426
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
402
427
  }
403
428
  return;
404
429
  }
405
430
  const dev = manager.getAgentConfig(task.agentId);
406
431
  if (!dev) {
407
- await holdConsumedSignal(task, task.agentId, entryKind, 'fix-no-dev-agent', '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);
408
433
  return;
409
434
  }
410
435
  await manager.refreshWorkdirCacheFor(task.agentId);
@@ -413,28 +438,35 @@ export function registerServerEventHandlers(bus, manager) {
413
438
  reviewHeadAnchorSha = await transport().readHeadSha(dev);
414
439
  }
415
440
  catch (err) {
416
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
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);
417
442
  return;
418
443
  }
419
- 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?.();
420
450
  if (!approved) {
421
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed');
451
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
422
452
  return;
423
453
  }
424
454
  const dispatched = await manager.dispatchServerAfterDone(task.id, afterDone);
425
455
  if (!dispatched) {
426
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-publish-dispatch-failed');
456
+ await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-publish-dispatch-failed', {}, onSideEffect);
427
457
  }
428
458
  }
429
459
  function specApprovalIsHuman(task) {
430
460
  return task.researchAgentId !== undefined
431
461
  || manager.getProjectConfig(task.projectId)?.specApproval === 'human';
432
462
  }
433
- async function advanceApprovedSpec(task, failurePhase) {
463
+ async function advanceApprovedSpec(task, failurePhase, expectedTask) {
434
464
  try {
435
465
  const result = specApprovalIsHuman(task)
436
- ? await manager.parkTaskAtSpecReady(task.id)
437
- : await manager.transitionToCodePhase(task.id);
466
+ ? await manager.parkTaskAtSpecReady(task.id, {
467
+ ...(expectedTask !== undefined ? { expectedTask } : {}),
468
+ })
469
+ : await manager.transitionToCodePhase(task.id, expectedTask);
438
470
  if (!result) {
439
471
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase);
440
472
  }
@@ -443,17 +475,19 @@ export function registerServerEventHandlers(bus, manager) {
443
475
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase, { error: err instanceof Error ? err.message : String(err) });
444
476
  }
445
477
  }
446
- async function autoApproveSpec(task) {
478
+ async function autoApproveSpec(task, expectedTask, onSideEffect) {
447
479
  try {
448
- const result = await manager.transitionToCodePhase(task.id);
480
+ const result = await manager.transitionToCodePhase(task.id, expectedTask);
481
+ if (result)
482
+ onSideEffect?.();
449
483
  if (!result) {
450
- await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', '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);
451
485
  return false;
452
486
  }
453
487
  return true;
454
488
  }
455
489
  catch (err) {
456
- 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) });
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);
457
491
  return false;
458
492
  }
459
493
  }
@@ -461,14 +495,18 @@ export function registerServerEventHandlers(bus, manager) {
461
495
  const dev = manager.getAgentConfig(task.agentId);
462
496
  if (!dev) {
463
497
  const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
464
- return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-code-review-no-dev-agent');
498
+ return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-code-review-no-dev-agent', {}, opts.onSideEffect);
465
499
  }
466
500
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
467
501
  const nextRound = opts.bumpRound === false ? task.reviewRound : task.reviewRound + 1;
468
502
  if (nextRound > cap) {
469
- 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
+ });
470
507
  if (!capResult)
471
508
  return false;
509
+ opts.onSideEffect?.();
472
510
  const paused = capResult.task;
473
511
  if (paused.qaAgentId)
474
512
  await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
@@ -509,7 +547,7 @@ export function registerServerEventHandlers(bus, manager) {
509
547
  }
510
548
  catch (err) {
511
549
  const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
512
- return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-content-read-failed', { error: err instanceof Error ? err.message : String(err) });
550
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
513
551
  }
514
552
  try {
515
553
  const violation = await manager.findLineageViolation(task.id, content.baseSha);
@@ -520,12 +558,12 @@ export function registerServerEventHandlers(bus, manager) {
520
558
  offendingBranch: violation.branch,
521
559
  offendingSha: violation.sha,
522
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.',
523
- });
561
+ }, opts.onSideEffect);
524
562
  }
525
563
  }
526
564
  catch (err) {
527
565
  const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
528
- return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-check-failed', { error: err instanceof Error ? err.message : String(err) });
566
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-check-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
529
567
  }
530
568
  }
531
569
  const round = {
@@ -538,7 +576,7 @@ export function registerServerEventHandlers(bus, manager) {
538
576
  ...(content.headTree ? { headTree: content.headTree } : {}),
539
577
  startedAt: new Date().toISOString(),
540
578
  };
541
- const putEntryRound = (data) => putVerdictRound(task, task.agentId, task.status === 'fixing' ? 'code-fixed' : 'code-done', data);
579
+ const putEntryRound = (data) => putVerdictRound(task, task.agentId, task.status === 'fixing' ? 'code-fixed' : 'code-done', data, opts);
542
580
  const resolveRecheckInterdiff = async () => {
543
581
  if (!opts.recheck)
544
582
  return undefined;
@@ -558,7 +596,7 @@ export function registerServerEventHandlers(bus, manager) {
558
596
  const dispatched = await manager.dispatchServerReviewToQa(task.id, {
559
597
  phase: 'code',
560
598
  recheck: opts.recheck,
561
- ...(opts.bumpRound === false ? { bumpRound: false } : {}),
599
+ ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
562
600
  content: content.content,
563
601
  reviewHeadAnchorSha,
564
602
  ...(content.baseSha ? { baseSha: content.baseSha } : {}),
@@ -568,11 +606,14 @@ export function registerServerEventHandlers(bus, manager) {
568
606
  ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
569
607
  ...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
570
608
  callerOwnsConsumedSignalFailure: true,
609
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
610
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
611
+ ...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
571
612
  });
572
613
  if (dispatched)
573
614
  return true;
574
615
  const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
575
- return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-review-dispatch-failed');
616
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-review-dispatch-failed', {}, opts.onSideEffect);
576
617
  }
577
618
  async function dispatchLegacyBatch(taskId, batches, index, diffstat, opts) {
578
619
  const text = batches[index].map(f => f.text).join('\n');
@@ -586,6 +627,8 @@ export function registerServerEventHandlers(bus, manager) {
586
627
  ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
587
628
  ...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
588
629
  callerOwnsConsumedSignalFailure: true,
630
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
631
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
589
632
  });
590
633
  return dispatched !== null;
591
634
  }
@@ -596,22 +639,22 @@ export function registerServerEventHandlers(bus, manager) {
596
639
  const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'code', requireServerMode: true });
597
640
  if (!gated)
598
641
  return;
599
- const { task } = gated;
642
+ const { task, expectedTask } = gated;
600
643
  await runConsumedSignal(task, task.agentId, 'code-done', async () => {
601
644
  if (await pauseForUnavailableQa(task, 'code-done', 'server-code-qa-unavailable'))
602
645
  return;
603
646
  if (!task.qaAgentId) {
604
- await autoApproveCode(task);
647
+ await autoApproveCode(task, expectedTask);
605
648
  return;
606
649
  }
607
- await prepareAndDispatchCodeReview(task, { recheck: false });
650
+ await prepareAndDispatchCodeReview(task, { recheck: false, expectedTask, settlePermit: settlePermitOf(event) });
608
651
  });
609
652
  });
610
653
  bus.on('server.code.review.submitted', async (event) => {
611
654
  const gated = await gate(bus, manager, event, { status: 'review', phase: 'code', requireServerMode: true });
612
655
  if (!gated)
613
656
  return;
614
- const { task } = gated;
657
+ const { task, expectedTask } = gated;
615
658
  await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', async () => {
616
659
  const qa = manager.getAgentConfig(task.qaAgentId ?? '');
617
660
  if (!qa) {
@@ -652,16 +695,16 @@ export function registerServerEventHandlers(bus, manager) {
652
695
  const batchFindings = [...(roundData.batchFindings ?? [])];
653
696
  batchFindings[task.batchIndex] = findings;
654
697
  current = { ...roundData, batchFindings };
655
- if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current)))
698
+ if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
656
699
  return;
657
- if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed')))
700
+ if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
658
701
  return;
659
702
  }
660
703
  else {
661
704
  current = { ...roundData, findings, completedAt: new Date().toISOString() };
662
- if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current)))
705
+ if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
663
706
  return;
664
- if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed')))
707
+ if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
665
708
  return;
666
709
  }
667
710
  const isRecheck = round > 1;
@@ -673,6 +716,8 @@ export function registerServerEventHandlers(bus, manager) {
673
716
  recheck: isRecheck,
674
717
  ...(prev?.findings ? { priorFindingsJson: JSON.stringify(prev.findings) } : {}),
675
718
  ...(prev?.response ? { priorResponseJson: JSON.stringify(prev.response) } : {}),
719
+ expectedTask,
720
+ settlePermit: settlePermitOf(event),
676
721
  });
677
722
  if (!dispatched) {
678
723
  await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-review-continuation-dispatch-failed');
@@ -686,28 +731,30 @@ export function registerServerEventHandlers(bus, manager) {
686
731
  ...current,
687
732
  findings: aggregated,
688
733
  completedAt: new Date().toISOString(),
689
- });
734
+ }, { expectedTask });
690
735
  if (!stored)
691
736
  return;
692
737
  }
693
- await manager.updateTask(task.id, { batchIndex: undefined, batchTotal: undefined });
694
- await routeCodeVerdict({ ...task, batchIndex: undefined, batchTotal: undefined }, aggregated);
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);
695
742
  return;
696
743
  }
697
- await routeCodeVerdict(task, current.findings);
744
+ await routeCodeVerdict(task, current.findings, expectedTask);
698
745
  });
699
746
  });
700
- async function routeCodeVerdict(task, findings) {
747
+ async function routeCodeVerdict(task, findings, expectedTask) {
701
748
  if (findings.verdict === 'approve') {
702
- const afterDone = await manager.commitServerAfterDone(task.id);
749
+ const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
703
750
  if (afterDone === null) {
704
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'] });
751
+ const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'], expectTask: expectedTask });
705
752
  if (!ready) {
706
753
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-ready-transition-failed');
707
754
  }
708
755
  return;
709
756
  }
710
- const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'] });
757
+ const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'], expectTask: expectedTask });
711
758
  if (!approved) {
712
759
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-approved-transition-failed');
713
760
  return;
@@ -720,7 +767,7 @@ export function registerServerEventHandlers(bus, manager) {
720
767
  }
721
768
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
722
769
  if (task.reviewRound >= cap) {
723
- 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 });
724
771
  if (!capResult)
725
772
  return;
726
773
  const paused = capResult.task;
@@ -738,30 +785,35 @@ export function registerServerEventHandlers(bus, manager) {
738
785
  });
739
786
  return;
740
787
  }
741
- const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings), { callerOwnsConsumedSignalFailure: true });
788
+ const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings), { callerOwnsConsumedSignalFailure: true, expectedTask });
742
789
  if (!dispatched) {
743
790
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-fix-dispatch-failed');
744
791
  }
745
792
  }
746
- async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson) {
747
- 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)) {
748
795
  return false;
749
796
  }
750
797
  if (!task.qaAgentId) {
751
- await autoApproveCode(task, 'code-fixed');
798
+ await autoApproveCode(task, opts.expectedTask, 'code-fixed', opts.onSideEffect);
752
799
  return true;
753
800
  }
754
- return prepareAndDispatchCodeReview(task, { recheck: true, priorFindingsJson, priorResponseJson });
801
+ return prepareAndDispatchCodeReview(task, {
802
+ recheck: true,
803
+ priorFindingsJson,
804
+ priorResponseJson,
805
+ ...opts,
806
+ });
755
807
  }
756
- async function runCodeFixSubmission(task) {
808
+ async function runCodeFixSubmission(task, opts = {}) {
757
809
  const dev = manager.getAgentConfig(task.agentId);
758
810
  if (!dev) {
759
- return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-no-dev-agent', 'server-code-fix-no-dev-agent');
811
+ return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-no-dev-agent', 'server-code-fix-no-dev-agent', {}, opts.onSideEffect);
760
812
  }
761
813
  const round = Math.max(task.reviewRound, 1);
762
814
  const roundData = await reviewStore.getRound(task.id, 'code', round);
763
815
  if (!roundData?.findings) {
764
- return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-findings-missing', 'server-code-fix-findings-missing', { round });
816
+ return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-findings-missing', 'server-code-fix-findings-missing', { round }, opts.onSideEffect);
765
817
  }
766
818
  await manager.refreshWorkdirCacheFor(task.agentId);
767
819
  let readResult;
@@ -772,10 +824,10 @@ export function registerServerEventHandlers(bus, manager) {
772
824
  return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', {
773
825
  reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
774
826
  error: err instanceof Error ? err.message : String(err),
775
- });
827
+ }, opts.onSideEffect);
776
828
  }
777
829
  if (readResult.kind === 'unknown') {
778
- return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message });
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);
779
831
  }
780
832
  if (readResult.kind === 'invalid') {
781
833
  return rejectServerResponse(task, dev, roundData, {
@@ -785,16 +837,16 @@ export function registerServerEventHandlers(bus, manager) {
785
837
  responseDigest: readResult.responseDigest,
786
838
  schemaViolationCodes: readResult.schemaViolationCodes,
787
839
  details: { error: readResult.error.message },
788
- });
840
+ }, opts.onSideEffect);
789
841
  }
790
842
  if (readResult.kind === 'absent') {
791
843
  if (roundData.response === undefined) {
792
844
  return rejectServerResponse(task, dev, roundData, {
793
845
  reason: 'response-missing',
794
846
  failurePhase: 'server-code-response-missing',
795
- });
847
+ }, opts.onSideEffect);
796
848
  }
797
- return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response));
849
+ return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response), opts);
798
850
  }
799
851
  const { response } = readResult;
800
852
  if (response.round !== round) {
@@ -804,7 +856,7 @@ export function registerServerEventHandlers(bus, manager) {
804
856
  rawResponse: readResult.raw,
805
857
  responseDigest: readResult.responseDigest,
806
858
  details: { payloadRound: response.round },
807
- });
859
+ }, opts.onSideEffect);
808
860
  }
809
861
  if (response.token !== task.signalToken) {
810
862
  return rejectServerResponse(task, dev, roundData, {
@@ -812,7 +864,7 @@ export function registerServerEventHandlers(bus, manager) {
812
864
  failurePhase: 'server-code-response-token-mismatch',
813
865
  rawResponse: readResult.raw,
814
866
  responseDigest: readResult.responseDigest,
815
- });
867
+ }, opts.onSideEffect);
816
868
  }
817
869
  const findingsDigest = reviewFindingsDigest(roundData.findings);
818
870
  if (response.findingsDigest !== findingsDigest) {
@@ -821,7 +873,7 @@ export function registerServerEventHandlers(bus, manager) {
821
873
  failurePhase: 'server-code-response-findings-digest-mismatch',
822
874
  rawResponse: readResult.raw,
823
875
  responseDigest: readResult.responseDigest,
824
- });
876
+ }, opts.onSideEffect);
825
877
  }
826
878
  const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
827
879
  if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
@@ -832,24 +884,20 @@ export function registerServerEventHandlers(bus, manager) {
832
884
  responseDigest: readResult.responseDigest,
833
885
  missingFindingIds: gaps.missing,
834
886
  unknownFindingIds: gaps.unknown,
835
- });
887
+ }, opts.onSideEffect);
836
888
  }
837
- if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response })))
889
+ if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response }, opts)))
838
890
  return false;
839
- try {
840
- await transport().deleteResponse(dev);
841
- }
842
- catch (err) {
843
- return holdConsumedSignal(task, dev.id, 'code-fixed', 'handler-failed', 'server-code-response-cleanup-failed', { error: err instanceof Error ? err.message : String(err) });
844
- }
845
- 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);
846
894
  }
847
895
  bus.on('server.code.fix.submitted', async (event) => {
848
896
  const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'code', requireServerMode: true });
849
897
  if (!gated)
850
898
  return;
851
899
  await runConsumedSignal(gated.task, gated.task.agentId, 'code-fixed', async () => {
852
- await runCodeFixSubmission(gated.task);
900
+ await runCodeFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
853
901
  });
854
902
  });
855
903
  bus.on('server.code.published', async (event) => {
@@ -884,6 +932,7 @@ export function registerServerEventHandlers(bus, manager) {
884
932
  });
885
933
  return;
886
934
  }
935
+ let verifiedBaseBranch;
887
936
  if (prNumber !== undefined) {
888
937
  const verified = await manager.verifyPaneSignalPrNumber(task.id, prNumber);
889
938
  if (!verified) {
@@ -899,19 +948,24 @@ export function registerServerEventHandlers(bus, manager) {
899
948
  });
900
949
  return;
901
950
  }
902
- await manager.updateTask(task.id, {
903
- prNumber,
904
- ...(task.baseBranch === undefined ? { baseBranch: verified.targetBranch } : {}),
905
- });
951
+ verifiedBaseBranch = verified.targetBranch;
906
952
  }
953
+ let ready;
907
954
  try {
908
- await manager.updateTask(task.id, { latestHeadSha: publishedHead });
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
+ });
909
964
  }
910
965
  catch (err) {
911
966
  await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
912
967
  return;
913
968
  }
914
- const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['approved'] });
915
969
  if (!ready) {
916
970
  await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-transition-failed');
917
971
  }
@@ -922,22 +976,26 @@ export function registerServerEventHandlers(bus, manager) {
922
976
  if (!gated)
923
977
  return;
924
978
  await runConsumedSignal(gated.task, gated.task.agentId, 'spec-done', async () => {
925
- await dispatchSpecReview(gated.task);
979
+ await dispatchSpecReview(gated.task, undefined, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
926
980
  });
927
981
  });
928
982
  async function dispatchSpecReview(task, prior, opts = {}) {
929
983
  const dev = manager.getAgentConfig(task.agentId);
930
984
  if (!dev) {
931
985
  const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
932
- return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-spec-review-no-dev-agent');
986
+ return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-spec-review-no-dev-agent', {}, opts.onSideEffect);
933
987
  }
934
988
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
935
989
  const currentRound = task.specReviewRound ?? 0;
936
990
  const nextRound = opts.bumpRound === false ? currentRound : currentRound + 1;
937
991
  if (nextRound > cap) {
938
- 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
+ });
939
996
  if (!capResult)
940
997
  return false;
998
+ opts.onSideEffect?.();
941
999
  const paused = capResult.task;
942
1000
  if (paused.qaAgentId)
943
1001
  await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
@@ -960,22 +1018,27 @@ export function registerServerEventHandlers(bus, manager) {
960
1018
  }
961
1019
  catch (err) {
962
1020
  const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
963
- return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-spec-content-read-failed', { error: err instanceof Error ? err.message : String(err) });
1021
+ return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-spec-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
964
1022
  }
965
1023
  try {
966
1024
  if (!content.documents)
967
1025
  throw new Error('spec transport returned no documents');
968
- await reviewStore.putRound(task.id, 'spec', {
1026
+ const roundData = {
969
1027
  round: nextRound,
970
1028
  phase: 'spec',
971
1029
  content: content.content,
972
1030
  documents: content.documents,
973
1031
  startedAt: new Date().toISOString(),
974
- });
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;
975
1038
  }
976
1039
  catch (err) {
977
1040
  const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
978
- return holdConsumedSignal(task, task.agentId, kind, 'verdict-store-failed', 'server-spec-round-store-failed', { error: err instanceof Error ? err.message : String(err) });
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);
979
1042
  }
980
1043
  }
981
1044
  if (!resolveActiveQa(task)) {
@@ -986,31 +1049,39 @@ export function registerServerEventHandlers(bus, manager) {
986
1049
  });
987
1050
  }
988
1051
  if (specApprovalIsHuman(task) || task.qaAgentId) {
989
- const parked = await manager.parkTaskAtSpecReady(task.id, { specReviewRound: nextRound });
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?.();
990
1058
  if (!parked) {
991
- await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-park-transition-failed');
1059
+ await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-park-transition-failed', {}, opts.onSideEffect);
992
1060
  }
993
1061
  return !!parked;
994
1062
  }
995
- return autoApproveSpec(task);
1063
+ return autoApproveSpec(task, opts.expectedTask, opts.onSideEffect);
996
1064
  }
997
1065
  const dispatched = await manager.dispatchServerReviewToQa(task.id, {
998
1066
  phase: 'spec',
999
- ...(opts.bumpRound === false ? { bumpRound: false } : {}),
1067
+ ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
1000
1068
  content: content.content,
1001
1069
  ...(prior ? { priorFindingsJson: prior.findingsJson } : {}),
1002
1070
  ...(prior?.responseJson ? { priorResponseJson: prior.responseJson } : {}),
1003
1071
  callerOwnsConsumedSignalFailure: true,
1072
+ ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
1073
+ ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
1074
+ ...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
1004
1075
  });
1005
1076
  if (dispatched)
1006
1077
  return true;
1007
- return holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-review-dispatch-failed');
1078
+ return holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-review-dispatch-failed', {}, opts.onSideEffect);
1008
1079
  }
1009
1080
  bus.on('server.spec.review.submitted', async (event) => {
1010
1081
  const gated = await gate(bus, manager, event, { status: 'review', phase: 'spec', requireServerMode: false });
1011
1082
  if (!gated)
1012
1083
  return;
1013
- const { task } = gated;
1084
+ const { task, expectedTask } = gated;
1014
1085
  await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', async () => {
1015
1086
  const qa = manager.getAgentConfig(task.qaAgentId ?? '');
1016
1087
  if (!qa) {
@@ -1049,19 +1120,22 @@ export function registerServerEventHandlers(bus, manager) {
1049
1120
  ...roundData,
1050
1121
  findings: effective,
1051
1122
  completedAt: new Date().toISOString(),
1052
- });
1123
+ }, { expectedTask });
1053
1124
  if (!stored)
1054
1125
  return;
1055
- if (!(await deleteFindingsOrHold(task, qa, 'spec-reviewed')))
1126
+ if (!(await deleteFindingsOrHold(task, qa, 'spec-reviewed', expectedTask)))
1056
1127
  return;
1057
1128
  }
1058
1129
  if (effective.verdict === 'approve') {
1059
- await advanceApprovedSpec(task, 'server-spec-approve-transition-failed');
1130
+ await advanceApprovedSpec(task, 'server-spec-approve-transition-failed', expectedTask);
1060
1131
  return;
1061
1132
  }
1062
1133
  const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
1063
1134
  if ((task.specReviewRound ?? 0) >= cap) {
1064
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'] });
1135
+ const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
1136
+ fromStatus: ['review'],
1137
+ expectTask: expectedTask,
1138
+ });
1065
1139
  if (!capResult)
1066
1140
  return;
1067
1141
  const paused = capResult.task;
@@ -1071,21 +1145,21 @@ export function registerServerEventHandlers(bus, manager) {
1071
1145
  await releaseAtCap(paused, paused.agentId, 'agentId');
1072
1146
  return;
1073
1147
  }
1074
- const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(effective), { callerOwnsConsumedSignalFailure: true });
1148
+ const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(effective), { callerOwnsConsumedSignalFailure: true, expectedTask });
1075
1149
  if (!dispatched) {
1076
1150
  await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', 'server-spec-fix-dispatch-failed');
1077
1151
  }
1078
1152
  });
1079
1153
  });
1080
- async function runSpecFixSubmission(task) {
1154
+ async function runSpecFixSubmission(task, opts = {}) {
1081
1155
  const dev = manager.getAgentConfig(task.agentId);
1082
1156
  if (!dev) {
1083
- return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-no-dev-agent', 'server-spec-fix-no-dev-agent');
1157
+ return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-no-dev-agent', 'server-spec-fix-no-dev-agent', {}, opts.onSideEffect);
1084
1158
  }
1085
1159
  const round = task.specReviewRound ?? 1;
1086
1160
  const roundData = await reviewStore.getRound(task.id, 'spec', round);
1087
1161
  if (!roundData?.findings) {
1088
- return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-findings-missing', 'server-spec-fix-findings-missing', { round });
1162
+ return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-findings-missing', 'server-spec-fix-findings-missing', { round }, opts.onSideEffect);
1089
1163
  }
1090
1164
  await manager.refreshWorkdirCacheFor(task.agentId);
1091
1165
  let readResult;
@@ -1096,10 +1170,10 @@ export function registerServerEventHandlers(bus, manager) {
1096
1170
  return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', {
1097
1171
  reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
1098
1172
  error: err instanceof Error ? err.message : String(err),
1099
- });
1173
+ }, opts.onSideEffect);
1100
1174
  }
1101
1175
  if (readResult.kind === 'unknown') {
1102
- return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message });
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);
1103
1177
  }
1104
1178
  if (readResult.kind === 'invalid') {
1105
1179
  return rejectServerResponse(task, dev, roundData, {
@@ -1109,19 +1183,19 @@ export function registerServerEventHandlers(bus, manager) {
1109
1183
  responseDigest: readResult.responseDigest,
1110
1184
  schemaViolationCodes: readResult.schemaViolationCodes,
1111
1185
  details: { error: readResult.error.message },
1112
- });
1186
+ }, opts.onSideEffect);
1113
1187
  }
1114
1188
  if (readResult.kind === 'absent') {
1115
1189
  if (roundData.response === undefined) {
1116
1190
  return rejectServerResponse(task, dev, roundData, {
1117
1191
  reason: 'response-missing',
1118
1192
  failurePhase: 'server-spec-response-missing',
1119
- });
1193
+ }, opts.onSideEffect);
1120
1194
  }
1121
1195
  return dispatchSpecReview(task, {
1122
1196
  findingsJson: JSON.stringify(roundData.findings),
1123
1197
  responseJson: JSON.stringify(roundData.response),
1124
- });
1198
+ }, opts);
1125
1199
  }
1126
1200
  const { response } = readResult;
1127
1201
  if (response.round !== round) {
@@ -1131,7 +1205,7 @@ export function registerServerEventHandlers(bus, manager) {
1131
1205
  rawResponse: readResult.raw,
1132
1206
  responseDigest: readResult.responseDigest,
1133
1207
  details: { payloadRound: response.round },
1134
- });
1208
+ }, opts.onSideEffect);
1135
1209
  }
1136
1210
  if (response.token !== task.signalToken) {
1137
1211
  return rejectServerResponse(task, dev, roundData, {
@@ -1139,7 +1213,7 @@ export function registerServerEventHandlers(bus, manager) {
1139
1213
  failurePhase: 'server-spec-response-token-mismatch',
1140
1214
  rawResponse: readResult.raw,
1141
1215
  responseDigest: readResult.responseDigest,
1142
- });
1216
+ }, opts.onSideEffect);
1143
1217
  }
1144
1218
  const findingsDigest = reviewFindingsDigest(roundData.findings);
1145
1219
  if (response.findingsDigest !== findingsDigest) {
@@ -1148,7 +1222,7 @@ export function registerServerEventHandlers(bus, manager) {
1148
1222
  failurePhase: 'server-spec-response-findings-digest-mismatch',
1149
1223
  rawResponse: readResult.raw,
1150
1224
  responseDigest: readResult.responseDigest,
1151
- });
1225
+ }, opts.onSideEffect);
1152
1226
  }
1153
1227
  const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
1154
1228
  if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
@@ -1159,27 +1233,23 @@ export function registerServerEventHandlers(bus, manager) {
1159
1233
  responseDigest: readResult.responseDigest,
1160
1234
  missingFindingIds: gaps.missing,
1161
1235
  unknownFindingIds: gaps.unknown,
1162
- });
1236
+ }, opts.onSideEffect);
1163
1237
  }
1164
- 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)))
1165
1241
  return false;
1166
- try {
1167
- await transport().deleteResponse(dev);
1168
- }
1169
- catch (err) {
1170
- return holdConsumedSignal(task, dev.id, 'spec-fixed', 'handler-failed', 'server-spec-response-cleanup-failed', { error: err instanceof Error ? err.message : String(err) });
1171
- }
1172
1242
  return dispatchSpecReview(task, {
1173
1243
  findingsJson: JSON.stringify(roundData.findings),
1174
1244
  responseJson: JSON.stringify(response),
1175
- });
1245
+ }, opts);
1176
1246
  }
1177
1247
  bus.on('server.spec.fix.submitted', async (event) => {
1178
1248
  const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'spec', requireServerMode: false });
1179
1249
  if (!gated)
1180
1250
  return;
1181
1251
  await runConsumedSignal(gated.task, gated.task.agentId, 'spec-fixed', async () => {
1182
- await runSpecFixSubmission(gated.task);
1252
+ await runSpecFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
1183
1253
  });
1184
1254
  });
1185
1255
  async function latestVerdictRound(taskId, phase, fromRound) {
@@ -1195,18 +1265,21 @@ export function registerServerEventHandlers(bus, manager) {
1195
1265
  manager.setServerReviewDriver({
1196
1266
  dispatchCodeReview: async (task, opts = {}) => {
1197
1267
  if (task.status === 'fixing')
1198
- return runCodeFixSubmission(task);
1268
+ return runCodeFixSubmission(task, opts);
1199
1269
  const prior = await latestVerdictRound(task.id, 'code', task.reviewRound);
1200
1270
  return prepareAndDispatchCodeReview(task, {
1201
1271
  recheck: prior !== null,
1202
- ...(opts.bumpRound === false ? { bumpRound: false } : {}),
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 } : {}),
1203
1276
  ...(prior?.findings ? { priorFindingsJson: JSON.stringify(prior.findings) } : {}),
1204
1277
  ...(prior?.response ? { priorResponseJson: JSON.stringify(prior.response) } : {}),
1205
1278
  });
1206
1279
  },
1207
1280
  dispatchSpecReview: async (task, opts = {}) => {
1208
1281
  if (task.status === 'fixing')
1209
- return runSpecFixSubmission(task);
1282
+ return runSpecFixSubmission(task, opts);
1210
1283
  const prior = await latestVerdictRound(task.id, 'spec', task.specReviewRound ?? 0);
1211
1284
  const priorContext = prior?.findings
1212
1285
  ? {