baxian 1.2.59 → 1.2.61

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 (168) hide show
  1. package/dist/agent/branch.d.ts.map +1 -1
  2. package/dist/agent/branch.js +19 -13
  3. package/dist/agent/branch.js.map +1 -1
  4. package/dist/agent/dispatch-reconciler.d.ts +45 -0
  5. package/dist/agent/dispatch-reconciler.d.ts.map +1 -0
  6. package/dist/agent/dispatch-reconciler.js +513 -0
  7. package/dist/agent/dispatch-reconciler.js.map +1 -0
  8. package/dist/agent/manager.d.ts +123 -12
  9. package/dist/agent/manager.d.ts.map +1 -1
  10. package/dist/agent/manager.js +1607 -420
  11. package/dist/agent/manager.js.map +1 -1
  12. package/dist/agent/pane-streamer-manager.d.ts +70 -0
  13. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  14. package/dist/agent/pane-streamer-manager.js +487 -5
  15. package/dist/agent/pane-streamer-manager.js.map +1 -1
  16. package/dist/agent/pane-streamer.d.ts +48 -1
  17. package/dist/agent/pane-streamer.d.ts.map +1 -1
  18. package/dist/agent/pane-streamer.js +338 -15
  19. package/dist/agent/pane-streamer.js.map +1 -1
  20. package/dist/agent/phase-signal-watcher.d.ts +43 -2
  21. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  22. package/dist/agent/phase-signal-watcher.js +301 -43
  23. package/dist/agent/phase-signal-watcher.js.map +1 -1
  24. package/dist/agent/phase-signal.d.ts +9 -0
  25. package/dist/agent/phase-signal.d.ts.map +1 -1
  26. package/dist/agent/phase-signal.js +63 -5
  27. package/dist/agent/phase-signal.js.map +1 -1
  28. package/dist/agent/preflight.d.ts +20 -1
  29. package/dist/agent/preflight.d.ts.map +1 -1
  30. package/dist/agent/preflight.js +235 -27
  31. package/dist/agent/preflight.js.map +1 -1
  32. package/dist/agent/prompt.d.ts +8 -0
  33. package/dist/agent/prompt.d.ts.map +1 -1
  34. package/dist/agent/prompt.js +45 -2
  35. package/dist/agent/prompt.js.map +1 -1
  36. package/dist/agent/repo-store.d.ts +5 -1
  37. package/dist/agent/repo-store.d.ts.map +1 -1
  38. package/dist/agent/repo-store.js +199 -70
  39. package/dist/agent/repo-store.js.map +1 -1
  40. package/dist/agent/review-transport.d.ts.map +1 -1
  41. package/dist/agent/review-transport.js +4 -2
  42. package/dist/agent/review-transport.js.map +1 -1
  43. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  44. package/dist/agent/tmux-probe-poller.js +29 -20
  45. package/dist/agent/tmux-probe-poller.js.map +1 -1
  46. package/dist/agent/tmux.d.ts +79 -32
  47. package/dist/agent/tmux.d.ts.map +1 -1
  48. package/dist/agent/tmux.js +435 -225
  49. package/dist/agent/tmux.js.map +1 -1
  50. package/dist/api/config.d.ts.map +1 -1
  51. package/dist/api/config.js +18 -0
  52. package/dist/api/config.js.map +1 -1
  53. package/dist/api/projects.d.ts.map +1 -1
  54. package/dist/api/projects.js +241 -98
  55. package/dist/api/projects.js.map +1 -1
  56. package/dist/api/tasks.d.ts.map +1 -1
  57. package/dist/api/tasks.js +29 -2
  58. package/dist/api/tasks.js.map +1 -1
  59. package/dist/app.d.ts +2 -0
  60. package/dist/app.d.ts.map +1 -1
  61. package/dist/app.js +1 -0
  62. package/dist/app.js.map +1 -1
  63. package/dist/cli.d.ts.map +1 -1
  64. package/dist/cli.js +18 -3
  65. package/dist/cli.js.map +1 -1
  66. package/dist/config/hot-reload.d.ts.map +1 -1
  67. package/dist/config/hot-reload.js +1 -0
  68. package/dist/config/hot-reload.js.map +1 -1
  69. package/dist/config/loader.d.ts.map +1 -1
  70. package/dist/config/loader.js +48 -3
  71. package/dist/config/loader.js.map +1 -1
  72. package/dist/config/validator.js +11 -1
  73. package/dist/config/validator.js.map +1 -1
  74. package/dist/errors.d.ts +2 -1
  75. package/dist/errors.d.ts.map +1 -1
  76. package/dist/errors.js +3 -1
  77. package/dist/errors.js.map +1 -1
  78. package/dist/event/handlers.d.ts.map +1 -1
  79. package/dist/event/handlers.js +138 -41
  80. package/dist/event/handlers.js.map +1 -1
  81. package/dist/event/log.d.ts.map +1 -1
  82. package/dist/event/log.js +17 -2
  83. package/dist/event/log.js.map +1 -1
  84. package/dist/github/pr-conversation-cache.d.ts +48 -0
  85. package/dist/github/pr-conversation-cache.d.ts.map +1 -0
  86. package/dist/github/pr-conversation-cache.js +199 -0
  87. package/dist/github/pr-conversation-cache.js.map +1 -0
  88. package/dist/github/pr-conversation.d.ts +2 -2
  89. package/dist/github/pr-conversation.d.ts.map +1 -1
  90. package/dist/github/pr-conversation.js.map +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +16 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/platform/driver-spec.d.ts.map +1 -1
  95. package/dist/platform/driver-spec.js +33 -0
  96. package/dist/platform/driver-spec.js.map +1 -1
  97. package/dist/platform/git-driver.d.ts +7 -1
  98. package/dist/platform/git-driver.d.ts.map +1 -1
  99. package/dist/platform/git-driver.js +33 -2
  100. package/dist/platform/git-driver.js.map +1 -1
  101. package/dist/platform/platform-poller.d.ts +8 -0
  102. package/dist/platform/platform-poller.d.ts.map +1 -1
  103. package/dist/platform/platform-poller.js +80 -1
  104. package/dist/platform/platform-poller.js.map +1 -1
  105. package/dist/platform/plugin-registry.d.ts.map +1 -1
  106. package/dist/platform/plugin-registry.js +14 -2
  107. package/dist/platform/plugin-registry.js.map +1 -1
  108. package/dist/platform/plugins/github/driver.json +3 -0
  109. package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +16 -7
  110. package/dist/platform/preflight-exec.d.ts +21 -0
  111. package/dist/platform/preflight-exec.d.ts.map +1 -0
  112. package/dist/platform/preflight-exec.js +62 -0
  113. package/dist/platform/preflight-exec.js.map +1 -0
  114. package/dist/platform/review-timeline.d.ts +16 -0
  115. package/dist/platform/review-timeline.d.ts.map +1 -0
  116. package/dist/platform/review-timeline.js +195 -0
  117. package/dist/platform/review-timeline.js.map +1 -0
  118. package/dist/platform/startup.d.ts +5 -1
  119. package/dist/platform/startup.d.ts.map +1 -1
  120. package/dist/platform/startup.js +25 -0
  121. package/dist/platform/startup.js.map +1 -1
  122. package/dist/platform/types.d.ts +2 -0
  123. package/dist/platform/types.d.ts.map +1 -1
  124. package/dist/platform/types.js +1 -1
  125. package/dist/platform/types.js.map +1 -1
  126. package/dist/shared/constants.d.ts +6 -0
  127. package/dist/shared/constants.d.ts.map +1 -1
  128. package/dist/shared/constants.js +6 -0
  129. package/dist/shared/constants.js.map +1 -1
  130. package/dist/shared/git-url.d.ts +1 -0
  131. package/dist/shared/git-url.d.ts.map +1 -1
  132. package/dist/shared/git-url.js +3 -0
  133. package/dist/shared/git-url.js.map +1 -1
  134. package/dist/shared/types.d.ts +23 -6
  135. package/dist/shared/types.d.ts.map +1 -1
  136. package/dist/shared/types.js.map +1 -1
  137. package/dist/skill/registry.d.ts +9 -2
  138. package/dist/skill/registry.d.ts.map +1 -1
  139. package/dist/skill/registry.js +88 -54
  140. package/dist/skill/registry.js.map +1 -1
  141. package/dist/skills/baxian-pr-feedback/SKILL.md +2 -0
  142. package/dist/skills/baxian-pr-recheck/SKILL.md +2 -0
  143. package/dist/skills/baxian-pr-review/SKILL.md +2 -0
  144. package/dist/skills/baxian-research/SKILL.md +1 -1
  145. package/dist/skills/baxian-signals/SKILL.md +19 -4
  146. package/dist/skills/baxian-task-check/SKILL.md +4 -2
  147. package/dist/state/agent-store.d.ts +5 -1
  148. package/dist/state/agent-store.d.ts.map +1 -1
  149. package/dist/state/agent-store.js +56 -17
  150. package/dist/state/agent-store.js.map +1 -1
  151. package/dist/state/lock.d.ts +9 -0
  152. package/dist/state/lock.d.ts.map +1 -1
  153. package/dist/state/lock.js +24 -1
  154. package/dist/state/lock.js.map +1 -1
  155. package/dist/state/task-store.js +4 -4
  156. package/dist/state/task-store.js.map +1 -1
  157. package/dist/terminal/attach.d.ts +10 -1
  158. package/dist/terminal/attach.d.ts.map +1 -1
  159. package/dist/terminal/attach.js +41 -20
  160. package/dist/terminal/attach.js.map +1 -1
  161. package/dist/terminal/stream-ws.d.ts.map +1 -1
  162. package/dist/terminal/stream-ws.js +27 -0
  163. package/dist/terminal/stream-ws.js.map +1 -1
  164. package/dist/web/assets/{index-DfcbGriX.css → index-BPv6vv3U.css} +1 -1
  165. package/dist/web/assets/index-CUJtd0bC.js +16 -0
  166. package/dist/web/index.html +2 -2
  167. package/package.json +1 -1
  168. package/dist/web/assets/index-DskVWe7e.js +0 -13
