@overdeck/core 0.50.3 → 0.50.4

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 (208) hide show
  1. package/dist/agents-DNMRPC8n.js.map +1 -1
  2. package/dist/{build-info-BJuEVWOh.js → build-info-ioRdOmqT.js} +3 -3
  3. package/dist/{build-info-BJuEVWOh.js.map → build-info-ioRdOmqT.js.map} +1 -1
  4. package/dist/build-info-kMMitDOP.js +2 -0
  5. package/dist/cli/index.js +4 -4
  6. package/dist/conversation-compaction-GFfO5UZL.js.map +1 -1
  7. package/dist/dashboard/{advancing-selfheal-Bq8V0QUa.js → advancing-selfheal-Dp5_Gai3.js} +3 -3
  8. package/dist/dashboard/{advancing-selfheal-Bq8V0QUa.js.map → advancing-selfheal-Dp5_Gai3.js.map} +1 -1
  9. package/dist/dashboard/{agent-gc-Coz8zQt_.js → agent-gc-JzoSs5Pi.js} +3 -3
  10. package/dist/dashboard/{agent-gc-Coz8zQt_.js.map → agent-gc-JzoSs5Pi.js.map} +1 -1
  11. package/dist/dashboard/{agent-idle-CAwB-K6M.js → agent-idle-D-Qy5BN_.js} +2 -2
  12. package/dist/dashboard/{agent-idle-CAwB-K6M.js.map → agent-idle-D-Qy5BN_.js.map} +1 -1
  13. package/dist/dashboard/{agent-state-Ci6PhjOE.js → agent-state-B196fj1h.js} +38 -16
  14. package/dist/dashboard/agent-state-B196fj1h.js.map +1 -0
  15. package/dist/dashboard/{agent-state-CbUEwUEQ.js → agent-state-IpWnw8lv.js} +1 -1
  16. package/dist/dashboard/{agent-state-service-D6IVrX3B.js → agent-state-service-CCo5-9DR.js} +3 -3
  17. package/dist/dashboard/{agent-state-service-D6IVrX3B.js.map → agent-state-service-CCo5-9DR.js.map} +1 -1
  18. package/dist/dashboard/{agent-state-service-DcN1j6eK.js → agent-state-service-D_F3VZ23.js} +1 -1
  19. package/dist/dashboard/{agents-BVvL4o_4.js → agents-CkGhnAmu.js} +1 -1
  20. package/dist/dashboard/{backlog-auto-trigger-BuSBqG2V.js → backlog-auto-trigger-B5Z2zBtC.js} +2 -2
  21. package/dist/dashboard/{backlog-auto-trigger-BuSBqG2V.js.map → backlog-auto-trigger-B5Z2zBtC.js.map} +1 -1
  22. package/dist/dashboard/build-info-BTjOB0Xr.js +2 -0
  23. package/dist/dashboard/{build-info-Cj3Y0K87.js → build-info-D0EJNj6C.js} +3 -3
  24. package/dist/dashboard/{build-info-Cj3Y0K87.js.map → build-info-D0EJNj6C.js.map} +1 -1
  25. package/dist/dashboard/cli-CIznFrcE.js +2 -0
  26. package/dist/dashboard/{cli-DIhQieif.js → cli-DCbVrSsg.js} +2 -2
  27. package/dist/dashboard/{cli-DIhQieif.js.map → cli-DCbVrSsg.js.map} +1 -1
  28. package/dist/dashboard/{close-issue-CRlmrE5g.js → close-issue-Bmu9aOIx.js} +2 -2
  29. package/dist/dashboard/{close-issue-CRlmrE5g.js.map → close-issue-Bmu9aOIx.js.map} +1 -1
  30. package/dist/dashboard/{closed-issue-reaper-DrHyYgJR.js → closed-issue-reaper-COxfTtLz.js} +5 -5
  31. package/dist/dashboard/{closed-issue-reaper-DrHyYgJR.js.map → closed-issue-reaper-COxfTtLz.js.map} +1 -1
  32. package/dist/dashboard/{closed-issue-reaper-DWlw2EtL.js → closed-issue-reaper-DwMwG_Pz.js} +1 -1
  33. package/dist/dashboard/{compaction-continuation-DhYgsXmF.js → compaction-continuation-Bqc212Zm.js} +2 -2
  34. package/dist/dashboard/{compaction-continuation-DhYgsXmF.js.map → compaction-continuation-Bqc212Zm.js.map} +1 -1
  35. package/dist/dashboard/{conflict-gate-vczgdVTa.js → conflict-gate-9mPVg7-5.js} +1 -1
  36. package/dist/dashboard/{conflict-gate-Bt-lLgYf.js → conflict-gate-CGQTcR0I.js} +3 -3
  37. package/dist/dashboard/{conflict-gate-Bt-lLgYf.js.map → conflict-gate-CGQTcR0I.js.map} +1 -1
  38. package/dist/dashboard/{conversation-compaction-CsdCbcQB.js → conversation-compaction-C7P6V8Iw.js} +5 -5
  39. package/dist/dashboard/{conversation-compaction-CsdCbcQB.js.map → conversation-compaction-C7P6V8Iw.js.map} +1 -1
  40. package/dist/dashboard/{conversation-compaction-D0zgWM1f.js → conversation-compaction-Cck5oi-V.js} +1 -1
  41. package/dist/dashboard/{conversation-service-6LaymUV-.js → conversation-service-CsOpwI9Y.js} +37 -5
  42. package/dist/dashboard/{conversation-service-6LaymUV-.js.map → conversation-service-CsOpwI9Y.js.map} +1 -1
  43. package/dist/dashboard/conversation-service-plOFkoMo.js +3 -0
  44. package/dist/dashboard/dashboard-db-worker.js +1 -1
  45. package/dist/dashboard/{deacon-CU47dU7U.js → deacon-B8oMMIgN.js} +62 -62
  46. package/dist/dashboard/{deacon-CU47dU7U.js.map → deacon-B8oMMIgN.js.map} +1 -1
  47. package/dist/dashboard/{deacon-DYsOa_ss.js → deacon-b7J57Svt.js} +2 -2
  48. package/dist/dashboard/{deacon-pending-lifecycle-ijGX8wLb.js → deacon-pending-lifecycle-3NIJ7ew5.js} +2 -2
  49. package/dist/dashboard/{deacon-pending-lifecycle-ijGX8wLb.js.map → deacon-pending-lifecycle-3NIJ7ew5.js.map} +1 -1
  50. package/dist/dashboard/deacon.js +5 -5
  51. package/dist/dashboard/{delivery-DMYxIE0J.js → delivery-lnI-gQKo.js} +1 -1
  52. package/dist/dashboard/{deploy-patrol-C4eXco1S.js → deploy-patrol-u3v2krg9.js} +2 -2
  53. package/dist/dashboard/{deploy-patrol-C4eXco1S.js.map → deploy-patrol-u3v2krg9.js.map} +1 -1
  54. package/dist/dashboard/{feedback-stuck-retirement-Dd5XNrMr.js → feedback-stuck-retirement-CoFNqjCc.js} +2 -2
  55. package/dist/dashboard/{feedback-stuck-retirement-Dd5XNrMr.js.map → feedback-stuck-retirement-CoFNqjCc.js.map} +1 -1
  56. package/dist/dashboard/{feedback-target-BzdJ2wIR.js → feedback-target-BBCh3RRq.js} +1 -1
  57. package/dist/dashboard/{feedback-target-BcsA_GRV.js → feedback-target-CeCq6EhW.js} +5 -5
  58. package/dist/dashboard/{feedback-target-BcsA_GRV.js.map → feedback-target-CeCq6EhW.js.map} +1 -1
  59. package/dist/dashboard/{flywheel-Is3w2lcT.js → flywheel-DKldI63J.js} +7 -7
  60. package/dist/dashboard/{flywheel-Is3w2lcT.js.map → flywheel-DKldI63J.js.map} +1 -1
  61. package/dist/dashboard/{flywheel-D0guNqkh.js → flywheel-DpRPLErh.js} +7 -7
  62. package/dist/dashboard/{flywheel-D0guNqkh.js.map → flywheel-DpRPLErh.js.map} +1 -1
  63. package/dist/dashboard/{flywheel-B34cdfbd.js → flywheel-jeZFYvm-.js} +1 -1
  64. package/dist/dashboard/{flywheel-merge-order-6HiPR2WG.js → flywheel-merge-order-0OnoosaO.js} +1 -1
  65. package/dist/dashboard/{flywheel-merge-order-BsNPMu0S.js → flywheel-merge-order-PG5op4j7.js} +2 -2
  66. package/dist/dashboard/{flywheel-merge-order-BsNPMu0S.js.map → flywheel-merge-order-PG5op4j7.js.map} +1 -1
  67. package/dist/dashboard/{harness-resolve-CzTFVZ5l.js → harness-resolve-C0HWj3H9.js} +2 -2
  68. package/dist/dashboard/{harness-resolve-CzTFVZ5l.js.map → harness-resolve-C0HWj3H9.js.map} +1 -1
  69. package/dist/dashboard/{identity-Dc7rQLln.js → identity-Cot1Hg4G.js} +2 -2
  70. package/dist/dashboard/{identity-Dc7rQLln.js.map → identity-Cot1Hg4G.js.map} +1 -1
  71. package/dist/dashboard/{inspect-agent-CP7Bu47O.js → inspect-agent-BDmLWbGx.js} +3 -3
  72. package/dist/dashboard/{inspect-agent-CP7Bu47O.js.map → inspect-agent-BDmLWbGx.js.map} +1 -1
  73. package/dist/dashboard/inspect-agent-BcOkA-hC.js +2 -0
  74. package/dist/dashboard/{issue-lifecycle-BPbczKyy.js → issue-lifecycle-1B_v8Rf5.js} +6 -6
  75. package/dist/dashboard/{issue-lifecycle-BPbczKyy.js.map → issue-lifecycle-1B_v8Rf5.js.map} +1 -1
  76. package/dist/dashboard/{issue-lifecycle-FaugTi39.js → issue-lifecycle-CIrfbIbj.js} +1 -1
  77. package/dist/dashboard/{issue-service-singleton-Dsad9NTt.js → issue-service-singleton-BVwPzjwF.js} +1 -1
  78. package/dist/dashboard/{issue-service-singleton-BrZQJLRI.js → issue-service-singleton-KcRrOiFD.js} +2 -2
  79. package/dist/dashboard/{issue-service-singleton-BrZQJLRI.js.map → issue-service-singleton-KcRrOiFD.js.map} +1 -1
  80. package/dist/dashboard/{jsonl-resolver-DQEe0DHM.js → jsonl-resolver-jxbif7MD.js} +3 -3
  81. package/dist/dashboard/{jsonl-resolver-DQEe0DHM.js.map → jsonl-resolver-jxbif7MD.js.map} +1 -1
  82. package/dist/dashboard/{kimi-code-Cx5YUJpS.js → kimi-code-BZh3NgcN.js} +1 -1
  83. package/dist/dashboard/{lifecycle-DZFHmpGI.js → lifecycle-CvcZ4_fA.js} +4 -4
  84. package/dist/dashboard/{lifecycle-DZFHmpGI.js.map → lifecycle-CvcZ4_fA.js.map} +1 -1
  85. package/dist/dashboard/{linear-mcp-auth-DylPJABZ.js → linear-mcp-auth-CZoj_-d7.js} +1 -1
  86. package/dist/dashboard/{linear-mcp-auth-BFlpMvWh.js → linear-mcp-auth-Dx74yCAY.js} +2 -2
  87. package/dist/dashboard/{linear-mcp-auth-BFlpMvWh.js.map → linear-mcp-auth-Dx74yCAY.js.map} +1 -1
  88. package/dist/dashboard/{memory-governor-B7uDWRo9.js → memory-governor-Bv3LAqk3.js} +5 -5
  89. package/dist/dashboard/{memory-governor-B7uDWRo9.js.map → memory-governor-Bv3LAqk3.js.map} +1 -1
  90. package/dist/dashboard/{merge-agent-ddEcZDZD.js → merge-agent-1zVLl6k_.js} +1 -1
  91. package/dist/dashboard/{merge-agent-ttRY053V.js → merge-agent-8zrQu48t.js} +8 -8
  92. package/dist/dashboard/{merge-agent-ttRY053V.js.map → merge-agent-8zrQu48t.js.map} +1 -1
  93. package/dist/dashboard/merge-ops-CFJaMPLu.js +2 -0
  94. package/dist/dashboard/{merge-train-f_un38UJ.js → merge-train-BYH_cF7G.js} +2 -2
  95. package/dist/dashboard/{merge-train-f_un38UJ.js.map → merge-train-BYH_cF7G.js.map} +1 -1
  96. package/dist/dashboard/{merge-train-deps-GPlHOjK6.js → merge-train-deps-BTfXM5AR.js} +2 -2
  97. package/dist/dashboard/{merge-train-deps-GPlHOjK6.js.map → merge-train-deps-BTfXM5AR.js.map} +1 -1
  98. package/dist/dashboard/{messaging-BasbftDu.js → messaging-Bk6vGsMw.js} +1 -1
  99. package/dist/dashboard/{orphan-proposed-reconciler-DFHdQSEg.js → orphan-proposed-reconciler-B898fk26.js} +6 -6
  100. package/dist/dashboard/{orphan-proposed-reconciler-DFHdQSEg.js.map → orphan-proposed-reconciler-B898fk26.js.map} +1 -1
  101. package/dist/dashboard/{orphan-proposed-reconciler-DzAxrM99.js → orphan-proposed-reconciler-Bx_aWS4B.js} +1 -1
  102. package/dist/dashboard/{parked-residue-DMtbxJml.js → parked-residue-q2kzrYSm.js} +9 -9
  103. package/dist/dashboard/{parked-residue-DMtbxJml.js.map → parked-residue-q2kzrYSm.js.map} +1 -1
  104. package/dist/dashboard/{post-merge-lifecycle-worker-jzN2g_EY.js → post-merge-lifecycle-worker-Cl7-uf4g.js} +2 -2
  105. package/dist/dashboard/{post-merge-lifecycle-worker-jzN2g_EY.js.map → post-merge-lifecycle-worker-Cl7-uf4g.js.map} +1 -1
  106. package/dist/dashboard/{project-resource-refresh-queue-cqOeJ0fB.js → project-resource-refresh-queue-O_dRfPHn.js} +1 -1
  107. package/dist/dashboard/{project-resource-refresh-queue-DebwjPPA.js → project-resource-refresh-queue-se3T9IYP.js} +4 -4
  108. package/dist/dashboard/{project-resource-refresh-queue-DebwjPPA.js.map → project-resource-refresh-queue-se3T9IYP.js.map} +1 -1
  109. package/dist/dashboard/{reconstruct-cache-Bb2Jg2DZ.js → reconstruct-cache-XpWC9L0F.js} +2 -2
  110. package/dist/dashboard/{reconstruct-cache-Bb2Jg2DZ.js.map → reconstruct-cache-XpWC9L0F.js.map} +1 -1
  111. package/dist/dashboard/{release-engine-78iCajIc.js → release-engine-BuzxOamf.js} +2 -2
  112. package/dist/dashboard/{release-engine-78iCajIc.js.map → release-engine-BuzxOamf.js.map} +1 -1
  113. package/dist/dashboard/{remote-completion-D5wzZCte.js → remote-completion-BCZgeKPS.js} +3 -3
  114. package/dist/dashboard/{remote-completion-D5wzZCte.js.map → remote-completion-BCZgeKPS.js.map} +1 -1
  115. package/dist/dashboard/{resolver-BGZh3lXA.js → resolver-BeNrqNou.js} +5 -5
  116. package/dist/dashboard/{resolver-BGZh3lXA.js.map → resolver-BeNrqNou.js.map} +1 -1
  117. package/dist/dashboard/{resource-pressure-patrol-Bg23rPV-.js → resource-pressure-patrol-Ccg_aCgO.js} +3 -3
  118. package/dist/dashboard/{resource-pressure-patrol-Bg23rPV-.js.map → resource-pressure-patrol-Ccg_aCgO.js.map} +1 -1
  119. package/dist/dashboard/{resume-BKyB4hA1.js → resume-KGCrn0DV.js} +1 -1
  120. package/dist/dashboard/{review-agent-ZdP-HmwY.js → review-agent-BmM6YMZW.js} +1 -1
  121. package/dist/dashboard/{review-agent-DHs-JngN.js → review-agent-DRWVU8fP.js} +13 -13
  122. package/dist/dashboard/{review-agent-DHs-JngN.js.map → review-agent-DRWVU8fP.js.map} +1 -1
  123. package/dist/dashboard/{review-convoy-liveness-BTMvU4CA.js → review-convoy-liveness-EvCDv8Q_.js} +3 -3
  124. package/dist/dashboard/{review-convoy-liveness-BTMvU4CA.js.map → review-convoy-liveness-EvCDv8Q_.js.map} +1 -1
  125. package/dist/dashboard/{review-run-recovery-BYalQzbh.js → review-run-recovery-Ctm48Snt.js} +1 -1
  126. package/dist/dashboard/{review-run-recovery-BvVG3oUr.js → review-run-recovery-DNDI58cM.js} +2 -2
  127. package/dist/dashboard/{review-run-recovery-BvVG3oUr.js.map → review-run-recovery-DNDI58cM.js.map} +1 -1
  128. package/dist/dashboard/{review-status-DXTFzCmR.js → review-status-FnlvQuPI.js} +1 -1
  129. package/dist/dashboard/{review-status-emit-C9YW6SQL.js → review-status-emit-CeMS31DX.js} +2 -2
  130. package/dist/dashboard/{review-status-emit-C9YW6SQL.js.map → review-status-emit-CeMS31DX.js.map} +1 -1
  131. package/dist/dashboard/{review-status-BegAr86G.js → review-status-yY5qt7Rl.js} +5 -5
  132. package/dist/dashboard/{review-status-BegAr86G.js.map → review-status-yY5qt7Rl.js.map} +1 -1
  133. package/dist/dashboard/{review-verdict-feedback-qlVBi19-.js → review-verdict-feedback-BNgRj1e3.js} +4 -4
  134. package/dist/dashboard/{review-verdict-feedback-qlVBi19-.js.map → review-verdict-feedback-BNgRj1e3.js.map} +1 -1
  135. package/dist/dashboard/{review-verdict-feedback-Dp139Lmv.js → review-verdict-feedback-D39cuSV1.js} +1 -1
  136. package/dist/dashboard/{review-verdict-writer-CJr_Ob0Y.js → review-verdict-writer-DvFfKAn-.js} +2 -2
  137. package/dist/dashboard/{review-verdict-writer-CJr_Ob0Y.js.map → review-verdict-writer-DvFfKAn-.js.map} +1 -1
  138. package/dist/dashboard/{review-verdict-writer-BVco3WoI.js → review-verdict-writer-PcGBkvJ4.js} +1 -1
  139. package/dist/dashboard/{sequence-io-iljWlslY.js → sequence-io-DZyschsf.js} +2 -2
  140. package/dist/dashboard/{sequence-io-iljWlslY.js.map → sequence-io-DZyschsf.js.map} +1 -1
  141. package/dist/dashboard/{sequencer-agent-BGGVKxtw.js → sequencer-agent-BXDw5cNx.js} +5 -5
  142. package/dist/dashboard/{sequencer-agent-BGGVKxtw.js.map → sequencer-agent-BXDw5cNx.js.map} +1 -1
  143. package/dist/dashboard/server.js +79 -72
  144. package/dist/dashboard/server.js.map +1 -1
  145. package/dist/dashboard/{service-kzhN0DLg.js → service-DP50Omvn.js} +33 -33
  146. package/dist/dashboard/{service-kzhN0DLg.js.map → service-DP50Omvn.js.map} +1 -1
  147. package/dist/dashboard/{smart-compaction-owKHJDbL.js → smart-compaction-944GL8PN.js} +1 -1
  148. package/dist/dashboard/{smart-compaction-C96F2aFQ.js → smart-compaction-BzDfF6yb.js} +2 -2
  149. package/dist/dashboard/{smart-compaction-C96F2aFQ.js.map → smart-compaction-BzDfF6yb.js.map} +1 -1
  150. package/dist/dashboard/{spawn-planning-session-CiJdOHTD.js → spawn-planning-session-BcVnHJjm.js} +1 -1
  151. package/dist/dashboard/{spawn-planning-session-FmJa6PCS.js → spawn-planning-session-CH8IYli7.js} +3 -3
  152. package/dist/dashboard/{spawn-planning-session-FmJa6PCS.js.map → spawn-planning-session-CH8IYli7.js.map} +1 -1
  153. package/dist/dashboard/{specialist-context-_ootmzn2.js → specialist-context-DhDTXVht.js} +3 -3
  154. package/dist/dashboard/{specialist-context-_ootmzn2.js.map → specialist-context-DhDTXVht.js.map} +1 -1
  155. package/dist/dashboard/{specialist-logs-BbAX221-.js → specialist-logs-C1bcnZ-G.js} +2 -2
  156. package/dist/dashboard/{specialist-logs-BbAX221-.js.map → specialist-logs-C1bcnZ-G.js.map} +1 -1
  157. package/dist/dashboard/{specialists-COZoYaiz.js → specialists-BKmReEWT.js} +53 -53
  158. package/dist/dashboard/{specialists-COZoYaiz.js.map → specialists-BKmReEWT.js.map} +1 -1
  159. package/dist/dashboard/{specialists-RPLJnYc1.js → specialists-Bvrvt0br.js} +12 -12
  160. package/dist/dashboard/{specialists-RPLJnYc1.js.map → specialists-Bvrvt0br.js.map} +1 -1
  161. package/dist/dashboard/{specialists-DTAOVZpU.js → specialists-CD6GEKXk.js} +1 -1
  162. package/dist/dashboard/{specialists-D8sYZnPb.js → specialists-eFWpJRBc.js} +1 -1
  163. package/dist/dashboard/{stall-sweeper-9bRBZhrQ.js → stall-sweeper-D3fZvg7O.js} +3 -3
  164. package/dist/dashboard/{stall-sweeper-9bRBZhrQ.js.map → stall-sweeper-D3fZvg7O.js.map} +1 -1
  165. package/dist/dashboard/{state-migrate-J3tFJnbd.js → state-migrate-KZ2goscQ.js} +2 -2
  166. package/dist/dashboard/{state-migrate-J3tFJnbd.js.map → state-migrate-KZ2goscQ.js.map} +1 -1
  167. package/dist/dashboard/{stuck-remediation-BvTK_NYN.js → stuck-remediation-Cb2V1roj.js} +1 -1
  168. package/dist/dashboard/{stuck-remediation-4Rf_xMEb.js → stuck-remediation-CnoV0kcv.js} +5 -5
  169. package/dist/dashboard/{stuck-remediation-4Rf_xMEb.js.map → stuck-remediation-CnoV0kcv.js.map} +1 -1
  170. package/dist/dashboard/{summary-fork-PLcckTRE.js → summary-fork-CxuNBM0F.js} +2 -2
  171. package/dist/dashboard/{summary-fork-D-cylknJ.js → summary-fork-lC2vMXly.js} +4 -4
  172. package/dist/dashboard/{summary-fork-D-cylknJ.js.map → summary-fork-lC2vMXly.js.map} +1 -1
  173. package/dist/dashboard/{teardown-workspace-CTL16rnG.js → teardown-workspace-C-9murBw.js} +4 -4
  174. package/dist/dashboard/{teardown-workspace-CTL16rnG.js.map → teardown-workspace-C-9murBw.js.map} +1 -1
  175. package/dist/dashboard/{test-agent-queue-CMgYZiR_.js → test-agent-queue-YCBRzaAB.js} +3 -3
  176. package/dist/dashboard/{test-agent-queue-CMgYZiR_.js.map → test-agent-queue-YCBRzaAB.js.map} +1 -1
  177. package/dist/dashboard/{uat-failure-feedback-DS6W9X3I.js → uat-failure-feedback-B-1mL8Zp.js} +3 -3
  178. package/dist/dashboard/{uat-failure-feedback-DS6W9X3I.js.map → uat-failure-feedback-B-1mL8Zp.js.map} +1 -1
  179. package/dist/dashboard/{uat-promote-verification-CRsIHE_q.js → uat-promote-verification-BEBPcpPg.js} +2 -2
  180. package/dist/dashboard/{uat-promote-verification-CRsIHE_q.js.map → uat-promote-verification-BEBPcpPg.js.map} +1 -1
  181. package/dist/dashboard/{uat-train-BoA9r6_I.js → uat-train-BFo3OQUB.js} +1 -1
  182. package/dist/dashboard/{uat-train-Cd8CJeNe.js → uat-train-COgoJomz.js} +8 -8
  183. package/dist/dashboard/{uat-train-Cd8CJeNe.js.map → uat-train-COgoJomz.js.map} +1 -1
  184. package/dist/dashboard/{validate-BdmvTIym.js → validate-069_N1e2.js} +2 -2
  185. package/dist/dashboard/{validate-BdmvTIym.js.map → validate-069_N1e2.js.map} +1 -1
  186. package/dist/dashboard/{verification-runner-BO0K3xNA.js → verification-runner-CQnZyC5A.js} +1 -1
  187. package/dist/dashboard/{verification-runner-fEY1Qj-l.js → verification-runner-fy3wNrWn.js} +4 -4
  188. package/dist/dashboard/{verification-runner-fEY1Qj-l.js.map → verification-runner-fy3wNrWn.js.map} +1 -1
  189. package/dist/dashboard/workflows-Bk6Kt0ZQ.js +2 -0
  190. package/dist/dashboard/{workflows-CxVnygYU.js → workflows-N4QMkDzN.js} +10 -10
  191. package/dist/dashboard/{workflows-CxVnygYU.js.map → workflows-N4QMkDzN.js.map} +1 -1
  192. package/dist/dashboard/{workspaces-Cpz0xJWQ.js → workspaces-DmoyfHoU.js} +33 -33
  193. package/dist/dashboard/{workspaces-Cpz0xJWQ.js.map → workspaces-DmoyfHoU.js.map} +1 -1
  194. package/dist/{lifecycle-C6k0HWEO.js → lifecycle-DCFcjg7y.js} +1 -1
  195. package/dist/{lifecycle-D5oRpvvB.js → lifecycle-GkaHQJNK.js} +2 -2
  196. package/dist/{lifecycle-D5oRpvvB.js.map → lifecycle-GkaHQJNK.js.map} +1 -1
  197. package/dist/{merge-agent-Drufc7tU.js → merge-agent-CogNFasY.js} +2 -2
  198. package/dist/{merge-agent-Drufc7tU.js.map → merge-agent-CogNFasY.js.map} +1 -1
  199. package/dist/{merge-agent-CBOVY_eC.js → merge-agent-eteKQ9Rb.js} +1 -1
  200. package/package.json +1 -1
  201. package/dist/build-info-BgrwLUy6.js +0 -2
  202. package/dist/dashboard/agent-state-Ci6PhjOE.js.map +0 -1
  203. package/dist/dashboard/build-info-CYlxvZSQ.js +0 -2
  204. package/dist/dashboard/cli-ZAS502uC.js +0 -2
  205. package/dist/dashboard/conversation-service-Dlks_hSS.js +0 -3
  206. package/dist/dashboard/inspect-agent-DTuSg95i.js +0 -2
  207. package/dist/dashboard/merge-ops-Bgc-EScO.js +0 -2
  208. package/dist/dashboard/workflows-CgGNyl1z.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"closed-issue-reaper-DrHyYgJR.js","names":["execAsync","promisify","exec","execAsync","existsSync","existsSync","readdirSync"],"sources":["../../src/lib/close-out.ts","../../src/lib/cloister/reap-issue-residue.ts","../../src/lib/cloister/closed-issue-reaper.ts"],"sourcesContent":["/**\n * Close-Out Ceremony — Human-gated verification and cleanup after merge.\n *\n * Verifies PRD is preserved, branch is merged, then cleans up agent state,\n * closes the issue on the tracker, and applies a `closed-out` label.\n */\n\nimport { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect, Data } from 'effect';\nimport {\n OVERDECK_HOME,\n AGENTS_DIR,\n PROJECT_DOCS_SUBDIR,\n PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR,\n} from './paths.js';\nimport { findPrdAtStatusSync, canonicalPrdSubdirSync } from './prd-locations.js';\nimport { killSession, sessionExistsSync, listSessionNames } from './tmux.js';\nimport { loadReviewStatuses } from './review-status.js';\nimport { getLinearApiKey } from './lifecycle/types.js';\nimport { POST_MERGE_RESIDUE_LABELS, WORKFLOW_LABELS } from './lifecycle/close-issue.js';\nimport { findWorkspacePath } from './lifecycle/archive-planning.js';\nimport { teardownWorkspaceDockerByNamePromise } from './workspace-manager/docker.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from './issue-id.js';\nimport { removeAgentStateDir } from './agents/state-dir-removal.js';\n\nconst execAsync = promisify(exec);\n\n// Planning/pipeline artifact paths ignored when deciding whether a branch\n// still holds unmerged CODE. `.pan/` is the current artifact home (PAN-967);\n// `.planning/` and `docs/prds/` are legacy; `.overdeck/` is workspace runtime.\nconst ARTIFACT_PATH_PREFIXES = ['.pan/', '.planning/', 'docs/prds/', '.overdeck/'];\nconst ARTIFACT_EXCLUSIONS = ARTIFACT_PATH_PREFIXES\n .map((prefix) => `':!${prefix.slice(0, -1)}'`)\n .join(' ');\n\n/**\n * Squash-merge detection via the forge. A squash-merged branch is never an\n * ancestor of main and its three-dot diff never empties (the pre-squash\n * commits stay visible against the merge-base), so pure-git checks report it\n * unmerged forever. The merged PR is the authoritative record: if a merged PR\n * has this branch as head, its merge commit landed on main, and the branch has\n * no post-merge code commits, the branch is merged. Returns false on any\n * failure (no gh, GitLab remote, no PR) so callers fall through conservatively.\n */\nasync function isSquashMergedViaPr(\n branchName: string,\n tipRef: string,\n projectPath: string,\n): Promise<boolean> {\n try {\n const { stdout } = await execAsync(\n `gh pr list --head \"${branchName}\" --state merged --json headRefOid,mergeCommit --limit 1`,\n { cwd: projectPath, encoding: 'utf-8', timeout: 15000 },\n );\n const prs = JSON.parse(stdout) as Array<{ headRefOid?: string; mergeCommit?: { oid?: string } }>;\n const pr = prs[0];\n if (!pr?.headRefOid || !pr.mergeCommit?.oid) return false;\n\n // The PR's merge commit must actually be on main.\n await execAsync(`git merge-base --is-ancestor ${pr.mergeCommit.oid} main`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n const { stdout: tipSha } = await execAsync(`git rev-parse \"${tipRef}\"`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (tipSha.trim() === pr.headRefOid) return true;\n\n // Commits after the merged PR head: merged only if they touch artifacts alone.\n const { stdout: filesOut } = await execAsync(\n `git log ${pr.headRefOid}..${tipRef} --name-only --pretty=format:`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const files = filesOut.split('\\n').map((line) => line.trim()).filter(Boolean);\n return files.every((file) =>\n ARTIFACT_PATH_PREFIXES.some((prefix) => file.startsWith(prefix)),\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a feature branch has been merged into main.\n *\n * Uses `git merge-base --is-ancestor` for regular merges, plus a\n * code-diff fallback to detect squash merges where the branch still exists.\n * Also checks review-status.json as authoritative — the merge specialist\n * validates the merge before setting mergeStatus to 'merged'.\n */\nexport async function isBranchMerged(\n branchName: string,\n projectPath: string,\n): Promise<{ status: 'merged' | 'unmerged' | 'no-branch'; message: string }> {\n // Check review-status as a hint — but NEVER trust it over the live git state.\n // An issue that merged once can be re-activated and accumulate new unmerged\n // commits; honoring a stale mergeStatus here would let close-out tear down a\n // workspace with un-landed work. Only honor it when the branch has nothing\n // beyond main; otherwise fall through to the robust git checks below (which\n // still recognise squash-merges via the code-diff check).\n try {\n const issueId = branchName.replace('feature/', '').toUpperCase();\n const statuses = loadReviewStatuses();\n if (statuses[issueId]?.mergeStatus === 'merged') {\n const { stdout: aheadOfMain } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!aheadOfMain.trim()) {\n return { status: 'merged', message: 'Merge specialist confirmed merge completed' };\n }\n }\n } catch {\n // review-status.json may not exist, continue with git checks\n }\n\n // Check if branch exists locally\n const { stdout: branchExists } = await execAsync(\n `git branch --list \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (branchExists.trim()) {\n // Use merge-base --is-ancestor: checks if the branch tip is reachable from main\n // This works for regular merges, squash merges, and cherry-picks\n try {\n await execAsync(\n `git merge-base --is-ancestor ${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'All commits merged to main' };\n } catch {\n // --is-ancestor fails for squash merges where the branch still exists.\n // Check if the code diff (excluding planning artifacts) is empty — if so,\n // the code was squash-merged and only planning files remain on the branch.\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...${branchName} -- ${ARTIFACT_EXCLUSIONS} 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through to unmerged report\n }\n\n // The three-dot diff can never go empty for a squash-merged branch unless\n // main was merged back into it afterwards — the pre-squash commits stay\n // visible relative to the merge-base forever. Ask the forge: a merged PR\n // whose head is this branch, with no post-merge code commits, IS merged.\n if (await isSquashMergedViaPr(branchName, branchName, projectPath)) {\n return { status: 'merged', message: `Merged PR found for ${branchName} (squash merge)` };\n }\n\n const { stdout: unmerged } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = unmerged.trim() ? unmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on ${branchName}. Merge before closing out.`,\n };\n }\n }\n\n // Check remote\n const { stdout: remoteBranch } = await execAsync(\n `git ls-remote --heads origin \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (remoteBranch.trim()) {\n await execAsync(`git fetch origin ${branchName}`, { cwd: projectPath }).catch(() => {});\n try {\n await execAsync(\n `git merge-base --is-ancestor origin/${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'Remote branch fully merged' };\n } catch {\n // Squash-merge detection for remote branch\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...origin/${branchName} -- ${ARTIFACT_EXCLUSIONS} 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Remote code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through\n }\n\n if (await isSquashMergedViaPr(branchName, `origin/${branchName}`, projectPath)) {\n return { status: 'merged', message: `Merged PR found for ${branchName} (squash merge)` };\n }\n\n const { stdout: remoteUnmerged } = await execAsync(\n `git log main..origin/${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = remoteUnmerged.trim() ? remoteUnmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on remote ${branchName}.`,\n };\n }\n }\n\n // No branch at all — assume squash-merged and branch deleted\n return { status: 'no-branch', message: 'Branch already cleaned up (squash-merged)' };\n}\n\n\nexport interface CloseOutStep {\n name: string;\n status: 'passed' | 'failed' | 'skipped';\n message?: string;\n}\n\nexport interface CloseOutResult {\n success: boolean;\n issueId: string;\n steps: CloseOutStep[];\n error?: string;\n}\n\nexport interface CloseOutContext {\n issueId: string;\n projectPath: string;\n isGitHub: boolean;\n owner?: string;\n repo?: string;\n number?: number;\n}\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';async function executeCloseOutPromise(ctx: CloseOutContext): Promise<CloseOutResult> {\n const steps: CloseOutStep[] = [];\n const issueLower = ctx.issueId.toLowerCase();\n\n // Step 1: Verify PRD preserved.\n // Tolerates all four legacy formats (subdir/flat × lowercase/uppercase) via findPrdAtStatus.\n try {\n if (findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'completed')) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'PRD in completed/' });\n } else {\n const source = findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'active');\n if (!source) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: 'No PRD found (may not have had one)' });\n } else {\n const completedSubdir = canonicalPrdSubdirSync(ctx.projectPath, ctx.issueId, 'completed');\n const completedFlat = join(\n ctx.projectPath, PROJECT_DOCS_SUBDIR, PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR, `${issueLower}-plan.md`,\n );\n const dest = source.format === 'subdir' ? completedSubdir : completedFlat;\n const destParent = dirname(dest);\n if (!existsSync(destParent)) {\n mkdirSync(destParent, { recursive: true });\n }\n try {\n await execAsync(`git mv \"${source.path}\" \"${dest}\"`, { cwd: ctx.projectPath });\n await execAsync(`git commit -m \"Move ${ctx.issueId} PRD to completed (close-out)\"`, { cwd: ctx.projectPath });\n await execAsync(`git push`, { cwd: ctx.projectPath });\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Moved PRD from active/ to completed/' });\n } catch {\n try {\n cpSync(source.path, dest, { recursive: true });\n if (existsSync(dest)) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Copied PRD to completed/ (git mv failed, plain copy succeeded)' });\n } else {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: 'PRD copy appeared to succeed but file not found at destination' });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — file not at destination after copy' };\n }\n } catch (cpErr) {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: `Failed to copy PRD: ${(cpErr as Error).message}` });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — both git mv and copy failed' };\n }\n }\n }\n }\n } catch (err) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 2: Verify branch merged (hard fail)\n try {\n const branchName = `feature/${issueLower}`;\n const merged = await isBranchMerged(branchName, ctx.projectPath);\n\n if (merged.status === 'merged') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else if (merged.status === 'no-branch') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: merged.message,\n });\n return { success: false, issueId: ctx.issueId, steps, error: merged.message };\n }\n } catch (err) {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: `Could not verify merge: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: 'Could not verify branch merge status' };\n }\n\n // Step 2.5: Refuse close-out while the work agent session is still alive.\n // postMergeLifecycle pauses the work agent at merge, so a live agent-<issue>\n // session means the issue was re-activated and is doing in-progress work that\n // tearing down the workspace would destroy (the verifying_on_main tag can be\n // stale — it is set at first merge and not cleared when work resumes).\n const liveAgentSession = `agent-${issueLower}`;\n if (sessionExistsSync(liveAgentSession)) {\n const activeMsg = `${liveAgentSession} is still running — pause/stop it before close-out (it was re-activated after merge; closing now would kill in-progress work).`;\n steps.push({ name: 'No active work agent', status: 'failed', message: activeMsg });\n return { success: false, issueId: ctx.issueId, steps, error: activeMsg };\n }\n\n // Step 3: Clean up workspace\n try {\n const workspacePath = findWorkspacePath(ctx.projectPath, issueLower);\n const agentSession = `agent-${issueLower}`;\n let cleaned = false;\n\n // Kill tmux sessions for this issue\n const exactPatterns = [agentSession, `test-${issueLower}`, `merge-${issueLower}`];\n for (const session of exactPatterns) {\n if (sessionExistsSync(session)) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n }\n\n // Review sessions use timestamped names: review-<issue>-<timestamp>-<role>\n try {\n const allSessions = await Effect.runPromise(listSessionNames());\n const reviewRegex = new RegExp(`^review-${issueLower.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}-\\\\d+`);\n const reviewSessions = allSessions.filter(s => reviewRegex.test(s));\n for (const session of reviewSessions) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n } catch { /* tmux server may not be running */ }\n\n // Stop Docker containers (name-based, works even if workspace dir is gone)\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n cleaned = true;\n steps.push({\n name: 'Docker stack removed',\n status: 'passed',\n message: `Stopped and removed Docker stack for ${issueLower}`,\n });\n } else {\n const networksPhrase = teardownResult.remainingNetworks?.length\n ? teardownResult.remainingNetworks.join(', ')\n : 'one or more workspace devnets';\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker network ${networksPhrase} still present; the closed-issue reaper will retry`,\n });\n }\n } catch (err) {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker teardown failed: ${(err as Error).message}; the closed-issue reaper will retry`,\n });\n }\n\n // Remove git worktree\n if (workspacePath && existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch {\n // Try direct removal if worktree remove fails\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n cleaned = true;\n } catch { /* Already gone */ }\n }\n }\n\n // Delete local and remote feature branches (safe — we verified merge in step 2)\n const branchName = `feature/${issueLower}`;\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist locally */ }\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist on remote */ }\n\n steps.push({\n name: 'Clean up workspace',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Workspace cleaned up' : 'No workspace to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up workspace', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 4: Prune checkpoint refs\n // Primary: delete all refs/pan/turn/<agentId>/* for this issue's agents.\n // Global: also sweep any checkpoint refs older than 30 days (safety net for\n // abandoned workspaces whose postMergeLifecycle was never called or skipped).\n try {\n const { pruneCheckpointRefsForAgents, pruneStaleCheckpointRefs } = await import('./checkpoint/checkpoint-manager.js');\n const issueLower = ctx.issueId.toLowerCase();\n const agentIds = [`agent-${issueLower}`, `planning-${issueLower}`];\n await Effect.runPromise(pruneCheckpointRefsForAgents(ctx.projectPath, agentIds));\n const stalePruned = await Effect.runPromise(pruneStaleCheckpointRefs(ctx.projectPath, 30));\n steps.push({\n name: 'Prune checkpoint refs',\n status: 'passed',\n message: stalePruned > 0\n ? `Issue refs removed; ${stalePruned} stale ref(s) >30 days also pruned`\n : 'Issue checkpoint refs removed',\n });\n } catch (err) {\n steps.push({ name: 'Prune checkpoint refs', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 5: Clean up agent state\n try {\n let cleaned = false;\n let preservedTranscripts = 0;\n const agentDir = join(AGENTS_DIR, `agent-${issueLower}`);\n const planningDir = join(AGENTS_DIR, `planning-${issueLower}`);\n\n if (existsSync(agentDir)) {\n const result = await removeAgentStateDir(agentDir);\n preservedTranscripts += result.preservedTranscripts;\n cleaned = true;\n }\n if (existsSync(planningDir)) {\n const result = await removeAgentStateDir(planningDir);\n preservedTranscripts += result.preservedTranscripts;\n cleaned = true;\n }\n\n const transcriptLabel = `transcript file${preservedTranscripts === 1 ? '' : 's'} preserved`;\n steps.push({\n name: 'Clean up agent state',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned\n ? `Agent state directories cleaned (${preservedTranscripts} ${transcriptLabel})`\n : 'No agent state to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up agent state', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 6: Close issue on tracker (hard fail)\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n await execAsync(\n `gh issue close ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --comment \"Closed via close-out ceremony\"`,\n { encoding: 'utf-8' }\n );\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `GitHub issue #${ctx.number} closed` });\n } else {\n // Linear issue\n const linearApiKey = await getLinearApiKey();\n if (!linearApiKey) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: 'LINEAR_API_KEY not configured' });\n return { success: false, issueId: ctx.issueId, steps, error: 'LINEAR_API_KEY not configured' };\n }\n\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n\n // Find the issue by identifier using issues filter (searchIssues returns\n // IssueSearchResult which lacks .update(); client.issue() needs the UUID)\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Could not parse issue ID: ${ctx.issueId}` });\n return { success: false, issueId: ctx.issueId, steps, error: `Could not parse issue ID: ${ctx.issueId}` };\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Issue ${ctx.issueId} not found in Linear` });\n return { success: false, issueId: ctx.issueId, steps, error: `Issue ${ctx.issueId} not found in Linear` };\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `Linear issue ${ctx.issueId} moved to Done` });\n }\n } catch (err) {\n steps.push({\n name: 'Close issue on tracker',\n status: 'failed',\n message: `Failed to close: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: `Failed to close issue: ${(err as Error).message}` };\n }\n\n // Step 7: Apply closed-out label\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n // Ensure the label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${ctx.owner}/${ctx.repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n // Add the label\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --add-label \"${CLOSED_OUT_LABEL}\"`,\n { encoding: 'utf-8' }\n );\n // Remove all current-phase workflow labels (single source: WORKFLOW_LABELS)\n for (const label of WORKFLOW_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n for (const label of POST_MERGE_RESIDUE_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: `Added '${CLOSED_OUT_LABEL}' label` });\n } else {\n // Linear: add label if possible\n try {\n const linearApiKey = await getLinearApiKey();\n if (linearApiKey) {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum !== null && teamKey !== null) {\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length > 0) {\n const issue = results.nodes[0];\n // Find or create the closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n if (labelId) {\n const existingLabels = await issue.labels();\n const residueLabelNames = new Set(POST_MERGE_RESIDUE_LABELS);\n const labelIds = existingLabels.nodes\n .filter(l => !residueLabelNames.has(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n await issue.update({ labelIds });\n }\n }\n }\n }\n }\n } catch { /* Non-fatal for Linear */ }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: 'Label applied' });\n }\n } catch (err) {\n steps.push({ name: 'Apply closed-out label', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 8: Mark the durable pipeline journal terminal before clearing the DB cache.\n try {\n const { markRecordPipelineClosedOutSync } = await import('./pan-dir/records.js');\n markRecordPipelineClosedOutSync({ name: 'inferred', path: ctx.projectPath }, ctx.issueId.toUpperCase());\n steps.push({ name: 'Mark pipeline terminal', status: 'passed', message: 'Pipeline journal marked closed-out' });\n } catch (err) {\n steps.push({ name: 'Mark pipeline terminal', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 9: Clear review status\n try {\n // Dynamically import to avoid circular dependency with server\n const { clearReviewStatus } = await import('./review-status.js');\n clearReviewStatus(ctx.issueId.toUpperCase());\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared' });\n } catch {\n // review-status module may not be available in CLI context\n // Try cleaning the file directly\n try {\n const statusFile = join(OVERDECK_HOME, 'review-status.json');\n if (existsSync(statusFile)) {\n const data = JSON.parse(readFileSync(statusFile, 'utf-8'));\n const upperKey = ctx.issueId.toUpperCase();\n if (data[upperKey]) {\n delete data[upperKey];\n const { writeFileSync } = await import('fs');\n writeFileSync(statusFile, JSON.stringify(data, null, 2));\n }\n }\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared (direct)' });\n } catch (innerErr) {\n steps.push({ name: 'Clear review status', status: 'skipped', message: `Warning: ${(innerErr as Error).message}` });\n }\n }\n\n return { success: true, issueId: ctx.issueId, steps };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** A close-out ceremony step failed unexpectedly outside its recovery branches. */\nexport class CloseOutError extends Data.TaggedError('CloseOutError')<{\n readonly issueId: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect-native executeCloseOut. The underlying ceremony already aggregates\n * per-step results into the returned payload (success boolean + steps). The\n * Effect variant only fails with CloseOutError if the call itself throws\n * (e.g. dynamic import of @linear/sdk explodes). Step-level failures stay\n * in the returned `success/steps` shape so callers can display them.\n */\nexport const executeCloseOut = (\n ctx: CloseOutContext,\n): Effect.Effect<CloseOutResult, CloseOutError> =>\n Effect.tryPromise({\n try: () => executeCloseOutPromise(ctx),\n catch: (cause) =>\n new CloseOutError({\n issueId: ctx.issueId,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","import { existsSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nimport { isBranchMerged } from '../close-out.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { killSession, listSessionNames } from '../tmux.js';\nimport { teardownWorkspaceDockerByNamePromise } from '../workspace-manager/docker.js';\nimport { removeAgentStateDir } from '../agents/state-dir-removal.js';\n\nconst execAsync = promisify(exec);\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport async function reapIssueResidue(projectPath: string, issueId: string): Promise<string[]> {\n const actions: string[] = [];\n const issueLower = issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n const sessions = await Effect.runPromise(listSessionNames());\n const exactSessions = new Set([\n `agent-${issueLower}`,\n `test-${issueLower}`,\n `merge-${issueLower}`,\n ]);\n const reviewRegex = new RegExp(`^review-${escapeRegex(issueLower)}-\\\\d+`);\n for (const session of sessions) {\n if (!exactSessions.has(session) && !reviewRegex.test(session)) continue;\n try {\n await Effect.runPromise(killSession(session));\n actions.push(`killed tmux session ${session}`);\n } catch {\n // Session may already be gone.\n }\n }\n } catch {\n // tmux server may not be running.\n }\n\n // Remove Docker stack by name, independent of whether the workspace dir still\n // exists — and independent of the merged check below. Containers and networks\n // are disposable runtime state (rebuildWorkspaceStack no-ops for terminal\n // issues), so tearing them down for a closed issue destroys no work, while\n // leaked `_devnet` networks eventually exhaust Docker's address pools.\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n actions.push(`removed Docker stack for feature-${issueLower}`);\n } else {\n actions.push(`Docker network for feature-${issueLower} still present`);\n }\n } catch (err) {\n actions.push(`Docker teardown failed for ${issueId}: ${(err as Error).message}`);\n }\n\n // Destructive disk/branch cleanup stays gated on the merged check: a closed\n // but genuinely unmerged branch may hold work worth recovering.\n const merged = await isBranchMerged(branchName, projectPath);\n if (merged.status === 'unmerged') {\n actions.push(`skipped disk reap for ${issueId} — branch unmerged`);\n return actions;\n }\n\n const workspacePath = join(projectPath, 'workspaces', `feature-${issueLower}`);\n if (existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: projectPath });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n }\n\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted local branch ${branchName}`);\n } catch {\n // Branch may not exist locally.\n }\n\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted remote branch ${branchName}`);\n } catch {\n // Branch may not exist remotely.\n }\n\n for (const agentDirName of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n const agentDir = join(AGENTS_DIR, agentDirName);\n if (!existsSync(agentDir)) continue;\n try {\n const result = await removeAgentStateDir(agentDir);\n const transcriptLabel = `transcript file${result.preservedTranscripts === 1 ? '' : 's'} preserved`;\n actions.push(`cleaned agent state ${agentDirName} (${result.preservedTranscripts} ${transcriptLabel})`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n\n return actions;\n}\n","import { Effect } from 'effect';\nimport { exec } from 'node:child_process';\nimport { existsSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { listRunningAgents, stopAgent } from '../agents.js';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { listProjectsSync } from '../projects.js';\nimport { readJournalStatus } from '../overdeck/review-status-record-sync.js';\nimport { resolveProjectForIssue } from '../pan-dir/record.js';\nimport { loadReviewStatuses, setReviewStatusSync } from '../review-status.js';\nimport type { ReviewStatus } from '../review-status-reconcile.js';\nimport { listSessionNames } from '../tmux.js';\nimport { isIssueClosed, isTrackerIssueClosed } from './issue-closed.js';\nimport { reapIssueResidue } from './reap-issue-residue.js';\n\n// Sessions reaped by NAME as a backstop: inspect sessions never have agent\n// state, and strike sessions can outlive their state entry (e.g. state already\n// stopped or removed while the tmux session idles — PAN-1721).\nfunction issueIdFromStatelessSession(sessionName: string): string | null {\n const match = sessionName.match(/^(?:inspect|strike)-([a-z0-9]+-\\d+)(?:-|$)/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromFeatureWorkspace(entryName: string): string | null {\n const match = entryName.match(/^feature-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromAgentDir(entryName: string): string | null {\n const match = entryName.match(/^agent-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nconst execAsync = promisify(exec);\nconst DEVNET_CLOSURE_CHECK_CONCURRENCY = 4;\nexport const REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY = 4;\n\n// Leaked `_devnet` networks are a residue source of their own: a closed issue\n// whose sessions, workspace, and agent dirs are already gone can still hold a\n// bridge network, and Docker's default address pools support only ~31 of them.\nasync function listFeatureDevnetIssueIds(): Promise<string[] | null> {\n try {\n const { stdout } = await execAsync(`docker network ls --format '{{.Name}}'`, {\n encoding: 'utf-8',\n timeout: 30000,\n });\n const issueIds = new Set<string>();\n for (const name of stdout.trim().split('\\n')) {\n const match = name.match(/-feature-([a-z]+-\\d+)_devnet$/i);\n if (match) issueIds.add(match[1].toUpperCase());\n }\n return [...issueIds];\n } catch {\n return null;\n }\n}\n\nasync function isClosedIssue(\n issueId: string,\n closedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = closedChecks.get(issueId);\n if (!promise) {\n promise = isIssueClosed(issueId);\n closedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nasync function isTrackerClosedIssue(\n issueId: string,\n trackerClosedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = trackerClosedChecks.get(issueId);\n if (!promise) {\n promise = isTrackerIssueClosed(issueId);\n trackerClosedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nfunction hasUnservicedReviewRequest(status: ReviewStatus): boolean {\n if (status.reviewStatus !== 'pending' || !status.reviewRequestedAt) return false;\n return !status.reviewSpawnedAt ||\n new Date(status.reviewRequestedAt).getTime() > new Date(status.reviewSpawnedAt).getTime();\n}\n\nexport async function reapClosedIssueReviewRequests(\n trackerClosedChecks: Map<string, Promise<boolean>>,\n): Promise<string[]> {\n const actions: string[] = [];\n const pendingStatuses = Object.entries(loadReviewStatuses())\n .filter(([, status]) => status.reviewStatus === 'pending');\n\n for (let offset = 0; offset < pendingStatuses.length; offset += REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY) {\n const batch = pendingStatuses.slice(offset, offset + REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY);\n const candidates = (await Promise.all(batch.map(async ([issueId, dbStatus]) => {\n const journal = await readJournalStatus(issueId);\n const existing = {\n ...dbStatus,\n ...(journal?.durable ?? {}),\n issueId,\n } as ReviewStatus;\n for (const field of journal?.clearedFields ?? []) {\n delete (existing as unknown as Record<string, unknown>)[field];\n }\n return hasUnservicedReviewRequest(existing) ? [issueId, existing] as const : null;\n }))).filter((candidate): candidate is readonly [string, ReviewStatus] => candidate !== null);\n\n const trackerClosedCandidates = (await Promise.all(candidates.map(async ([issueId, existing]) => (\n await isTrackerClosedIssue(issueId, trackerClosedChecks) ? [issueId, existing] as const : null\n )))).filter((candidate): candidate is readonly [string, ReviewStatus] => candidate !== null);\n\n for (const [issueId, existing] of trackerClosedCandidates) {\n setReviewStatusSync(issueId, {\n reviewRequestedAt: undefined,\n reviewSpawnedAt: undefined,\n }, existing);\n const action = `Cleared unserviced review request for ${issueId} — parent issue is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] cleared unserviced review request for ${issueId} — parent issue is closed`,\n });\n }\n }\n\n return actions;\n}\n\nasync function reapClosedIssueResidue(\n projectPath: string,\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const key = `${projectPath}:${issueId}`;\n if (reapedIssueKeys.has(key)) return;\n reapedIssueKeys.add(key);\n actions.push(...await reapIssueResidue(projectPath, issueId));\n}\n\nasync function reapResolvedIssueResidue(\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const project = resolveProjectForIssue(issueId);\n if (!project) return;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n}\n\nfunction listConfiguredProjects(): Array<{ path: string }> {\n try {\n return listProjectsSync().map(({ config }) => ({ path: config.path }));\n } catch {\n return [];\n }\n}\n\nfunction listDirectoryNames(path: string): string[] {\n if (!existsSync(path)) return [];\n try {\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n}\n\nasync function stopClosedAgent(agentId: string, issueId: string, actions: string[]): Promise<void> {\n await Effect.runPromise(stopAgent(agentId));\n const action = `Reaped ${agentId} — parent issue ${issueId} is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] reaped ${agentId} — parent issue ${issueId} is closed`,\n });\n}\n\n/**\n * PAN-1908: reactive closed-issue reaper. When an issue is closed, stop all\n * running agents and stateless sessions belonging to it without scanning the\n * full agent table or tmux session list.\n */\nexport async function handleIssueStatusChangedClosed(issueId: string): Promise<string[]> {\n const upperIssueId = issueId.trim().toUpperCase();\n if (!upperIssueId) return [];\n\n // Guard against spurious events: only act when the issue is actually closed.\n if (!await isIssueClosed(upperIssueId)) return [];\n\n const actions: string[] = [];\n const reapedAgentIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n\n // Stop running agents for this issue.\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const agentIssueId = (agent.issueId ?? '').trim().toUpperCase();\n if (agentIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(agent.id, upperIssueId, actions);\n reapedAgentIds.add(agent.id);\n }\n\n // Stop stateless inspect/strike sessions for this issue.\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n const sessionIssueId = issueIdFromStatelessSession(sessionName);\n if (sessionIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(sessionName, upperIssueId, actions);\n reapedAgentIds.add(sessionName);\n }\n\n await reapResolvedIssueResidue(upperIssueId, actions, reapedIssueKeys);\n return actions;\n}\n\n/**\n * PAN-1908: keep a thin table+session safety net for dropped closed-issue\n * events. The primary path is reactive via handleIssueStatusChangedClosed.\n */\nexport async function reconcileClosedIssueAgents(): Promise<string[]> {\n const actions: string[] = [];\n const closedChecks = new Map<string, Promise<boolean>>();\n const trackerClosedChecks = new Map<string, Promise<boolean>>();\n const reapedAgentIds = new Set<string>();\n const closedIssueIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n const agents = await Effect.runPromise(listRunningAgents());\n\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(agent.id, issueId, actions);\n reapedAgentIds.add(agent.id);\n closedIssueIds.add(issueId);\n }\n\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n\n const issueId = issueIdFromStatelessSession(sessionName);\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(sessionName, issueId, actions);\n reapedAgentIds.add(sessionName);\n closedIssueIds.add(issueId);\n }\n\n for (const issueId of closedIssueIds) {\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n for (const project of listConfiguredProjects()) {\n const workspacesDir = join(project.path, 'workspaces');\n for (const entryName of listDirectoryNames(workspacesDir)) {\n const issueId = issueIdFromFeatureWorkspace(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n }\n }\n\n for (const entryName of listDirectoryNames(AGENTS_DIR)) {\n const issueId = issueIdFromAgentDir(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n const devnetIssueIds = await listFeatureDevnetIssueIds();\n const openDevnetIssueIds: string[] = [];\n if (devnetIssueIds) {\n for (let offset = 0; offset < devnetIssueIds.length; offset += DEVNET_CLOSURE_CHECK_CONCURRENCY) {\n const batch = devnetIssueIds.slice(offset, offset + DEVNET_CLOSURE_CHECK_CONCURRENCY);\n const closureResults = await Promise.all(batch.map(async (issueId) => ({\n issueId,\n closed: await isClosedIssue(issueId, closedChecks),\n })));\n for (const { issueId, closed } of closureResults) {\n if (closed) await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n else openDevnetIssueIds.push(issueId);\n }\n }\n }\n\n let mergedIssueIds: string[] | null = devnetIssueIds ? [] : null;\n if (openDevnetIssueIds.length > 0) {\n try {\n const { getReviewStatusesSync } = await import('../review-status.js');\n const statuses = getReviewStatusesSync(openDevnetIssueIds);\n mergedIssueIds = openDevnetIssueIds.filter(\n (issueId) => statuses[issueId]?.mergeStatus === 'merged',\n );\n } catch (error) {\n mergedIssueIds = null;\n actions.push(`Failed to resolve merged-issue Docker cleanup status: ${error}`);\n }\n }\n if (mergedIssueIds) {\n try {\n const { reconcileMergedDockerCleanupQueue } = await import('./merged-docker-cleanup-worker.js');\n actions.push(...reconcileMergedDockerCleanupQueue(mergedIssueIds));\n } catch (error) {\n actions.push(`Failed to reconcile merged-issue Docker cleanup queue: ${error}`);\n }\n }\n\n actions.push(...await reapClosedIssueReviewRequests(trackerClosedChecks));\n return actions;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"closed-issue-reaper-COxfTtLz.js","names":["execAsync","promisify","exec","execAsync","existsSync","existsSync","readdirSync"],"sources":["../../src/lib/close-out.ts","../../src/lib/cloister/reap-issue-residue.ts","../../src/lib/cloister/closed-issue-reaper.ts"],"sourcesContent":["/**\n * Close-Out Ceremony — Human-gated verification and cleanup after merge.\n *\n * Verifies PRD is preserved, branch is merged, then cleans up agent state,\n * closes the issue on the tracker, and applies a `closed-out` label.\n */\n\nimport { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect, Data } from 'effect';\nimport {\n OVERDECK_HOME,\n AGENTS_DIR,\n PROJECT_DOCS_SUBDIR,\n PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR,\n} from './paths.js';\nimport { findPrdAtStatusSync, canonicalPrdSubdirSync } from './prd-locations.js';\nimport { killSession, sessionExistsSync, listSessionNames } from './tmux.js';\nimport { loadReviewStatuses } from './review-status.js';\nimport { getLinearApiKey } from './lifecycle/types.js';\nimport { POST_MERGE_RESIDUE_LABELS, WORKFLOW_LABELS } from './lifecycle/close-issue.js';\nimport { findWorkspacePath } from './lifecycle/archive-planning.js';\nimport { teardownWorkspaceDockerByNamePromise } from './workspace-manager/docker.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from './issue-id.js';\nimport { removeAgentStateDir } from './agents/state-dir-removal.js';\n\nconst execAsync = promisify(exec);\n\n// Planning/pipeline artifact paths ignored when deciding whether a branch\n// still holds unmerged CODE. `.pan/` is the current artifact home (PAN-967);\n// `.planning/` and `docs/prds/` are legacy; `.overdeck/` is workspace runtime.\nconst ARTIFACT_PATH_PREFIXES = ['.pan/', '.planning/', 'docs/prds/', '.overdeck/'];\nconst ARTIFACT_EXCLUSIONS = ARTIFACT_PATH_PREFIXES\n .map((prefix) => `':!${prefix.slice(0, -1)}'`)\n .join(' ');\n\n/**\n * Squash-merge detection via the forge. A squash-merged branch is never an\n * ancestor of main and its three-dot diff never empties (the pre-squash\n * commits stay visible against the merge-base), so pure-git checks report it\n * unmerged forever. The merged PR is the authoritative record: if a merged PR\n * has this branch as head, its merge commit landed on main, and the branch has\n * no post-merge code commits, the branch is merged. Returns false on any\n * failure (no gh, GitLab remote, no PR) so callers fall through conservatively.\n */\nasync function isSquashMergedViaPr(\n branchName: string,\n tipRef: string,\n projectPath: string,\n): Promise<boolean> {\n try {\n const { stdout } = await execAsync(\n `gh pr list --head \"${branchName}\" --state merged --json headRefOid,mergeCommit --limit 1`,\n { cwd: projectPath, encoding: 'utf-8', timeout: 15000 },\n );\n const prs = JSON.parse(stdout) as Array<{ headRefOid?: string; mergeCommit?: { oid?: string } }>;\n const pr = prs[0];\n if (!pr?.headRefOid || !pr.mergeCommit?.oid) return false;\n\n // The PR's merge commit must actually be on main.\n await execAsync(`git merge-base --is-ancestor ${pr.mergeCommit.oid} main`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n const { stdout: tipSha } = await execAsync(`git rev-parse \"${tipRef}\"`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (tipSha.trim() === pr.headRefOid) return true;\n\n // Commits after the merged PR head: merged only if they touch artifacts alone.\n const { stdout: filesOut } = await execAsync(\n `git log ${pr.headRefOid}..${tipRef} --name-only --pretty=format:`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const files = filesOut.split('\\n').map((line) => line.trim()).filter(Boolean);\n return files.every((file) =>\n ARTIFACT_PATH_PREFIXES.some((prefix) => file.startsWith(prefix)),\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a feature branch has been merged into main.\n *\n * Uses `git merge-base --is-ancestor` for regular merges, plus a\n * code-diff fallback to detect squash merges where the branch still exists.\n * Also checks review-status.json as authoritative — the merge specialist\n * validates the merge before setting mergeStatus to 'merged'.\n */\nexport async function isBranchMerged(\n branchName: string,\n projectPath: string,\n): Promise<{ status: 'merged' | 'unmerged' | 'no-branch'; message: string }> {\n // Check review-status as a hint — but NEVER trust it over the live git state.\n // An issue that merged once can be re-activated and accumulate new unmerged\n // commits; honoring a stale mergeStatus here would let close-out tear down a\n // workspace with un-landed work. Only honor it when the branch has nothing\n // beyond main; otherwise fall through to the robust git checks below (which\n // still recognise squash-merges via the code-diff check).\n try {\n const issueId = branchName.replace('feature/', '').toUpperCase();\n const statuses = loadReviewStatuses();\n if (statuses[issueId]?.mergeStatus === 'merged') {\n const { stdout: aheadOfMain } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!aheadOfMain.trim()) {\n return { status: 'merged', message: 'Merge specialist confirmed merge completed' };\n }\n }\n } catch {\n // review-status.json may not exist, continue with git checks\n }\n\n // Check if branch exists locally\n const { stdout: branchExists } = await execAsync(\n `git branch --list \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (branchExists.trim()) {\n // Use merge-base --is-ancestor: checks if the branch tip is reachable from main\n // This works for regular merges, squash merges, and cherry-picks\n try {\n await execAsync(\n `git merge-base --is-ancestor ${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'All commits merged to main' };\n } catch {\n // --is-ancestor fails for squash merges where the branch still exists.\n // Check if the code diff (excluding planning artifacts) is empty — if so,\n // the code was squash-merged and only planning files remain on the branch.\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...${branchName} -- ${ARTIFACT_EXCLUSIONS} 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through to unmerged report\n }\n\n // The three-dot diff can never go empty for a squash-merged branch unless\n // main was merged back into it afterwards — the pre-squash commits stay\n // visible relative to the merge-base forever. Ask the forge: a merged PR\n // whose head is this branch, with no post-merge code commits, IS merged.\n if (await isSquashMergedViaPr(branchName, branchName, projectPath)) {\n return { status: 'merged', message: `Merged PR found for ${branchName} (squash merge)` };\n }\n\n const { stdout: unmerged } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = unmerged.trim() ? unmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on ${branchName}. Merge before closing out.`,\n };\n }\n }\n\n // Check remote\n const { stdout: remoteBranch } = await execAsync(\n `git ls-remote --heads origin \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (remoteBranch.trim()) {\n await execAsync(`git fetch origin ${branchName}`, { cwd: projectPath }).catch(() => {});\n try {\n await execAsync(\n `git merge-base --is-ancestor origin/${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'Remote branch fully merged' };\n } catch {\n // Squash-merge detection for remote branch\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...origin/${branchName} -- ${ARTIFACT_EXCLUSIONS} 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Remote code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through\n }\n\n if (await isSquashMergedViaPr(branchName, `origin/${branchName}`, projectPath)) {\n return { status: 'merged', message: `Merged PR found for ${branchName} (squash merge)` };\n }\n\n const { stdout: remoteUnmerged } = await execAsync(\n `git log main..origin/${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = remoteUnmerged.trim() ? remoteUnmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on remote ${branchName}.`,\n };\n }\n }\n\n // No branch at all — assume squash-merged and branch deleted\n return { status: 'no-branch', message: 'Branch already cleaned up (squash-merged)' };\n}\n\n\nexport interface CloseOutStep {\n name: string;\n status: 'passed' | 'failed' | 'skipped';\n message?: string;\n}\n\nexport interface CloseOutResult {\n success: boolean;\n issueId: string;\n steps: CloseOutStep[];\n error?: string;\n}\n\nexport interface CloseOutContext {\n issueId: string;\n projectPath: string;\n isGitHub: boolean;\n owner?: string;\n repo?: string;\n number?: number;\n}\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';async function executeCloseOutPromise(ctx: CloseOutContext): Promise<CloseOutResult> {\n const steps: CloseOutStep[] = [];\n const issueLower = ctx.issueId.toLowerCase();\n\n // Step 1: Verify PRD preserved.\n // Tolerates all four legacy formats (subdir/flat × lowercase/uppercase) via findPrdAtStatus.\n try {\n if (findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'completed')) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'PRD in completed/' });\n } else {\n const source = findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'active');\n if (!source) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: 'No PRD found (may not have had one)' });\n } else {\n const completedSubdir = canonicalPrdSubdirSync(ctx.projectPath, ctx.issueId, 'completed');\n const completedFlat = join(\n ctx.projectPath, PROJECT_DOCS_SUBDIR, PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR, `${issueLower}-plan.md`,\n );\n const dest = source.format === 'subdir' ? completedSubdir : completedFlat;\n const destParent = dirname(dest);\n if (!existsSync(destParent)) {\n mkdirSync(destParent, { recursive: true });\n }\n try {\n await execAsync(`git mv \"${source.path}\" \"${dest}\"`, { cwd: ctx.projectPath });\n await execAsync(`git commit -m \"Move ${ctx.issueId} PRD to completed (close-out)\"`, { cwd: ctx.projectPath });\n await execAsync(`git push`, { cwd: ctx.projectPath });\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Moved PRD from active/ to completed/' });\n } catch {\n try {\n cpSync(source.path, dest, { recursive: true });\n if (existsSync(dest)) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Copied PRD to completed/ (git mv failed, plain copy succeeded)' });\n } else {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: 'PRD copy appeared to succeed but file not found at destination' });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — file not at destination after copy' };\n }\n } catch (cpErr) {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: `Failed to copy PRD: ${(cpErr as Error).message}` });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — both git mv and copy failed' };\n }\n }\n }\n }\n } catch (err) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 2: Verify branch merged (hard fail)\n try {\n const branchName = `feature/${issueLower}`;\n const merged = await isBranchMerged(branchName, ctx.projectPath);\n\n if (merged.status === 'merged') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else if (merged.status === 'no-branch') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: merged.message,\n });\n return { success: false, issueId: ctx.issueId, steps, error: merged.message };\n }\n } catch (err) {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: `Could not verify merge: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: 'Could not verify branch merge status' };\n }\n\n // Step 2.5: Refuse close-out while the work agent session is still alive.\n // postMergeLifecycle pauses the work agent at merge, so a live agent-<issue>\n // session means the issue was re-activated and is doing in-progress work that\n // tearing down the workspace would destroy (the verifying_on_main tag can be\n // stale — it is set at first merge and not cleared when work resumes).\n const liveAgentSession = `agent-${issueLower}`;\n if (sessionExistsSync(liveAgentSession)) {\n const activeMsg = `${liveAgentSession} is still running — pause/stop it before close-out (it was re-activated after merge; closing now would kill in-progress work).`;\n steps.push({ name: 'No active work agent', status: 'failed', message: activeMsg });\n return { success: false, issueId: ctx.issueId, steps, error: activeMsg };\n }\n\n // Step 3: Clean up workspace\n try {\n const workspacePath = findWorkspacePath(ctx.projectPath, issueLower);\n const agentSession = `agent-${issueLower}`;\n let cleaned = false;\n\n // Kill tmux sessions for this issue\n const exactPatterns = [agentSession, `test-${issueLower}`, `merge-${issueLower}`];\n for (const session of exactPatterns) {\n if (sessionExistsSync(session)) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n }\n\n // Review sessions use timestamped names: review-<issue>-<timestamp>-<role>\n try {\n const allSessions = await Effect.runPromise(listSessionNames());\n const reviewRegex = new RegExp(`^review-${issueLower.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}-\\\\d+`);\n const reviewSessions = allSessions.filter(s => reviewRegex.test(s));\n for (const session of reviewSessions) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n } catch { /* tmux server may not be running */ }\n\n // Stop Docker containers (name-based, works even if workspace dir is gone)\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n cleaned = true;\n steps.push({\n name: 'Docker stack removed',\n status: 'passed',\n message: `Stopped and removed Docker stack for ${issueLower}`,\n });\n } else {\n const networksPhrase = teardownResult.remainingNetworks?.length\n ? teardownResult.remainingNetworks.join(', ')\n : 'one or more workspace devnets';\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker network ${networksPhrase} still present; the closed-issue reaper will retry`,\n });\n }\n } catch (err) {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker teardown failed: ${(err as Error).message}; the closed-issue reaper will retry`,\n });\n }\n\n // Remove git worktree\n if (workspacePath && existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch {\n // Try direct removal if worktree remove fails\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n cleaned = true;\n } catch { /* Already gone */ }\n }\n }\n\n // Delete local and remote feature branches (safe — we verified merge in step 2)\n const branchName = `feature/${issueLower}`;\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist locally */ }\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist on remote */ }\n\n steps.push({\n name: 'Clean up workspace',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Workspace cleaned up' : 'No workspace to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up workspace', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 4: Prune checkpoint refs\n // Primary: delete all refs/pan/turn/<agentId>/* for this issue's agents.\n // Global: also sweep any checkpoint refs older than 30 days (safety net for\n // abandoned workspaces whose postMergeLifecycle was never called or skipped).\n try {\n const { pruneCheckpointRefsForAgents, pruneStaleCheckpointRefs } = await import('./checkpoint/checkpoint-manager.js');\n const issueLower = ctx.issueId.toLowerCase();\n const agentIds = [`agent-${issueLower}`, `planning-${issueLower}`];\n await Effect.runPromise(pruneCheckpointRefsForAgents(ctx.projectPath, agentIds));\n const stalePruned = await Effect.runPromise(pruneStaleCheckpointRefs(ctx.projectPath, 30));\n steps.push({\n name: 'Prune checkpoint refs',\n status: 'passed',\n message: stalePruned > 0\n ? `Issue refs removed; ${stalePruned} stale ref(s) >30 days also pruned`\n : 'Issue checkpoint refs removed',\n });\n } catch (err) {\n steps.push({ name: 'Prune checkpoint refs', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 5: Clean up agent state\n try {\n let cleaned = false;\n let preservedTranscripts = 0;\n const agentDir = join(AGENTS_DIR, `agent-${issueLower}`);\n const planningDir = join(AGENTS_DIR, `planning-${issueLower}`);\n\n if (existsSync(agentDir)) {\n const result = await removeAgentStateDir(agentDir);\n preservedTranscripts += result.preservedTranscripts;\n cleaned = true;\n }\n if (existsSync(planningDir)) {\n const result = await removeAgentStateDir(planningDir);\n preservedTranscripts += result.preservedTranscripts;\n cleaned = true;\n }\n\n const transcriptLabel = `transcript file${preservedTranscripts === 1 ? '' : 's'} preserved`;\n steps.push({\n name: 'Clean up agent state',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned\n ? `Agent state directories cleaned (${preservedTranscripts} ${transcriptLabel})`\n : 'No agent state to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up agent state', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 6: Close issue on tracker (hard fail)\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n await execAsync(\n `gh issue close ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --comment \"Closed via close-out ceremony\"`,\n { encoding: 'utf-8' }\n );\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `GitHub issue #${ctx.number} closed` });\n } else {\n // Linear issue\n const linearApiKey = await getLinearApiKey();\n if (!linearApiKey) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: 'LINEAR_API_KEY not configured' });\n return { success: false, issueId: ctx.issueId, steps, error: 'LINEAR_API_KEY not configured' };\n }\n\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n\n // Find the issue by identifier using issues filter (searchIssues returns\n // IssueSearchResult which lacks .update(); client.issue() needs the UUID)\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Could not parse issue ID: ${ctx.issueId}` });\n return { success: false, issueId: ctx.issueId, steps, error: `Could not parse issue ID: ${ctx.issueId}` };\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Issue ${ctx.issueId} not found in Linear` });\n return { success: false, issueId: ctx.issueId, steps, error: `Issue ${ctx.issueId} not found in Linear` };\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `Linear issue ${ctx.issueId} moved to Done` });\n }\n } catch (err) {\n steps.push({\n name: 'Close issue on tracker',\n status: 'failed',\n message: `Failed to close: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: `Failed to close issue: ${(err as Error).message}` };\n }\n\n // Step 7: Apply closed-out label\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n // Ensure the label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${ctx.owner}/${ctx.repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n // Add the label\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --add-label \"${CLOSED_OUT_LABEL}\"`,\n { encoding: 'utf-8' }\n );\n // Remove all current-phase workflow labels (single source: WORKFLOW_LABELS)\n for (const label of WORKFLOW_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n for (const label of POST_MERGE_RESIDUE_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: `Added '${CLOSED_OUT_LABEL}' label` });\n } else {\n // Linear: add label if possible\n try {\n const linearApiKey = await getLinearApiKey();\n if (linearApiKey) {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum !== null && teamKey !== null) {\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length > 0) {\n const issue = results.nodes[0];\n // Find or create the closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n if (labelId) {\n const existingLabels = await issue.labels();\n const residueLabelNames = new Set(POST_MERGE_RESIDUE_LABELS);\n const labelIds = existingLabels.nodes\n .filter(l => !residueLabelNames.has(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n await issue.update({ labelIds });\n }\n }\n }\n }\n }\n } catch { /* Non-fatal for Linear */ }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: 'Label applied' });\n }\n } catch (err) {\n steps.push({ name: 'Apply closed-out label', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 8: Mark the durable pipeline journal terminal before clearing the DB cache.\n try {\n const { markRecordPipelineClosedOutSync } = await import('./pan-dir/records.js');\n markRecordPipelineClosedOutSync({ name: 'inferred', path: ctx.projectPath }, ctx.issueId.toUpperCase());\n steps.push({ name: 'Mark pipeline terminal', status: 'passed', message: 'Pipeline journal marked closed-out' });\n } catch (err) {\n steps.push({ name: 'Mark pipeline terminal', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 9: Clear review status\n try {\n // Dynamically import to avoid circular dependency with server\n const { clearReviewStatus } = await import('./review-status.js');\n clearReviewStatus(ctx.issueId.toUpperCase());\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared' });\n } catch {\n // review-status module may not be available in CLI context\n // Try cleaning the file directly\n try {\n const statusFile = join(OVERDECK_HOME, 'review-status.json');\n if (existsSync(statusFile)) {\n const data = JSON.parse(readFileSync(statusFile, 'utf-8'));\n const upperKey = ctx.issueId.toUpperCase();\n if (data[upperKey]) {\n delete data[upperKey];\n const { writeFileSync } = await import('fs');\n writeFileSync(statusFile, JSON.stringify(data, null, 2));\n }\n }\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared (direct)' });\n } catch (innerErr) {\n steps.push({ name: 'Clear review status', status: 'skipped', message: `Warning: ${(innerErr as Error).message}` });\n }\n }\n\n return { success: true, issueId: ctx.issueId, steps };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** A close-out ceremony step failed unexpectedly outside its recovery branches. */\nexport class CloseOutError extends Data.TaggedError('CloseOutError')<{\n readonly issueId: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect-native executeCloseOut. The underlying ceremony already aggregates\n * per-step results into the returned payload (success boolean + steps). The\n * Effect variant only fails with CloseOutError if the call itself throws\n * (e.g. dynamic import of @linear/sdk explodes). Step-level failures stay\n * in the returned `success/steps` shape so callers can display them.\n */\nexport const executeCloseOut = (\n ctx: CloseOutContext,\n): Effect.Effect<CloseOutResult, CloseOutError> =>\n Effect.tryPromise({\n try: () => executeCloseOutPromise(ctx),\n catch: (cause) =>\n new CloseOutError({\n issueId: ctx.issueId,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","import { existsSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nimport { isBranchMerged } from '../close-out.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { killSession, listSessionNames } from '../tmux.js';\nimport { teardownWorkspaceDockerByNamePromise } from '../workspace-manager/docker.js';\nimport { removeAgentStateDir } from '../agents/state-dir-removal.js';\n\nconst execAsync = promisify(exec);\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport async function reapIssueResidue(projectPath: string, issueId: string): Promise<string[]> {\n const actions: string[] = [];\n const issueLower = issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n const sessions = await Effect.runPromise(listSessionNames());\n const exactSessions = new Set([\n `agent-${issueLower}`,\n `test-${issueLower}`,\n `merge-${issueLower}`,\n ]);\n const reviewRegex = new RegExp(`^review-${escapeRegex(issueLower)}-\\\\d+`);\n for (const session of sessions) {\n if (!exactSessions.has(session) && !reviewRegex.test(session)) continue;\n try {\n await Effect.runPromise(killSession(session));\n actions.push(`killed tmux session ${session}`);\n } catch {\n // Session may already be gone.\n }\n }\n } catch {\n // tmux server may not be running.\n }\n\n // Remove Docker stack by name, independent of whether the workspace dir still\n // exists — and independent of the merged check below. Containers and networks\n // are disposable runtime state (rebuildWorkspaceStack no-ops for terminal\n // issues), so tearing them down for a closed issue destroys no work, while\n // leaked `_devnet` networks eventually exhaust Docker's address pools.\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n actions.push(`removed Docker stack for feature-${issueLower}`);\n } else {\n actions.push(`Docker network for feature-${issueLower} still present`);\n }\n } catch (err) {\n actions.push(`Docker teardown failed for ${issueId}: ${(err as Error).message}`);\n }\n\n // Destructive disk/branch cleanup stays gated on the merged check: a closed\n // but genuinely unmerged branch may hold work worth recovering.\n const merged = await isBranchMerged(branchName, projectPath);\n if (merged.status === 'unmerged') {\n actions.push(`skipped disk reap for ${issueId} — branch unmerged`);\n return actions;\n }\n\n const workspacePath = join(projectPath, 'workspaces', `feature-${issueLower}`);\n if (existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: projectPath });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n }\n\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted local branch ${branchName}`);\n } catch {\n // Branch may not exist locally.\n }\n\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted remote branch ${branchName}`);\n } catch {\n // Branch may not exist remotely.\n }\n\n for (const agentDirName of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n const agentDir = join(AGENTS_DIR, agentDirName);\n if (!existsSync(agentDir)) continue;\n try {\n const result = await removeAgentStateDir(agentDir);\n const transcriptLabel = `transcript file${result.preservedTranscripts === 1 ? '' : 's'} preserved`;\n actions.push(`cleaned agent state ${agentDirName} (${result.preservedTranscripts} ${transcriptLabel})`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n\n return actions;\n}\n","import { Effect } from 'effect';\nimport { exec } from 'node:child_process';\nimport { existsSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { listRunningAgents, stopAgent } from '../agents.js';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { listProjectsSync } from '../projects.js';\nimport { readJournalStatus } from '../overdeck/review-status-record-sync.js';\nimport { resolveProjectForIssue } from '../pan-dir/record.js';\nimport { loadReviewStatuses, setReviewStatusSync } from '../review-status.js';\nimport type { ReviewStatus } from '../review-status-reconcile.js';\nimport { listSessionNames } from '../tmux.js';\nimport { isIssueClosed, isTrackerIssueClosed } from './issue-closed.js';\nimport { reapIssueResidue } from './reap-issue-residue.js';\n\n// Sessions reaped by NAME as a backstop: inspect sessions never have agent\n// state, and strike sessions can outlive their state entry (e.g. state already\n// stopped or removed while the tmux session idles — PAN-1721).\nfunction issueIdFromStatelessSession(sessionName: string): string | null {\n const match = sessionName.match(/^(?:inspect|strike)-([a-z0-9]+-\\d+)(?:-|$)/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromFeatureWorkspace(entryName: string): string | null {\n const match = entryName.match(/^feature-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromAgentDir(entryName: string): string | null {\n const match = entryName.match(/^agent-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nconst execAsync = promisify(exec);\nconst DEVNET_CLOSURE_CHECK_CONCURRENCY = 4;\nexport const REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY = 4;\n\n// Leaked `_devnet` networks are a residue source of their own: a closed issue\n// whose sessions, workspace, and agent dirs are already gone can still hold a\n// bridge network, and Docker's default address pools support only ~31 of them.\nasync function listFeatureDevnetIssueIds(): Promise<string[] | null> {\n try {\n const { stdout } = await execAsync(`docker network ls --format '{{.Name}}'`, {\n encoding: 'utf-8',\n timeout: 30000,\n });\n const issueIds = new Set<string>();\n for (const name of stdout.trim().split('\\n')) {\n const match = name.match(/-feature-([a-z]+-\\d+)_devnet$/i);\n if (match) issueIds.add(match[1].toUpperCase());\n }\n return [...issueIds];\n } catch {\n return null;\n }\n}\n\nasync function isClosedIssue(\n issueId: string,\n closedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = closedChecks.get(issueId);\n if (!promise) {\n promise = isIssueClosed(issueId);\n closedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nasync function isTrackerClosedIssue(\n issueId: string,\n trackerClosedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = trackerClosedChecks.get(issueId);\n if (!promise) {\n promise = isTrackerIssueClosed(issueId);\n trackerClosedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nfunction hasUnservicedReviewRequest(status: ReviewStatus): boolean {\n if (status.reviewStatus !== 'pending' || !status.reviewRequestedAt) return false;\n return !status.reviewSpawnedAt ||\n new Date(status.reviewRequestedAt).getTime() > new Date(status.reviewSpawnedAt).getTime();\n}\n\nexport async function reapClosedIssueReviewRequests(\n trackerClosedChecks: Map<string, Promise<boolean>>,\n): Promise<string[]> {\n const actions: string[] = [];\n const pendingStatuses = Object.entries(loadReviewStatuses())\n .filter(([, status]) => status.reviewStatus === 'pending');\n\n for (let offset = 0; offset < pendingStatuses.length; offset += REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY) {\n const batch = pendingStatuses.slice(offset, offset + REVIEW_REQUEST_CLOSURE_CHECK_CONCURRENCY);\n const candidates = (await Promise.all(batch.map(async ([issueId, dbStatus]) => {\n const journal = await readJournalStatus(issueId);\n const existing = {\n ...dbStatus,\n ...(journal?.durable ?? {}),\n issueId,\n } as ReviewStatus;\n for (const field of journal?.clearedFields ?? []) {\n delete (existing as unknown as Record<string, unknown>)[field];\n }\n return hasUnservicedReviewRequest(existing) ? [issueId, existing] as const : null;\n }))).filter((candidate): candidate is readonly [string, ReviewStatus] => candidate !== null);\n\n const trackerClosedCandidates = (await Promise.all(candidates.map(async ([issueId, existing]) => (\n await isTrackerClosedIssue(issueId, trackerClosedChecks) ? [issueId, existing] as const : null\n )))).filter((candidate): candidate is readonly [string, ReviewStatus] => candidate !== null);\n\n for (const [issueId, existing] of trackerClosedCandidates) {\n setReviewStatusSync(issueId, {\n reviewRequestedAt: undefined,\n reviewSpawnedAt: undefined,\n }, existing);\n const action = `Cleared unserviced review request for ${issueId} — parent issue is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] cleared unserviced review request for ${issueId} — parent issue is closed`,\n });\n }\n }\n\n return actions;\n}\n\nasync function reapClosedIssueResidue(\n projectPath: string,\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const key = `${projectPath}:${issueId}`;\n if (reapedIssueKeys.has(key)) return;\n reapedIssueKeys.add(key);\n actions.push(...await reapIssueResidue(projectPath, issueId));\n}\n\nasync function reapResolvedIssueResidue(\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const project = resolveProjectForIssue(issueId);\n if (!project) return;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n}\n\nfunction listConfiguredProjects(): Array<{ path: string }> {\n try {\n return listProjectsSync().map(({ config }) => ({ path: config.path }));\n } catch {\n return [];\n }\n}\n\nfunction listDirectoryNames(path: string): string[] {\n if (!existsSync(path)) return [];\n try {\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n}\n\nasync function stopClosedAgent(agentId: string, issueId: string, actions: string[]): Promise<void> {\n await Effect.runPromise(stopAgent(agentId));\n const action = `Reaped ${agentId} — parent issue ${issueId} is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] reaped ${agentId} — parent issue ${issueId} is closed`,\n });\n}\n\n/**\n * PAN-1908: reactive closed-issue reaper. When an issue is closed, stop all\n * running agents and stateless sessions belonging to it without scanning the\n * full agent table or tmux session list.\n */\nexport async function handleIssueStatusChangedClosed(issueId: string): Promise<string[]> {\n const upperIssueId = issueId.trim().toUpperCase();\n if (!upperIssueId) return [];\n\n // Guard against spurious events: only act when the issue is actually closed.\n if (!await isIssueClosed(upperIssueId)) return [];\n\n const actions: string[] = [];\n const reapedAgentIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n\n // Stop running agents for this issue.\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const agentIssueId = (agent.issueId ?? '').trim().toUpperCase();\n if (agentIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(agent.id, upperIssueId, actions);\n reapedAgentIds.add(agent.id);\n }\n\n // Stop stateless inspect/strike sessions for this issue.\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n const sessionIssueId = issueIdFromStatelessSession(sessionName);\n if (sessionIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(sessionName, upperIssueId, actions);\n reapedAgentIds.add(sessionName);\n }\n\n await reapResolvedIssueResidue(upperIssueId, actions, reapedIssueKeys);\n return actions;\n}\n\n/**\n * PAN-1908: keep a thin table+session safety net for dropped closed-issue\n * events. The primary path is reactive via handleIssueStatusChangedClosed.\n */\nexport async function reconcileClosedIssueAgents(): Promise<string[]> {\n const actions: string[] = [];\n const closedChecks = new Map<string, Promise<boolean>>();\n const trackerClosedChecks = new Map<string, Promise<boolean>>();\n const reapedAgentIds = new Set<string>();\n const closedIssueIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n const agents = await Effect.runPromise(listRunningAgents());\n\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(agent.id, issueId, actions);\n reapedAgentIds.add(agent.id);\n closedIssueIds.add(issueId);\n }\n\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n\n const issueId = issueIdFromStatelessSession(sessionName);\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(sessionName, issueId, actions);\n reapedAgentIds.add(sessionName);\n closedIssueIds.add(issueId);\n }\n\n for (const issueId of closedIssueIds) {\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n for (const project of listConfiguredProjects()) {\n const workspacesDir = join(project.path, 'workspaces');\n for (const entryName of listDirectoryNames(workspacesDir)) {\n const issueId = issueIdFromFeatureWorkspace(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n }\n }\n\n for (const entryName of listDirectoryNames(AGENTS_DIR)) {\n const issueId = issueIdFromAgentDir(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n const devnetIssueIds = await listFeatureDevnetIssueIds();\n const openDevnetIssueIds: string[] = [];\n if (devnetIssueIds) {\n for (let offset = 0; offset < devnetIssueIds.length; offset += DEVNET_CLOSURE_CHECK_CONCURRENCY) {\n const batch = devnetIssueIds.slice(offset, offset + DEVNET_CLOSURE_CHECK_CONCURRENCY);\n const closureResults = await Promise.all(batch.map(async (issueId) => ({\n issueId,\n closed: await isClosedIssue(issueId, closedChecks),\n })));\n for (const { issueId, closed } of closureResults) {\n if (closed) await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n else openDevnetIssueIds.push(issueId);\n }\n }\n }\n\n let mergedIssueIds: string[] | null = devnetIssueIds ? [] : null;\n if (openDevnetIssueIds.length > 0) {\n try {\n const { getReviewStatusesSync } = await import('../review-status.js');\n const statuses = getReviewStatusesSync(openDevnetIssueIds);\n mergedIssueIds = openDevnetIssueIds.filter(\n (issueId) => statuses[issueId]?.mergeStatus === 'merged',\n );\n } catch (error) {\n mergedIssueIds = null;\n actions.push(`Failed to resolve merged-issue Docker cleanup status: ${error}`);\n }\n }\n if (mergedIssueIds) {\n try {\n const { reconcileMergedDockerCleanupQueue } = await import('./merged-docker-cleanup-worker.js');\n actions.push(...reconcileMergedDockerCleanupQueue(mergedIssueIds));\n } catch (error) {\n actions.push(`Failed to reconcile merged-issue Docker cleanup queue: ${error}`);\n }\n }\n\n actions.push(...await reapClosedIssueReviewRequests(trackerClosedChecks));\n return actions;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { t as handleIssueStatusChangedClosed } from "./closed-issue-reaper-DrHyYgJR.js";
1
+ import { t as handleIssueStatusChangedClosed } from "./closed-issue-reaper-COxfTtLz.js";
2
2
  export { handleIssueStatusChangedClosed };
@@ -1,5 +1,5 @@
1
1
  import { Ri as init_paths, Ui as sessionFilePath } from "./config-yaml-Dce9VUd6.js";
2
- import { G as init_runtime_state, W as getAgentRuntimeStateSync, _ as init_agent_state, g as getAgentStateSync, oa as hasCompletionMarkerForAgent, sa as init_supervisor_channels } from "./agent-state-Ci6PhjOE.js";
2
+ import { G as init_runtime_state, W as getAgentRuntimeStateSync, _ as init_agent_state, ca as hasCompletionMarkerForAgent, g as getAgentStateSync, la as init_supervisor_channels } from "./agent-state-B196fj1h.js";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { existsSync, readdirSync } from "node:fs";
@@ -237,4 +237,4 @@ async function continueCompactedAgentAfterHook(args) {
237
237
  //#endregion
238
238
  export { continueCompactedAgentAfterHook, maybeContinueCompactedAgent };
239
239
 
240
- //# sourceMappingURL=compaction-continuation-DhYgsXmF.js.map
240
+ //# sourceMappingURL=compaction-continuation-Bqc212Zm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-continuation-DhYgsXmF.js","names":[],"sources":["../../src/lib/conversations/transcript-path.ts","../../src/lib/cloister/compaction-continuation.ts"],"sourcesContent":["import { existsSync, readdirSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { sessionFilePath } from '../paths.js';\n\nexport type ConversationTranscriptStatus = 'ok' | 'expired' | 'unknown';\n\nexport interface ConversationTranscriptResolution {\n path: string | null;\n status: ConversationTranscriptStatus;\n}\n\nexport function resolveConversationTranscript(\n cwd: string | null | undefined,\n claudeSessionId: string | null | undefined,\n): ConversationTranscriptResolution {\n if (!claudeSessionId) return { path: null, status: 'unknown' };\n\n const derived = sessionFilePath(cwd ?? '', claudeSessionId);\n if (existsSync(derived)) return { path: derived, status: 'ok' };\n\n const fallback = findClaudeProjectSessionFile(claudeSessionId);\n if (fallback) return { path: fallback, status: 'ok' };\n\n return { path: derived, status: 'expired' };\n}\n\nfunction findClaudeProjectSessionFile(sessionId: string): string | null {\n const projectsDir = join(homedir(), '.claude', 'projects');\n if (!existsSync(projectsDir)) return null;\n\n for (const projectDir of readdirSync(projectsDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort()) {\n const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n if (existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n","/**\n * Continue an agent that compacted and then stopped (PAN-3057).\n *\n * A *manual* `/compact` ends the turn: Claude Code writes the summary, replays\n * the file reads, and drops to an idle prompt. Nothing re-drives it. That is\n * fine when Overdeck sent the `/compact` — `deliverOrchestratedCompact()`\n * registers the session and the patrol nudges it afterwards — but the harness\n * compacts on its own too. On 2026-07-25 a host restart resumed six MYN agents;\n * Claude Code classified each restored session as an interrupted turn and ran\n * `/compact` itself (`trigger: \"manual\"`, same promptId as its own resume\n * injection). All six then sat idle. Deacon never touched them: they were not\n * stopped, so `handleAgentStoppedEvent` skipped every one.\n *\n * The detector here is deliberately NOT \"did Overdeck send a compact\" — that is\n * the assumption that failed. It reads the fact from the transcript: a\n * `compact_boundary` with no model turn after it means the session compacted and\n * never resumed, whoever pressed the button. That signal is durable across a\n * dashboard restart (unlike the in-memory continuation map) and needs no new\n * event type, hook payload, or marker file.\n */\n\nimport { open, stat } from 'node:fs/promises';\n\nimport { getAgentStateSync, type AgentState } from '../agents/agent-state.js';\nimport { getAgentRuntimeStateSync } from '../agents/runtime-state.js';\nimport { hasCompletionMarkerForAgent } from '../agents/supervisor-channels.js';\nimport { resolveConversationTranscript } from '../conversations/transcript-path.js';\n\n/** Re-nudge cooldown per agent, so an agent that ignores the nudge isn't spammed. */\nexport const COMPACTION_CONTINUE_COOLDOWN_MS = 10 * 60_000;\n\n/**\n * Cap on the bytes scanned after the compact boundary. The tail holds the\n * summary plus replayed file attachments, so it can be large; we only need to\n * know whether a model turn happened, and that entry lands early.\n */\nconst MAX_TAIL_SCAN_BYTES = 4 * 1024 * 1024;\n\nconst lastContinuationAt = new Map<string, number>();\nconst postCompactContinuationInFlight = new Set<string>();\n\nexport function resetCompactionContinuationState(): void {\n lastContinuationAt.clear();\n postCompactContinuationInFlight.clear();\n}\n\nexport interface CompactedIdleVerdict {\n /** True when the transcript's last compaction was never followed by a model turn. */\n stalledAfterCompaction: boolean;\n /** Byte offset of the boundary we judged, for logging. */\n boundaryOffset: number;\n}\n\n/**\n * Does this transcript end at a compaction that no model turn followed?\n *\n * \"Model turn\" means a real `assistant` entry. Claude Code writes synthetic\n * assistant entries (`model: \"<synthetic>\"`, zero tokens) for meta messages it\n * answers itself — those are not evidence the agent did anything. Everything\n * else Claude writes after a compaction (the summary user message, replayed file\n * attachments, hook results) is bookkeeping, not work.\n *\n * A non-meta user message after the boundary also counts as \"someone already\n * prompted it\": the agent is mid-turn or has just been nudged, so we stay out.\n */\nexport async function transcriptStalledAfterCompaction(\n transcriptPath: string,\n findBoundary: (path: string) => Promise<number>,\n): Promise<CompactedIdleVerdict> {\n let boundaryOffset = 0;\n try {\n boundaryOffset = await findBoundary(transcriptPath);\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset: 0 };\n }\n // Offset 0 means \"no compaction in this transcript\" (findLastCompactBoundary's\n // documented not-found value), not \"compacted at the very first byte\".\n if (boundaryOffset <= 0) return { stalledAfterCompaction: false, boundaryOffset: 0 };\n\n let size = 0;\n try {\n size = (await stat(transcriptPath)).size;\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n if (size <= boundaryOffset) return { stalledAfterCompaction: true, boundaryOffset };\n\n const toRead = Math.min(size - boundaryOffset, MAX_TAIL_SCAN_BYTES);\n let tail = '';\n const fh = await open(transcriptPath, 'r').catch(() => null);\n if (!fh) return { stalledAfterCompaction: false, boundaryOffset };\n try {\n const buf = Buffer.alloc(toRead);\n const { bytesRead } = await fh.read(buf, 0, toRead, boundaryOffset);\n tail = buf.subarray(0, bytesRead).toString('utf-8');\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset };\n } finally {\n await fh.close().catch(() => {});\n }\n\n // Drop a trailing partial line — the agent may be writing as we read.\n const lines = tail.split('\\n');\n if (!tail.endsWith('\\n')) lines.pop();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line) as Record<string, unknown>;\n } catch {\n continue;\n }\n const type = entry['type'];\n const message = entry['message'] as { role?: string; model?: string } | undefined;\n\n if (type === 'assistant') {\n // Synthetic entries are Claude answering its own meta message.\n if (message?.model && message.model !== '<synthetic>') {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n continue;\n }\n if (type === 'user' && entry['isMeta'] !== true && entry['isCompactSummary'] !== true) {\n const content = message?.role === 'user' ? (entry['message'] as { content?: unknown }).content : undefined;\n // `<local-command-stdout>` and the `/compact` command echo are the harness\n // narrating its own compaction, not a prompt someone sent the agent.\n const text = typeof content === 'string' ? content : '';\n if (!text.startsWith('<local-command-') && !text.startsWith('<command-')) {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n }\n }\n\n return { stalledAfterCompaction: true, boundaryOffset };\n}\n\n/**\n * Should this agent be re-driven at all?\n *\n * Mirrors `buildResumeContinueMessage`'s phase-awareness: an agent whose work is\n * already handed off (completed marker) must never be told to \"continue\" — that\n * is how PAN-2974 had a resumed agent re-run verification and fire review\n * commands while the operator was mid-UAT. Gated agents stay gated.\n */\nexport function shouldContinueAfterCompaction(state: AgentState): { ok: boolean; reason?: string } {\n if (state.paused === true) return { ok: false, reason: 'paused' };\n if (state.troubled === true) return { ok: false, reason: 'troubled' };\n if (state.stoppedByUser === true) return { ok: false, reason: 'stopped-by-user' };\n if (state.status !== 'running') return { ok: false, reason: `status=${state.status}` };\n if (hasCompletionMarkerForAgent(state)) return { ok: false, reason: 'handed-off (completed marker)' };\n return { ok: true };\n}\n\nexport function buildCompactionContinueMessage(state: AgentState): string {\n const issueId = state.issueId;\n if (state.role === 'work') {\n return (\n `Your context was compacted and the turn ended, so you stopped mid-task on ${issueId}. `\n + 'Continue from the compacted summary now: re-read your current xBRIEF item '\n + '(`pan task show`) and your latest .pan/feedback/ file if you need them, then resume '\n + 'work. Do not start over and do not wait for further input.'\n );\n }\n return (\n `Your context was compacted and the turn ended, so you stopped mid-task on ${issueId}. `\n + 'Continue from the compacted summary now, finish the task you were on, and signal the '\n + 'result through your normal lifecycle command. Do not start over and do not wait for '\n + 'further input.'\n );\n}\n\nexport interface ContinueCompactedAgentArgs {\n agentId: string;\n /** Live pane text; an idle prompt is required before we send anything. */\n tmuxOutput: string;\n now?: number;\n send: (agentId: string, message: string) => Promise<unknown>;\n findBoundary: (path: string) => Promise<number>;\n /** Injected for tests; defaults to the real agent-state read. */\n readState?: (agentId: string) => AgentState | null;\n}\n\n/**\n * Nudge an agent that compacted and stopped. Returns an action string when a\n * nudge was delivered, or null when nothing was owed.\n */\nexport async function maybeContinueCompactedAgent(\n args: ContinueCompactedAgentArgs,\n): Promise<string | null> {\n const now = args.now ?? Date.now();\n const readState = args.readState ?? getAgentStateSync;\n\n if (!args.tmuxOutput.includes('❯')) return null;\n\n const last = lastContinuationAt.get(args.agentId);\n if (last !== undefined && now - last < COMPACTION_CONTINUE_COOLDOWN_MS) return null;\n\n const state = readState(args.agentId);\n if (!state) return null;\n\n const gate = shouldContinueAfterCompaction(state);\n if (!gate.ok) return null;\n\n // Same session resolution as maybeProactivelyCompactContext: agent state first,\n // runtime snapshot second — a recovered agent may only carry the runtime value.\n const sessionId = state.sessionId ?? getAgentRuntimeStateSync(args.agentId)?.claudeSessionId;\n if (!sessionId || !state.workspace) return null;\n const transcript = resolveConversationTranscript(state.workspace, sessionId);\n if (!transcript.path || transcript.status !== 'ok') return null;\n\n const verdict = await transcriptStalledAfterCompaction(transcript.path, args.findBoundary);\n if (!verdict.stalledAfterCompaction) return null;\n\n await args.send(args.agentId, buildCompactionContinueMessage(state));\n lastContinuationAt.set(args.agentId, now);\n return `Compaction continuation: nudged ${args.agentId} — compacted and idle with no turn after the boundary`;\n}\n\nconst POST_COMPACT_PROMPT_POLL_INTERVAL_MS = 250;\nconst POST_COMPACT_PROMPT_POLL_ATTEMPTS = 20;\n\nexport interface ContinueAfterPostCompactHookArgs\n extends Omit<ContinueCompactedAgentArgs, 'tmuxOutput' | 'now'> {\n capturePane: (agentId: string) => Promise<string>;\n attempts?: number;\n intervalMs?: number;\n sleep?: (ms: number) => Promise<void>;\n continueAgent?: typeof maybeContinueCompactedAgent;\n}\n\n/**\n * Deterministic PostCompact continuation. The hook event can arrive while its\n * own shell process is still exiting, so wait briefly for Claude's idle prompt\n * instead of racing an injection into the compaction teardown. The 10-minute\n * patrol remains a durable fallback, but a healthy hook path re-drives within\n * seconds of the boundary.\n */\nexport async function continueCompactedAgentAfterHook(\n args: ContinueAfterPostCompactHookArgs,\n): Promise<string | null> {\n const readState = args.readState ?? getAgentStateSync;\n const state = readState(args.agentId);\n if (!state || !shouldContinueAfterCompaction(state).ok) return null;\n if (postCompactContinuationInFlight.has(args.agentId)) return null;\n postCompactContinuationInFlight.add(args.agentId);\n\n try {\n const attempts = args.attempts ?? POST_COMPACT_PROMPT_POLL_ATTEMPTS;\n const intervalMs = args.intervalMs ?? POST_COMPACT_PROMPT_POLL_INTERVAL_MS;\n const sleep = args.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n const continueAgent = args.continueAgent ?? maybeContinueCompactedAgent;\n\n for (let attempt = 1; attempt <= attempts; attempt += 1) {\n try {\n const tmuxOutput = await args.capturePane(args.agentId);\n const continued = await continueAgent({\n agentId: args.agentId,\n tmuxOutput,\n send: args.send,\n findBoundary: args.findBoundary,\n readState,\n });\n if (continued) return continued;\n } catch {\n // The pane or transcript can be between compaction writes. Retry inside\n // this hook-owned window; the patrol remains the fallback after it ends.\n }\n if (attempt < attempts) await sleep(intervalMs);\n }\n\n return null;\n } finally {\n postCompactContinuationInFlight.delete(args.agentId);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"compaction-continuation-Bqc212Zm.js","names":[],"sources":["../../src/lib/conversations/transcript-path.ts","../../src/lib/cloister/compaction-continuation.ts"],"sourcesContent":["import { existsSync, readdirSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { sessionFilePath } from '../paths.js';\n\nexport type ConversationTranscriptStatus = 'ok' | 'expired' | 'unknown';\n\nexport interface ConversationTranscriptResolution {\n path: string | null;\n status: ConversationTranscriptStatus;\n}\n\nexport function resolveConversationTranscript(\n cwd: string | null | undefined,\n claudeSessionId: string | null | undefined,\n): ConversationTranscriptResolution {\n if (!claudeSessionId) return { path: null, status: 'unknown' };\n\n const derived = sessionFilePath(cwd ?? '', claudeSessionId);\n if (existsSync(derived)) return { path: derived, status: 'ok' };\n\n const fallback = findClaudeProjectSessionFile(claudeSessionId);\n if (fallback) return { path: fallback, status: 'ok' };\n\n return { path: derived, status: 'expired' };\n}\n\nfunction findClaudeProjectSessionFile(sessionId: string): string | null {\n const projectsDir = join(homedir(), '.claude', 'projects');\n if (!existsSync(projectsDir)) return null;\n\n for (const projectDir of readdirSync(projectsDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort()) {\n const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n if (existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n","/**\n * Continue an agent that compacted and then stopped (PAN-3057).\n *\n * A *manual* `/compact` ends the turn: Claude Code writes the summary, replays\n * the file reads, and drops to an idle prompt. Nothing re-drives it. That is\n * fine when Overdeck sent the `/compact` — `deliverOrchestratedCompact()`\n * registers the session and the patrol nudges it afterwards — but the harness\n * compacts on its own too. On 2026-07-25 a host restart resumed six MYN agents;\n * Claude Code classified each restored session as an interrupted turn and ran\n * `/compact` itself (`trigger: \"manual\"`, same promptId as its own resume\n * injection). All six then sat idle. Deacon never touched them: they were not\n * stopped, so `handleAgentStoppedEvent` skipped every one.\n *\n * The detector here is deliberately NOT \"did Overdeck send a compact\" — that is\n * the assumption that failed. It reads the fact from the transcript: a\n * `compact_boundary` with no model turn after it means the session compacted and\n * never resumed, whoever pressed the button. That signal is durable across a\n * dashboard restart (unlike the in-memory continuation map) and needs no new\n * event type, hook payload, or marker file.\n */\n\nimport { open, stat } from 'node:fs/promises';\n\nimport { getAgentStateSync, type AgentState } from '../agents/agent-state.js';\nimport { getAgentRuntimeStateSync } from '../agents/runtime-state.js';\nimport { hasCompletionMarkerForAgent } from '../agents/supervisor-channels.js';\nimport { resolveConversationTranscript } from '../conversations/transcript-path.js';\n\n/** Re-nudge cooldown per agent, so an agent that ignores the nudge isn't spammed. */\nexport const COMPACTION_CONTINUE_COOLDOWN_MS = 10 * 60_000;\n\n/**\n * Cap on the bytes scanned after the compact boundary. The tail holds the\n * summary plus replayed file attachments, so it can be large; we only need to\n * know whether a model turn happened, and that entry lands early.\n */\nconst MAX_TAIL_SCAN_BYTES = 4 * 1024 * 1024;\n\nconst lastContinuationAt = new Map<string, number>();\nconst postCompactContinuationInFlight = new Set<string>();\n\nexport function resetCompactionContinuationState(): void {\n lastContinuationAt.clear();\n postCompactContinuationInFlight.clear();\n}\n\nexport interface CompactedIdleVerdict {\n /** True when the transcript's last compaction was never followed by a model turn. */\n stalledAfterCompaction: boolean;\n /** Byte offset of the boundary we judged, for logging. */\n boundaryOffset: number;\n}\n\n/**\n * Does this transcript end at a compaction that no model turn followed?\n *\n * \"Model turn\" means a real `assistant` entry. Claude Code writes synthetic\n * assistant entries (`model: \"<synthetic>\"`, zero tokens) for meta messages it\n * answers itself — those are not evidence the agent did anything. Everything\n * else Claude writes after a compaction (the summary user message, replayed file\n * attachments, hook results) is bookkeeping, not work.\n *\n * A non-meta user message after the boundary also counts as \"someone already\n * prompted it\": the agent is mid-turn or has just been nudged, so we stay out.\n */\nexport async function transcriptStalledAfterCompaction(\n transcriptPath: string,\n findBoundary: (path: string) => Promise<number>,\n): Promise<CompactedIdleVerdict> {\n let boundaryOffset = 0;\n try {\n boundaryOffset = await findBoundary(transcriptPath);\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset: 0 };\n }\n // Offset 0 means \"no compaction in this transcript\" (findLastCompactBoundary's\n // documented not-found value), not \"compacted at the very first byte\".\n if (boundaryOffset <= 0) return { stalledAfterCompaction: false, boundaryOffset: 0 };\n\n let size = 0;\n try {\n size = (await stat(transcriptPath)).size;\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n if (size <= boundaryOffset) return { stalledAfterCompaction: true, boundaryOffset };\n\n const toRead = Math.min(size - boundaryOffset, MAX_TAIL_SCAN_BYTES);\n let tail = '';\n const fh = await open(transcriptPath, 'r').catch(() => null);\n if (!fh) return { stalledAfterCompaction: false, boundaryOffset };\n try {\n const buf = Buffer.alloc(toRead);\n const { bytesRead } = await fh.read(buf, 0, toRead, boundaryOffset);\n tail = buf.subarray(0, bytesRead).toString('utf-8');\n } catch {\n return { stalledAfterCompaction: false, boundaryOffset };\n } finally {\n await fh.close().catch(() => {});\n }\n\n // Drop a trailing partial line — the agent may be writing as we read.\n const lines = tail.split('\\n');\n if (!tail.endsWith('\\n')) lines.pop();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line) as Record<string, unknown>;\n } catch {\n continue;\n }\n const type = entry['type'];\n const message = entry['message'] as { role?: string; model?: string } | undefined;\n\n if (type === 'assistant') {\n // Synthetic entries are Claude answering its own meta message.\n if (message?.model && message.model !== '<synthetic>') {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n continue;\n }\n if (type === 'user' && entry['isMeta'] !== true && entry['isCompactSummary'] !== true) {\n const content = message?.role === 'user' ? (entry['message'] as { content?: unknown }).content : undefined;\n // `<local-command-stdout>` and the `/compact` command echo are the harness\n // narrating its own compaction, not a prompt someone sent the agent.\n const text = typeof content === 'string' ? content : '';\n if (!text.startsWith('<local-command-') && !text.startsWith('<command-')) {\n return { stalledAfterCompaction: false, boundaryOffset };\n }\n }\n }\n\n return { stalledAfterCompaction: true, boundaryOffset };\n}\n\n/**\n * Should this agent be re-driven at all?\n *\n * Mirrors `buildResumeContinueMessage`'s phase-awareness: an agent whose work is\n * already handed off (completed marker) must never be told to \"continue\" — that\n * is how PAN-2974 had a resumed agent re-run verification and fire review\n * commands while the operator was mid-UAT. Gated agents stay gated.\n */\nexport function shouldContinueAfterCompaction(state: AgentState): { ok: boolean; reason?: string } {\n if (state.paused === true) return { ok: false, reason: 'paused' };\n if (state.troubled === true) return { ok: false, reason: 'troubled' };\n if (state.stoppedByUser === true) return { ok: false, reason: 'stopped-by-user' };\n if (state.status !== 'running') return { ok: false, reason: `status=${state.status}` };\n if (hasCompletionMarkerForAgent(state)) return { ok: false, reason: 'handed-off (completed marker)' };\n return { ok: true };\n}\n\nexport function buildCompactionContinueMessage(state: AgentState): string {\n const issueId = state.issueId;\n if (state.role === 'work') {\n return (\n `Your context was compacted and the turn ended, so you stopped mid-task on ${issueId}. `\n + 'Continue from the compacted summary now: re-read your current xBRIEF item '\n + '(`pan task show`) and your latest .pan/feedback/ file if you need them, then resume '\n + 'work. Do not start over and do not wait for further input.'\n );\n }\n return (\n `Your context was compacted and the turn ended, so you stopped mid-task on ${issueId}. `\n + 'Continue from the compacted summary now, finish the task you were on, and signal the '\n + 'result through your normal lifecycle command. Do not start over and do not wait for '\n + 'further input.'\n );\n}\n\nexport interface ContinueCompactedAgentArgs {\n agentId: string;\n /** Live pane text; an idle prompt is required before we send anything. */\n tmuxOutput: string;\n now?: number;\n send: (agentId: string, message: string) => Promise<unknown>;\n findBoundary: (path: string) => Promise<number>;\n /** Injected for tests; defaults to the real agent-state read. */\n readState?: (agentId: string) => AgentState | null;\n}\n\n/**\n * Nudge an agent that compacted and stopped. Returns an action string when a\n * nudge was delivered, or null when nothing was owed.\n */\nexport async function maybeContinueCompactedAgent(\n args: ContinueCompactedAgentArgs,\n): Promise<string | null> {\n const now = args.now ?? Date.now();\n const readState = args.readState ?? getAgentStateSync;\n\n if (!args.tmuxOutput.includes('❯')) return null;\n\n const last = lastContinuationAt.get(args.agentId);\n if (last !== undefined && now - last < COMPACTION_CONTINUE_COOLDOWN_MS) return null;\n\n const state = readState(args.agentId);\n if (!state) return null;\n\n const gate = shouldContinueAfterCompaction(state);\n if (!gate.ok) return null;\n\n // Same session resolution as maybeProactivelyCompactContext: agent state first,\n // runtime snapshot second — a recovered agent may only carry the runtime value.\n const sessionId = state.sessionId ?? getAgentRuntimeStateSync(args.agentId)?.claudeSessionId;\n if (!sessionId || !state.workspace) return null;\n const transcript = resolveConversationTranscript(state.workspace, sessionId);\n if (!transcript.path || transcript.status !== 'ok') return null;\n\n const verdict = await transcriptStalledAfterCompaction(transcript.path, args.findBoundary);\n if (!verdict.stalledAfterCompaction) return null;\n\n await args.send(args.agentId, buildCompactionContinueMessage(state));\n lastContinuationAt.set(args.agentId, now);\n return `Compaction continuation: nudged ${args.agentId} — compacted and idle with no turn after the boundary`;\n}\n\nconst POST_COMPACT_PROMPT_POLL_INTERVAL_MS = 250;\nconst POST_COMPACT_PROMPT_POLL_ATTEMPTS = 20;\n\nexport interface ContinueAfterPostCompactHookArgs\n extends Omit<ContinueCompactedAgentArgs, 'tmuxOutput' | 'now'> {\n capturePane: (agentId: string) => Promise<string>;\n attempts?: number;\n intervalMs?: number;\n sleep?: (ms: number) => Promise<void>;\n continueAgent?: typeof maybeContinueCompactedAgent;\n}\n\n/**\n * Deterministic PostCompact continuation. The hook event can arrive while its\n * own shell process is still exiting, so wait briefly for Claude's idle prompt\n * instead of racing an injection into the compaction teardown. The 10-minute\n * patrol remains a durable fallback, but a healthy hook path re-drives within\n * seconds of the boundary.\n */\nexport async function continueCompactedAgentAfterHook(\n args: ContinueAfterPostCompactHookArgs,\n): Promise<string | null> {\n const readState = args.readState ?? getAgentStateSync;\n const state = readState(args.agentId);\n if (!state || !shouldContinueAfterCompaction(state).ok) return null;\n if (postCompactContinuationInFlight.has(args.agentId)) return null;\n postCompactContinuationInFlight.add(args.agentId);\n\n try {\n const attempts = args.attempts ?? POST_COMPACT_PROMPT_POLL_ATTEMPTS;\n const intervalMs = args.intervalMs ?? POST_COMPACT_PROMPT_POLL_INTERVAL_MS;\n const sleep = args.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n const continueAgent = args.continueAgent ?? maybeContinueCompactedAgent;\n\n for (let attempt = 1; attempt <= attempts; attempt += 1) {\n try {\n const tmuxOutput = await args.capturePane(args.agentId);\n const continued = await continueAgent({\n agentId: args.agentId,\n tmuxOutput,\n send: args.send,\n findBoundary: args.findBoundary,\n readState,\n });\n if (continued) return continued;\n } catch {\n // The pane or transcript can be between compaction writes. Retry inside\n // this hook-owned window; the patrol remains the fallback after it ends.\n }\n if (attempt < attempts) await sleep(intervalMs);\n }\n\n return null;\n } finally {\n postCompactContinuationInFlight.delete(args.agentId);\n }\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAA,8BAAA,KAAA,iBAAA;AAIE,KAAA,CAAA,gBAAA,QAAA;;;;;AAGA,KAAA,WAAA,QAAA,CAAA,QAAA;;;;;AAGA,KAAA,SAAA,QAAA;;;;AAEA,QAAA;;;;;AAGF,SAAA,6BAAA,WAAA;;AAEE,KAAA,CAAA,WAAA,YAAA,CAAA,QAAA;AAEA,MAAA,MAAA,cAAA,YAAA,aAAA,EAAA,eAAA,MAAA,CAAA,CAAA,QAAA,UAAA,MAAA,aAAA,CAAA,CAAA,KAAA,UAAA,MAAA,KAAA,CAAA,MAAA,EAAA;;AAKE,MAAA,WAAA,UAAA,CAAA,QAAA;;AAGF,QAAA;;;;;;;ACJF,MAAM,sBAAsB,IAAI,OAAO;AAEvC,MAAM,qCAAqB,IAAI,KAAqB;AACpD,MAAM,kDAAkC,IAAI,KAAa;;;;;;;;;;;;;AA0BzD,eAAsB,iCACpB,gBACA,cAC+B;CAC/B,IAAI,iBAAiB;AACrB,KAAI;AACF,mBAAiB,MAAM,aAAa,eAAe;SAC7C;AACN,SAAO;GAAE,wBAAwB;GAAO,gBAAgB;GAAG;;AAI7D,KAAI,kBAAkB,EAAG,QAAO;EAAE,wBAAwB;EAAO,gBAAgB;EAAG;CAEpF,IAAI,OAAO;AACX,KAAI;AACF,UAAQ,MAAM,KAAK,eAAe,EAAE;SAC9B;AACN,SAAO;GAAE,wBAAwB;GAAO;GAAgB;;AAE1D,KAAI,QAAQ,eAAgB,QAAO;EAAE,wBAAwB;EAAM;EAAgB;CAEnF,MAAM,SAAS,KAAK,IAAI,OAAO,gBAAgB,oBAAoB;CACnE,IAAI,OAAO;CACX,MAAM,KAAK,MAAM,KAAK,gBAAgB,IAAI,CAAC,YAAY,KAAK;AAC5D,KAAI,CAAC,GAAI,QAAO;EAAE,wBAAwB;EAAO;EAAgB;AACjE,KAAI;EACF,MAAM,MAAM,OAAO,MAAM,OAAO;EAChC,MAAM,EAAE,cAAc,MAAM,GAAG,KAAK,KAAK,GAAG,QAAQ,eAAe;AACnE,SAAO,IAAI,SAAS,GAAG,UAAU,CAAC,SAAS,QAAQ;SAC7C;AACN,SAAO;GAAE,wBAAwB;GAAO;GAAgB;WAChD;AACR,QAAM,GAAG,OAAO,CAAC,YAAY,GAAG;;CAIlC,MAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,KAAI,CAAC,KAAK,SAAS,KAAK,CAAE,OAAM,KAAK;AAErC,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,CAAC,KAAK,MAAM,CAAE;EAClB,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,KAAK;UAClB;AACN;;EAEF,MAAM,OAAO,MAAM;EACnB,MAAM,UAAU,MAAM;AAEtB,MAAI,SAAS,aAAa;AAExB,OAAI,SAAS,SAAS,QAAQ,UAAU,cACtC,QAAO;IAAE,wBAAwB;IAAO;IAAgB;AAE1D;;AAEF,MAAI,SAAS,UAAU,MAAM,cAAc,QAAQ,MAAM,wBAAwB,MAAM;GACrF,MAAM,UAAU,SAAS,SAAS,SAAU,MAAM,WAAqC,UAAU,KAAA;GAGjG,MAAM,OAAO,OAAO,YAAY,WAAW,UAAU;AACrD,OAAI,CAAC,KAAK,WAAW,kBAAkB,IAAI,CAAC,KAAK,WAAW,YAAY,CACtE,QAAO;IAAE,wBAAwB;IAAO;IAAgB;;;AAK9D,QAAO;EAAE,wBAAwB;EAAM;EAAgB;;;;;;;;;;AAWzD,SAAgB,8BAA8B,OAAqD;AACjG,KAAI,MAAM,WAAW,KAAM,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAU;AACjE,KAAI,MAAM,aAAa,KAAM,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAY;AACrE,KAAI,MAAM,kBAAkB,KAAM,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAmB;AACjF,KAAI,MAAM,WAAW,UAAW,QAAO;EAAE,IAAI;EAAO,QAAQ,UAAU,MAAM;EAAU;AACtF,KAAI,4BAA4B,MAAM,CAAE,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAiC;AACrG,QAAO,EAAE,IAAI,MAAM;;AAGrB,SAAgB,+BAA+B,OAA2B;CACxE,MAAM,UAAU,MAAM;AACtB,KAAI,MAAM,SAAS,OACjB,QACE,6EAA6E,QAAQ;AAMzF,QACE,6EAA6E,QAAQ;;;;;;AAsBzF,eAAsB,4BACpB,MACwB;CACxB,MAAM,MAAM,KAAK,OAAO,KAAK,KAAK;CAClC,MAAM,YAAY,KAAK,aAAa;AAEpC,KAAI,CAAC,KAAK,WAAW,SAAS,IAAI,CAAE,QAAO;CAE3C,MAAM,OAAO,mBAAmB,IAAI,KAAK,QAAQ;AACjD,KAAI,SAAS,KAAA,KAAa,MAAM,OAAA,IAAwC,QAAO;CAE/E,MAAM,QAAQ,UAAU,KAAK,QAAQ;AACrC,KAAI,CAAC,MAAO,QAAO;AAGnB,KAAI,CADS,8BAA8B,MAClC,CAAC,GAAI,QAAO;CAIrB,MAAM,YAAY,MAAM,aAAa,yBAAyB,KAAK,QAAQ,EAAE;AAC7E,KAAI,CAAC,aAAa,CAAC,MAAM,UAAW,QAAO;CAC3C,MAAM,aAAa,8BAA8B,MAAM,WAAW,UAAU;AAC5E,KAAI,CAAC,WAAW,QAAQ,WAAW,WAAW,KAAM,QAAO;AAG3D,KAAI,EAAC,MADiB,iCAAiC,WAAW,MAAM,KAAK,aAAa,EAC7E,uBAAwB,QAAO;AAE5C,OAAM,KAAK,KAAK,KAAK,SAAS,+BAA+B,MAAM,CAAC;AACpE,oBAAmB,IAAI,KAAK,SAAS,IAAI;AACzC,QAAO,mCAAmC,KAAK,QAAQ;;AAGzD,MAAM,uCAAuC;AAC7C,MAAM,oCAAoC;;;;;;;;AAkB1C,eAAsB,gCACpB,MACwB;CACxB,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,QAAQ,UAAU,KAAK,QAAQ;AACrC,KAAI,CAAC,SAAS,CAAC,8BAA8B,MAAM,CAAC,GAAI,QAAO;AAC/D,KAAI,gCAAgC,IAAI,KAAK,QAAQ,CAAE,QAAO;AAC9D,iCAAgC,IAAI,KAAK,QAAQ;AAEjD,KAAI;EACF,MAAM,WAAW,KAAK,YAAY;EAClC,MAAM,aAAa,KAAK,cAAc;EACtC,MAAM,QAAQ,KAAK,WAAW,OAAe,IAAI,SAAe,YAAY,WAAW,SAAS,GAAG,CAAC;EACpG,MAAM,gBAAgB,KAAK,iBAAiB;AAE5C,OAAK,IAAI,UAAU,GAAG,WAAW,UAAU,WAAW,GAAG;AACvD,OAAI;IACF,MAAM,aAAa,MAAM,KAAK,YAAY,KAAK,QAAQ;IACvD,MAAM,YAAY,MAAM,cAAc;KACpC,SAAS,KAAK;KACd;KACA,MAAM,KAAK;KACX,cAAc,KAAK;KACnB;KACD,CAAC;AACF,QAAI,UAAW,QAAO;WAChB;AAIR,OAAI,UAAU,SAAU,OAAM,MAAM,WAAW;;AAGjD,SAAO;WACC;AACR,kCAAgC,OAAO,KAAK,QAAQ"}
@@ -1,2 +1,2 @@
1
- import { o as resolveConflictGate, t as buildRealConflictGateDeps } from "./conflict-gate-Bt-lLgYf.js";
1
+ import { o as resolveConflictGate, t as buildRealConflictGateDeps } from "./conflict-gate-CGQTcR0I.js";
2
2
  export { buildRealConflictGateDeps, resolveConflictGate };
@@ -1,6 +1,6 @@
1
1
  import { l as init_activity_logger, o as emitActivityEntrySync } from "./activity-logger-DNYDzAOU.js";
2
- import { Gt as messageAgent, Wt as init_messaging, ln as spawnRun, sn as init_spawn } from "./agent-state-Ci6PhjOE.js";
3
- import { d as getReviewStatusSync, p as init_review_status, w as setReviewStatusSync } from "./review-status-BegAr86G.js";
2
+ import { Gt as messageAgent, Wt as init_messaging, ln as spawnRun, sn as init_spawn } from "./agent-state-B196fj1h.js";
3
+ import { d as getReviewStatusSync, p as init_review_status, w as setReviewStatusSync } from "./review-status-yY5qt7Rl.js";
4
4
  import { exec } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  //#region ../../lib/cloister/conflict-gate.ts
@@ -270,4 +270,4 @@ function shellQuote(value) {
270
270
  //#endregion
271
271
  export { probeBranchConflictPaths as a, parseMergeTreeNameOnly as i, checkBranchMergeability as n, resolveConflictGate as o, getCachedConflictGateMergeability as r, buildRealConflictGateDeps as t };
272
272
 
273
- //# sourceMappingURL=conflict-gate-Bt-lLgYf.js.map
273
+ //# sourceMappingURL=conflict-gate-CGQTcR0I.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"conflict-gate-Bt-lLgYf.js","names":["maybe"],"sources":["../../src/lib/cloister/conflict-gate.ts"],"sourcesContent":["import { exec, type ExecOptions } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { messageAgent } from '../agents/messaging.js';\nimport { spawnRun } from '../agents/spawn.js';\nimport { getReviewStatusSync, setReviewStatusSync, type BlockerReason, type ReviewStatus, type ReviewStatusUpdate } from '../review-status.js';\n\nconst execAsync = promisify(exec);\nconst GIT_TIMEOUT_MS = 30_000;\nconst GIT_MAX_BUFFER = 4 * 1024 * 1024;\nconst MERGE_BLOCKER_TYPES = new Set<BlockerReason['type']>(['merge_conflict', 'not_mergeable']);\nconst PROBE_CACHE_MS = 3 * 60 * 1000;\nconst DISPATCH_THROTTLE_MS = 30 * 60 * 1000;\n\nexport type BranchMergeability = 'clean' | 'conflicts' | 'unknown';\n\nexport interface ExecResult {\n stdout: string | Buffer;\n stderr: string | Buffer;\n}\n\nexport type ExecRunner = (command: string, options: ExecOptions) => Promise<ExecResult>;\n\nexport interface CheckBranchMergeabilityDeps {\n exec?: ExecRunner;\n}\n\nexport type ResolverDispatchState = 'dispatched' | 'already_running' | 'throttled';\n\nexport interface ConflictGateResult {\n gated: boolean;\n reason?: string;\n clearedStaleBlocker?: boolean;\n resolverDispatchState?: ResolverDispatchState;\n}\n\nexport interface DispatchResolverInput {\n issueId: string;\n workspacePath: string;\n targetBranch: string;\n blockerReasons: BlockerReason[];\n reason: string;\n}\n\nexport interface ResolveConflictGateDeps {\n getReviewStatus: (issueId: string) => ReviewStatus | null | Promise<ReviewStatus | null>;\n setReviewStatus: (\n issueId: string,\n update: ReviewStatusUpdate,\n existing?: ReviewStatus,\n ) => ReviewStatus | Promise<ReviewStatus>;\n probeMergeability?: (workspacePath: string, targetBranch: string) => BranchMergeability | Promise<BranchMergeability>;\n dispatchResolver: (input: DispatchResolverInput) => ResolverDispatchState | void | Promise<ResolverDispatchState | void>;\n now?: () => Date;\n log?: (message: string) => void;\n}\n\ninterface RealConflictGateDepsOverrides {\n spawnRun?: typeof spawnRun;\n messageAgent?: typeof messageAgent;\n getReviewStatus?: typeof getReviewStatusSync;\n setReviewStatus?: typeof setReviewStatusSync;\n emitActivityEntry?: typeof emitActivityEntrySync;\n now?: () => Date;\n log?: (message: string) => void;\n}\n\nconst PROBE_CACHE_MAX_ENTRIES = 256;\n\nconst probeCache = new Map<string, { checkedAtMs: number; result: BranchMergeability }>();\nconst probeInFlight = new Map<string, Promise<BranchMergeability>>();\n\nexport interface BranchConflictProbeResult {\n mergeability: BranchMergeability;\n paths: string[];\n}\n\n/**\n * Non-destructively checks whether HEAD can merge with origin/<targetBranch>,\n * returning the conflicting paths (if any). Uses git merge-tree only — never\n * git merge — so HEAD, the index, and the working tree are left untouched.\n */\nexport async function probeBranchConflictPaths(\n workspacePath: string,\n targetBranch: string,\n deps: CheckBranchMergeabilityDeps = {},\n): Promise<BranchConflictProbeResult> {\n const run = deps.exec ?? execAsync;\n const options: ExecOptions = {\n cwd: workspacePath,\n encoding: 'utf-8',\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: GIT_MAX_BUFFER,\n };\n\n try {\n await run(`git fetch origin ${shellQuote(targetBranch)}`, options);\n } catch {\n return { mergeability: 'unknown', paths: [] };\n }\n\n try {\n const result = await run(\n `git merge-tree --write-tree --name-only HEAD ${shellQuote(`origin/${targetBranch}`)}`,\n options,\n );\n if (outputHasConflictMarker(result.stdout, result.stderr)) {\n return { mergeability: 'conflicts', paths: parseMergeTreeNameOnly(toText(result.stdout)) };\n }\n return { mergeability: 'clean', paths: [] };\n } catch (err) {\n if (mergeTreeFailureIndicatesConflict(err)) {\n const maybe = err as { stdout?: unknown };\n return { mergeability: 'conflicts', paths: parseMergeTreeNameOnly(toText(maybe.stdout)) };\n }\n return { mergeability: 'unknown', paths: [] };\n }\n}\n\n/**\n * Non-destructively checks whether HEAD can merge with origin/<targetBranch>.\n * Thin wrapper over probeBranchConflictPaths — kept for callers that only\n * need the tri-state result.\n */\nexport async function checkBranchMergeability(\n workspacePath: string,\n targetBranch: string,\n deps: CheckBranchMergeabilityDeps = {},\n): Promise<BranchMergeability> {\n return (await probeBranchConflictPaths(workspacePath, targetBranch, deps)).mergeability;\n}\n\n/**\n * Parses `git merge-tree --write-tree --name-only` stdout. Line 1 is the\n * tree OID; subsequent lines up to (not including) the first blank line are\n * the conflicting paths.\n */\nexport function parseMergeTreeNameOnly(stdout: string): string[] {\n const lines = stdout.split('\\n');\n const paths: string[] = [];\n for (let i = 1; i < lines.length; i += 1) {\n const line = lines[i];\n if (line === '') break;\n paths.push(line);\n }\n return paths;\n}\n\nexport function buildRealConflictGateDeps(overrides: RealConflictGateDepsOverrides = {}): ResolveConflictGateDeps {\n const runSpawn = overrides.spawnRun ?? spawnRun;\n const deliverMessage = overrides.messageAgent ?? messageAgent;\n const readStatus = overrides.getReviewStatus ?? getReviewStatusSync;\n const writeStatus = overrides.setReviewStatus ?? setReviewStatusSync;\n const emitActivity = overrides.emitActivityEntry ?? emitActivityEntrySync;\n const now = overrides.now ?? (() => new Date());\n\n return {\n getReviewStatus: readStatus,\n setReviewStatus: writeStatus,\n probeMergeability: checkBranchMergeability,\n now,\n log: overrides.log,\n dispatchResolver: async (input) => {\n const prompt = buildConflictResolverPrompt(input);\n let dispatchState: ResolverDispatchState = 'dispatched';\n try {\n await runSpawn(input.issueId, 'work', { prompt, startedBy: 'conflict-gate' });\n } catch (err) {\n if (isAlreadyRunningError(err)) {\n await deliverMessage(`agent-${input.issueId.toLowerCase()}`, prompt, 'conflict-gate');\n dispatchState = 'already_running';\n overrides.log?.(`[conflict-gate] ${input.issueId}: conflict resolver already running; delivered resolver prompt`);\n } else {\n throw err;\n }\n }\n\n const dispatchedAt = now().toISOString();\n const existing = readStatus(input.issueId) ?? undefined;\n writeStatus(input.issueId, { conflictResolutionDispatchedAt: dispatchedAt }, existing);\n emitActivity({\n source: 'review',\n level: 'info',\n issueId: input.issueId,\n message: dispatchState === 'already_running'\n ? `Review deferred — conflict resolver already running for ${input.issueId}; delivered resolver prompt`\n : `Review deferred — conflict resolver dispatched for ${input.issueId}`,\n details: input.reason,\n });\n return dispatchState;\n },\n };\n}\n\nexport async function resolveConflictGate(\n issueId: string,\n workspacePath: string,\n targetBranch: string,\n deps: ResolveConflictGateDeps,\n): Promise<ConflictGateResult> {\n const status = await deps.getReviewStatus(issueId);\n const mergeBlockers = (status?.blockerReasons ?? []).filter(isMergeBlocker);\n if (!status || mergeBlockers.length === 0) return { gated: false };\n\n const now = deps.now ?? (() => new Date());\n const checkedAtMs = now().getTime();\n const mergeability = await getCachedMergeability(issueId, checkedAtMs, workspacePath, targetBranch, deps);\n\n if (mergeability === 'clean') {\n const remainingBlockers = (status.blockerReasons ?? []).filter((blocker) => !isMergeBlocker(blocker));\n await deps.setReviewStatus(\n issueId,\n {\n blockerReasons: remainingBlockers.length > 0 ? remainingBlockers : undefined,\n },\n status,\n );\n deps.log?.(`[conflict-gate] ${issueId}: cleared stale merge blocker; review can proceed`);\n return { gated: false, clearedStaleBlocker: true };\n }\n\n const reason = mergeability === 'conflicts'\n ? `merge conflict with ${targetBranch} must be resolved before review dispatch`\n : `mergeability against ${targetBranch} could not be verified; deferring review conservatively`;\n\n let resolverDispatchState: ResolverDispatchState;\n if (!isResolverDispatchThrottled(status, mergeBlockers, checkedAtMs)) {\n resolverDispatchState = await deps.dispatchResolver({\n issueId,\n workspacePath,\n targetBranch,\n blockerReasons: mergeBlockers,\n reason,\n }) ?? 'dispatched';\n } else {\n resolverDispatchState = 'throttled';\n deps.log?.(`[conflict-gate] ${issueId}: conflict resolver dispatch is throttled`);\n }\n\n return {\n gated: true,\n reason: `${reason}; ${formatResolverDispatchState(resolverDispatchState)}`,\n resolverDispatchState,\n };\n}\n\nexport function __resetConflictGateProbeCacheForTests(): void {\n probeCache.clear();\n probeInFlight.clear();\n}\n\n/**\n * Synchronous, non-blocking read of the conflict-gate probe cache.\n * Returns the cached mergeability result only when a fresh value is available;\n * returns undefined when the cache is absent or stale. This function performs\n * NO I/O and NO status writes so it is safe on HTTP request hot paths.\n */\nexport function getCachedConflictGateMergeability(\n issueId: string,\n): BranchMergeability | undefined {\n const nowMs = Date.now();\n sweepExpiredProbeCache(nowMs);\n const key = issueId.toUpperCase();\n const cached = probeCache.get(key);\n if (cached && nowMs - cached.checkedAtMs < PROBE_CACHE_MS) return cached.result;\n return undefined;\n}\n\nasync function getCachedMergeability(\n issueId: string,\n nowMs: number,\n workspacePath: string,\n targetBranch: string,\n deps: ResolveConflictGateDeps,\n): Promise<BranchMergeability> {\n const key = issueId.toUpperCase();\n sweepExpiredProbeCache(nowMs);\n const cached = probeCache.get(key);\n if (cached && nowMs - cached.checkedAtMs < PROBE_CACHE_MS) return cached.result;\n\n const existing = probeInFlight.get(key);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const result = await (deps.probeMergeability ?? checkBranchMergeability)(workspacePath, targetBranch);\n // Move to end for LRU ordering, then enforce size cap.\n probeCache.delete(key);\n probeCache.set(key, { checkedAtMs: nowMs, result });\n enforceProbeCacheSizeLimit();\n return result;\n } finally {\n probeInFlight.delete(key);\n }\n })();\n\n probeInFlight.set(key, promise);\n return promise;\n}\n\nfunction isResolverDispatchThrottled(\n status: ReviewStatus,\n mergeBlockers: BlockerReason[],\n nowMs: number,\n): boolean {\n if (!status.conflictResolutionDispatchedAt) return false;\n const dispatchedAtMs = Date.parse(status.conflictResolutionDispatchedAt);\n if (!Number.isFinite(dispatchedAtMs)) return false;\n\n const newestBlockerMs = Math.max(\n ...mergeBlockers\n .map((blocker) => Date.parse(blocker.detectedAt))\n .filter((detectedAtMs) => Number.isFinite(detectedAtMs)),\n );\n if (Number.isFinite(newestBlockerMs) && dispatchedAtMs < newestBlockerMs) return false;\n\n return nowMs - dispatchedAtMs < DISPATCH_THROTTLE_MS;\n}\n\nfunction formatResolverDispatchState(state: ResolverDispatchState): string {\n switch (state) {\n case 'already_running':\n return 'conflict resolver already running; resolver prompt delivered';\n case 'throttled':\n return 'conflict resolver dispatch throttled';\n case 'dispatched':\n return 'conflict resolver dispatched';\n }\n}\n\nfunction sweepExpiredProbeCache(nowMs: number): void {\n for (const [key, cached] of probeCache.entries()) {\n if (nowMs - cached.checkedAtMs >= PROBE_CACHE_MS) probeCache.delete(key);\n }\n}\n\nfunction enforceProbeCacheSizeLimit(): void {\n while (probeCache.size > PROBE_CACHE_MAX_ENTRIES) {\n const first = probeCache.keys().next().value;\n if (first) probeCache.delete(first);\n }\n}\n\nfunction isMergeBlocker(blocker: BlockerReason): boolean {\n return MERGE_BLOCKER_TYPES.has(blocker.type);\n}\n\nfunction buildConflictResolverPrompt(input: DispatchResolverInput): string {\n const blockerSummary = input.blockerReasons\n .map((blocker) => `- ${blocker.type}: ${blocker.summary} (detected ${blocker.detectedAt})`)\n .join('\\n');\n\n return [\n `Review for ${input.issueId} was deferred because the feature branch has a standing merge conflict with origin/${input.targetBranch}.`,\n '',\n 'Resolve the conflict WITHOUT degrading either side of the change:',\n `1. Read what origin/${input.targetBranch} changed and what ${input.issueId} intended before editing.`,\n `2. Run pan sync-main ${input.issueId} to merge origin/${input.targetBranch} through the repository-approved sync path, then resolve every conflict so BOTH intents are preserved.`,\n '3. Build, run the relevant tests, and commit the resolved state.',\n '4. Re-request review when the branch is clean (use pan done or pan review request as appropriate for this workspace).',\n '',\n 'Do not blindly accept one side of a conflict — understand both changesets first.',\n '',\n 'Current blocker details:',\n blockerSummary || '- mergeability blocker present',\n ].join('\\n');\n}\n\nfunction isAlreadyRunningError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /already running/i.test(message);\n}\n\nfunction mergeTreeFailureIndicatesConflict(err: unknown): boolean {\n const maybe = err as { code?: unknown; stdout?: unknown; stderr?: unknown; message?: unknown };\n const output = [maybe.stdout, maybe.stderr, maybe.message].map(toText).join('\\n');\n\n if (outputHasConflictMarker(output, '')) return true;\n\n // `git merge-tree --write-tree` exits 1 for merge conflicts. Treat fatal/git\n // usage failures as unknown so missing workspaces, old git, and bad refs do not\n // masquerade as real branch conflicts.\n return maybe.code === 1 && !/\\b(fatal|usage:|unknown option|not a git repository)\\b/i.test(output);\n}\n\nfunction outputHasConflictMarker(stdout: unknown, stderr: unknown): boolean {\n const output = `${toText(stdout)}\\n${toText(stderr)}`;\n return /\\bCONFLICT\\b/i.test(output) || /contains conflicts/i.test(output);\n}\n\nfunction toText(value: unknown): string {\n if (typeof value === 'string') return value;\n if (Buffer.isBuffer(value)) return value.toString('utf-8');\n if (value instanceof Error) return value.message;\n return value == null ? '' : String(value);\n}\n\nfunction shellQuote(value: string): string {\n if (/^[A-Za-z0-9._/@:-]+$/.test(value)) return value;\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n"],"mappings":";;;;;;sBAE8D;gBACR;YACR;oBACiG;AAE/I,MAAM,YAAY,UAAU,KAAK;AACjC,MAAM,iBAAiB;AACvB,MAAM,iBAAiB,IAAI,OAAO;AAClC,MAAM,sBAAsB,IAAI,IAA2B,CAAC,kBAAkB,gBAAgB,CAAC;AAC/F,MAAM,iBAAiB,MAAS;AAChC,MAAM,uBAAuB,OAAU;AAuDvC,MAAM,0BAA0B;AAEhC,MAAM,6BAAa,IAAI,KAAkE;AACzF,MAAM,gCAAgB,IAAI,KAA0C;;;;;;AAYpE,eAAsB,yBACpB,eACA,cACA,OAAoC,EAAE,EACF;CACpC,MAAM,MAAM,KAAK,QAAQ;CACzB,MAAM,UAAuB;EAC3B,KAAK;EACL,UAAU;EACV,SAAS;EACT,WAAW;EACZ;AAED,KAAI;AACF,QAAM,IAAI,oBAAoB,WAAW,aAAa,IAAI,QAAQ;SAC5D;AACN,SAAO;GAAE,cAAc;GAAW,OAAO,EAAE;GAAE;;AAG/C,KAAI;EACF,MAAM,SAAS,MAAM,IACnB,gDAAgD,WAAW,UAAU,eAAe,IACpF,QACD;AACD,MAAI,wBAAwB,OAAO,QAAQ,OAAO,OAAO,CACvD,QAAO;GAAE,cAAc;GAAa,OAAO,uBAAuB,OAAO,OAAO,OAAO,CAAC;GAAE;AAE5F,SAAO;GAAE,cAAc;GAAS,OAAO,EAAE;GAAE;UACpC,KAAK;AACZ,MAAI,kCAAkC,IAAI,CAExC,QAAO;GAAE,cAAc;GAAa,OAAO,uBAAuB,OAAOA,IAAM,OAAO,CAAC;GAAE;AAE3F,SAAO;GAAE,cAAc;GAAW,OAAO,EAAE;GAAE;;;;;;;;AASjD,eAAsB,wBACpB,eACA,cACA,OAAoC,EAAE,EACT;AAC7B,SAAQ,MAAM,yBAAyB,eAAe,cAAc,KAAK,EAAE;;;;;;;AAQ7E,SAAgB,uBAAuB,QAA0B;CAC/D,MAAM,QAAQ,OAAO,MAAM,KAAK;CAChC,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxC,MAAM,OAAO,MAAM;AACnB,MAAI,SAAS,GAAI;AACjB,QAAM,KAAK,KAAK;;AAElB,QAAO;;AAGT,SAAgB,0BAA0B,YAA2C,EAAE,EAA2B;CAChH,MAAM,WAAW,UAAU,YAAY;CACvC,MAAM,iBAAiB,UAAU,gBAAgB;CACjD,MAAM,aAAa,UAAU,mBAAmB;CAChD,MAAM,cAAc,UAAU,mBAAmB;CACjD,MAAM,eAAe,UAAU,qBAAqB;CACpD,MAAM,MAAM,UAAU,8BAAc,IAAI,MAAM;AAE9C,QAAO;EACL,iBAAiB;EACjB,iBAAiB;EACjB,mBAAmB;EACnB;EACA,KAAK,UAAU;EACf,kBAAkB,OAAO,UAAU;GACjC,MAAM,SAAS,4BAA4B,MAAM;GACjD,IAAI,gBAAuC;AAC3C,OAAI;AACF,UAAM,SAAS,MAAM,SAAS,QAAQ;KAAE;KAAQ,WAAW;KAAiB,CAAC;YACtE,KAAK;AACZ,QAAI,sBAAsB,IAAI,EAAE;AAC9B,WAAM,eAAe,SAAS,MAAM,QAAQ,aAAa,IAAI,QAAQ,gBAAgB;AACrF,qBAAgB;AAChB,eAAU,MAAM,mBAAmB,MAAM,QAAQ,gEAAgE;UAEjH,OAAM;;GAIV,MAAM,eAAe,KAAK,CAAC,aAAa;GACxC,MAAM,WAAW,WAAW,MAAM,QAAQ,IAAI,KAAA;AAC9C,eAAY,MAAM,SAAS,EAAE,gCAAgC,cAAc,EAAE,SAAS;AACtF,gBAAa;IACX,QAAQ;IACR,OAAO;IACP,SAAS,MAAM;IACf,SAAS,kBAAkB,oBACvB,2DAA2D,MAAM,QAAQ,+BACzE,sDAAsD,MAAM;IAChE,SAAS,MAAM;IAChB,CAAC;AACF,UAAO;;EAEV;;AAGH,eAAsB,oBACpB,SACA,eACA,cACA,MAC6B;CAC7B,MAAM,SAAS,MAAM,KAAK,gBAAgB,QAAQ;CAClD,MAAM,iBAAiB,QAAQ,kBAAkB,EAAE,EAAE,OAAO,eAAe;AAC3E,KAAI,CAAC,UAAU,cAAc,WAAW,EAAG,QAAO,EAAE,OAAO,OAAO;CAGlE,MAAM,eADM,KAAK,8BAAc,IAAI,MAAM,IAChB,CAAC,SAAS;CACnC,MAAM,eAAe,MAAM,sBAAsB,SAAS,aAAa,eAAe,cAAc,KAAK;AAEzG,KAAI,iBAAiB,SAAS;EAC5B,MAAM,qBAAqB,OAAO,kBAAkB,EAAE,EAAE,QAAQ,YAAY,CAAC,eAAe,QAAQ,CAAC;AACrG,QAAM,KAAK,gBACT,SACA,EACE,gBAAgB,kBAAkB,SAAS,IAAI,oBAAoB,KAAA,GACpE,EACD,OACD;AACD,OAAK,MAAM,mBAAmB,QAAQ,mDAAmD;AACzF,SAAO;GAAE,OAAO;GAAO,qBAAqB;GAAM;;CAGpD,MAAM,SAAS,iBAAiB,cAC5B,uBAAuB,aAAa,4CACpC,wBAAwB,aAAa;CAEzC,IAAI;AACJ,KAAI,CAAC,4BAA4B,QAAQ,eAAe,YAAY,CAClE,yBAAwB,MAAM,KAAK,iBAAiB;EAClD;EACA;EACA;EACA,gBAAgB;EAChB;EACD,CAAC,IAAI;MACD;AACL,0BAAwB;AACxB,OAAK,MAAM,mBAAmB,QAAQ,2CAA2C;;AAGnF,QAAO;EACL,OAAO;EACP,QAAQ,GAAG,OAAO,IAAI,4BAA4B,sBAAsB;EACxE;EACD;;;;;;;;AAcH,SAAgB,kCACd,SACgC;CAChC,MAAM,QAAQ,KAAK,KAAK;AACxB,wBAAuB,MAAM;CAC7B,MAAM,MAAM,QAAQ,aAAa;CACjC,MAAM,SAAS,WAAW,IAAI,IAAI;AAClC,KAAI,UAAU,QAAQ,OAAO,cAAc,eAAgB,QAAO,OAAO;;AAI3E,eAAe,sBACb,SACA,OACA,eACA,cACA,MAC6B;CAC7B,MAAM,MAAM,QAAQ,aAAa;AACjC,wBAAuB,MAAM;CAC7B,MAAM,SAAS,WAAW,IAAI,IAAI;AAClC,KAAI,UAAU,QAAQ,OAAO,cAAc,eAAgB,QAAO,OAAO;CAEzE,MAAM,WAAW,cAAc,IAAI,IAAI;AACvC,KAAI,SAAU,QAAO;CAErB,MAAM,WAAW,YAAY;AAC3B,MAAI;GACF,MAAM,SAAS,OAAO,KAAK,qBAAqB,yBAAyB,eAAe,aAAa;AAErG,cAAW,OAAO,IAAI;AACtB,cAAW,IAAI,KAAK;IAAE,aAAa;IAAO;IAAQ,CAAC;AACnD,+BAA4B;AAC5B,UAAO;YACC;AACR,iBAAc,OAAO,IAAI;;KAEzB;AAEJ,eAAc,IAAI,KAAK,QAAQ;AAC/B,QAAO;;AAGT,SAAS,4BACP,QACA,eACA,OACS;AACT,KAAI,CAAC,OAAO,+BAAgC,QAAO;CACnD,MAAM,iBAAiB,KAAK,MAAM,OAAO,+BAA+B;AACxE,KAAI,CAAC,OAAO,SAAS,eAAe,CAAE,QAAO;CAE7C,MAAM,kBAAkB,KAAK,IAC3B,GAAG,cACA,KAAK,YAAY,KAAK,MAAM,QAAQ,WAAW,CAAC,CAChD,QAAQ,iBAAiB,OAAO,SAAS,aAAa,CAAC,CAC3D;AACD,KAAI,OAAO,SAAS,gBAAgB,IAAI,iBAAiB,gBAAiB,QAAO;AAEjF,QAAO,QAAQ,iBAAiB;;AAGlC,SAAS,4BAA4B,OAAsC;AACzE,SAAQ,OAAR;EACE,KAAK,kBACH,QAAO;EACT,KAAK,YACH,QAAO;EACT,KAAK,aACH,QAAO;;;AAIb,SAAS,uBAAuB,OAAqB;AACnD,MAAK,MAAM,CAAC,KAAK,WAAW,WAAW,SAAS,CAC9C,KAAI,QAAQ,OAAO,eAAe,eAAgB,YAAW,OAAO,IAAI;;AAI5E,SAAS,6BAAmC;AAC1C,QAAO,WAAW,OAAO,yBAAyB;EAChD,MAAM,QAAQ,WAAW,MAAM,CAAC,MAAM,CAAC;AACvC,MAAI,MAAO,YAAW,OAAO,MAAM;;;AAIvC,SAAS,eAAe,SAAiC;AACvD,QAAO,oBAAoB,IAAI,QAAQ,KAAK;;AAG9C,SAAS,4BAA4B,OAAsC;CACzE,MAAM,iBAAiB,MAAM,eAC1B,KAAK,YAAY,KAAK,QAAQ,KAAK,IAAI,QAAQ,QAAQ,aAAa,QAAQ,WAAW,GAAG,CAC1F,KAAK,KAAK;AAEb,QAAO;EACL,cAAc,MAAM,QAAQ,qFAAqF,MAAM,aAAa;EACpI;EACA;EACA,uBAAuB,MAAM,aAAa,oBAAoB,MAAM,QAAQ;EAC5E,wBAAwB,MAAM,QAAQ,mBAAmB,MAAM,aAAa;EAC5E;EACA;EACA;EACA;EACA;EACA;EACA,kBAAkB;EACnB,CAAC,KAAK,KAAK;;AAGd,SAAS,sBAAsB,KAAuB;CACpD,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,QAAO,mBAAmB,KAAK,QAAQ;;AAGzC,SAAS,kCAAkC,KAAuB;CAChE,MAAM,QAAQ;CACd,MAAM,SAAS;EAAC,MAAM;EAAQ,MAAM;EAAQ,MAAM;EAAQ,CAAC,IAAI,OAAO,CAAC,KAAK,KAAK;AAEjF,KAAI,wBAAwB,QAAQ,GAAG,CAAE,QAAO;AAKhD,QAAO,MAAM,SAAS,KAAK,CAAC,0DAA0D,KAAK,OAAO;;AAGpG,SAAS,wBAAwB,QAAiB,QAA0B;CAC1E,MAAM,SAAS,GAAG,OAAO,OAAO,CAAC,IAAI,OAAO,OAAO;AACnD,QAAO,gBAAgB,KAAK,OAAO,IAAI,sBAAsB,KAAK,OAAO;;AAG3E,SAAS,OAAO,OAAwB;AACtC,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,OAAO,SAAS,MAAM,CAAE,QAAO,MAAM,SAAS,QAAQ;AAC1D,KAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,QAAO,SAAS,OAAO,KAAK,OAAO,MAAM;;AAG3C,SAAS,WAAW,OAAuB;AACzC,KAAI,uBAAuB,KAAK,MAAM,CAAE,QAAO;AAC/C,QAAO,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"conflict-gate-CGQTcR0I.js","names":["maybe"],"sources":["../../src/lib/cloister/conflict-gate.ts"],"sourcesContent":["import { exec, type ExecOptions } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { messageAgent } from '../agents/messaging.js';\nimport { spawnRun } from '../agents/spawn.js';\nimport { getReviewStatusSync, setReviewStatusSync, type BlockerReason, type ReviewStatus, type ReviewStatusUpdate } from '../review-status.js';\n\nconst execAsync = promisify(exec);\nconst GIT_TIMEOUT_MS = 30_000;\nconst GIT_MAX_BUFFER = 4 * 1024 * 1024;\nconst MERGE_BLOCKER_TYPES = new Set<BlockerReason['type']>(['merge_conflict', 'not_mergeable']);\nconst PROBE_CACHE_MS = 3 * 60 * 1000;\nconst DISPATCH_THROTTLE_MS = 30 * 60 * 1000;\n\nexport type BranchMergeability = 'clean' | 'conflicts' | 'unknown';\n\nexport interface ExecResult {\n stdout: string | Buffer;\n stderr: string | Buffer;\n}\n\nexport type ExecRunner = (command: string, options: ExecOptions) => Promise<ExecResult>;\n\nexport interface CheckBranchMergeabilityDeps {\n exec?: ExecRunner;\n}\n\nexport type ResolverDispatchState = 'dispatched' | 'already_running' | 'throttled';\n\nexport interface ConflictGateResult {\n gated: boolean;\n reason?: string;\n clearedStaleBlocker?: boolean;\n resolverDispatchState?: ResolverDispatchState;\n}\n\nexport interface DispatchResolverInput {\n issueId: string;\n workspacePath: string;\n targetBranch: string;\n blockerReasons: BlockerReason[];\n reason: string;\n}\n\nexport interface ResolveConflictGateDeps {\n getReviewStatus: (issueId: string) => ReviewStatus | null | Promise<ReviewStatus | null>;\n setReviewStatus: (\n issueId: string,\n update: ReviewStatusUpdate,\n existing?: ReviewStatus,\n ) => ReviewStatus | Promise<ReviewStatus>;\n probeMergeability?: (workspacePath: string, targetBranch: string) => BranchMergeability | Promise<BranchMergeability>;\n dispatchResolver: (input: DispatchResolverInput) => ResolverDispatchState | void | Promise<ResolverDispatchState | void>;\n now?: () => Date;\n log?: (message: string) => void;\n}\n\ninterface RealConflictGateDepsOverrides {\n spawnRun?: typeof spawnRun;\n messageAgent?: typeof messageAgent;\n getReviewStatus?: typeof getReviewStatusSync;\n setReviewStatus?: typeof setReviewStatusSync;\n emitActivityEntry?: typeof emitActivityEntrySync;\n now?: () => Date;\n log?: (message: string) => void;\n}\n\nconst PROBE_CACHE_MAX_ENTRIES = 256;\n\nconst probeCache = new Map<string, { checkedAtMs: number; result: BranchMergeability }>();\nconst probeInFlight = new Map<string, Promise<BranchMergeability>>();\n\nexport interface BranchConflictProbeResult {\n mergeability: BranchMergeability;\n paths: string[];\n}\n\n/**\n * Non-destructively checks whether HEAD can merge with origin/<targetBranch>,\n * returning the conflicting paths (if any). Uses git merge-tree only — never\n * git merge — so HEAD, the index, and the working tree are left untouched.\n */\nexport async function probeBranchConflictPaths(\n workspacePath: string,\n targetBranch: string,\n deps: CheckBranchMergeabilityDeps = {},\n): Promise<BranchConflictProbeResult> {\n const run = deps.exec ?? execAsync;\n const options: ExecOptions = {\n cwd: workspacePath,\n encoding: 'utf-8',\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: GIT_MAX_BUFFER,\n };\n\n try {\n await run(`git fetch origin ${shellQuote(targetBranch)}`, options);\n } catch {\n return { mergeability: 'unknown', paths: [] };\n }\n\n try {\n const result = await run(\n `git merge-tree --write-tree --name-only HEAD ${shellQuote(`origin/${targetBranch}`)}`,\n options,\n );\n if (outputHasConflictMarker(result.stdout, result.stderr)) {\n return { mergeability: 'conflicts', paths: parseMergeTreeNameOnly(toText(result.stdout)) };\n }\n return { mergeability: 'clean', paths: [] };\n } catch (err) {\n if (mergeTreeFailureIndicatesConflict(err)) {\n const maybe = err as { stdout?: unknown };\n return { mergeability: 'conflicts', paths: parseMergeTreeNameOnly(toText(maybe.stdout)) };\n }\n return { mergeability: 'unknown', paths: [] };\n }\n}\n\n/**\n * Non-destructively checks whether HEAD can merge with origin/<targetBranch>.\n * Thin wrapper over probeBranchConflictPaths — kept for callers that only\n * need the tri-state result.\n */\nexport async function checkBranchMergeability(\n workspacePath: string,\n targetBranch: string,\n deps: CheckBranchMergeabilityDeps = {},\n): Promise<BranchMergeability> {\n return (await probeBranchConflictPaths(workspacePath, targetBranch, deps)).mergeability;\n}\n\n/**\n * Parses `git merge-tree --write-tree --name-only` stdout. Line 1 is the\n * tree OID; subsequent lines up to (not including) the first blank line are\n * the conflicting paths.\n */\nexport function parseMergeTreeNameOnly(stdout: string): string[] {\n const lines = stdout.split('\\n');\n const paths: string[] = [];\n for (let i = 1; i < lines.length; i += 1) {\n const line = lines[i];\n if (line === '') break;\n paths.push(line);\n }\n return paths;\n}\n\nexport function buildRealConflictGateDeps(overrides: RealConflictGateDepsOverrides = {}): ResolveConflictGateDeps {\n const runSpawn = overrides.spawnRun ?? spawnRun;\n const deliverMessage = overrides.messageAgent ?? messageAgent;\n const readStatus = overrides.getReviewStatus ?? getReviewStatusSync;\n const writeStatus = overrides.setReviewStatus ?? setReviewStatusSync;\n const emitActivity = overrides.emitActivityEntry ?? emitActivityEntrySync;\n const now = overrides.now ?? (() => new Date());\n\n return {\n getReviewStatus: readStatus,\n setReviewStatus: writeStatus,\n probeMergeability: checkBranchMergeability,\n now,\n log: overrides.log,\n dispatchResolver: async (input) => {\n const prompt = buildConflictResolverPrompt(input);\n let dispatchState: ResolverDispatchState = 'dispatched';\n try {\n await runSpawn(input.issueId, 'work', { prompt, startedBy: 'conflict-gate' });\n } catch (err) {\n if (isAlreadyRunningError(err)) {\n await deliverMessage(`agent-${input.issueId.toLowerCase()}`, prompt, 'conflict-gate');\n dispatchState = 'already_running';\n overrides.log?.(`[conflict-gate] ${input.issueId}: conflict resolver already running; delivered resolver prompt`);\n } else {\n throw err;\n }\n }\n\n const dispatchedAt = now().toISOString();\n const existing = readStatus(input.issueId) ?? undefined;\n writeStatus(input.issueId, { conflictResolutionDispatchedAt: dispatchedAt }, existing);\n emitActivity({\n source: 'review',\n level: 'info',\n issueId: input.issueId,\n message: dispatchState === 'already_running'\n ? `Review deferred — conflict resolver already running for ${input.issueId}; delivered resolver prompt`\n : `Review deferred — conflict resolver dispatched for ${input.issueId}`,\n details: input.reason,\n });\n return dispatchState;\n },\n };\n}\n\nexport async function resolveConflictGate(\n issueId: string,\n workspacePath: string,\n targetBranch: string,\n deps: ResolveConflictGateDeps,\n): Promise<ConflictGateResult> {\n const status = await deps.getReviewStatus(issueId);\n const mergeBlockers = (status?.blockerReasons ?? []).filter(isMergeBlocker);\n if (!status || mergeBlockers.length === 0) return { gated: false };\n\n const now = deps.now ?? (() => new Date());\n const checkedAtMs = now().getTime();\n const mergeability = await getCachedMergeability(issueId, checkedAtMs, workspacePath, targetBranch, deps);\n\n if (mergeability === 'clean') {\n const remainingBlockers = (status.blockerReasons ?? []).filter((blocker) => !isMergeBlocker(blocker));\n await deps.setReviewStatus(\n issueId,\n {\n blockerReasons: remainingBlockers.length > 0 ? remainingBlockers : undefined,\n },\n status,\n );\n deps.log?.(`[conflict-gate] ${issueId}: cleared stale merge blocker; review can proceed`);\n return { gated: false, clearedStaleBlocker: true };\n }\n\n const reason = mergeability === 'conflicts'\n ? `merge conflict with ${targetBranch} must be resolved before review dispatch`\n : `mergeability against ${targetBranch} could not be verified; deferring review conservatively`;\n\n let resolverDispatchState: ResolverDispatchState;\n if (!isResolverDispatchThrottled(status, mergeBlockers, checkedAtMs)) {\n resolverDispatchState = await deps.dispatchResolver({\n issueId,\n workspacePath,\n targetBranch,\n blockerReasons: mergeBlockers,\n reason,\n }) ?? 'dispatched';\n } else {\n resolverDispatchState = 'throttled';\n deps.log?.(`[conflict-gate] ${issueId}: conflict resolver dispatch is throttled`);\n }\n\n return {\n gated: true,\n reason: `${reason}; ${formatResolverDispatchState(resolverDispatchState)}`,\n resolverDispatchState,\n };\n}\n\nexport function __resetConflictGateProbeCacheForTests(): void {\n probeCache.clear();\n probeInFlight.clear();\n}\n\n/**\n * Synchronous, non-blocking read of the conflict-gate probe cache.\n * Returns the cached mergeability result only when a fresh value is available;\n * returns undefined when the cache is absent or stale. This function performs\n * NO I/O and NO status writes so it is safe on HTTP request hot paths.\n */\nexport function getCachedConflictGateMergeability(\n issueId: string,\n): BranchMergeability | undefined {\n const nowMs = Date.now();\n sweepExpiredProbeCache(nowMs);\n const key = issueId.toUpperCase();\n const cached = probeCache.get(key);\n if (cached && nowMs - cached.checkedAtMs < PROBE_CACHE_MS) return cached.result;\n return undefined;\n}\n\nasync function getCachedMergeability(\n issueId: string,\n nowMs: number,\n workspacePath: string,\n targetBranch: string,\n deps: ResolveConflictGateDeps,\n): Promise<BranchMergeability> {\n const key = issueId.toUpperCase();\n sweepExpiredProbeCache(nowMs);\n const cached = probeCache.get(key);\n if (cached && nowMs - cached.checkedAtMs < PROBE_CACHE_MS) return cached.result;\n\n const existing = probeInFlight.get(key);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const result = await (deps.probeMergeability ?? checkBranchMergeability)(workspacePath, targetBranch);\n // Move to end for LRU ordering, then enforce size cap.\n probeCache.delete(key);\n probeCache.set(key, { checkedAtMs: nowMs, result });\n enforceProbeCacheSizeLimit();\n return result;\n } finally {\n probeInFlight.delete(key);\n }\n })();\n\n probeInFlight.set(key, promise);\n return promise;\n}\n\nfunction isResolverDispatchThrottled(\n status: ReviewStatus,\n mergeBlockers: BlockerReason[],\n nowMs: number,\n): boolean {\n if (!status.conflictResolutionDispatchedAt) return false;\n const dispatchedAtMs = Date.parse(status.conflictResolutionDispatchedAt);\n if (!Number.isFinite(dispatchedAtMs)) return false;\n\n const newestBlockerMs = Math.max(\n ...mergeBlockers\n .map((blocker) => Date.parse(blocker.detectedAt))\n .filter((detectedAtMs) => Number.isFinite(detectedAtMs)),\n );\n if (Number.isFinite(newestBlockerMs) && dispatchedAtMs < newestBlockerMs) return false;\n\n return nowMs - dispatchedAtMs < DISPATCH_THROTTLE_MS;\n}\n\nfunction formatResolverDispatchState(state: ResolverDispatchState): string {\n switch (state) {\n case 'already_running':\n return 'conflict resolver already running; resolver prompt delivered';\n case 'throttled':\n return 'conflict resolver dispatch throttled';\n case 'dispatched':\n return 'conflict resolver dispatched';\n }\n}\n\nfunction sweepExpiredProbeCache(nowMs: number): void {\n for (const [key, cached] of probeCache.entries()) {\n if (nowMs - cached.checkedAtMs >= PROBE_CACHE_MS) probeCache.delete(key);\n }\n}\n\nfunction enforceProbeCacheSizeLimit(): void {\n while (probeCache.size > PROBE_CACHE_MAX_ENTRIES) {\n const first = probeCache.keys().next().value;\n if (first) probeCache.delete(first);\n }\n}\n\nfunction isMergeBlocker(blocker: BlockerReason): boolean {\n return MERGE_BLOCKER_TYPES.has(blocker.type);\n}\n\nfunction buildConflictResolverPrompt(input: DispatchResolverInput): string {\n const blockerSummary = input.blockerReasons\n .map((blocker) => `- ${blocker.type}: ${blocker.summary} (detected ${blocker.detectedAt})`)\n .join('\\n');\n\n return [\n `Review for ${input.issueId} was deferred because the feature branch has a standing merge conflict with origin/${input.targetBranch}.`,\n '',\n 'Resolve the conflict WITHOUT degrading either side of the change:',\n `1. Read what origin/${input.targetBranch} changed and what ${input.issueId} intended before editing.`,\n `2. Run pan sync-main ${input.issueId} to merge origin/${input.targetBranch} through the repository-approved sync path, then resolve every conflict so BOTH intents are preserved.`,\n '3. Build, run the relevant tests, and commit the resolved state.',\n '4. Re-request review when the branch is clean (use pan done or pan review request as appropriate for this workspace).',\n '',\n 'Do not blindly accept one side of a conflict — understand both changesets first.',\n '',\n 'Current blocker details:',\n blockerSummary || '- mergeability blocker present',\n ].join('\\n');\n}\n\nfunction isAlreadyRunningError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /already running/i.test(message);\n}\n\nfunction mergeTreeFailureIndicatesConflict(err: unknown): boolean {\n const maybe = err as { code?: unknown; stdout?: unknown; stderr?: unknown; message?: unknown };\n const output = [maybe.stdout, maybe.stderr, maybe.message].map(toText).join('\\n');\n\n if (outputHasConflictMarker(output, '')) return true;\n\n // `git merge-tree --write-tree` exits 1 for merge conflicts. Treat fatal/git\n // usage failures as unknown so missing workspaces, old git, and bad refs do not\n // masquerade as real branch conflicts.\n return maybe.code === 1 && !/\\b(fatal|usage:|unknown option|not a git repository)\\b/i.test(output);\n}\n\nfunction outputHasConflictMarker(stdout: unknown, stderr: unknown): boolean {\n const output = `${toText(stdout)}\\n${toText(stderr)}`;\n return /\\bCONFLICT\\b/i.test(output) || /contains conflicts/i.test(output);\n}\n\nfunction toText(value: unknown): string {\n if (typeof value === 'string') return value;\n if (Buffer.isBuffer(value)) return value.toString('utf-8');\n if (value instanceof Error) return value.message;\n return value == null ? '' : String(value);\n}\n\nfunction shellQuote(value: string): string {\n if (/^[A-Za-z0-9._/@:-]+$/.test(value)) return value;\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime-DNCzzKnI.js";
2
2
  import { p as loadConfigSync, t as init_config_yaml } from "./config-yaml-Dce9VUd6.js";
3
3
  import { a as init_event_store, n as getEventStore } from "./event-store-BYOh686E.js";
4
- import { Yi as getProviderExportsForModel, q as init_agents, si as getAgentRuntimeBaseCommand } from "./agent-state-Ci6PhjOE.js";
4
+ import { Zi as getProviderExportsForModel, li as getAgentRuntimeBaseCommand, q as init_agents } from "./agent-state-B196fj1h.js";
5
5
  import { n as isBackgroundFeatureEnabled, t as init_features } from "./features-B1O5F33h.js";
6
- import { d as findLastCompactBoundary, t as init_conversation_service } from "./conversation-service-6LaymUV-.js";
7
- import { r as init_smart_compaction, t as generateSmartSummary } from "./smart-compaction-C96F2aFQ.js";
8
- import { a as generateFallbackSummary, l as init_summary_fork } from "./summary-fork-D-cylknJ.js";
6
+ import { f as findLastCompactBoundary, t as init_conversation_service } from "./conversation-service-CsOpwI9Y.js";
7
+ import { r as init_smart_compaction, t as generateSmartSummary } from "./smart-compaction-BzDfF6yb.js";
8
+ import { a as generateFallbackSummary, l as init_summary_fork } from "./summary-fork-lC2vMXly.js";
9
9
  import { dirname, join } from "node:path";
10
10
  import { Effect } from "effect";
11
11
  import { existsSync } from "node:fs";
@@ -179,4 +179,4 @@ var init_conversation_compaction = __esmMin((() => {
179
179
  //#endregion
180
180
  export { init_conversation_compaction as a, shouldInterceptManualCompact as c, getConversationCompactionSettings as i, compactConversationNative as n, isCompacting as o, estimateContextTokens as r, maybeCompactBeforeRespawn as s, buildCompactionRuntimeInfo as t };
181
181
 
182
- //# sourceMappingURL=conversation-compaction-CsdCbcQB.js.map
182
+ //# sourceMappingURL=conversation-compaction-C7P6V8Iw.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-compaction-CsdCbcQB.js","names":[],"sources":["../../src/dashboard/server/services/conversation-compaction.ts"],"sourcesContent":["import { Effect } from 'effect';\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\nimport { findLastCompactBoundary } from './conversation-service.js';\nimport { generateSmartSummary } from '../../../lib/conversations/smart-compaction.js';\nimport { generateFallbackSummary } from '../../../lib/conversations/summary-fork.js';\nimport { loadConfigSync } from '../../../lib/config-yaml.js';\nimport { getAgentRuntimeBaseCommand, getProviderExportsForModel } from '../../../lib/agents.js';\nimport { getEventStore } from '../event-store.js';\nimport { isBackgroundFeatureEnabled } from '../../../lib/background-ai/features.js';\n\nconst activeCompactions = new Set<string>();\nexport function isCompacting(sessionFile: string): boolean {\n return activeCompactions.has(sessionFile);\n}\n\nexport interface NativeCompactionResult {\n summary: string;\n tokensBefore: number;\n boundaryUuid: string;\n model: string;\n forkedSessionId: string;\n forkedSessionFile: string;\n}\n\nexport interface MaybeCompactBeforeRespawnOptions {\n sessionFile: string | null | undefined;\n cwd: string;\n shouldCompact: boolean;\n}\n\nexport function getConversationCompactionSettings() {\n const { config } = loadConfigSync();\n return {\n model: config.conversations.compactionModel,\n manualCompactMode: config.conversations.manualCompactMode,\n richCompaction: config.conversations.richCompaction,\n };\n}\n\nexport function shouldInterceptManualCompact(message: string): boolean {\n const trimmed = message.trim();\n if (trimmed !== '/compact') return false;\n return getConversationCompactionSettings().manualCompactMode === 'overdeck-native';\n}\n\nexport async function estimateContextTokens(sessionFile: string | null | undefined): Promise<number> {\n if (!sessionFile || !existsSync(sessionFile)) return 0;\n try {\n const content = await readFile(sessionFile, 'utf-8');\n const lines = content.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n try {\n const entry = JSON.parse(line);\n const usage = entry?.message?.usage ?? entry?.usage;\n if (usage && typeof usage.input_tokens === 'number') {\n const total = (usage.input_tokens ?? 0)\n + (usage.cache_creation_input_tokens ?? 0)\n + (usage.cache_read_input_tokens ?? 0);\n if (total > 0) return total;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch (err) {\n console.warn(`[conversation-compaction] estimateContextTokens failed for ${sessionFile}:`, err);\n }\n return 0;\n}\n\nfunction buildContinuationSummary(summary: string, model: string): string {\n return [\n 'This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.',\n '',\n summary,\n '',\n `Overdeck native compaction model: ${model}`,\n '',\n 'Continue from this summary without redoing already-completed work.',\n ].join('\\n');\n}\n\nexport async function compactConversationNative(sessionFile: string, conversationName?: string): Promise<NativeCompactionResult> {\n if (!existsSync(sessionFile)) {\n throw new Error(`Session file not found: ${sessionFile}`);\n }\n activeCompactions.add(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: true } });\n }\n try {\n return await doCompact(sessionFile);\n } finally {\n activeCompactions.delete(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: false } });\n }\n }\n}\n\nasync function doCompact(sessionFile: string): Promise<NativeCompactionResult> {\n\n const settings = getConversationCompactionSettings();\n const tokensBefore = await estimateContextTokens(sessionFile);\n\n let summary: string;\n try {\n const result = await Effect.runPromise(generateSmartSummary({\n jsonlPath: sessionFile,\n model: settings.model,\n richMode: settings.richCompaction,\n mode: 'fork',\n }));\n summary = result.summary;\n } catch (error) {\n console.warn(`[conversation-compaction] Smart summary failed, falling back to heuristic:`, error);\n summary = await Effect.runPromise(generateFallbackSummary(sessionFile));\n }\n\n const continuation = buildContinuationSummary(summary, settings.model);\n const boundaryUuid = randomUUID();\n const timestamp = new Date().toISOString();\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n\n const entries = [\n JSON.stringify({\n parentUuid: null,\n isSidechain: false,\n type: 'system',\n subtype: 'compact_boundary',\n content: 'Conversation compacted',\n isMeta: false,\n timestamp,\n uuid: boundaryUuid,\n level: 'info',\n compactMetadata: {\n trigger: 'overdeck-native',\n preTokens: tokensBefore,\n model: settings.model,\n previousBoundaryOffset: boundaryOffset,\n },\n }),\n JSON.stringify({\n parentUuid: boundaryUuid,\n isSidechain: false,\n type: 'user',\n message: {\n role: 'user',\n content: continuation,\n },\n isVisibleInTranscriptOnly: true,\n isCompactSummary: true,\n uuid: randomUUID(),\n timestamp,\n }),\n ];\n\n const forkedSessionId = randomUUID();\n const forkedSessionFile = join(dirname(sessionFile), `${forkedSessionId}.jsonl`);\n await writeFile(forkedSessionFile, `${entries.join('\\n')}\\n`, 'utf-8');\n\n return {\n summary: continuation,\n tokensBefore,\n boundaryUuid,\n model: settings.model,\n forkedSessionId,\n forkedSessionFile,\n };\n}\n\nexport async function maybeCompactBeforeRespawn(\n opts: MaybeCompactBeforeRespawnOptions,\n): Promise<{ forkedSessionId: string; forkedSessionFile: string } | null> {\n if (!opts.sessionFile || !existsSync(opts.sessionFile)) return null;\n if (!opts.shouldCompact) {\n console.log('[conversation-compaction] Skipping compact (shouldCompact=false)');\n return null;\n }\n // Background AI gate: low-cost mode (or the summaryFork toggle) skips\n // automatic LLM compaction before respawn.\n if (!isBackgroundFeatureEnabled('summaryFork')) return null;\n\n const tokens = await estimateContextTokens(opts.sessionFile);\n console.log(`[conversation-compaction] Compacting before respawn (shouldCompact=true, tokens=${tokens})`);\n const result = await compactConversationNative(opts.sessionFile);\n return { forkedSessionId: result.forkedSessionId, forkedSessionFile: result.forkedSessionFile };\n}\n\nexport async function buildCompactionRuntimeInfo(model: string): Promise<{ command: string; exports: string }> {\n return {\n command: await getAgentRuntimeBaseCommand(model),\n exports: await getProviderExportsForModel(model),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"conversation-compaction-C7P6V8Iw.js","names":[],"sources":["../../src/dashboard/server/services/conversation-compaction.ts"],"sourcesContent":["import { Effect } from 'effect';\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\nimport { findLastCompactBoundary } from './conversation-service.js';\nimport { generateSmartSummary } from '../../../lib/conversations/smart-compaction.js';\nimport { generateFallbackSummary } from '../../../lib/conversations/summary-fork.js';\nimport { loadConfigSync } from '../../../lib/config-yaml.js';\nimport { getAgentRuntimeBaseCommand, getProviderExportsForModel } from '../../../lib/agents.js';\nimport { getEventStore } from '../event-store.js';\nimport { isBackgroundFeatureEnabled } from '../../../lib/background-ai/features.js';\n\nconst activeCompactions = new Set<string>();\nexport function isCompacting(sessionFile: string): boolean {\n return activeCompactions.has(sessionFile);\n}\n\nexport interface NativeCompactionResult {\n summary: string;\n tokensBefore: number;\n boundaryUuid: string;\n model: string;\n forkedSessionId: string;\n forkedSessionFile: string;\n}\n\nexport interface MaybeCompactBeforeRespawnOptions {\n sessionFile: string | null | undefined;\n cwd: string;\n shouldCompact: boolean;\n}\n\nexport function getConversationCompactionSettings() {\n const { config } = loadConfigSync();\n return {\n model: config.conversations.compactionModel,\n manualCompactMode: config.conversations.manualCompactMode,\n richCompaction: config.conversations.richCompaction,\n };\n}\n\nexport function shouldInterceptManualCompact(message: string): boolean {\n const trimmed = message.trim();\n if (trimmed !== '/compact') return false;\n return getConversationCompactionSettings().manualCompactMode === 'overdeck-native';\n}\n\nexport async function estimateContextTokens(sessionFile: string | null | undefined): Promise<number> {\n if (!sessionFile || !existsSync(sessionFile)) return 0;\n try {\n const content = await readFile(sessionFile, 'utf-8');\n const lines = content.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n try {\n const entry = JSON.parse(line);\n const usage = entry?.message?.usage ?? entry?.usage;\n if (usage && typeof usage.input_tokens === 'number') {\n const total = (usage.input_tokens ?? 0)\n + (usage.cache_creation_input_tokens ?? 0)\n + (usage.cache_read_input_tokens ?? 0);\n if (total > 0) return total;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch (err) {\n console.warn(`[conversation-compaction] estimateContextTokens failed for ${sessionFile}:`, err);\n }\n return 0;\n}\n\nfunction buildContinuationSummary(summary: string, model: string): string {\n return [\n 'This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.',\n '',\n summary,\n '',\n `Overdeck native compaction model: ${model}`,\n '',\n 'Continue from this summary without redoing already-completed work.',\n ].join('\\n');\n}\n\nexport async function compactConversationNative(sessionFile: string, conversationName?: string): Promise<NativeCompactionResult> {\n if (!existsSync(sessionFile)) {\n throw new Error(`Session file not found: ${sessionFile}`);\n }\n activeCompactions.add(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: true } });\n }\n try {\n return await doCompact(sessionFile);\n } finally {\n activeCompactions.delete(sessionFile);\n if (conversationName) {\n getEventStore().emitOnly({ type: 'conversation.compacting_changed', timestamp: new Date().toISOString(), payload: { conversationName, compacting: false } });\n }\n }\n}\n\nasync function doCompact(sessionFile: string): Promise<NativeCompactionResult> {\n\n const settings = getConversationCompactionSettings();\n const tokensBefore = await estimateContextTokens(sessionFile);\n\n let summary: string;\n try {\n const result = await Effect.runPromise(generateSmartSummary({\n jsonlPath: sessionFile,\n model: settings.model,\n richMode: settings.richCompaction,\n mode: 'fork',\n }));\n summary = result.summary;\n } catch (error) {\n console.warn(`[conversation-compaction] Smart summary failed, falling back to heuristic:`, error);\n summary = await Effect.runPromise(generateFallbackSummary(sessionFile));\n }\n\n const continuation = buildContinuationSummary(summary, settings.model);\n const boundaryUuid = randomUUID();\n const timestamp = new Date().toISOString();\n const boundaryOffset = await findLastCompactBoundary(sessionFile);\n\n const entries = [\n JSON.stringify({\n parentUuid: null,\n isSidechain: false,\n type: 'system',\n subtype: 'compact_boundary',\n content: 'Conversation compacted',\n isMeta: false,\n timestamp,\n uuid: boundaryUuid,\n level: 'info',\n compactMetadata: {\n trigger: 'overdeck-native',\n preTokens: tokensBefore,\n model: settings.model,\n previousBoundaryOffset: boundaryOffset,\n },\n }),\n JSON.stringify({\n parentUuid: boundaryUuid,\n isSidechain: false,\n type: 'user',\n message: {\n role: 'user',\n content: continuation,\n },\n isVisibleInTranscriptOnly: true,\n isCompactSummary: true,\n uuid: randomUUID(),\n timestamp,\n }),\n ];\n\n const forkedSessionId = randomUUID();\n const forkedSessionFile = join(dirname(sessionFile), `${forkedSessionId}.jsonl`);\n await writeFile(forkedSessionFile, `${entries.join('\\n')}\\n`, 'utf-8');\n\n return {\n summary: continuation,\n tokensBefore,\n boundaryUuid,\n model: settings.model,\n forkedSessionId,\n forkedSessionFile,\n };\n}\n\nexport async function maybeCompactBeforeRespawn(\n opts: MaybeCompactBeforeRespawnOptions,\n): Promise<{ forkedSessionId: string; forkedSessionFile: string } | null> {\n if (!opts.sessionFile || !existsSync(opts.sessionFile)) return null;\n if (!opts.shouldCompact) {\n console.log('[conversation-compaction] Skipping compact (shouldCompact=false)');\n return null;\n }\n // Background AI gate: low-cost mode (or the summaryFork toggle) skips\n // automatic LLM compaction before respawn.\n if (!isBackgroundFeatureEnabled('summaryFork')) return null;\n\n const tokens = await estimateContextTokens(opts.sessionFile);\n console.log(`[conversation-compaction] Compacting before respawn (shouldCompact=true, tokens=${tokens})`);\n const result = await compactConversationNative(opts.sessionFile);\n return { forkedSessionId: result.forkedSessionId, forkedSessionFile: result.forkedSessionFile };\n}\n\nexport async function buildCompactionRuntimeInfo(model: string): Promise<{ command: string; exports: string }> {\n return {\n command: await getAgentRuntimeBaseCommand(model),\n exports: await getProviderExportsForModel(model),\n };\n}\n"],"mappings":"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"}