@@ -3,6 +3,8 @@ import { ackRevisionKey } from '../platform/markers.js';
3
3
  import { SHA_HEX_SOURCE } from '../platform/types.js';
4
4
  import { BRANCH_PREFIX, TASK_TERMINAL_STATUS_SET, isRecord, isSpecStagePhase, isValidBranchName, } from '../shared/index.js';
5
5
  import { DispatchTerminalError, EnsureSessionError, isRecoverableQaDispatchHold } from '../agent/manager.js';
6
+ import { ReplNotReadyError } from '../agent/tmux.js';
7
+ import { DirtyWorkdirError } from '../agent/branch.js';
6
8
  // 平台 SHA 文法单点(spec §5.3:7-64 位 hex);legacy github 值恒 40 位,放宽为 additive
7
9
  const HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
8
10
  function validHeadSha(value) {
@@ -509,7 +511,7 @@ async function recoverOneGitPendingTask(bus, manager, task) {
509
511
  await dispatchDevPostApproveCheck(bus, manager, task, approvedHead, { redispatchCount: nextCount });
510
512
  }
511
513
  }
512
- async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix) {
514
+ async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix, ownAcquire = {}) {
513
515
  const now = await manager.getTask(taskId);
514
516
  if (now && !TASK_TERMINAL_STATUS_SET.has(now.status)) {
515
517
  // pass 被并发接管:QA 已被接管方 re-acquire,释放会误清其绑定;
@@ -518,8 +520,9 @@ async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix) {
518
520
  await manager.rearmPhaseSignalForCurrentPass(taskId);
519
521
  return;
520
522
  }
521
- // 终态/已删:没有接管方持有这只 QA,而 cancel 清理可能没见到刚写入的 qaAgentId
522
- await manager.releaseAgentForTask(qaId, taskId, 'idle').catch(err => {
523
+ // 终态/已删:没有接管方持有这只 QA,而 cancel 清理可能没见到刚写入的 qaAgentId。
524
+ // 仍钉住本次 acquire 世代——本 handler 的每一条失败释放都只清自己那一代
525
+ await manager.releaseAgentForTask(qaId, taskId, 'idle', ownAcquire).catch(err => {
523
526
  console.error(`${logPrefix} releaseAgentForTask(QA=${qaId}) after terminal-skip rollback failed:`, err);
524
527
  return false;
525
528
  });
@@ -563,6 +566,12 @@ async function handlePrCodePush(bus, manager, event) {
563
566
  ...(anchorAtDispatch ? { reviewHeadAnchorSha: anchorAtDispatch } : {}),
564
567
  reviewDispatchedAt: new Date().toISOString(),
565
568
  signalToken: createSignalToken(),
569
+ // 这些起点的轮次在 prompt 确认送达后才计(consumeReviewRoundIntent);持久化「未计」意图,
570
+ // 供 busy pending/可恢复 hold/进程重启后的任何补派方恰好补计一次
571
+ reviewRoundPending: (taskBeforeTransition.status === 'in_progress'
572
+ || taskBeforeTransition.status === 'approved'
573
+ || taskBeforeTransition.status === 'merge-ready') ? true
574
+ : (taskBeforeTransition.status === 'review' ? taskBeforeTransition.reviewRoundPending : undefined),
566
575
  // pair 与新轮可见性原子(spec §7):transition 后到 rotate 的窗口内旧 pair 不得授权本轮
567
576
  ...(taskBeforeTransition.reviewMode === 'git'
568
577
  ? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true }
@@ -571,7 +580,6 @@ async function handlePrCodePush(bus, manager, event) {
571
580
  if (!result)
572
581
  return;
573
582
  const { task: transitioned, previousStatus } = result;
574
- const expectedRound = transitioned.reviewRound;
575
583
  await manager.clearPostApproveCompletion(transitioned.id);
576
584
  let devAlreadyWaiting = false;
577
585
  if (previousStatus === 'approved' || previousStatus === 'merge-ready') {
@@ -589,14 +597,34 @@ async function handlePrCodePush(bus, manager, event) {
589
597
  return;
590
598
  }
591
599
  }
600
+ // review 起点且首评从未送达(round=0 + 未计轮 intent 在场)仍是首评:不得换成 recheck prompt/skill
601
+ const deferredFirstReview = previousStatus === 'review'
602
+ && taskBeforeTransition.reviewRound === 0
603
+ && taskBeforeTransition.reviewRoundPending === true;
604
+ const qaPhase = previousStatus === 'in_progress' || deferredFirstReview ? 'review' : 'recheck';
592
605
  if (previousStatus === 'review' && transitioned.qaAgentId) {
593
606
  const released = await manager
594
- .releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle')
607
+ .releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { deferWhenBusy: true })
595
608
  .catch(err => {
596
609
  console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${transitioned.qaAgentId}) for review→review push failed:`, err);
597
610
  return false;
598
611
  });
599
612
  if (!released) {
613
+ // 忙碌拒绝的形状:QA 仍绑定本任务且未落 hold(release 对 REPL 忙碌不再写 hold)。
614
+ // 这是「欠一次投递」而非故障:登记 pending,由对账在 pane 空闲后按标准入口补派
615
+ const qaAfter = await manager.getAgentState(transitioned.qaAgentId).catch(() => null);
616
+ const deferrable = qaAfter?.taskId === transitioned.id && qaAfter.status !== 'awaiting_human';
617
+ if (deferrable && transitioned.signalToken !== undefined) {
618
+ manager.registerPendingDispatchRetry(transitioned.id, {
619
+ kind: 'qa-recheck',
620
+ agentId: transitioned.qaAgentId,
621
+ signalToken: transitioned.signalToken,
622
+ qaPhase,
623
+ });
624
+ console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA still busy on the ` +
625
+ `previous turn; reconciler will redispatch when idle`);
626
+ return;
627
+ }
600
628
  await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
601
629
  phase: 'qa-release-failed-cannot-recheck',
602
630
  qaAgentId: transitioned.qaAgentId,
@@ -604,10 +632,6 @@ async function handlePrCodePush(bus, manager, event) {
604
632
  return;
605
633
  }
606
634
  }
607
- const qaPhase = previousStatus === 'fixing' || previousStatus === 'review'
608
- || previousStatus === 'approved' || previousStatus === 'merge-ready'
609
- ? 'recheck'
610
- : 'review';
611
635
  const qaId = transitioned.qaAgentId;
612
636
  if (!qaId) {
613
637
  if (!devAlreadyWaiting && !(await manager.markAgentWaiting(agentId, transitioned.id))) {
@@ -627,7 +651,12 @@ async function handlePrCodePush(bus, manager, event) {
627
651
  });
628
652
  return;
629
653
  }
630
- const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, qaPhase);
654
+ // 锁代由 acquire 原子交出(事后重读只会读到 successor 的代):派发放弃时只释放自己这一代,
655
+ // successor 若已重新 acquire 同一 QA(同 task 同 agent,绕过 release 的 taskId 校验)不得被误清
656
+ let ownLockToken;
657
+ const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, qaPhase, {
658
+ onAcquired: (lockToken) => { ownLockToken = lockToken; },
659
+ });
631
660
  if (!acquired) {
632
661
  await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
633
662
  phase: 'qa-acquire-failed',
@@ -636,6 +665,7 @@ async function handlePrCodePush(bus, manager, event) {
636
665
  });
637
666
  return;
638
667
  }
668
+ const ownAcquire = ownLockToken !== undefined ? { expectedLockToken: ownLockToken } : {};
639
669
  const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
640
670
  if (!armed) {
641
671
  console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
@@ -646,16 +676,21 @@ async function handlePrCodePush(bus, manager, event) {
646
676
  signalToken: taskBeforeTransition.signalToken,
647
677
  reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
648
678
  reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
679
+ // git 回滚保留 durable pending 供 sweep 重试:计轮 intent 必须同持(transition 写入值),
680
+ // 否则最终送达的首评按 stale intent 少计一轮
681
+ reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
682
+ ? transitioned.reviewRoundPending
683
+ : taskBeforeTransition.reviewRoundPending,
649
684
  ...(taskBeforeTransition.reviewMode === 'git'
650
685
  ? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
651
686
  : {}),
652
687
  }, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
653
688
  if (!rolledBack) {
654
- await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure');
689
+ await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure', ownAcquire);
655
690
  return;
656
691
  }
657
692
  }
658
- await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle').catch(err => {
693
+ await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
659
694
  console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
660
695
  return false;
661
696
  });
@@ -669,7 +704,9 @@ async function handlePrCodePush(bus, manager, event) {
669
704
  let started = false;
670
705
  let dispatchErr = null;
671
706
  try {
672
- started = await manager.startSession(transitioned.id, qa.id, qaPhase);
707
+ started = await manager.startSession(transitioned.id, qa.id, qaPhase, {
708
+ dispatchPassToken: dispatchToken,
709
+ });
673
710
  }
674
711
  catch (err) {
675
712
  dispatchErr = err;
@@ -680,6 +717,11 @@ async function handlePrCodePush(bus, manager, event) {
680
717
  if (dispatchErr instanceof DispatchTerminalError) {
681
718
  await manager.failTaskForDispatchError(transitioned.id, qaPhase, qa.id, dispatchErr);
682
719
  }
720
+ else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.busyPending) {
721
+ // 遇忙不是故障:pass 保持 armed、QA 保持绑定,pending 登记已在 startSession 内完成,交对账补派
722
+ console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA pane busy; ` +
723
+ `reconciler will redispatch when idle`);
724
+ }
683
725
  else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
684
726
  // handled 不止代表可恢复派发 hold(checkout-cleanup-failed / dialog 等也走这里),指引须按实际相位分流
685
727
  const qaState = await manager.getAgentState(qa.id).catch((stateErr) => {
@@ -707,23 +749,27 @@ async function handlePrCodePush(bus, manager, event) {
707
749
  signalToken: taskBeforeTransition.signalToken,
708
750
  reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
709
751
  reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
752
+ // git 保留 durable pending 即保留计轮 intent(同 arm-failure 站点)
753
+ reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
754
+ ? transitioned.reviewRoundPending
755
+ : taskBeforeTransition.reviewRoundPending,
710
756
  ...(taskBeforeTransition.reviewMode === 'git'
711
757
  ? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
712
758
  : {}),
713
759
  }, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
714
760
  if (rolledBack) {
715
- await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
761
+ await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
716
762
  .catch(err => {
717
763
  console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
718
764
  return false;
719
765
  });
720
766
  }
721
767
  else {
722
- await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated recheck');
768
+ await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated recheck', ownAcquire);
723
769
  }
724
770
  }
725
771
  else {
726
- await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
772
+ await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
727
773
  .catch(err => {
728
774
  console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
729
775
  return false;
@@ -746,9 +792,9 @@ async function handlePrCodePush(bus, manager, event) {
746
792
  if (taskBeforeTransition.reviewMode === 'git') {
747
793
  await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
748
794
  }
749
- if (previousStatus === 'in_progress' || previousStatus === 'approved' || previousStatus === 'merge-ready') {
750
- await manager.bumpReviewRoundIfStillAt(transitioned.id, expectedRound);
751
- }
795
+ // 送达确认后按本次 pass CAS 消费未计轮 intent:flag 本身已编码「哪些起点开新轮」
796
+ // (review→review 的既计轮 recheck 无 flag 即天然 no-op),successor 换代则留给它自己消费
797
+ await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
752
798
  const ok = devAlreadyWaiting || (await manager.markAgentWaiting(agentId, transitioned.id));
753
799
  if (!ok) {
754
800
  await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {
@@ -777,10 +823,15 @@ async function redispatchReviewForStaleVerdict(manager, task) {
777
823
  try {
778
824
  // fromStatus/expectSignalToken 在 dispatch 锁内复核:本地快照到这里的窗口里任务可能已离开 review
779
825
  // 或被并发 dispatcher 换代 pass,不能靠上面的预检
826
+ const entry = manager.getPendingDispatchRetry(task.id);
827
+ const qaPhase = entry?.kind === 'qa-recheck' && entry.signalToken === task.signalToken && entry.qaPhase !== undefined
828
+ ? entry.qaPhase
829
+ : (task.reviewRound === 0 ? 'review' : undefined);
780
830
  await manager.dispatchReviewToQa(task.id, {
781
831
  fromStatus: ['review'],
782
- bumpRound: false,
832
+ bumpRound: task.reviewRoundPending === true,
783
833
  expectSignalToken: task.signalToken,
834
+ ...(qaPhase !== undefined ? { qaPhase } : {}),
784
835
  });
785
836
  }
786
837
  catch (err) {
@@ -833,9 +884,17 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
833
884
  fromStatus: ['review'],
834
885
  // 校验后提交前的并发换代(push/重派轮换 token+pair)使旧 APPROVE 失效(spec §7)
835
886
  ...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
836
- }, task.reviewRound === 0
837
- ? { ...prPatch, ...provenanceFields, ...gitHeadPatch(task, anchor.headSha), reviewRound: 1 }
838
- : { ...prPatch, ...provenanceFields, ...gitHeadPatch(task, anchor.headSha) });
887
+ }, {
888
+ ...prPatch,
889
+ ...provenanceFields,
890
+ ...gitHeadPatch(task, anchor.headSha),
891
+ // 崩溃在「prompt 已送达、bump 未落盘」窗口时 pending 轮次由 verdict 消费补计
892
+ reviewRound: Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0)),
893
+ reviewRoundPending: undefined,
894
+ // verdict 即送达证据:durable pending 随 transition 原子消费,封死「送达后 verdict 抢先、
895
+ // startSession 的 clear 因 status/token 已变而拒绝」留下 stale 凭据的竞态
896
+ reviewDispatchPending: undefined,
897
+ });
839
898
  if (!result)
840
899
  return;
841
900
  const { task: transitioned } = result;
@@ -874,7 +933,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
874
933
  });
875
934
  return;
876
935
  }
877
- const reviewedRound = task.reviewRound === 0 ? 1 : task.reviewRound;
936
+ const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
878
937
  const nextRound = reviewedRound + 1;
879
938
  if (task.status === 'approved' && nextRound <= manager.getConfig().review.rounds) {
880
939
  const devState = await manager.getAgentState(task.agentId);
@@ -919,7 +978,15 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
919
978
  fromStatus: ['review', 'approved', 'merge-ready'],
920
979
  // fail 方向同样按入口代际 CAS(spec §7):await 窗口内 push/重派换代后旧 RC 不得推进新轮
921
980
  ...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
922
- }, { ...prPatch, ...gitHeadPatch(task, anchor.headSha), reviewRound: nextRound, fixDispatchedAt: new Date().toISOString() });
981
+ }, {
982
+ ...prPatch,
983
+ ...gitHeadPatch(task, anchor.headSha),
984
+ reviewRound: nextRound,
985
+ reviewRoundPending: undefined,
986
+ // verdict 即送达证据(同 APPROVE):不消费会让 sweep 在 fixing 态按 stale pending 把任务拽回 review
987
+ reviewDispatchPending: undefined,
988
+ fixDispatchedAt: new Date().toISOString(),
989
+ });
923
990
  if (!result)
924
991
  return;
925
992
  const { task: transitioned, previousStatus } = result;
@@ -947,7 +1014,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
947
1014
  });
948
1015
  return;
949
1016
  }
950
- const { armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
1017
+ const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
951
1018
  if (!armed) {
952
1019
  console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
953
1020
  await manager.markAwaitingHuman(transitioned.agentId, 'signal-arm-failed:pr-fixed', 'pr-fixed watcher failed to arm; the fix was not dispatched (its completion signal would have no consumer). Cancel the task or delete the agent to retry.', { expectedTaskId: transitioned.id });
@@ -956,7 +1023,13 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
956
1023
  let resumed = false;
957
1024
  let dispatchErr = null;
958
1025
  try {
959
- resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix');
1026
+ resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix', {
1027
+ signalToken: fixToken,
1028
+ guardBeforeInject: async () => {
1029
+ const freshTask = await manager.getTask(transitioned.id);
1030
+ return freshTask?.status === 'fixing' && freshTask.signalToken === fixToken;
1031
+ },
1032
+ });
960
1033
  }
961
1034
  catch (err) {
962
1035
  dispatchErr = err;
@@ -969,15 +1042,28 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
969
1042
  await manager.failTaskForDispatchError(transitioned.id, 'fix', transitioned.agentId, dispatchErr);
970
1043
  }
971
1044
  else {
972
- await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
1045
+ const parked = await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
973
1046
  .catch(err => {
974
1047
  console.error(`[EventHandler] REQUEST_CHANGES markAgentWaiting(dev=${transitioned.agentId}) rollback failed:`, err);
975
1048
  return false;
976
1049
  });
977
- await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
978
- phase: 'fix-resume-failed',
979
- reviewRound: transitioned.reviewRound,
980
- });
1050
+ if (parked && (dispatchErr instanceof ReplNotReadyError || dispatchErr instanceof DirtyWorkdirError)) {
1051
+ // dev 回合在途(忙碌/未提交改动)是常态而非故障:登记待补派,回合结束后由对账 re-continue。
1052
+ // 停驻失败说明绑定/锁已不受控,pending 无法证明还有可承接的 session,必须走人工告警
1053
+ manager.registerPendingDispatchRetry(transitioned.id, {
1054
+ kind: 'dev-fix',
1055
+ agentId: transitioned.agentId,
1056
+ signalToken: fixToken,
1057
+ });
1058
+ console.warn(`[EventHandler] REQUEST_CHANGES fix deferred for task=${transitioned.id}: dev pane busy or workdir dirty ` +
1059
+ `(${dispatchErr.name}); reconciler will re-continue when idle`);
1060
+ }
1061
+ else {
1062
+ await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
1063
+ phase: 'fix-resume-failed',
1064
+ reviewRound: transitioned.reviewRound,
1065
+ });
1066
+ }
981
1067
  }
982
1068
  }
983
1069
  }
@@ -985,7 +1071,7 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
985
1071
  const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
986
1072
  fromStatus: ['review', 'approved', 'merge-ready'],
987
1073
  ...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
988
- }, { ...prPatch, reviewRound: reviewedRound });
1074
+ }, { ...prPatch, reviewRound: reviewedRound, reviewRoundPending: undefined, reviewDispatchPending: undefined });
989
1075
  if (!result)
990
1076
  return;
991
1077
  const { task: transitioned } = result;
@@ -1230,6 +1316,7 @@ export function registerEventHandlers(bus, manager) {
1230
1316
  ...(reconciledBranch ? { branch: reconciledBranch } : {}),
1231
1317
  ...gitAdoptionFields,
1232
1318
  reviewDispatchedAt: new Date().toISOString(),
1319
+ reviewRoundPending: taskBeforeTransition?.status === 'in_progress' ? true : undefined,
1233
1320
  signalToken: createSignalToken(),
1234
1321
  ...(isGit ? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true } : {}),
1235
1322
  });
@@ -1253,7 +1340,6 @@ export function registerEventHandlers(bus, manager) {
1253
1340
  console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ` +
1254
1341
  `${taskBeforeTransition?.branch} → ${reconciledBranch}`);
1255
1342
  }
1256
- const expectedRound = transitioned.reviewRound;
1257
1343
  const qaId = transitioned.qaAgentId;
1258
1344
  if (!qaId) {
1259
1345
  const ok = await manager.markAgentWaiting(event.agentId, transitioned.id);
@@ -1273,7 +1359,10 @@ export function registerEventHandlers(bus, manager) {
1273
1359
  });
1274
1360
  return;
1275
1361
  }
1276
- const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, 'review');
1362
+ let ownLockToken;
1363
+ const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, 'review', {
1364
+ onAcquired: (lockToken) => { ownLockToken = lockToken; },
1365
+ });
1277
1366
  if (!acquired) {
1278
1367
  await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
1279
1368
  phase: 'qa-acquire-failed',
@@ -1281,6 +1370,7 @@ export function registerEventHandlers(bus, manager) {
1281
1370
  });
1282
1371
  return;
1283
1372
  }
1373
+ const ownAcquire = ownLockToken !== undefined ? { expectedLockToken: ownLockToken } : {};
1284
1374
  const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
1285
1375
  if (!armed) {
1286
1376
  console.warn(`[EventHandler] pr.created verdict watcher failed to arm for task=${transitioned.id}; rolling back review dispatch`);
@@ -1289,15 +1379,19 @@ export function registerEventHandlers(bus, manager) {
1289
1379
  signalToken: taskBeforeTransition?.signalToken,
1290
1380
  reviewHeadAnchorSha: taskBeforeTransition?.reviewHeadAnchorSha,
1291
1381
  reviewDispatchedAt: taskBeforeTransition?.reviewDispatchedAt,
1382
+ // git 保留 durable pending 即保留计轮 intent(同 pr.updated 回滚站点)
1383
+ reviewRoundPending: taskBeforeTransition?.reviewMode === 'git'
1384
+ ? transitioned.reviewRoundPending
1385
+ : taskBeforeTransition?.reviewRoundPending,
1292
1386
  ...(taskBeforeTransition?.reviewMode === 'git'
1293
1387
  ? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
1294
1388
  : {}),
1295
1389
  }, { expect: { status: 'review', signalToken: dispatchToken } });
1296
1390
  if (!rolledBack) {
1297
- await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure');
1391
+ await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure', ownAcquire);
1298
1392
  return;
1299
1393
  }
1300
- await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle').catch(err => {
1394
+ await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
1301
1395
  console.error(`[EventHandler] pr.created releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
1302
1396
  return false;
1303
1397
  });
@@ -1310,7 +1404,9 @@ export function registerEventHandlers(bus, manager) {
1310
1404
  let started = false;
1311
1405
  let dispatchErr = null;
1312
1406
  try {
1313
- started = await manager.startSession(transitioned.id, qa.id, 'review');
1407
+ started = await manager.startSession(transitioned.id, qa.id, 'review', {
1408
+ dispatchPassToken: dispatchToken,
1409
+ });
1314
1410
  }
1315
1411
  catch (err) {
1316
1412
  dispatchErr = err;
@@ -1325,7 +1421,7 @@ export function registerEventHandlers(bus, manager) {
1325
1421
  else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
1326
1422
  }
1327
1423
  else {
1328
- await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
1424
+ await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
1329
1425
  .catch(err => {
1330
1426
  console.error(`[EventHandler] pr.created releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
1331
1427
  return false;
@@ -1337,9 +1433,10 @@ export function registerEventHandlers(bus, manager) {
1337
1433
  }
1338
1434
  return;
1339
1435
  }
1340
- if (previousStatus === 'in_progress') {
1341
- await manager.bumpReviewRoundIfStillAt(transitioned.id, expectedRound);
1436
+ if (taskBeforeTransition?.reviewMode === 'git') {
1437
+ await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
1342
1438
  }
1439
+ await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
1343
1440
  const ok = await manager.markAgentWaiting(event.agentId, transitioned.id);
1344
1441
  if (!ok) {
1345
1442
  await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {