@overdeck/core 0.45.10 → 0.45.11

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 (219) hide show
  1. package/dist/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
  2. package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
  3. package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
  4. package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
  5. package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
  6. package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
  7. package/dist/cli/index.js +45 -45
  8. package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
  9. package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
  10. package/dist/cli-MTB9IOoR.js +3 -0
  11. package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
  12. package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
  13. package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
  14. package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
  15. package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
  16. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
  17. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
  18. package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
  19. package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
  20. package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
  21. package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
  22. package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
  23. package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
  24. package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
  25. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
  26. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
  27. package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
  28. package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
  29. package/dist/dashboard/cli-DCqy_wFa.js +3 -0
  30. package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
  31. package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
  32. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
  33. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
  34. package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
  35. package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
  36. package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
  37. package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
  38. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
  39. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
  40. package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
  41. package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
  42. package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
  43. package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
  44. package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
  45. package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
  46. package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
  47. package/dist/dashboard/deacon.js +4 -4
  48. package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
  49. package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
  50. package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
  51. package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
  52. package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
  53. package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
  54. package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
  55. package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
  56. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
  57. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
  58. package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
  59. package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
  60. package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
  61. package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
  62. package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
  63. package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
  64. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
  65. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
  66. package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
  67. package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
  68. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
  69. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
  70. package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
  71. package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
  72. package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
  73. package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
  74. package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
  75. package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
  76. package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
  77. package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
  78. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
  79. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
  80. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
  81. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
  82. package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
  83. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
  84. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
  85. package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
  86. package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
  87. package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
  88. package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
  89. package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
  90. package/dist/dashboard/review-agent-DTINEffB.js +3 -0
  91. package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
  92. package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
  93. package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
  94. package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
  95. package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
  96. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
  97. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
  98. package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
  99. package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
  100. package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
  101. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
  102. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
  103. package/dist/dashboard/server.js +66 -55
  104. package/dist/dashboard/server.js.map +1 -1
  105. package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
  106. package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
  107. package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
  108. package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
  109. package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
  110. package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
  111. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
  112. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
  113. package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
  114. package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
  115. package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
  116. package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
  117. package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
  118. package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
  119. package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
  120. package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
  121. package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
  122. package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
  123. package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
  124. package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
  125. package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
  126. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
  127. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
  128. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
  129. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
  130. package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
  131. package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
  132. package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
  133. package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
  134. package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
  135. package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
  136. package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
  137. package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
  138. package/dist/dashboard/workflows-DVwXauef.js +2 -0
  139. package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
  140. package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
  141. package/dist/docs-index.sqlite +0 -0
  142. package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
  143. package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
  144. package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
  145. package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
  146. package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
  147. package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
  148. package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
  149. package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
  150. package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
  151. package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
  152. package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
  153. package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
  154. package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
  155. package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
  156. package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
  157. package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
  158. package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
  159. package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
  160. package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
  161. package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
  162. package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
  163. package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
  164. package/dist/records-backfill-BQcmTmwF.js +2 -0
  165. package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
  166. package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
  167. package/dist/resume-Bn4kDk9x.js +3 -0
  168. package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
  169. package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
  170. package/dist/review-agent-hLbRRxZF.js +3 -0
  171. package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
  172. package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
  173. package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
  174. package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
  175. package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
  176. package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
  177. package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
  178. package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
  179. package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
  180. package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
  181. package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
  182. package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
  183. package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
  184. package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
  185. package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
  186. package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
  187. package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
  188. package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
  189. package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
  190. package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
  191. package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
  192. package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
  193. package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
  194. package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
  195. package/dist/sync-CylE_SjB.js +2 -0
  196. package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
  197. package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
  198. package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
  199. package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
  200. package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
  201. package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
  202. package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
  203. package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
  204. package/package.json +1 -1
  205. package/scripts/file-size-baseline.txt +1 -1
  206. package/dist/cli-DBWG12XK.js +0 -3
  207. package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
  208. package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
  209. package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
  210. package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
  211. package/dist/dashboard/resume-DnIsmIgC.js +0 -3
  212. package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
  213. package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
  214. package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
  215. package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
  216. package/dist/records-backfill-DEgtRyKL.js +0 -2
  217. package/dist/resume-DGymuXZM.js +0 -3
  218. package/dist/review-agent-DjVadURa.js +0 -3
  219. package/dist/sync-4jqukkxz.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-handlers-DHBSEDEm.js","names":[],"sources":["../src/lib/cloister/ci-failure-feedback.ts","../src/lib/github-graphql-cooldown.ts","../src/lib/webhook-handlers.ts"],"sourcesContent":["/**\n * CI failure feedback relay (PAN-1801)\n *\n * When a PR's GitHub checks fail, this module writes a feedback file to the\n * work agent's workspace and messages the agent so it can address the failure\n * instead of waiting for a human to notice a red merge gate.\n *\n * - Debounces per head SHA so retries do not spam the agent.\n * - Fetches failed log excerpts with `gh run view --log-failed`.\n * - Diff's the PR's failing check names against main's current failing set so\n * inherited main-red failures are labelled as such.\n */\n\nimport { execFile } from 'node:child_process';\nimport { join } from 'node:path';\nimport { Effect } from 'effect';\nimport { getAgentStateSync, messageAgent } from '../agents.js';\nimport { resolveProjectFromIssueSync } from '../projects.js';\nimport { writeFeedbackFile } from './feedback-writer.js';\n\nfunction execFilePromise(\n file: string,\n args: string[],\n options: { encoding: 'utf-8'; timeout?: number; maxBuffer?: number },\n): Promise<{ stdout: string; stderr: string }> {\n return new Promise((resolve, reject) => {\n execFile(file, args, options, (error, stdout, stderr) => {\n if (error) {\n reject(error);\n } else {\n resolve({ stdout: stdout as string, stderr: stderr as string });\n }\n });\n });\n}\n\nexport interface CiFailureFeedbackOptions {\n issueId: string;\n repo: string;\n prNumber: number;\n headSha: string;\n headRef: string;\n prUrl?: string;\n source: string;\n}\n\nexport interface CiFailure {\n name: string;\n workflowName?: string;\n runId: number;\n excerpt: string;\n inheritedFromMain: boolean;\n}\n\nexport interface CiFailureFeedbackResult {\n feedbackPath?: string;\n agentMessageSent: boolean;\n}\n\n/** Per-issue head SHA we last sent CI failure feedback for. */\nconst lastNotifiedSha = new Map<string, string>();\n\n/** Reset internal debounce state — for tests only. */\nexport function resetCiFailureFeedbackStateForTests(): void {\n lastNotifiedSha.clear();\n}\n\nfunction agentIdForIssue(issueId: string): string {\n return `agent-${issueId.toLowerCase()}`;\n}\n\nfunction parseRepo(repo: string): { owner: string; repo: string } {\n const [owner, ...rest] = repo.split('/');\n return { owner, repo: rest.join('/') };\n}\n\ninterface GhRunListItem {\n databaseId: number;\n name: string;\n workflowName: string;\n headSha?: string;\n conclusion: string;\n}\n\nasync function listFailingRuns(\n owner: string,\n repo: string,\n branch: string,\n headSha?: string,\n): Promise<GhRunListItem[]> {\n try {\n const { stdout } = await execFilePromise(\n 'gh',\n [\n 'run', 'list',\n '--repo', `${owner}/${repo}`,\n '--branch', branch,\n '--status', 'failure',\n '--json', 'databaseId,name,workflowName,headSha,conclusion',\n '--limit', '100',\n ],\n { encoding: 'utf-8', timeout: 30000 },\n );\n const runs = JSON.parse(stdout) as GhRunListItem[];\n if (headSha) {\n return runs.filter((r) => r.headSha === headSha);\n }\n return runs;\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Failed to list failing runs for ${branch}:`,\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n}\n\nasync function fetchRunFailedLogExcerpt(owner: string, repo: string, runId: number, maxChars = 2000): Promise<string> {\n try {\n const { stdout } = await execFilePromise(\n 'gh',\n ['run', 'view', String(runId), '--repo', `${owner}/${repo}`, '--log-failed'],\n { encoding: 'utf-8', timeout: 30000, maxBuffer: 2 * 1024 * 1024 },\n );\n return stdout.slice(0, maxChars).trim();\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Failed to fetch log for run ${runId}:`,\n err instanceof Error ? err.message : String(err),\n );\n return '';\n }\n}\n\nasync function collectFailures(\n owner: string,\n repo: string,\n branch: string,\n headSha: string,\n mainFailingNames: Set<string>,\n): Promise<CiFailure[]> {\n const runs = await listFailingRuns(owner, repo, branch, headSha);\n const failures: CiFailure[] = [];\n for (const run of runs) {\n const excerpt = await fetchRunFailedLogExcerpt(owner, repo, run.databaseId);\n const name = run.name || run.workflowName || `run-${run.databaseId}`;\n failures.push({\n name,\n workflowName: run.workflowName,\n runId: run.databaseId,\n excerpt,\n inheritedFromMain: mainFailingNames.has(name),\n });\n }\n return failures;\n}\n\nfunction buildFeedbackBody(opts: {\n issueId: string;\n repo: string;\n prNumber: number;\n headSha: string;\n prUrl?: string;\n failures: CiFailure[];\n source: string;\n}): string {\n const shortSha = opts.headSha.slice(0, 8);\n const prLine = opts.prUrl\n ? `Pull request: ${opts.prUrl} (head \\`${shortSha}\\`)`\n : `Pull request #${opts.prNumber} in ${opts.repo} (head \\`${shortSha}\\`)`;\n\n let body = `# CI Failure Feedback for ${opts.issueId}\\n\\n${prLine}\\n\\nSource: ${opts.source}\\n\\n`;\n\n if (opts.failures.length === 0) {\n body +=\n 'GitHub reported a failing CI status, but no failing workflow runs were found for this commit. Check the PR checks page directly.\\n\\n';\n } else {\n body += `## Failing checks (${opts.failures.length})\\n\\n`;\n for (const f of opts.failures) {\n const inheritedTag = f.inheritedFromMain ? ' [INHERITED FROM MAIN — also failing on main]' : '';\n body += `### ${f.name}${inheritedTag}\\n\\n`;\n if (f.workflowName && f.workflowName !== f.name) {\n body += `Workflow: ${f.workflowName} \\n`;\n }\n body += `Run: \\`gh run view ${f.runId} --repo ${opts.repo}\\` \\n`;\n if (f.excerpt) {\n body += '```\\n' + f.excerpt + '\\n```\\n\\n';\n } else {\n body += '*(No log excerpt available.)*\\n\\n';\n }\n }\n }\n\n body += `## Required action\\n\\n`;\n body +=\n 'Fix the failing checks, commit the fixes, and push an update. If a failure is marked [INHERITED FROM MAIN], it is not caused by your PR and does not need to be fixed in this branch.\\n';\n\n return body;\n}\n\nasync function relayCiFailureFeedbackPromise(\n opts: CiFailureFeedbackOptions,\n): Promise<CiFailureFeedbackResult> {\n const issueId = opts.issueId.toUpperCase();\n\n // Only relay for work agents. The feedback file/message would not be useful\n // for plan/review/test/ship/strike roles.\n const agentId = agentIdForIssue(issueId);\n const agentState = getAgentStateSync(agentId);\n if (!agentState || agentState.role !== 'work') {\n return { agentMessageSent: false };\n }\n\n // Debounce per head SHA so duplicate webhook deliveries / retries do not spam.\n const lastSha = lastNotifiedSha.get(issueId);\n if (lastSha === opts.headSha) {\n console.log(`[ci-failure-feedback] Skipping duplicate feedback for ${issueId} @ ${opts.headSha.slice(0, 8)}`);\n return { agentMessageSent: false };\n }\n\n const { owner, repo } = parseRepo(opts.repo);\n\n // Diff against main's current failing checks so agents do not chase inherited failures.\n const mainRuns = await listFailingRuns(owner, repo, 'main');\n const mainFailingNames = new Set(mainRuns.map((r) => r.name || r.workflowName || `run-${r.databaseId}`));\n\n // Collect the concrete failures for this PR head.\n const failures = await collectFailures(owner, repo, opts.headRef, opts.headSha, mainFailingNames);\n\n // If we cannot find any failing workflow run for this SHA, still write a short\n // feedback file for explicit status events so the agent is not left in the dark.\n if (failures.length === 0 && !opts.source.startsWith('status:')) {\n console.log(\n `[ci-failure-feedback] No failing runs found for ${issueId} @ ${opts.headSha.slice(0, 8)}; skipping feedback`,\n );\n return { agentMessageSent: false };\n }\n\n const resolved = resolveProjectFromIssueSync(issueId);\n const workspacePath = resolved\n ? join(resolved.projectPath, 'workspaces', `feature-${issueId.toLowerCase()}`)\n : undefined;\n\n const markdownBody = buildFeedbackBody({\n issueId,\n repo: opts.repo,\n prNumber: opts.prNumber,\n headSha: opts.headSha,\n prUrl: opts.prUrl,\n failures,\n source: opts.source,\n });\n\n const fileResult = await Effect.runPromise(\n writeFeedbackFile({\n issueId,\n workspacePath,\n specialist: 'ci-monitor',\n outcome: 'failed',\n summary: `CI failure: ${failures.map((f) => f.name).join(', ') || opts.source}`.slice(0, 120),\n markdownBody,\n }),\n );\n\n if (!fileResult.success || !fileResult.filePath) {\n console.error(`[ci-failure-feedback] Failed to write feedback for ${issueId}: ${fileResult.error}`);\n return { agentMessageSent: false };\n }\n\n let agentMessageSent = false;\n const message =\n `SPECIALIST FEEDBACK: ci-monitor reported CI FAILED for ${issueId}.\\n\\n` +\n `MUST READ: ${fileResult.filePath}\\n\\n` +\n 'Use your Read tool to open this file, read every line, then fix ALL failing checks. Do NOT stop at the prompt.';\n try {\n await messageAgent(agentId, message);\n agentMessageSent = true;\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Could not message ${agentId}; feedback file remains available: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n lastNotifiedSha.set(issueId, opts.headSha);\n return { feedbackPath: fileResult.filePath, agentMessageSent };\n}\n\n/** Effect variant of {@link relayCiFailureFeedbackPromise}. */\nexport const relayCiFailureFeedback = (\n opts: CiFailureFeedbackOptions,\n): Effect.Effect<CiFailureFeedbackResult> => Effect.promise(() => relayCiFailureFeedbackPromise(opts));\n","const GRAPHQL_COOLDOWN_MS = 60_000;\n\nlet cooldownStartedAt: number | null = null;\n\nfunction messageFromError(err: unknown): string {\n if (err instanceof Error) {\n const extra = 'stderr' in err && typeof err.stderr === 'string' ? ` ${err.stderr}` : '';\n return `${err.message}${extra}`;\n }\n if (typeof err === 'string') return err;\n try {\n return JSON.stringify(err);\n } catch {\n return String(err);\n }\n}\n\nfunction isGraphQLRateLimitError(err: unknown): boolean {\n const message = messageFromError(err);\n return /GraphQL:.*rate limit/i.test(message)\n || /rate limit already exceeded/i.test(message)\n || /API rate limit/i.test(message);\n}\n\nexport function noteGraphQLRateLimit(err: unknown): void {\n if (!isGraphQLRateLimitError(err)) return;\n cooldownStartedAt = Date.now();\n}\n\nexport function isInGraphQLCooldown(): boolean {\n if (cooldownStartedAt == null) return false;\n return Date.now() - cooldownStartedAt < GRAPHQL_COOLDOWN_MS;\n}\n","/**\n * GitHub webhook event handlers (PAN-905)\n *\n * Dispatches verified webhook events to per-type handlers that update\n * review_status blockerReasons based on GitHub-native merge blockers.\n */\n\nimport { Effect } from 'effect';\nimport { setReviewStatus, getReviewStatus, loadReviewStatuses, type BlockerReason, type ReviewStatus } from './review-status.js';\nimport { getGitHubConfig } from '../dashboard/server/services/tracker-config.js';\nimport { GitHubApiError } from './errors.js';\nimport { relayCiFailureFeedback } from './cloister/ci-failure-feedback.js';\nimport { isInGraphQLCooldown, noteGraphQLRateLimit } from './github-graphql-cooldown.js';\nimport { bumpIssuePrTabCacheGeneration } from '../dashboard/server/services/pr-tab-cache.js';\n\nexport interface WebhookPayload {\n action?: string;\n pull_request?: {\n number: number;\n head: { ref: string; sha?: string };\n html_url?: string;\n mergeable?: boolean | null;\n mergeable_state?: string;\n draft?: boolean;\n state?: string;\n merged?: boolean;\n };\n check_suite?: {\n status?: string;\n conclusion?: string | null;\n pull_requests?: Array<{ number: number; head: { ref: string; sha?: string } }>;\n };\n check_run?: {\n id?: number;\n name?: string;\n status?: string;\n conclusion?: string | null;\n pull_requests?: Array<{ number: number; head: { ref: string; sha?: string } }>;\n };\n repository?: { full_name: string };\n issue?: {\n number?: number;\n pull_request?: unknown;\n };\n review?: { state: string };\n thread?: { id?: number; resolved?: boolean };\n // status event payload\n sha?: string;\n state?: string;\n context?: string;\n branches?: Array<{ name: string }>;\n}\n\nexport function issueIdFromBranch(ref: string): string | null {\n const match = ref.match(/(?:feature|strike)\\/([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\n// ─── Repository authorization (defense-in-depth) ─────────────────────────────\n\n// ─── Cached repository allowlist (refreshed every 5 min) ─────────────────────\n\nlet cachedTrackedRepos: Set<string> | null = null;\nlet cachedTrackedReposAt = 0;\nconst REPO_CACHE_TTL_MS = 5 * 60 * 1000;\n\nfunction getTrackedRepos(): Set<string> {\n const now = Date.now();\n if (!cachedTrackedRepos || now - cachedTrackedReposAt > REPO_CACHE_TTL_MS) {\n const config = getGitHubConfig();\n cachedTrackedRepos = config\n ? new Set(config.repos.map(({ owner, repo }) => `${owner}/${repo}`.toLowerCase()))\n : new Set();\n cachedTrackedReposAt = now;\n }\n return cachedTrackedRepos;\n}\n\nexport function isTrackedRepositorySync(fullName: string | undefined): boolean {\n if (!fullName) return false;\n return getTrackedRepos().has(fullName.toLowerCase());\n}\n\n// ─── PR identity validation ──────────────────────────────────────────────────\n\nfunction getRepoFromPrUrl(prUrl: string): string | null {\n const m = prUrl.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull\\/\\d+/i);\n return m ? `${m[1]}/${m[2]}`.toLowerCase() : null;\n}\n\nfunction parseGitHubPrUrl(prUrl: string): { owner: string; repo: string; number: number } | null {\n const m = prUrl.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull\\/(\\d+)/i);\n if (!m) return null;\n return { owner: m[1]!, repo: m[2]!, number: Number.parseInt(m[3]!, 10) };\n}\n\nfunction repoFromPrUrl(prUrl: string | undefined): string | null {\n if (!prUrl) return null;\n return getRepoFromPrUrl(prUrl);\n}\n\nasync function loadAndValidateStatus(\n issueId: string,\n repo: string,\n prNumber?: number,\n headSha?: string,\n): Promise<ReviewStatus | null> {\n const status = await Effect.runPromise(getReviewStatus(issueId));\n if (!status) return null;\n\n // If no PR identity is stored yet, allow the event (identity will be populated lazily).\n if (!status.prUrl && !status.prNumber && !status.prHeadSha) {\n return status;\n }\n\n const storedRepo = status.prUrl ? getRepoFromPrUrl(status.prUrl) : null;\n if (storedRepo && storedRepo !== repo.toLowerCase()) {\n console.warn(`[webhook] Repo mismatch for ${issueId}: stored=${storedRepo} event=${repo}`);\n return null;\n }\n\n if (prNumber != null && status.prNumber != null && prNumber !== status.prNumber) {\n console.warn(`[webhook] PR number mismatch for ${issueId}: stored=${status.prNumber} event=${prNumber}`);\n return null;\n }\n\n if (headSha && status.prHeadSha && headSha !== status.prHeadSha) {\n console.warn(`[webhook] Head SHA mismatch for ${issueId}: stored=${status.prHeadSha} event=${headSha}`);\n return null;\n }\n\n return status;\n}\n\n// ─── Per-source failing_checks tracking ──────────────────────────────────────\n\nfunction getFailingChecksBlocker(blockers: BlockerReason[]): BlockerReason | undefined {\n return blockers.find((b) => b.type === 'failing_checks');\n}\n\nfunction parseFailingSources(details?: string): Record<string, string> {\n if (!details) return {};\n try {\n const parsed = JSON.parse(details) as { sources?: Record<string, string> };\n return parsed.sources ?? {};\n } catch {\n return {};\n }\n}\n\nfunction serializeFailingSources(sources: Record<string, string>): string {\n return JSON.stringify({ sources });\n}\n\nfunction addFailingSource(blockers: BlockerReason[], sourceKey: string, summary: string): BlockerReason[] {\n const existing = getFailingChecksBlocker(blockers);\n const sources = parseFailingSources(existing?.details);\n sources[sourceKey] = summary;\n const updated: BlockerReason = {\n type: 'failing_checks',\n summary: Object.values(sources).join('; '),\n details: serializeFailingSources(sources),\n detectedAt: existing?.detectedAt ?? new Date().toISOString(),\n };\n return [...blockers.filter((b) => b.type !== 'failing_checks'), updated];\n}\n\nfunction removeFailingSource(blockers: BlockerReason[], sourceKey: string): BlockerReason[] {\n const existing = getFailingChecksBlocker(blockers);\n if (!existing) return blockers;\n const sources = parseFailingSources(existing.details);\n delete sources[sourceKey];\n if (Object.keys(sources).length === 0) {\n return blockers.filter((b) => b.type !== 'failing_checks');\n }\n const updated: BlockerReason = {\n type: 'failing_checks',\n summary: Object.values(sources).join('; '),\n details: serializeFailingSources(sources),\n detectedAt: existing.detectedAt,\n };\n return [...blockers.filter((b) => b.type !== 'failing_checks'), updated];\n}\n\n// ─── Merge state reconciliation for unknown mergeable_state ──────────────────\n\nconst KNOWN_CONFLICT_STATES = new Set(['dirty']);\nconst KNOWN_NON_BLOCKING_STATES = new Set(['clean', 'unstable']);\nconst KNOWN_NOT_MERGEABLE_STATES = new Set(['blocked', 'behind']);\n\n/** `gh` statusCheckRollup conclusions/states that count as a failing required check. */\nexport const FAILING_CHECK_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE', 'STALE']);\n\nconst pendingReconciliation = new Set<string>();\nconst reconciliationTimeouts = new Map<string, NodeJS.Timeout>();\n\nexport function clearAllReconciliationTimeouts(): void {\n for (const timeout of reconciliationTimeouts.values()) {\n clearTimeout(timeout);\n }\n reconciliationTimeouts.clear();\n pendingReconciliation.clear();\n}\n\nfunction scheduleMergeStateReconciliation(issueId: string, repo: string, prNumber: number): void {\n if (pendingReconciliation.has(issueId)) return;\n pendingReconciliation.add(issueId);\n const timeout = setTimeout(() => {\n pendingReconciliation.delete(issueId);\n reconciliationTimeouts.delete(issueId);\n refreshMergeStateFromGitHub(issueId, repo, prNumber).catch(() => {});\n }, 30000);\n timeout.unref();\n reconciliationTimeouts.set(issueId, timeout);\n}\n\nexport async function refreshMergeStateFromGitHub(issueId: string, repo: string, prNumber: number): Promise<void> {\n try {\n // Resolve PR merge state. Prefer the GitHub App REST path (installation\n // token, separate rate-limit budget, no GraphQL) when configured; fall back\n // to `gh pr view` (GraphQL) only when the App is not set up. Previously this\n // unconditionally used `gh` GraphQL even with the App configured, which\n // burned the GraphQL budget on every pull_request webhook + every\n // merge-blocker poll. PAN-2265.\n const [owner, repoName] = repo.split('/');\n if (!owner || !repoName) return;\n\n let mergeable: string;\n let mergeState: string;\n let isDraft: boolean;\n let checksFailed: boolean;\n\n const { isGitHubAppConfigured, getPullRequestState } = await import('./github-app.js');\n if (isGitHubAppConfigured()) {\n // App REST path — installation token, separate rate-limit budget, no GraphQL.\n const prState = await Effect.runPromise(getPullRequestState(owner, repoName, prNumber));\n mergeable = prState.mergeable === null ? 'UNKNOWN' : prState.mergeable ? 'MERGEABLE' : 'CONFLICTING';\n mergeState = (prState.mergeableState ?? '').toUpperCase();\n isDraft = prState.draft;\n checksFailed = prState.checksFailed;\n } else {\n // gh CLI fallback (GraphQL) — only when the App is not configured.\n if (isInGraphQLCooldown()) return;\n const { execFile } = await import('child_process');\n const { promisify } = await import('util');\n const execFileAsync = promisify(execFile);\n // gh GraphQL fields (NOT the REST webhook shape): mergeable is an enum\n // (MERGEABLE | CONFLICTING | UNKNOWN), mergeStateStatus is CLEAN | DIRTY |\n // UNSTABLE | BLOCKED | BEHIND | …, isDraft is a bool, and statusCheckRollup\n // carries the required-check results. PAN-1620.\n let stdout: string;\n try {\n ({ stdout } = await execFileAsync(\n 'gh',\n ['pr', 'view', String(prNumber), '--repo', repo, '--json', 'mergeable,mergeStateStatus,isDraft,statusCheckRollup'],\n { encoding: 'utf-8', timeout: 15000 },\n ));\n } catch (err) {\n noteGraphQLRateLimit(err);\n throw err;\n }\n if (!stdout.trim()) return;\n const pr = JSON.parse(stdout) as {\n mergeable?: string | null;\n mergeStateStatus?: string | null;\n isDraft?: boolean;\n statusCheckRollup?: Array<{ conclusion?: string | null; state?: string | null }>;\n };\n mergeable = (pr.mergeable ?? '').toUpperCase();\n mergeState = (pr.mergeStateStatus ?? '').toUpperCase();\n isDraft = pr.isDraft === true;\n checksFailed = (pr.statusCheckRollup ?? []).some((c) =>\n FAILING_CHECK_CONCLUSIONS.has((c.conclusion || c.state || '').toUpperCase()),\n );\n }\n\n const status = await Effect.runPromise(getReviewStatus(issueId));\n if (!status) return;\n\n const isConflicting = mergeable === 'CONFLICTING' || mergeState === 'DIRTY';\n\n // Rebuild the GitHub-native blockers from live state, preserving any\n // non-GitHub-native ones (e.g. unresolved_conversations, changes_requested).\n const GH_NATIVE = new Set<BlockerReason['type']>(['merge_conflict', 'not_mergeable', 'failing_checks', 'draft_pr']);\n const now = new Date().toISOString();\n const blockers: BlockerReason[] = (status.blockerReasons ?? []).filter((b) => !GH_NATIVE.has(b.type));\n if (isDraft) blockers.push({ type: 'draft_pr', summary: 'Pull request is in draft state', detectedAt: now });\n if (isConflicting) {\n blockers.push({ type: 'merge_conflict', summary: 'Merge conflict with target branch', detectedAt: now });\n } else if (mergeState === 'BLOCKED' && !checksFailed) {\n blockers.push({ type: 'not_mergeable', summary: 'PR blocked by branch protection', detectedAt: now });\n }\n if (checksFailed) blockers.push({ type: 'failing_checks', summary: 'Required checks are failing', detectedAt: now });\n\n // Only write when the GitHub-native blocker set actually changed.\n const prevTypes = new Set((status.blockerReasons ?? []).filter((b) => GH_NATIVE.has(b.type)).map((b) => b.type));\n const nextTypes = new Set(blockers.filter((b) => GH_NATIVE.has(b.type)).map((b) => b.type));\n const changed = prevTypes.size !== nextTypes.size || [...nextTypes].some((t) => !prevTypes.has(t));\n if (changed) {\n await Effect.runPromise(setReviewStatus(issueId, { blockerReasons: blockers.length > 0 ? blockers : undefined }, status));\n }\n } catch (err) {\n console.warn(`[webhook] Merge state reconciliation failed for ${issueId}:`, err);\n }\n}async function handleCheckSuitePromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const suite = payload.check_suite;\n if (!suite) return;\n if (!suite.pull_requests || suite.pull_requests.length === 0) return;\n\n const repo = payload.repository!.full_name;\n\n for (const pr of suite.pull_requests) {\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) continue;\n\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, pr.number, pr.head.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (suite.conclusion === 'success') {\n blockers = removeFailingSource(blockers, 'check_suite');\n } else if (suite.conclusion && FAILING_CHECK_CONCLUSIONS.has(suite.conclusion.toUpperCase())) {\n // Any terminal failure conclusion is blocking\n blockers = addFailingSource(blockers, 'check_suite', `CI check suite ${suite.conclusion}`);\n if (pr.head.sha && pr.number != null) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: pr.number,\n headSha: pr.head.sha,\n headRef: pr.head.ref,\n prUrl: status.prUrl,\n source: 'check_suite',\n }));\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n }\n}async function handleCheckRunPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const run = payload.check_run;\n if (!run) return;\n if (!run.pull_requests || run.pull_requests.length === 0) return;\n\n const repo = payload.repository!.full_name;\n const sourceKey = `check_run:${run.name ?? String(run.id ?? 'unknown')}`;\n\n for (const pr of run.pull_requests) {\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) continue;\n\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, pr.number, pr.head.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (run.conclusion === 'success') {\n blockers = removeFailingSource(blockers, sourceKey);\n } else if (run.conclusion && FAILING_CHECK_CONCLUSIONS.has(run.conclusion.toUpperCase())) {\n blockers = addFailingSource(blockers, sourceKey, `CI check run ${run.conclusion}: ${run.name ?? 'unknown'}`);\n if (pr.head.sha && pr.number != null) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: pr.number,\n headSha: pr.head.sha,\n headRef: pr.head.ref,\n prUrl: status.prUrl,\n source: sourceKey,\n }));\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n }\n}async function handlePullRequestPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n if (!pr) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n\n const repo = payload.repository!.full_name;\n\n // PAN-1513: fire postMergeLifecycle when GitHub reports the PR closed+merged.\n // Without this, admin-merges (gh pr merge --admin) and any merge that doesn't\n // route through Overdeck's own merge flow leave work agents, strikes, tmux\n // sessions, and worktrees orphaned. postMergeLifecycle has its own\n // single-flight guard (specialists.ts L116) and _completedPostMerge marker,\n // so duplicate webhook deliveries are idempotent.\n if (payload.action === 'closed' && pr.merged === true) {\n try {\n const { postMergeLifecycle } = await import('./cloister/merge-agent.js');\n const { resolveProjectFromIssueSync } = await import('./projects.js');\n const project = resolveProjectFromIssueSync(issueId);\n if (project) {\n const branchName = pr.head.ref;\n const markReviewPassed = branchName.startsWith('strike/');\n postMergeLifecycle(issueId, project.projectPath, branchName, { markReviewPassed }).catch(err =>\n console.warn(`[webhook] postMergeLifecycle failed for ${issueId} (${branchName}): ${err?.message ?? err}`),\n );\n }\n } catch (err: any) {\n console.warn(`[webhook] Failed to dispatch postMergeLifecycle for ${issueId}: ${err?.message ?? err}`);\n }\n }\n\n // For synchronize/opened/reopened the head SHA may have changed — skip SHA\n // validation so the handler can refresh prHeadSha and recompute blockers.\n const headMayHaveMoved = ['synchronize', 'opened', 'reopened'].includes(payload.action ?? '');\n const status = await loadAndValidateStatus(issueId, repo, pr.number, headMayHaveMoved ? undefined : pr.head.sha);\n if (!status) return;\n\n const update: Partial<ReviewStatus> = {};\n let blockers = [...(status.blockerReasons ?? [])];\n\n // Populate missing PR identity and keep head SHA in sync on synchronize\n if (!status.prUrl) update.prUrl = pr.html_url ?? `https://github.com/${repo}/pull/${pr.number}`;\n if (!status.prNumber) update.prNumber = pr.number;\n if (pr.head.sha && (!status.prHeadSha || payload.action === 'synchronize')) {\n update.prHeadSha = pr.head.sha;\n }\n\n // Dismissed reviews\n if (payload.action === 'review_dismissed') {\n blockers = blockers.filter((b) => b.type !== 'changes_requested');\n }\n\n // Draft PR\n if (pr.draft) {\n const draftBlocker: BlockerReason = {\n type: 'draft_pr',\n summary: 'Pull request is in draft state',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'draft_pr'), draftBlocker];\n } else {\n blockers = blockers.filter((b) => b.type !== 'draft_pr');\n }\n\n // Merge state\n if (pr.mergeable_state === 'unknown') {\n scheduleMergeStateReconciliation(issueId, repo, pr.number);\n } else if (pr.mergeable_state && KNOWN_CONFLICT_STATES.has(pr.mergeable_state)) {\n const mergeConflictBlocker: BlockerReason = {\n type: 'merge_conflict',\n summary: 'Merge conflict with target branch',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), mergeConflictBlocker];\n } else if (pr.mergeable === false && (pr.mergeable_state === null || pr.mergeable_state === undefined)) {\n const mergeConflictBlocker: BlockerReason = {\n type: 'merge_conflict',\n summary: 'Merge conflict with target branch',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), mergeConflictBlocker];\n } else if ((pr.mergeable_state && KNOWN_NON_BLOCKING_STATES.has(pr.mergeable_state)) || pr.mergeable === true) {\n blockers = blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable');\n } else if (pr.mergeable_state && KNOWN_NOT_MERGEABLE_STATES.has(pr.mergeable_state)) {\n const notMergeableBlocker: BlockerReason = {\n type: 'not_mergeable',\n summary: `PR not mergeable: ${pr.mergeable_state}`,\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), notMergeableBlocker];\n }\n\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}async function handlePullRequestReviewPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n const review = payload.review;\n if (!pr || !review) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n\n const repo = payload.repository!.full_name;\n const status = await loadAndValidateStatus(issueId, repo, pr.number);\n if (!status) return;\n\n // Dismissed reviews are handled by the pull_request review_dismissed action.\n if (review.state === 'dismissed') return;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (review.state === 'changes_requested') {\n blockers = [...blockers.filter((b) => b.type !== 'changes_requested'), {\n type: 'changes_requested',\n summary: 'Changes requested on pull request',\n detectedAt: new Date().toISOString(),\n }];\n } else if (review.state === 'approved') {\n blockers = blockers.filter((b) => b.type !== 'changes_requested');\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}\n\nasync function handlePullRequestReviewCommentPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n if (!pr) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n}\n\nasync function handleIssueCommentPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const issue = payload.issue;\n if (!issue?.pull_request || issue.number == null) return;\n\n const repo = payload.repository!.full_name.toLowerCase();\n const statuses = loadReviewStatuses();\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.prNumber !== issue.number) continue;\n const statusRepo = repoFromPrUrl(status.prUrl);\n if (statusRepo && statusRepo !== repo) continue;\n bumpIssuePrTabCacheGeneration(issueId);\n }\n}\n\nasync function handlePullRequestReviewThreadPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n const thread = payload.thread;\n if (!pr || !thread) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n\n const repo = payload.repository!.full_name;\n const status = await loadAndValidateStatus(issueId, repo, pr.number);\n if (!status) return;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (thread.resolved === false) {\n if (thread.id == null) {\n console.warn(`[webhook] Unresolved review thread without id for ${issueId} — cannot track conversation`);\n }\n const existing = blockers.find((b) => b.type === 'unresolved_conversations');\n let threadIds: Set<string>;\n try {\n threadIds = new Set<string>(JSON.parse(existing?.details ?? '[]') as string[]);\n } catch {\n threadIds = new Set<string>();\n }\n if (thread.id != null) threadIds.add(String(thread.id));\n const updated: BlockerReason = {\n type: 'unresolved_conversations',\n summary: 'Unresolved review conversation',\n details: JSON.stringify([...threadIds]),\n detectedAt: existing?.detectedAt ?? new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'unresolved_conversations'), updated];\n } else if (thread.resolved === true) {\n if (thread.id == null) return;\n const existing = blockers.find((b) => b.type === 'unresolved_conversations');\n if (!existing) return;\n let threadIds: Set<string>;\n try {\n threadIds = new Set<string>(JSON.parse(existing.details ?? '[]') as string[]);\n } catch {\n threadIds = new Set<string>();\n }\n threadIds.delete(String(thread.id));\n if (threadIds.size === 0) {\n blockers = blockers.filter((b) => b.type !== 'unresolved_conversations');\n } else {\n const updated: BlockerReason = {\n type: 'unresolved_conversations',\n summary: 'Unresolved review conversation',\n details: JSON.stringify([...threadIds]),\n detectedAt: existing.detectedAt,\n };\n blockers = [...blockers.filter((b) => b.type !== 'unresolved_conversations'), updated];\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}async function handleStatusPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const state = payload.state;\n const branches = payload.branches;\n if (!state || !branches || branches.length === 0) return;\n\n const repo = payload.repository!.full_name;\n const context = payload.context ?? 'default';\n const sourceKey = `status:${context}`;\n\n for (const branch of branches) {\n const issueId = issueIdFromBranch(branch.name);\n if (issueId) {\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, undefined, payload.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (state === 'success') {\n blockers = removeFailingSource(blockers, sourceKey);\n } else if (state === 'failure' || state === 'error') {\n blockers = addFailingSource(blockers, sourceKey, `Commit status: ${state}`);\n if (payload.sha && status.prNumber != null && status.prUrl) {\n const parsed = parseGitHubPrUrl(status.prUrl);\n if (parsed) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: status.prNumber,\n headSha: payload.sha,\n headRef: branch.name,\n prUrl: status.prUrl,\n source: sourceKey,\n }));\n }\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n // Do NOT populate prHeadSha from status events — we don't have the PR number\n // to construct a prUrl, and the SHA alone isn't enough to bind identity.\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n\n // Keep scanning in case the payload includes multiple feature branches.\n continue;\n }\n }\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\nconst toGhError = (op: string, cause: unknown): GitHubApiError =>\n new GitHubApiError({\n operation: op,\n status: 0,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n });\n\n/** Effect: handle a `check_suite` GitHub webhook payload. */\nexport const handleCheckSuite = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleCheckSuitePromise(payload),\n catch: (cause) => toGhError('handleCheckSuite', cause),\n });\n\n/** Effect: handle a `check_run` GitHub webhook payload. */\nexport const handleCheckRun = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleCheckRunPromise(payload),\n catch: (cause) => toGhError('handleCheckRun', cause),\n });\n\n/** Effect: handle a `pull_request` GitHub webhook payload. */\nexport const handlePullRequest = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestPromise(payload),\n catch: (cause) => toGhError('handlePullRequest', cause),\n });\n\n/** Effect: handle a `pull_request_review` GitHub webhook payload. */\nexport const handlePullRequestReview = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReview', cause),\n });\n\n/** Effect: handle a `pull_request_review_comment` GitHub webhook payload. */\nexport const handlePullRequestReviewComment = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewCommentPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReviewComment', cause),\n });\n\n/** Effect: handle an `issue_comment` GitHub webhook payload for PR tab cache invalidation. */\nexport const handleIssueComment = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleIssueCommentPromise(payload),\n catch: (cause) => toGhError('handleIssueComment', cause),\n });\n\n/** Effect: handle a `pull_request_review_thread` GitHub webhook payload. */\nexport const handlePullRequestReviewThread = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewThreadPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReviewThread', cause),\n });\n\n/** Effect: handle a `status` GitHub webhook payload. */\nexport const handleStatus = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleStatusPromise(payload),\n catch: (cause) => toGhError('handleStatus', cause),\n });\n\n/** True if the repo is in the cached tracked-repos allowlist. Pure. */\nexport const isTrackedRepository = (\n fullName: string | undefined,\n): Effect.Effect<boolean> =>\n Effect.sync(() => isTrackedRepositorySync(fullName));\n\n// ─── Boot-time blocker reconciliation (PAN-1771) ─────────────────────────────\n\nconst GH_NATIVE_BLOCKER_TYPES = new Set<BlockerReason['type']>([\n 'merge_conflict',\n 'not_mergeable',\n 'failing_checks',\n 'draft_pr',\n]);\n\nfunction prNumberFromUrl(prUrl: string): number | null {\n const m = prUrl.match(/\\/pull\\/(\\d+)/);\n return m ? Number(m[1]) : null;\n}\n\n/**\n * Pure boot-sweep filter: returns the PR identity to refresh when a status\n * carries GitHub-native blockers that may have gone stale, null otherwise.\n * Exported for tests.\n */\nexport function needsBlockerReconciliation(\n status: Pick<ReviewStatus, 'mergeStatus' | 'blockerReasons' | 'prUrl' | 'prNumber'>,\n): { repo: string; prNumber: number } | null {\n if (status.mergeStatus === 'merged') return null;\n const ghBlockers = (status.blockerReasons ?? []).filter((b) => GH_NATIVE_BLOCKER_TYPES.has(b.type));\n if (ghBlockers.length === 0) return null;\n const repo = status.prUrl ? getRepoFromPrUrl(status.prUrl) : null;\n const prNumber = status.prNumber ?? (status.prUrl ? prNumberFromUrl(status.prUrl) : null);\n if (!repo || !prNumber) return null;\n return { repo, prNumber };\n}\n\n/**\n * PAN-1771: GitHub-native blockers (failing_checks, merge_conflict, draft_pr,\n * not_mergeable) are refreshed only by webhook-driven\n * refreshMergeStateFromGitHub calls. Webhooks that arrive while the server is\n * down (reboot, deploy restart, crash) are lost, so a blocker recorded before\n * the outage can outlive the condition it describes — and a stale blocker pins\n * readyForMerge=false forever under the PAN-1650 event-driven derivation.\n *\n * Re-derives the blocker set from live PR state for every non-merged status\n * still carrying GitHub-native blockers. Sequential on purpose: the candidate\n * set is small and this avoids a gh subprocess burst at boot. Returns the\n * number of statuses refreshed.\n */\nexport async function reconcileStaleGitHubBlockers(): Promise<number> {\n const statuses = loadReviewStatuses();\n let refreshed = 0;\n for (const status of Object.values(statuses)) {\n const target = needsBlockerReconciliation(status);\n if (!target) continue;\n await refreshMergeStateFromGitHub(status.issueId, target.repo, target.prNumber);\n refreshed++;\n }\n return refreshed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"webhook-handlers-BjsfIL7u.js","names":[],"sources":["../src/lib/cloister/ci-failure-feedback.ts","../src/lib/github-graphql-cooldown.ts","../src/lib/webhook-handlers.ts"],"sourcesContent":["/**\n * CI failure feedback relay (PAN-1801)\n *\n * When a PR's GitHub checks fail, this module writes a feedback file to the\n * work agent's workspace and messages the agent so it can address the failure\n * instead of waiting for a human to notice a red merge gate.\n *\n * - Debounces per head SHA so retries do not spam the agent.\n * - Fetches failed log excerpts with `gh run view --log-failed`.\n * - Diff's the PR's failing check names against main's current failing set so\n * inherited main-red failures are labelled as such.\n */\n\nimport { execFile } from 'node:child_process';\nimport { join } from 'node:path';\nimport { Effect } from 'effect';\nimport { getAgentStateSync, messageAgent } from '../agents.js';\nimport { resolveProjectFromIssueSync } from '../projects.js';\nimport { writeFeedbackFile } from './feedback-writer.js';\n\nfunction execFilePromise(\n file: string,\n args: string[],\n options: { encoding: 'utf-8'; timeout?: number; maxBuffer?: number },\n): Promise<{ stdout: string; stderr: string }> {\n return new Promise((resolve, reject) => {\n execFile(file, args, options, (error, stdout, stderr) => {\n if (error) {\n reject(error);\n } else {\n resolve({ stdout: stdout as string, stderr: stderr as string });\n }\n });\n });\n}\n\nexport interface CiFailureFeedbackOptions {\n issueId: string;\n repo: string;\n prNumber: number;\n headSha: string;\n headRef: string;\n prUrl?: string;\n source: string;\n}\n\nexport interface CiFailure {\n name: string;\n workflowName?: string;\n runId: number;\n excerpt: string;\n inheritedFromMain: boolean;\n}\n\nexport interface CiFailureFeedbackResult {\n feedbackPath?: string;\n agentMessageSent: boolean;\n}\n\n/** Per-issue head SHA we last sent CI failure feedback for. */\nconst lastNotifiedSha = new Map<string, string>();\n\n/** Reset internal debounce state — for tests only. */\nexport function resetCiFailureFeedbackStateForTests(): void {\n lastNotifiedSha.clear();\n}\n\nfunction agentIdForIssue(issueId: string): string {\n return `agent-${issueId.toLowerCase()}`;\n}\n\nfunction parseRepo(repo: string): { owner: string; repo: string } {\n const [owner, ...rest] = repo.split('/');\n return { owner, repo: rest.join('/') };\n}\n\ninterface GhRunListItem {\n databaseId: number;\n name: string;\n workflowName: string;\n headSha?: string;\n conclusion: string;\n}\n\nasync function listFailingRuns(\n owner: string,\n repo: string,\n branch: string,\n headSha?: string,\n): Promise<GhRunListItem[]> {\n try {\n const { stdout } = await execFilePromise(\n 'gh',\n [\n 'run', 'list',\n '--repo', `${owner}/${repo}`,\n '--branch', branch,\n '--status', 'failure',\n '--json', 'databaseId,name,workflowName,headSha,conclusion',\n '--limit', '100',\n ],\n { encoding: 'utf-8', timeout: 30000 },\n );\n const runs = JSON.parse(stdout) as GhRunListItem[];\n if (headSha) {\n return runs.filter((r) => r.headSha === headSha);\n }\n return runs;\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Failed to list failing runs for ${branch}:`,\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n}\n\nasync function fetchRunFailedLogExcerpt(owner: string, repo: string, runId: number, maxChars = 2000): Promise<string> {\n try {\n const { stdout } = await execFilePromise(\n 'gh',\n ['run', 'view', String(runId), '--repo', `${owner}/${repo}`, '--log-failed'],\n { encoding: 'utf-8', timeout: 30000, maxBuffer: 2 * 1024 * 1024 },\n );\n return stdout.slice(0, maxChars).trim();\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Failed to fetch log for run ${runId}:`,\n err instanceof Error ? err.message : String(err),\n );\n return '';\n }\n}\n\nasync function collectFailures(\n owner: string,\n repo: string,\n branch: string,\n headSha: string,\n mainFailingNames: Set<string>,\n): Promise<CiFailure[]> {\n const runs = await listFailingRuns(owner, repo, branch, headSha);\n const failures: CiFailure[] = [];\n for (const run of runs) {\n const excerpt = await fetchRunFailedLogExcerpt(owner, repo, run.databaseId);\n const name = run.name || run.workflowName || `run-${run.databaseId}`;\n failures.push({\n name,\n workflowName: run.workflowName,\n runId: run.databaseId,\n excerpt,\n inheritedFromMain: mainFailingNames.has(name),\n });\n }\n return failures;\n}\n\nfunction buildFeedbackBody(opts: {\n issueId: string;\n repo: string;\n prNumber: number;\n headSha: string;\n prUrl?: string;\n failures: CiFailure[];\n source: string;\n}): string {\n const shortSha = opts.headSha.slice(0, 8);\n const prLine = opts.prUrl\n ? `Pull request: ${opts.prUrl} (head \\`${shortSha}\\`)`\n : `Pull request #${opts.prNumber} in ${opts.repo} (head \\`${shortSha}\\`)`;\n\n let body = `# CI Failure Feedback for ${opts.issueId}\\n\\n${prLine}\\n\\nSource: ${opts.source}\\n\\n`;\n\n if (opts.failures.length === 0) {\n body +=\n 'GitHub reported a failing CI status, but no failing workflow runs were found for this commit. Check the PR checks page directly.\\n\\n';\n } else {\n body += `## Failing checks (${opts.failures.length})\\n\\n`;\n for (const f of opts.failures) {\n const inheritedTag = f.inheritedFromMain ? ' [INHERITED FROM MAIN — also failing on main]' : '';\n body += `### ${f.name}${inheritedTag}\\n\\n`;\n if (f.workflowName && f.workflowName !== f.name) {\n body += `Workflow: ${f.workflowName} \\n`;\n }\n body += `Run: \\`gh run view ${f.runId} --repo ${opts.repo}\\` \\n`;\n if (f.excerpt) {\n body += '```\\n' + f.excerpt + '\\n```\\n\\n';\n } else {\n body += '*(No log excerpt available.)*\\n\\n';\n }\n }\n }\n\n body += `## Required action\\n\\n`;\n body +=\n 'Fix the failing checks, commit the fixes, and push an update. If a failure is marked [INHERITED FROM MAIN], it is not caused by your PR and does not need to be fixed in this branch.\\n';\n\n return body;\n}\n\nasync function relayCiFailureFeedbackPromise(\n opts: CiFailureFeedbackOptions,\n): Promise<CiFailureFeedbackResult> {\n const issueId = opts.issueId.toUpperCase();\n\n // Only relay for work agents. The feedback file/message would not be useful\n // for plan/review/test/ship/strike roles.\n const agentId = agentIdForIssue(issueId);\n const agentState = getAgentStateSync(agentId);\n if (!agentState || agentState.role !== 'work') {\n return { agentMessageSent: false };\n }\n\n // Debounce per head SHA so duplicate webhook deliveries / retries do not spam.\n const lastSha = lastNotifiedSha.get(issueId);\n if (lastSha === opts.headSha) {\n console.log(`[ci-failure-feedback] Skipping duplicate feedback for ${issueId} @ ${opts.headSha.slice(0, 8)}`);\n return { agentMessageSent: false };\n }\n\n const { owner, repo } = parseRepo(opts.repo);\n\n // Diff against main's current failing checks so agents do not chase inherited failures.\n const mainRuns = await listFailingRuns(owner, repo, 'main');\n const mainFailingNames = new Set(mainRuns.map((r) => r.name || r.workflowName || `run-${r.databaseId}`));\n\n // Collect the concrete failures for this PR head.\n const failures = await collectFailures(owner, repo, opts.headRef, opts.headSha, mainFailingNames);\n\n // If we cannot find any failing workflow run for this SHA, still write a short\n // feedback file for explicit status events so the agent is not left in the dark.\n if (failures.length === 0 && !opts.source.startsWith('status:')) {\n console.log(\n `[ci-failure-feedback] No failing runs found for ${issueId} @ ${opts.headSha.slice(0, 8)}; skipping feedback`,\n );\n return { agentMessageSent: false };\n }\n\n const resolved = resolveProjectFromIssueSync(issueId);\n const workspacePath = resolved\n ? join(resolved.projectPath, 'workspaces', `feature-${issueId.toLowerCase()}`)\n : undefined;\n\n const markdownBody = buildFeedbackBody({\n issueId,\n repo: opts.repo,\n prNumber: opts.prNumber,\n headSha: opts.headSha,\n prUrl: opts.prUrl,\n failures,\n source: opts.source,\n });\n\n const fileResult = await Effect.runPromise(\n writeFeedbackFile({\n issueId,\n workspacePath,\n specialist: 'ci-monitor',\n outcome: 'failed',\n summary: `CI failure: ${failures.map((f) => f.name).join(', ') || opts.source}`.slice(0, 120),\n markdownBody,\n }),\n );\n\n if (!fileResult.success || !fileResult.filePath) {\n console.error(`[ci-failure-feedback] Failed to write feedback for ${issueId}: ${fileResult.error}`);\n return { agentMessageSent: false };\n }\n\n let agentMessageSent = false;\n const message =\n `SPECIALIST FEEDBACK: ci-monitor reported CI FAILED for ${issueId}.\\n\\n` +\n `MUST READ: ${fileResult.filePath}\\n\\n` +\n 'Use your Read tool to open this file, read every line, then fix ALL failing checks. Do NOT stop at the prompt.';\n try {\n await messageAgent(agentId, message);\n agentMessageSent = true;\n } catch (err) {\n console.warn(\n `[ci-failure-feedback] Could not message ${agentId}; feedback file remains available: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n lastNotifiedSha.set(issueId, opts.headSha);\n return { feedbackPath: fileResult.filePath, agentMessageSent };\n}\n\n/** Effect variant of {@link relayCiFailureFeedbackPromise}. */\nexport const relayCiFailureFeedback = (\n opts: CiFailureFeedbackOptions,\n): Effect.Effect<CiFailureFeedbackResult> => Effect.promise(() => relayCiFailureFeedbackPromise(opts));\n","const GRAPHQL_COOLDOWN_MS = 60_000;\n\nlet cooldownStartedAt: number | null = null;\n\nfunction messageFromError(err: unknown): string {\n if (err instanceof Error) {\n const extra = 'stderr' in err && typeof err.stderr === 'string' ? ` ${err.stderr}` : '';\n return `${err.message}${extra}`;\n }\n if (typeof err === 'string') return err;\n try {\n return JSON.stringify(err);\n } catch {\n return String(err);\n }\n}\n\nfunction isGraphQLRateLimitError(err: unknown): boolean {\n const message = messageFromError(err);\n return /GraphQL:.*rate limit/i.test(message)\n || /rate limit already exceeded/i.test(message)\n || /API rate limit/i.test(message);\n}\n\nexport function noteGraphQLRateLimit(err: unknown): void {\n if (!isGraphQLRateLimitError(err)) return;\n cooldownStartedAt = Date.now();\n}\n\nexport function isInGraphQLCooldown(): boolean {\n if (cooldownStartedAt == null) return false;\n return Date.now() - cooldownStartedAt < GRAPHQL_COOLDOWN_MS;\n}\n","/**\n * GitHub webhook event handlers (PAN-905)\n *\n * Dispatches verified webhook events to per-type handlers that update\n * review_status blockerReasons based on GitHub-native merge blockers.\n */\n\nimport { Effect } from 'effect';\nimport { setReviewStatus, getReviewStatus, loadReviewStatuses, type BlockerReason, type ReviewStatus } from './review-status.js';\nimport { getGitHubConfig } from '../dashboard/server/services/tracker-config.js';\nimport { GitHubApiError } from './errors.js';\nimport { relayCiFailureFeedback } from './cloister/ci-failure-feedback.js';\nimport { isInGraphQLCooldown, noteGraphQLRateLimit } from './github-graphql-cooldown.js';\nimport { bumpIssuePrTabCacheGeneration } from '../dashboard/server/services/pr-tab-cache.js';\n\nexport interface WebhookPayload {\n action?: string;\n pull_request?: {\n number: number;\n head: { ref: string; sha?: string };\n html_url?: string;\n mergeable?: boolean | null;\n mergeable_state?: string;\n draft?: boolean;\n state?: string;\n merged?: boolean;\n };\n check_suite?: {\n status?: string;\n conclusion?: string | null;\n pull_requests?: Array<{ number: number; head: { ref: string; sha?: string } }>;\n };\n check_run?: {\n id?: number;\n name?: string;\n status?: string;\n conclusion?: string | null;\n pull_requests?: Array<{ number: number; head: { ref: string; sha?: string } }>;\n };\n repository?: { full_name: string };\n issue?: {\n number?: number;\n pull_request?: unknown;\n };\n review?: { state: string };\n thread?: { id?: number; resolved?: boolean };\n // status event payload\n sha?: string;\n state?: string;\n context?: string;\n branches?: Array<{ name: string }>;\n}\n\nexport function issueIdFromBranch(ref: string): string | null {\n const match = ref.match(/(?:feature|strike)\\/([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\n// ─── Repository authorization (defense-in-depth) ─────────────────────────────\n\n// ─── Cached repository allowlist (refreshed every 5 min) ─────────────────────\n\nlet cachedTrackedRepos: Set<string> | null = null;\nlet cachedTrackedReposAt = 0;\nconst REPO_CACHE_TTL_MS = 5 * 60 * 1000;\n\nfunction getTrackedRepos(): Set<string> {\n const now = Date.now();\n if (!cachedTrackedRepos || now - cachedTrackedReposAt > REPO_CACHE_TTL_MS) {\n const config = getGitHubConfig();\n cachedTrackedRepos = config\n ? new Set(config.repos.map(({ owner, repo }) => `${owner}/${repo}`.toLowerCase()))\n : new Set();\n cachedTrackedReposAt = now;\n }\n return cachedTrackedRepos;\n}\n\nexport function isTrackedRepositorySync(fullName: string | undefined): boolean {\n if (!fullName) return false;\n return getTrackedRepos().has(fullName.toLowerCase());\n}\n\n// ─── PR identity validation ──────────────────────────────────────────────────\n\nfunction getRepoFromPrUrl(prUrl: string): string | null {\n const m = prUrl.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull\\/\\d+/i);\n return m ? `${m[1]}/${m[2]}`.toLowerCase() : null;\n}\n\nfunction parseGitHubPrUrl(prUrl: string): { owner: string; repo: string; number: number } | null {\n const m = prUrl.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull\\/(\\d+)/i);\n if (!m) return null;\n return { owner: m[1]!, repo: m[2]!, number: Number.parseInt(m[3]!, 10) };\n}\n\nfunction repoFromPrUrl(prUrl: string | undefined): string | null {\n if (!prUrl) return null;\n return getRepoFromPrUrl(prUrl);\n}\n\nasync function loadAndValidateStatus(\n issueId: string,\n repo: string,\n prNumber?: number,\n headSha?: string,\n): Promise<ReviewStatus | null> {\n const status = await Effect.runPromise(getReviewStatus(issueId));\n if (!status) return null;\n\n // If no PR identity is stored yet, allow the event (identity will be populated lazily).\n if (!status.prUrl && !status.prNumber && !status.prHeadSha) {\n return status;\n }\n\n const storedRepo = status.prUrl ? getRepoFromPrUrl(status.prUrl) : null;\n if (storedRepo && storedRepo !== repo.toLowerCase()) {\n console.warn(`[webhook] Repo mismatch for ${issueId}: stored=${storedRepo} event=${repo}`);\n return null;\n }\n\n if (prNumber != null && status.prNumber != null && prNumber !== status.prNumber) {\n console.warn(`[webhook] PR number mismatch for ${issueId}: stored=${status.prNumber} event=${prNumber}`);\n return null;\n }\n\n if (headSha && status.prHeadSha && headSha !== status.prHeadSha) {\n console.warn(`[webhook] Head SHA mismatch for ${issueId}: stored=${status.prHeadSha} event=${headSha}`);\n return null;\n }\n\n return status;\n}\n\n// ─── Per-source failing_checks tracking ──────────────────────────────────────\n\nfunction getFailingChecksBlocker(blockers: BlockerReason[]): BlockerReason | undefined {\n return blockers.find((b) => b.type === 'failing_checks');\n}\n\nfunction parseFailingSources(details?: string): Record<string, string> {\n if (!details) return {};\n try {\n const parsed = JSON.parse(details) as { sources?: Record<string, string> };\n return parsed.sources ?? {};\n } catch {\n return {};\n }\n}\n\nfunction serializeFailingSources(sources: Record<string, string>): string {\n return JSON.stringify({ sources });\n}\n\nfunction addFailingSource(blockers: BlockerReason[], sourceKey: string, summary: string): BlockerReason[] {\n const existing = getFailingChecksBlocker(blockers);\n const sources = parseFailingSources(existing?.details);\n sources[sourceKey] = summary;\n const updated: BlockerReason = {\n type: 'failing_checks',\n summary: Object.values(sources).join('; '),\n details: serializeFailingSources(sources),\n detectedAt: existing?.detectedAt ?? new Date().toISOString(),\n };\n return [...blockers.filter((b) => b.type !== 'failing_checks'), updated];\n}\n\nfunction removeFailingSource(blockers: BlockerReason[], sourceKey: string): BlockerReason[] {\n const existing = getFailingChecksBlocker(blockers);\n if (!existing) return blockers;\n const sources = parseFailingSources(existing.details);\n delete sources[sourceKey];\n if (Object.keys(sources).length === 0) {\n return blockers.filter((b) => b.type !== 'failing_checks');\n }\n const updated: BlockerReason = {\n type: 'failing_checks',\n summary: Object.values(sources).join('; '),\n details: serializeFailingSources(sources),\n detectedAt: existing.detectedAt,\n };\n return [...blockers.filter((b) => b.type !== 'failing_checks'), updated];\n}\n\n// ─── Merge state reconciliation for unknown mergeable_state ──────────────────\n\nconst KNOWN_CONFLICT_STATES = new Set(['dirty']);\nconst KNOWN_NON_BLOCKING_STATES = new Set(['clean', 'unstable']);\nconst KNOWN_NOT_MERGEABLE_STATES = new Set(['blocked', 'behind']);\n\n/** `gh` statusCheckRollup conclusions/states that count as a failing required check. */\nexport const FAILING_CHECK_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE', 'STALE']);\n\nconst pendingReconciliation = new Set<string>();\nconst reconciliationTimeouts = new Map<string, NodeJS.Timeout>();\n\nexport function clearAllReconciliationTimeouts(): void {\n for (const timeout of reconciliationTimeouts.values()) {\n clearTimeout(timeout);\n }\n reconciliationTimeouts.clear();\n pendingReconciliation.clear();\n}\n\nfunction scheduleMergeStateReconciliation(issueId: string, repo: string, prNumber: number): void {\n if (pendingReconciliation.has(issueId)) return;\n pendingReconciliation.add(issueId);\n const timeout = setTimeout(() => {\n pendingReconciliation.delete(issueId);\n reconciliationTimeouts.delete(issueId);\n refreshMergeStateFromGitHub(issueId, repo, prNumber).catch(() => {});\n }, 30000);\n timeout.unref();\n reconciliationTimeouts.set(issueId, timeout);\n}\n\nexport async function refreshMergeStateFromGitHub(issueId: string, repo: string, prNumber: number): Promise<void> {\n try {\n // Resolve PR merge state. Prefer the GitHub App REST path (installation\n // token, separate rate-limit budget, no GraphQL) when configured; fall back\n // to `gh pr view` (GraphQL) only when the App is not set up. Previously this\n // unconditionally used `gh` GraphQL even with the App configured, which\n // burned the GraphQL budget on every pull_request webhook + every\n // merge-blocker poll. PAN-2265.\n const [owner, repoName] = repo.split('/');\n if (!owner || !repoName) return;\n\n let mergeable: string;\n let mergeState: string;\n let isDraft: boolean;\n let checksFailed: boolean;\n\n const { isGitHubAppConfigured, getPullRequestState } = await import('./github-app.js');\n if (isGitHubAppConfigured()) {\n // App REST path — installation token, separate rate-limit budget, no GraphQL.\n const prState = await Effect.runPromise(getPullRequestState(owner, repoName, prNumber));\n mergeable = prState.mergeable === null ? 'UNKNOWN' : prState.mergeable ? 'MERGEABLE' : 'CONFLICTING';\n mergeState = (prState.mergeableState ?? '').toUpperCase();\n isDraft = prState.draft;\n checksFailed = prState.checksFailed;\n } else {\n // gh CLI fallback (GraphQL) — only when the App is not configured.\n if (isInGraphQLCooldown()) return;\n const { execFile } = await import('child_process');\n const { promisify } = await import('util');\n const execFileAsync = promisify(execFile);\n // gh GraphQL fields (NOT the REST webhook shape): mergeable is an enum\n // (MERGEABLE | CONFLICTING | UNKNOWN), mergeStateStatus is CLEAN | DIRTY |\n // UNSTABLE | BLOCKED | BEHIND | …, isDraft is a bool, and statusCheckRollup\n // carries the required-check results. PAN-1620.\n let stdout: string;\n try {\n ({ stdout } = await execFileAsync(\n 'gh',\n ['pr', 'view', String(prNumber), '--repo', repo, '--json', 'mergeable,mergeStateStatus,isDraft,statusCheckRollup'],\n { encoding: 'utf-8', timeout: 15000 },\n ));\n } catch (err) {\n noteGraphQLRateLimit(err);\n throw err;\n }\n if (!stdout.trim()) return;\n const pr = JSON.parse(stdout) as {\n mergeable?: string | null;\n mergeStateStatus?: string | null;\n isDraft?: boolean;\n statusCheckRollup?: Array<{ conclusion?: string | null; state?: string | null }>;\n };\n mergeable = (pr.mergeable ?? '').toUpperCase();\n mergeState = (pr.mergeStateStatus ?? '').toUpperCase();\n isDraft = pr.isDraft === true;\n checksFailed = (pr.statusCheckRollup ?? []).some((c) =>\n FAILING_CHECK_CONCLUSIONS.has((c.conclusion || c.state || '').toUpperCase()),\n );\n }\n\n const status = await Effect.runPromise(getReviewStatus(issueId));\n if (!status) return;\n\n const isConflicting = mergeable === 'CONFLICTING' || mergeState === 'DIRTY';\n\n // Rebuild the GitHub-native blockers from live state, preserving any\n // non-GitHub-native ones (e.g. unresolved_conversations, changes_requested).\n const GH_NATIVE = new Set<BlockerReason['type']>(['merge_conflict', 'not_mergeable', 'failing_checks', 'draft_pr']);\n const now = new Date().toISOString();\n const blockers: BlockerReason[] = (status.blockerReasons ?? []).filter((b) => !GH_NATIVE.has(b.type));\n if (isDraft) blockers.push({ type: 'draft_pr', summary: 'Pull request is in draft state', detectedAt: now });\n if (isConflicting) {\n blockers.push({ type: 'merge_conflict', summary: 'Merge conflict with target branch', detectedAt: now });\n } else if (mergeState === 'BLOCKED' && !checksFailed) {\n blockers.push({ type: 'not_mergeable', summary: 'PR blocked by branch protection', detectedAt: now });\n }\n if (checksFailed) blockers.push({ type: 'failing_checks', summary: 'Required checks are failing', detectedAt: now });\n\n // Only write when the GitHub-native blocker set actually changed.\n const prevTypes = new Set((status.blockerReasons ?? []).filter((b) => GH_NATIVE.has(b.type)).map((b) => b.type));\n const nextTypes = new Set(blockers.filter((b) => GH_NATIVE.has(b.type)).map((b) => b.type));\n const changed = prevTypes.size !== nextTypes.size || [...nextTypes].some((t) => !prevTypes.has(t));\n if (changed) {\n await Effect.runPromise(setReviewStatus(issueId, { blockerReasons: blockers.length > 0 ? blockers : undefined }, status));\n }\n } catch (err) {\n console.warn(`[webhook] Merge state reconciliation failed for ${issueId}:`, err);\n }\n}async function handleCheckSuitePromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const suite = payload.check_suite;\n if (!suite) return;\n if (!suite.pull_requests || suite.pull_requests.length === 0) return;\n\n const repo = payload.repository!.full_name;\n\n for (const pr of suite.pull_requests) {\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) continue;\n\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, pr.number, pr.head.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (suite.conclusion === 'success') {\n blockers = removeFailingSource(blockers, 'check_suite');\n } else if (suite.conclusion && FAILING_CHECK_CONCLUSIONS.has(suite.conclusion.toUpperCase())) {\n // Any terminal failure conclusion is blocking\n blockers = addFailingSource(blockers, 'check_suite', `CI check suite ${suite.conclusion}`);\n if (pr.head.sha && pr.number != null) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: pr.number,\n headSha: pr.head.sha,\n headRef: pr.head.ref,\n prUrl: status.prUrl,\n source: 'check_suite',\n }));\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n }\n}async function handleCheckRunPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const run = payload.check_run;\n if (!run) return;\n if (!run.pull_requests || run.pull_requests.length === 0) return;\n\n const repo = payload.repository!.full_name;\n const sourceKey = `check_run:${run.name ?? String(run.id ?? 'unknown')}`;\n\n for (const pr of run.pull_requests) {\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) continue;\n\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, pr.number, pr.head.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (run.conclusion === 'success') {\n blockers = removeFailingSource(blockers, sourceKey);\n } else if (run.conclusion && FAILING_CHECK_CONCLUSIONS.has(run.conclusion.toUpperCase())) {\n blockers = addFailingSource(blockers, sourceKey, `CI check run ${run.conclusion}: ${run.name ?? 'unknown'}`);\n if (pr.head.sha && pr.number != null) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: pr.number,\n headSha: pr.head.sha,\n headRef: pr.head.ref,\n prUrl: status.prUrl,\n source: sourceKey,\n }));\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n }\n}async function handlePullRequestPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n if (!pr) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n\n const repo = payload.repository!.full_name;\n\n // PAN-1513: fire postMergeLifecycle when GitHub reports the PR closed+merged.\n // Without this, admin-merges (gh pr merge --admin) and any merge that doesn't\n // route through Overdeck's own merge flow leave work agents, strikes, tmux\n // sessions, and worktrees orphaned. postMergeLifecycle has its own\n // single-flight guard (specialists.ts L116) and _completedPostMerge marker,\n // so duplicate webhook deliveries are idempotent.\n if (payload.action === 'closed' && pr.merged === true) {\n try {\n const { postMergeLifecycle } = await import('./cloister/merge-agent.js');\n const { resolveProjectFromIssueSync } = await import('./projects.js');\n const project = resolveProjectFromIssueSync(issueId);\n if (project) {\n const branchName = pr.head.ref;\n const markReviewPassed = branchName.startsWith('strike/');\n postMergeLifecycle(issueId, project.projectPath, branchName, { markReviewPassed }).catch(err =>\n console.warn(`[webhook] postMergeLifecycle failed for ${issueId} (${branchName}): ${err?.message ?? err}`),\n );\n }\n } catch (err: any) {\n console.warn(`[webhook] Failed to dispatch postMergeLifecycle for ${issueId}: ${err?.message ?? err}`);\n }\n }\n\n // For synchronize/opened/reopened the head SHA may have changed — skip SHA\n // validation so the handler can refresh prHeadSha and recompute blockers.\n const headMayHaveMoved = ['synchronize', 'opened', 'reopened'].includes(payload.action ?? '');\n const status = await loadAndValidateStatus(issueId, repo, pr.number, headMayHaveMoved ? undefined : pr.head.sha);\n if (!status) return;\n\n const update: Partial<ReviewStatus> = {};\n let blockers = [...(status.blockerReasons ?? [])];\n\n // Populate missing PR identity and keep head SHA in sync on synchronize\n if (!status.prUrl) update.prUrl = pr.html_url ?? `https://github.com/${repo}/pull/${pr.number}`;\n if (!status.prNumber) update.prNumber = pr.number;\n if (pr.head.sha && (!status.prHeadSha || payload.action === 'synchronize')) {\n update.prHeadSha = pr.head.sha;\n }\n\n // Dismissed reviews\n if (payload.action === 'review_dismissed') {\n blockers = blockers.filter((b) => b.type !== 'changes_requested');\n }\n\n // Draft PR\n if (pr.draft) {\n const draftBlocker: BlockerReason = {\n type: 'draft_pr',\n summary: 'Pull request is in draft state',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'draft_pr'), draftBlocker];\n } else {\n blockers = blockers.filter((b) => b.type !== 'draft_pr');\n }\n\n // Merge state\n if (pr.mergeable_state === 'unknown') {\n scheduleMergeStateReconciliation(issueId, repo, pr.number);\n } else if (pr.mergeable_state && KNOWN_CONFLICT_STATES.has(pr.mergeable_state)) {\n const mergeConflictBlocker: BlockerReason = {\n type: 'merge_conflict',\n summary: 'Merge conflict with target branch',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), mergeConflictBlocker];\n } else if (pr.mergeable === false && (pr.mergeable_state === null || pr.mergeable_state === undefined)) {\n const mergeConflictBlocker: BlockerReason = {\n type: 'merge_conflict',\n summary: 'Merge conflict with target branch',\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), mergeConflictBlocker];\n } else if ((pr.mergeable_state && KNOWN_NON_BLOCKING_STATES.has(pr.mergeable_state)) || pr.mergeable === true) {\n blockers = blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable');\n } else if (pr.mergeable_state && KNOWN_NOT_MERGEABLE_STATES.has(pr.mergeable_state)) {\n const notMergeableBlocker: BlockerReason = {\n type: 'not_mergeable',\n summary: `PR not mergeable: ${pr.mergeable_state}`,\n detectedAt: new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'merge_conflict' && b.type !== 'not_mergeable'), notMergeableBlocker];\n }\n\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}async function handlePullRequestReviewPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n const review = payload.review;\n if (!pr || !review) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n\n const repo = payload.repository!.full_name;\n const status = await loadAndValidateStatus(issueId, repo, pr.number);\n if (!status) return;\n\n // Dismissed reviews are handled by the pull_request review_dismissed action.\n if (review.state === 'dismissed') return;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (review.state === 'changes_requested') {\n blockers = [...blockers.filter((b) => b.type !== 'changes_requested'), {\n type: 'changes_requested',\n summary: 'Changes requested on pull request',\n detectedAt: new Date().toISOString(),\n }];\n } else if (review.state === 'approved') {\n blockers = blockers.filter((b) => b.type !== 'changes_requested');\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}\n\nasync function handlePullRequestReviewCommentPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n if (!pr) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n bumpIssuePrTabCacheGeneration(issueId);\n}\n\nasync function handleIssueCommentPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const issue = payload.issue;\n if (!issue?.pull_request || issue.number == null) return;\n\n const repo = payload.repository!.full_name.toLowerCase();\n const statuses = loadReviewStatuses();\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.prNumber !== issue.number) continue;\n const statusRepo = repoFromPrUrl(status.prUrl);\n if (statusRepo && statusRepo !== repo) continue;\n bumpIssuePrTabCacheGeneration(issueId);\n }\n}\n\nasync function handlePullRequestReviewThreadPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const pr = payload.pull_request;\n const thread = payload.thread;\n if (!pr || !thread) return;\n const issueId = issueIdFromBranch(pr.head.ref);\n if (!issueId) return;\n\n const repo = payload.repository!.full_name;\n const status = await loadAndValidateStatus(issueId, repo, pr.number);\n if (!status) return;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (thread.resolved === false) {\n if (thread.id == null) {\n console.warn(`[webhook] Unresolved review thread without id for ${issueId} — cannot track conversation`);\n }\n const existing = blockers.find((b) => b.type === 'unresolved_conversations');\n let threadIds: Set<string>;\n try {\n threadIds = new Set<string>(JSON.parse(existing?.details ?? '[]') as string[]);\n } catch {\n threadIds = new Set<string>();\n }\n if (thread.id != null) threadIds.add(String(thread.id));\n const updated: BlockerReason = {\n type: 'unresolved_conversations',\n summary: 'Unresolved review conversation',\n details: JSON.stringify([...threadIds]),\n detectedAt: existing?.detectedAt ?? new Date().toISOString(),\n };\n blockers = [...blockers.filter((b) => b.type !== 'unresolved_conversations'), updated];\n } else if (thread.resolved === true) {\n if (thread.id == null) return;\n const existing = blockers.find((b) => b.type === 'unresolved_conversations');\n if (!existing) return;\n let threadIds: Set<string>;\n try {\n threadIds = new Set<string>(JSON.parse(existing.details ?? '[]') as string[]);\n } catch {\n threadIds = new Set<string>();\n }\n threadIds.delete(String(thread.id));\n if (threadIds.size === 0) {\n blockers = blockers.filter((b) => b.type !== 'unresolved_conversations');\n } else {\n const updated: BlockerReason = {\n type: 'unresolved_conversations',\n summary: 'Unresolved review conversation',\n details: JSON.stringify([...threadIds]),\n detectedAt: existing.detectedAt,\n };\n blockers = [...blockers.filter((b) => b.type !== 'unresolved_conversations'), updated];\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n}async function handleStatusPromise(payload: WebhookPayload): Promise<void> {\n if (!isTrackedRepositorySync(payload.repository?.full_name)) return;\n const state = payload.state;\n const branches = payload.branches;\n if (!state || !branches || branches.length === 0) return;\n\n const repo = payload.repository!.full_name;\n const context = payload.context ?? 'default';\n const sourceKey = `status:${context}`;\n\n for (const branch of branches) {\n const issueId = issueIdFromBranch(branch.name);\n if (issueId) {\n bumpIssuePrTabCacheGeneration(issueId);\n\n const status = await loadAndValidateStatus(issueId, repo, undefined, payload.sha);\n if (!status) continue;\n\n let blockers = [...(status.blockerReasons ?? [])];\n\n if (state === 'success') {\n blockers = removeFailingSource(blockers, sourceKey);\n } else if (state === 'failure' || state === 'error') {\n blockers = addFailingSource(blockers, sourceKey, `Commit status: ${state}`);\n if (payload.sha && status.prNumber != null && status.prUrl) {\n const parsed = parseGitHubPrUrl(status.prUrl);\n if (parsed) {\n await Effect.runPromise(relayCiFailureFeedback({\n issueId,\n repo,\n prNumber: status.prNumber,\n headSha: payload.sha,\n headRef: branch.name,\n prUrl: status.prUrl,\n source: sourceKey,\n }));\n }\n }\n }\n\n const update: Partial<ReviewStatus> = {};\n if (blockers.length > 0) update.blockerReasons = blockers;\n else if (status.blockerReasons && status.blockerReasons.length > 0) update.blockerReasons = undefined;\n\n // Do NOT populate prHeadSha from status events — we don't have the PR number\n // to construct a prUrl, and the SHA alone isn't enough to bind identity.\n\n if (Object.keys(update).length > 0) {\n await Effect.runPromise(setReviewStatus(issueId, update, status));\n }\n\n // Keep scanning in case the payload includes multiple feature branches.\n continue;\n }\n }\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\nconst toGhError = (op: string, cause: unknown): GitHubApiError =>\n new GitHubApiError({\n operation: op,\n status: 0,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n });\n\n/** Effect: handle a `check_suite` GitHub webhook payload. */\nexport const handleCheckSuite = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleCheckSuitePromise(payload),\n catch: (cause) => toGhError('handleCheckSuite', cause),\n });\n\n/** Effect: handle a `check_run` GitHub webhook payload. */\nexport const handleCheckRun = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleCheckRunPromise(payload),\n catch: (cause) => toGhError('handleCheckRun', cause),\n });\n\n/** Effect: handle a `pull_request` GitHub webhook payload. */\nexport const handlePullRequest = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestPromise(payload),\n catch: (cause) => toGhError('handlePullRequest', cause),\n });\n\n/** Effect: handle a `pull_request_review` GitHub webhook payload. */\nexport const handlePullRequestReview = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReview', cause),\n });\n\n/** Effect: handle a `pull_request_review_comment` GitHub webhook payload. */\nexport const handlePullRequestReviewComment = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewCommentPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReviewComment', cause),\n });\n\n/** Effect: handle an `issue_comment` GitHub webhook payload for PR tab cache invalidation. */\nexport const handleIssueComment = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleIssueCommentPromise(payload),\n catch: (cause) => toGhError('handleIssueComment', cause),\n });\n\n/** Effect: handle a `pull_request_review_thread` GitHub webhook payload. */\nexport const handlePullRequestReviewThread = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handlePullRequestReviewThreadPromise(payload),\n catch: (cause) => toGhError('handlePullRequestReviewThread', cause),\n });\n\n/** Effect: handle a `status` GitHub webhook payload. */\nexport const handleStatus = (\n payload: WebhookPayload,\n): Effect.Effect<void, GitHubApiError> =>\n Effect.tryPromise({\n try: () => handleStatusPromise(payload),\n catch: (cause) => toGhError('handleStatus', cause),\n });\n\n/** True if the repo is in the cached tracked-repos allowlist. Pure. */\nexport const isTrackedRepository = (\n fullName: string | undefined,\n): Effect.Effect<boolean> =>\n Effect.sync(() => isTrackedRepositorySync(fullName));\n\n// ─── Boot-time blocker reconciliation (PAN-1771) ─────────────────────────────\n\nconst GH_NATIVE_BLOCKER_TYPES = new Set<BlockerReason['type']>([\n 'merge_conflict',\n 'not_mergeable',\n 'failing_checks',\n 'draft_pr',\n]);\n\nfunction prNumberFromUrl(prUrl: string): number | null {\n const m = prUrl.match(/\\/pull\\/(\\d+)/);\n return m ? Number(m[1]) : null;\n}\n\n/**\n * Pure boot-sweep filter: returns the PR identity to refresh when a status\n * carries GitHub-native blockers that may have gone stale, null otherwise.\n * Exported for tests.\n */\nexport function needsBlockerReconciliation(\n status: Pick<ReviewStatus, 'mergeStatus' | 'blockerReasons' | 'prUrl' | 'prNumber'>,\n): { repo: string; prNumber: number } | null {\n if (status.mergeStatus === 'merged') return null;\n const ghBlockers = (status.blockerReasons ?? []).filter((b) => GH_NATIVE_BLOCKER_TYPES.has(b.type));\n if (ghBlockers.length === 0) return null;\n const repo = status.prUrl ? getRepoFromPrUrl(status.prUrl) : null;\n const prNumber = status.prNumber ?? (status.prUrl ? prNumberFromUrl(status.prUrl) : null);\n if (!repo || !prNumber) return null;\n return { repo, prNumber };\n}\n\n/**\n * PAN-1771: GitHub-native blockers (failing_checks, merge_conflict, draft_pr,\n * not_mergeable) are refreshed only by webhook-driven\n * refreshMergeStateFromGitHub calls. Webhooks that arrive while the server is\n * down (reboot, deploy restart, crash) are lost, so a blocker recorded before\n * the outage can outlive the condition it describes — and a stale blocker pins\n * readyForMerge=false forever under the PAN-1650 event-driven derivation.\n *\n * Re-derives the blocker set from live PR state for every non-merged status\n * still carrying GitHub-native blockers. Sequential on purpose: the candidate\n * set is small and this avoids a gh subprocess burst at boot. Returns the\n * number of statuses refreshed.\n */\nexport async function reconcileStaleGitHubBlockers(): Promise<number> {\n const statuses = loadReviewStatuses();\n let refreshed = 0;\n for (const status of Object.values(statuses)) {\n const target = needsBlockerReconciliation(status);\n if (!target) continue;\n await refreshMergeStateFromGitHub(status.issueId, target.repo, target.prNumber);\n refreshed++;\n }\n return refreshed;\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { A as resolveProjectFromIssueSync, r as extractTeamPrefix, s as findProjectByTeamSync, v as init_projects } from "./projects-D2nMJ9C4.js";
2
2
  import { f as init_config, m as loadConfigSync } from "./config-CPTQFN9R.js";
3
3
  import { j as sessionExistsSync, y as init_tmux } from "./tmux-Ceg1ulra.js";
4
- import { in as setAgentPausedSync, t as init_agents, z as stopAgentSync } from "./agents-CQHK0CSA.js";
4
+ import { in as setAgentPausedSync, t as init_agents, z as stopAgentSync } from "./agents-CgVr77Qq.js";
5
5
  import { a as PAN_DIRNAME, i as PAN_CONTINUE_FILENAME, n as PAN_CONTEXT_FILENAME, s as PAN_FEEDBACK_DIRNAME, u as PAN_SPEC_FILENAME } from "./types-BjVYEbAq.js";
6
6
  import { t as init_pan_dir } from "./pan-dir-QGdE4IAd.js";
7
7
  import { i as init_workspace_manager, n as createWorkspace, o as removeWorkspace } from "./workspace-manager-CxoFBq_Y.js";
@@ -472,4 +472,4 @@ async function migrateWorkspace(issueId, options) {
472
472
  //#endregion
473
473
  export { migrateWorkspace };
474
474
 
475
- //# sourceMappingURL=workspace-migrate-duUsWNpd.js.map
475
+ //# sourceMappingURL=workspace-migrate-BBQ8nAnP.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-migrate-duUsWNpd.js","names":[],"sources":["../src/cli/commands/workspace-migrate.ts"],"sourcesContent":["/**\n * Workspace Migration Command\n *\n * Migrates workspaces between local and remote (Fly.io) environments.\n *\n * Usage:\n * pan workspace migrate <issue-id> --to-remote # Local -> Remote\n * pan workspace migrate <issue-id> --to-local # Remote -> Local\n *\n * Options:\n * --keep Keep source workspace after migration\n * --force Overwrite if destination exists, ignore running agents\n * --no-docker Don't start Docker containers after migration\n */\n\nimport chalk from 'chalk';\nimport { Effect } from 'effect';\nimport ora from 'ora';\nimport { existsSync, readdirSync, readFileSync, mkdirSync, statSync } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { loadConfigSync } from '../../lib/config.js';\nimport { resolveProjectFromIssueSync, extractTeamPrefix, findProjectByTeamSync, type ProjectConfig } from '../../lib/projects.js';\nimport {\n loadWorkspaceMetadataSync,\n saveWorkspaceMetadataSync,\n deleteWorkspaceMetadataSync,\n} from '../../lib/remote/workspace-metadata.js';\nimport {\n createFlyProviderFromConfig,\n} from '../../lib/remote/index.js';\nimport { PAN_CONTEXT_FILENAME, PAN_CONTINUE_FILENAME, PAN_DIRNAME, PAN_FEEDBACK_DIRNAME, PAN_SPEC_FILENAME } from '../../lib/pan-dir/index.js';\nimport { createWorkspace, removeWorkspace } from '../../lib/workspace-manager.js';\nimport { stopAgentSync, setAgentPausedSync } from '../../lib/agents.js';\nimport { sessionExistsSync } from '../../lib/tmux.js';\nimport type { RemoteWorkspaceMetadata } from '../../lib/remote/interface.js';\nimport type { RemoteProvider } from '../../lib/remote/interface.js';\n\nconst execAsync = promisify(exec);\n\nexport interface MigrateOptions {\n toRemote?: boolean;\n toLocal?: boolean;\n keep?: boolean;\n force?: boolean;\n noDocker?: boolean;\n}\n\ninterface MigrationResult {\n success: boolean;\n message: string;\n errors: string[];\n steps: string[];\n}\n\n/**\n * Detect current workspace location\n */\nfunction detectWorkspaceLocation(issueId: string): 'local' | 'remote' | 'none' {\n const normalizedId = issueId.toLowerCase();\n\n // Check for remote workspace metadata\n const remoteMetadata = loadWorkspaceMetadataSync(issueId);\n if (remoteMetadata) {\n return 'remote';\n }\n\n // Check for local workspace\n const resolved = resolveProjectFromIssueSync(issueId, []);\n if (resolved) {\n const workspacePath = join(resolved.projectPath, 'workspaces', `feature-${normalizedId}`);\n if (existsSync(workspacePath)) {\n return 'local';\n }\n }\n\n return 'none';\n}\n\n/**\n * Find local workspace path\n */\nfunction findLocalWorkspacePath(issueId: string): string | null {\n const normalizedId = issueId.toLowerCase();\n const resolved = resolveProjectFromIssueSync(issueId, []);\n\n if (resolved) {\n const workspacePath = join(resolved.projectPath, 'workspaces', `feature-${normalizedId}`);\n if (existsSync(workspacePath)) {\n return workspacePath;\n }\n }\n\n return null;\n}\n\n\n\n\ninterface MigratableWorkspaceFile {\n localPath: string;\n remotePath: string;\n label: string;\n}\n\nfunction collectPanWorkspaceFiles(workspacePath: string): MigratableWorkspaceFile[] {\n const panDir = join(workspacePath, PAN_DIRNAME);\n if (!existsSync(panDir)) return [];\n\n const files: MigratableWorkspaceFile[] = [];\n const directFiles = [\n { name: PAN_SPEC_FILENAME, label: 'workspace vBRIEF' },\n { name: PAN_CONTINUE_FILENAME, label: 'continue state' },\n { name: PAN_CONTEXT_FILENAME, label: 'feature context' },\n ];\n\n for (const file of directFiles) {\n const localPath = join(panDir, file.name);\n if (existsSync(localPath) && statSync(localPath).isFile()) {\n files.push({\n localPath,\n remotePath: `/workspace/${PAN_DIRNAME}/${file.name}`,\n label: file.label,\n });\n }\n }\n\n const feedbackDir = join(panDir, PAN_FEEDBACK_DIRNAME);\n if (existsSync(feedbackDir) && statSync(feedbackDir).isDirectory()) {\n for (const entry of readdirSync(feedbackDir)) {\n const localPath = join(feedbackDir, entry);\n if (!statSync(localPath).isFile()) continue;\n files.push({\n localPath,\n remotePath: `/workspace/${PAN_DIRNAME}/${PAN_FEEDBACK_DIRNAME}/${entry}`,\n label: `feedback/${entry}`,\n });\n }\n }\n\n return files;\n}\n\nasync function copyWorkspacePanStateToRemote(\n provider: RemoteProvider,\n vmName: string,\n workspacePath: string,\n): Promise<{ steps: string[]; errors: string[] }> {\n const steps: string[] = [];\n const errors: string[] = [];\n const files = collectPanWorkspaceFiles(workspacePath);\n\n if (files.length === 0) {\n steps.push('No workspace .pan state found (skipped)');\n return { steps, errors };\n }\n\n try {\n await Effect.runPromise(provider.ssh(vmName, `mkdir -p /workspace/${PAN_DIRNAME}/${PAN_FEEDBACK_DIRNAME}`));\n for (const file of files) {\n const remoteDir = dirname(file.remotePath);\n await Effect.runPromise(provider.ssh(vmName, `mkdir -p ${remoteDir}`));\n await Effect.runPromise(provider.copyToVm(vmName, file.localPath, file.remotePath));\n steps.push(`Copied ${file.label}`);\n }\n } catch (error: any) {\n errors.push(`Workspace .pan state: ${error.message}`);\n }\n\n return { steps, errors };\n}\n\nasync function copyWorkspacePanStateFromRemote(\n provider: RemoteProvider,\n vmName: string,\n workspacePath: string,\n): Promise<{ steps: string[]; errors: string[] }> {\n const steps: string[] = [];\n const errors: string[] = [];\n\n const remotePanDir = `/workspace/${PAN_DIRNAME}`;\n const panCheck = await Effect.runPromise(provider.ssh(vmName, `[ -d ${remotePanDir} ] && echo present`));\n if (panCheck.exitCode !== 0 || !panCheck.stdout.trim()) {\n steps.push('No workspace .pan state on remote (skipped)');\n return { steps, errors };\n }\n\n const localPanDir = join(workspacePath, PAN_DIRNAME);\n const localFeedbackDir = join(localPanDir, PAN_FEEDBACK_DIRNAME);\n mkdirSync(localPanDir, { recursive: true });\n mkdirSync(localFeedbackDir, { recursive: true });\n\n const directFiles = [\n { name: PAN_SPEC_FILENAME, label: 'workspace vBRIEF' },\n { name: PAN_CONTINUE_FILENAME, label: 'continue state' },\n { name: PAN_CONTEXT_FILENAME, label: 'feature context' },\n ];\n\n for (const file of directFiles) {\n const remotePath = `${remotePanDir}/${file.name}`;\n const existsResult = await Effect.runPromise(provider.ssh(vmName, `[ -f ${remotePath} ] && echo present`));\n if (existsResult.exitCode === 0 && existsResult.stdout.trim()) {\n try {\n await Effect.runPromise(provider.copyFromVm(vmName, remotePath, join(localPanDir, file.name)));\n steps.push(`Copied ${file.label}`);\n } catch (error: any) {\n errors.push(`${file.label}: ${error.message}`);\n }\n }\n }\n\n const feedbackList = await Effect.runPromise(provider.ssh(vmName, `ls ${remotePanDir}/${PAN_FEEDBACK_DIRNAME} 2>/dev/null`));\n if (feedbackList.exitCode === 0 && feedbackList.stdout.trim()) {\n for (const entry of feedbackList.stdout.trim().split('\\n').filter(Boolean)) {\n try {\n await Effect.runPromise(provider.copyFromVm(\n vmName,\n `${remotePanDir}/${PAN_FEEDBACK_DIRNAME}/${entry}`,\n join(localFeedbackDir, entry),\n ));\n steps.push(`Copied feedback/${entry}`);\n } catch (error: any) {\n errors.push(`feedback/${entry}: ${error.message}`);\n }\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Copy workspace orchestration state (.pan/ and beads) to a remote VM.\n */\nasync function copyPlanningStateToRemote(\n provider: RemoteProvider,\n vmName: string,\n localPath: string,\n _issueId: string,\n projectConfig?: any\n): Promise<{ steps: string[]; errors: string[] }> {\n const workspacePanResult = await copyWorkspacePanStateToRemote(provider, vmName, localPath);\n const steps = [...workspacePanResult.steps];\n const errors = [...workspacePanResult.errors];\n\n const projectPath = projectConfig?.path;\n const beadsLocations = [\n join(localPath, '.beads'),\n projectPath ? join(projectPath, '.beads') : null,\n ].filter(Boolean) as string[];\n\n for (const beadsDir of beadsLocations) {\n if (existsSync(beadsDir)) {\n try {\n await Effect.runPromise(provider.ssh(vmName, 'mkdir -p /workspace/.beads'));\n const beadsFiles = readdirSync(beadsDir).filter(f => f.endsWith('.db') || f.endsWith('.jsonl'));\n for (const file of beadsFiles) {\n await Effect.runPromise(provider.copyToVm(vmName, join(beadsDir, file), `/workspace/.beads/${file}`));\n }\n steps.push('Copied beads database');\n break;\n } catch (error: any) {\n errors.push(`Beads: ${error.message}`);\n }\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Copy workspace orchestration state (.pan/ and beads) from a remote VM.\n */\nasync function copyPlanningStateFromRemote(\n provider: RemoteProvider,\n vmName: string,\n localPath: string,\n _issueId: string,\n _projectConfig?: any\n): Promise<{ steps: string[]; errors: string[] }> {\n const workspacePanResult = await copyWorkspacePanStateFromRemote(provider, vmName, localPath);\n const steps = [...workspacePanResult.steps];\n const errors = [...workspacePanResult.errors];\n\n const beadsCheck = await Effect.runPromise(provider.ssh(vmName, 'ls /workspace/.beads/ 2>/dev/null'));\n if (beadsCheck.exitCode === 0 && beadsCheck.stdout.trim()) {\n try {\n const localBeadsDir = join(localPath, '.beads');\n mkdirSync(localBeadsDir, { recursive: true });\n\n const files = beadsCheck.stdout.trim().split('\\n').filter(f => f.endsWith('.db') || f.endsWith('.jsonl'));\n for (const file of files) {\n try {\n await Effect.runPromise(provider.copyFromVm(vmName, `/workspace/.beads/${file}`, join(localBeadsDir, file)));\n } catch {\n // scp might not work for all files\n }\n }\n steps.push('Copied beads database');\n } catch (error: any) {\n errors.push(`Beads: ${error.message}`);\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Migrate local workspace to remote (Fly.io)\n */\nexport async function migrateLocalToRemote(\n issueId: string,\n options: MigrateOptions\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n success: false,\n message: '',\n errors: [],\n steps: [],\n };\n\n const spinner = ora('Preparing migration to remote...').start();\n\n try {\n // 1. Find local workspace\n const localPath = findLocalWorkspacePath(issueId);\n if (!localPath) {\n spinner.fail('Local workspace not found');\n result.errors.push(`No local workspace found for ${issueId}`);\n return result;\n }\n result.steps.push(`Found local workspace: ${localPath}`);\n\n // 2. Check if remote already exists\n const existingRemote = loadWorkspaceMetadataSync(issueId);\n if (existingRemote && !options.force) {\n spinner.fail('Remote workspace already exists');\n result.errors.push(`Remote workspace already exists for ${issueId}. Use --force to overwrite.`);\n return result;\n }\n\n // 3. Check remote provider availability\n spinner.text = 'Checking remote provider...';\n const config = loadConfigSync();\n if (!config.remote?.enabled) {\n spinner.fail('Remote workspaces not enabled');\n result.errors.push('Remote workspaces not enabled in config. Set remote.enabled = true');\n return result;\n }\n\n const provider = createFlyProviderFromConfig(config.remote);\n\n const isAuth = await Effect.runPromise(provider.isAuthenticated());\n if (!isAuth) {\n spinner.fail('Not authenticated with Fly.io');\n result.errors.push('Not authenticated with Fly.io. Run: flyctl auth login');\n return result;\n }\n result.steps.push('Authenticated with Fly.io');\n\n // 4. Polyrepo guard: the consolidated flow (shared remote-workspace module)\n // is single-repo. The old polyrepo path was already broken on fly (SSH\n // URLs on keyless VMs, macOS-only creds) — refuse honestly instead.\n const teamPrefix = extractTeamPrefix(issueId);\n const projectConfig: ProjectConfig | null = teamPrefix ? findProjectByTeamSync(teamPrefix) : null;\n const subRepos = readdirSync(localPath, { withFileTypes: true })\n .filter(d => d.isDirectory() && !d.name.startsWith('.') && existsSync(join(localPath, d.name, '.git')));\n if (subRepos.length > 0) {\n throw new Error('Polyrepo workspaces are not yet supported for remote migration');\n }\n if (!existsSync(join(localPath, '.git'))) {\n throw new Error(`${localPath} is not a git worktree`);\n }\n\n // 5. Quiesce the local agent so nothing writes mid-migration, and pause\n // it so deacon auto-resume can't respawn a local duplicate while the\n // issue runs remotely. `pan start <id> --remote --force` clears the gate.\n const agentId = `agent-${issueId.toLowerCase()}`;\n if (sessionExistsSync(agentId)) {\n spinner.text = 'Stopping local agent...';\n stopAgentSync(agentId);\n result.steps.push(`Stopped local agent ${agentId}`);\n }\n setAgentPausedSync(agentId, 'migrated to remote (fly.io)');\n result.steps.push('Paused local agent (deacon resume gate)');\n\n // 6. Make sure ALL local work reaches origin before anything else:\n // commit a checkpoint if the tree is dirty, then push the branch.\n spinner.text = 'Committing and pushing local work...';\n const { stdout: branchOut } = await execAsync('git branch --show-current', { cwd: localPath });\n const branchName = branchOut.trim();\n const expectedBranch = `feature/${issueId.toLowerCase()}`;\n if (branchName !== expectedBranch && !options.force) {\n throw new Error(\n `Workspace is on '${branchName}', expected '${expectedBranch}' — worktree drift. Use --force to migrate anyway.`\n );\n }\n const { stdout: porcelain } = await execAsync('git status --porcelain', { cwd: localPath });\n if (porcelain.trim()) {\n await execAsync('git add -A', { cwd: localPath });\n await execAsync(\n `git commit -m \"chore: wip checkpoint before remote migration (${issueId.toLowerCase()})\"`,\n { cwd: localPath }\n );\n result.steps.push('Committed WIP checkpoint (tree was dirty)');\n }\n await execAsync(`git push -u origin ${branchName}`, { cwd: localPath });\n result.steps.push(`Pushed ${branchName} to origin`);\n\n // 7. If --force and a remote workspace already exists, tear it down first\n // so the shared creation path starts clean.\n if (existingRemote && options.force) {\n spinner.text = `Destroying existing VM ${existingRemote.vmName}...`;\n try {\n await Effect.runPromise(provider.deleteVm(existingRemote.vmName));\n result.steps.push(`Destroyed existing VM ${existingRemote.vmName}`);\n } catch (error: any) {\n result.steps.push(`Warning: could not destroy old VM: ${error.message}`);\n }\n deleteWorkspaceMetadataSync(issueId);\n }\n\n // 8. Create the remote workspace via the shared module: VM, credential\n // sync (gh token + Claude), https clone tracking the just-pushed branch,\n // Claude Code config, beads install, and continue.json/beads sync from\n // the local workspace.\n spinner.text = 'Creating remote workspace...';\n const { createRemoteWorkspace } = await import('../../lib/remote-workspace.js');\n const metadata = await Effect.runPromise(createRemoteWorkspace(issueId, { spinner }));\n result.steps.push(`Remote workspace ready on ${metadata.vmName}`);\n\n // 9. Copy remaining workspace .pan state (feature context, feedback,\n // legacy workspace spec) and beads databases not covered by creation.\n spinner.text = 'Copying workspace state...';\n const planningResult = await copyPlanningStateToRemote(provider, metadata.vmName, localPath, issueId, projectConfig);\n result.steps.push(...planningResult.steps);\n if (planningResult.errors.length > 0) {\n // Non-fatal - planning state might not exist\n result.steps.push(`Warning: ${planningResult.errors.join(', ')}`);\n }\n\n // Safety: never delete the local workspace if the continue state failed\n // to reach the VM — it's gitignored, so the local copy is the only one.\n const localContinue = join(localPath, PAN_DIRNAME, PAN_CONTINUE_FILENAME);\n let keepLocal = options.keep;\n if (!keepLocal && existsSync(localContinue)) {\n const check = await Effect.runPromise(\n provider.ssh(metadata.vmName, `[ -f /workspace/${PAN_DIRNAME}/${PAN_CONTINUE_FILENAME} ] && echo present`)\n );\n if (!check.stdout.trim()) {\n result.steps.push('Warning: continue.json did not reach the VM — keeping local workspace');\n keepLocal = true;\n }\n }\n\n // 10. Cleanup local (unless --keep)\n // Docker containers are stopped by removeWorkspace() via stopWorkspaceDocker()\n if (!keepLocal) {\n spinner.text = 'Cleaning up local workspace...';\n try {\n if (projectConfig) {\n const removeResult = await Effect.runPromise(removeWorkspace({\n projectConfig,\n featureName: issueId.toLowerCase(),\n }));\n result.steps.push(...removeResult.steps);\n if (removeResult.errors.length > 0) {\n result.errors.push(...removeResult.errors);\n }\n }\n } catch (error: any) {\n result.errors.push(`Cleanup warning: ${error.message}`);\n }\n } else {\n result.steps.push('Local workspace kept (--keep flag)');\n }\n\n spinner.succeed(`Migrated ${issueId} to remote`);\n result.success = true;\n result.message = `Workspace migrated to ${metadata.vmName}`;\n\n } catch (error: any) {\n spinner.fail('Migration failed');\n result.errors.push(error.message);\n }\n\n return result;\n}\n\n/**\n * Migrate remote workspace to local\n */\nexport async function migrateRemoteToLocal(\n issueId: string,\n options: MigrateOptions\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n success: false,\n message: '',\n errors: [],\n steps: [],\n };\n\n const spinner = ora('Preparing migration to local...').start();\n\n try {\n // 1. Load remote workspace metadata\n const remoteMetadata = loadWorkspaceMetadataSync(issueId);\n if (!remoteMetadata) {\n spinner.fail('Remote workspace not found');\n result.errors.push(`No remote workspace found for ${issueId}`);\n return result;\n }\n result.steps.push(`Found remote workspace: ${remoteMetadata.vmName}`);\n\n // 2. Check if local already exists\n const existingLocal = findLocalWorkspacePath(issueId);\n if (existingLocal && !options.force) {\n spinner.fail('Local workspace already exists');\n result.errors.push(`Local workspace already exists at ${existingLocal}. Use --force to overwrite.`);\n return result;\n }\n\n // 3. Get provider\n const config = loadConfigSync();\n const provider = createFlyProviderFromConfig(config.remote);\n\n // 4. Verify VM is accessible\n spinner.text = 'Checking remote VM...';\n const vmStatus = await Effect.runPromise(provider.getStatus(remoteMetadata.vmName));\n if (vmStatus === 'unknown') {\n spinner.fail('Remote VM not found');\n result.errors.push(`VM ${remoteMetadata.vmName} not found on Fly.io`);\n return result;\n }\n result.steps.push(`VM status: ${vmStatus}`);\n\n // 5. Pull git changes from remote\n spinner.text = 'Pulling git changes...';\n // TODO: Implement git pull/fetch from remote branches\n result.steps.push('Git changes pulled');\n\n // 6. Create local workspace\n spinner.text = 'Creating local workspace...';\n const resolved = resolveProjectFromIssueSync(issueId, []);\n const teamPrefix = extractTeamPrefix(issueId);\n const projectConfig: ProjectConfig | null = teamPrefix ? findProjectByTeamSync(teamPrefix) : null;\n if (!projectConfig) {\n spinner.fail('Cannot resolve project config');\n result.errors.push(`Cannot resolve project config for ${issueId}`);\n return result;\n }\n\n const workspaceResult = await Effect.runPromise(createWorkspace({\n projectConfig,\n featureName: issueId.toLowerCase(),\n startDocker: !options.noDocker,\n }));\n\n if (!workspaceResult.success) {\n spinner.fail('Failed to create local workspace');\n result.errors.push(...workspaceResult.errors);\n return result;\n }\n result.steps.push(`Created local workspace: ${workspaceResult.workspacePath}`);\n result.steps.push(...workspaceResult.steps);\n\n // 7. Copy planning state from remote\n spinner.text = 'Copying planning state from remote...';\n const planningResult = await copyPlanningStateFromRemote(\n provider,\n remoteMetadata.vmName,\n workspaceResult.workspacePath,\n issueId,\n projectConfig\n );\n result.steps.push(...planningResult.steps);\n if (planningResult.errors.length > 0) {\n // Non-fatal\n result.steps.push(`Warning: ${planningResult.errors.join(', ')}`);\n }\n\n // 8. Cleanup remote (unless --keep)\n if (!options.keep) {\n spinner.text = 'Cleaning up remote workspace...';\n try {\n await Effect.runPromise(provider.deleteVm(remoteMetadata.vmName));\n result.steps.push(`Deleted VM: ${remoteMetadata.vmName}`);\n } catch (error: any) {\n result.errors.push(`Warning: Failed to delete VM: ${error.message}`);\n }\n deleteWorkspaceMetadataSync(issueId);\n result.steps.push('Deleted workspace metadata');\n } else {\n result.steps.push('Remote workspace kept (--keep flag)');\n }\n\n spinner.succeed(`Migrated ${issueId} to local`);\n result.success = true;\n result.message = `Workspace migrated to ${workspaceResult.workspacePath}`;\n\n } catch (error: any) {\n spinner.fail('Migration failed');\n result.errors.push(error.message);\n }\n\n return result;\n}\n\n/**\n * Main migrate command handler\n */\nexport async function migrateWorkspace(\n issueId: string,\n options: MigrateOptions\n): Promise<void> {\n console.log(chalk.bold(`\\nMigrating workspace: ${issueId}\\n`));\n\n // Validate options\n if (options.toRemote && options.toLocal) {\n console.error(chalk.red('Error: Cannot specify both --to-remote and --to-local'));\n process.exit(1);\n }\n\n // Detect current location\n const currentLocation = detectWorkspaceLocation(issueId);\n\n if (currentLocation === 'none') {\n console.error(chalk.red(`Error: No workspace found for ${issueId}`));\n console.error(chalk.dim('Create a workspace first with: pan workspace <issue-id>'));\n process.exit(1);\n }\n\n // Determine migration direction\n let direction: 'to-remote' | 'to-local';\n\n if (options.toRemote) {\n direction = 'to-remote';\n } else if (options.toLocal) {\n direction = 'to-local';\n } else {\n // Auto-detect based on current location\n direction = currentLocation === 'local' ? 'to-remote' : 'to-local';\n console.log(chalk.dim(`Auto-detected direction: ${direction} (current: ${currentLocation})`));\n }\n\n // Validate direction makes sense\n if (direction === 'to-remote' && currentLocation === 'remote') {\n console.error(chalk.red('Error: Workspace is already remote'));\n process.exit(1);\n }\n if (direction === 'to-local' && currentLocation === 'local') {\n console.error(chalk.red('Error: Workspace is already local'));\n process.exit(1);\n }\n\n // Execute migration\n let result: MigrationResult;\n\n if (direction === 'to-remote') {\n result = await migrateLocalToRemote(issueId, options);\n } else {\n result = await migrateRemoteToLocal(issueId, options);\n }\n\n // Print results\n console.log();\n\n if (result.steps.length > 0) {\n console.log(chalk.bold('Steps completed:'));\n for (const step of result.steps) {\n console.log(chalk.green(` ✓ ${step}`));\n }\n console.log();\n }\n\n if (result.errors.length > 0) {\n console.log(chalk.bold('Errors:'));\n for (const error of result.errors) {\n console.log(chalk.red(` ✗ ${error}`));\n }\n console.log();\n }\n\n if (result.success) {\n console.log(chalk.green(`✓ ${result.message}`));\n } else {\n console.error(chalk.red('Migration failed'));\n process.exit(1);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace-migrate-BBQ8nAnP.js","names":[],"sources":["../src/cli/commands/workspace-migrate.ts"],"sourcesContent":["/**\n * Workspace Migration Command\n *\n * Migrates workspaces between local and remote (Fly.io) environments.\n *\n * Usage:\n * pan workspace migrate <issue-id> --to-remote # Local -> Remote\n * pan workspace migrate <issue-id> --to-local # Remote -> Local\n *\n * Options:\n * --keep Keep source workspace after migration\n * --force Overwrite if destination exists, ignore running agents\n * --no-docker Don't start Docker containers after migration\n */\n\nimport chalk from 'chalk';\nimport { Effect } from 'effect';\nimport ora from 'ora';\nimport { existsSync, readdirSync, readFileSync, mkdirSync, statSync } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { loadConfigSync } from '../../lib/config.js';\nimport { resolveProjectFromIssueSync, extractTeamPrefix, findProjectByTeamSync, type ProjectConfig } from '../../lib/projects.js';\nimport {\n loadWorkspaceMetadataSync,\n saveWorkspaceMetadataSync,\n deleteWorkspaceMetadataSync,\n} from '../../lib/remote/workspace-metadata.js';\nimport {\n createFlyProviderFromConfig,\n} from '../../lib/remote/index.js';\nimport { PAN_CONTEXT_FILENAME, PAN_CONTINUE_FILENAME, PAN_DIRNAME, PAN_FEEDBACK_DIRNAME, PAN_SPEC_FILENAME } from '../../lib/pan-dir/index.js';\nimport { createWorkspace, removeWorkspace } from '../../lib/workspace-manager.js';\nimport { stopAgentSync, setAgentPausedSync } from '../../lib/agents.js';\nimport { sessionExistsSync } from '../../lib/tmux.js';\nimport type { RemoteWorkspaceMetadata } from '../../lib/remote/interface.js';\nimport type { RemoteProvider } from '../../lib/remote/interface.js';\n\nconst execAsync = promisify(exec);\n\nexport interface MigrateOptions {\n toRemote?: boolean;\n toLocal?: boolean;\n keep?: boolean;\n force?: boolean;\n noDocker?: boolean;\n}\n\ninterface MigrationResult {\n success: boolean;\n message: string;\n errors: string[];\n steps: string[];\n}\n\n/**\n * Detect current workspace location\n */\nfunction detectWorkspaceLocation(issueId: string): 'local' | 'remote' | 'none' {\n const normalizedId = issueId.toLowerCase();\n\n // Check for remote workspace metadata\n const remoteMetadata = loadWorkspaceMetadataSync(issueId);\n if (remoteMetadata) {\n return 'remote';\n }\n\n // Check for local workspace\n const resolved = resolveProjectFromIssueSync(issueId, []);\n if (resolved) {\n const workspacePath = join(resolved.projectPath, 'workspaces', `feature-${normalizedId}`);\n if (existsSync(workspacePath)) {\n return 'local';\n }\n }\n\n return 'none';\n}\n\n/**\n * Find local workspace path\n */\nfunction findLocalWorkspacePath(issueId: string): string | null {\n const normalizedId = issueId.toLowerCase();\n const resolved = resolveProjectFromIssueSync(issueId, []);\n\n if (resolved) {\n const workspacePath = join(resolved.projectPath, 'workspaces', `feature-${normalizedId}`);\n if (existsSync(workspacePath)) {\n return workspacePath;\n }\n }\n\n return null;\n}\n\n\n\n\ninterface MigratableWorkspaceFile {\n localPath: string;\n remotePath: string;\n label: string;\n}\n\nfunction collectPanWorkspaceFiles(workspacePath: string): MigratableWorkspaceFile[] {\n const panDir = join(workspacePath, PAN_DIRNAME);\n if (!existsSync(panDir)) return [];\n\n const files: MigratableWorkspaceFile[] = [];\n const directFiles = [\n { name: PAN_SPEC_FILENAME, label: 'workspace vBRIEF' },\n { name: PAN_CONTINUE_FILENAME, label: 'continue state' },\n { name: PAN_CONTEXT_FILENAME, label: 'feature context' },\n ];\n\n for (const file of directFiles) {\n const localPath = join(panDir, file.name);\n if (existsSync(localPath) && statSync(localPath).isFile()) {\n files.push({\n localPath,\n remotePath: `/workspace/${PAN_DIRNAME}/${file.name}`,\n label: file.label,\n });\n }\n }\n\n const feedbackDir = join(panDir, PAN_FEEDBACK_DIRNAME);\n if (existsSync(feedbackDir) && statSync(feedbackDir).isDirectory()) {\n for (const entry of readdirSync(feedbackDir)) {\n const localPath = join(feedbackDir, entry);\n if (!statSync(localPath).isFile()) continue;\n files.push({\n localPath,\n remotePath: `/workspace/${PAN_DIRNAME}/${PAN_FEEDBACK_DIRNAME}/${entry}`,\n label: `feedback/${entry}`,\n });\n }\n }\n\n return files;\n}\n\nasync function copyWorkspacePanStateToRemote(\n provider: RemoteProvider,\n vmName: string,\n workspacePath: string,\n): Promise<{ steps: string[]; errors: string[] }> {\n const steps: string[] = [];\n const errors: string[] = [];\n const files = collectPanWorkspaceFiles(workspacePath);\n\n if (files.length === 0) {\n steps.push('No workspace .pan state found (skipped)');\n return { steps, errors };\n }\n\n try {\n await Effect.runPromise(provider.ssh(vmName, `mkdir -p /workspace/${PAN_DIRNAME}/${PAN_FEEDBACK_DIRNAME}`));\n for (const file of files) {\n const remoteDir = dirname(file.remotePath);\n await Effect.runPromise(provider.ssh(vmName, `mkdir -p ${remoteDir}`));\n await Effect.runPromise(provider.copyToVm(vmName, file.localPath, file.remotePath));\n steps.push(`Copied ${file.label}`);\n }\n } catch (error: any) {\n errors.push(`Workspace .pan state: ${error.message}`);\n }\n\n return { steps, errors };\n}\n\nasync function copyWorkspacePanStateFromRemote(\n provider: RemoteProvider,\n vmName: string,\n workspacePath: string,\n): Promise<{ steps: string[]; errors: string[] }> {\n const steps: string[] = [];\n const errors: string[] = [];\n\n const remotePanDir = `/workspace/${PAN_DIRNAME}`;\n const panCheck = await Effect.runPromise(provider.ssh(vmName, `[ -d ${remotePanDir} ] && echo present`));\n if (panCheck.exitCode !== 0 || !panCheck.stdout.trim()) {\n steps.push('No workspace .pan state on remote (skipped)');\n return { steps, errors };\n }\n\n const localPanDir = join(workspacePath, PAN_DIRNAME);\n const localFeedbackDir = join(localPanDir, PAN_FEEDBACK_DIRNAME);\n mkdirSync(localPanDir, { recursive: true });\n mkdirSync(localFeedbackDir, { recursive: true });\n\n const directFiles = [\n { name: PAN_SPEC_FILENAME, label: 'workspace vBRIEF' },\n { name: PAN_CONTINUE_FILENAME, label: 'continue state' },\n { name: PAN_CONTEXT_FILENAME, label: 'feature context' },\n ];\n\n for (const file of directFiles) {\n const remotePath = `${remotePanDir}/${file.name}`;\n const existsResult = await Effect.runPromise(provider.ssh(vmName, `[ -f ${remotePath} ] && echo present`));\n if (existsResult.exitCode === 0 && existsResult.stdout.trim()) {\n try {\n await Effect.runPromise(provider.copyFromVm(vmName, remotePath, join(localPanDir, file.name)));\n steps.push(`Copied ${file.label}`);\n } catch (error: any) {\n errors.push(`${file.label}: ${error.message}`);\n }\n }\n }\n\n const feedbackList = await Effect.runPromise(provider.ssh(vmName, `ls ${remotePanDir}/${PAN_FEEDBACK_DIRNAME} 2>/dev/null`));\n if (feedbackList.exitCode === 0 && feedbackList.stdout.trim()) {\n for (const entry of feedbackList.stdout.trim().split('\\n').filter(Boolean)) {\n try {\n await Effect.runPromise(provider.copyFromVm(\n vmName,\n `${remotePanDir}/${PAN_FEEDBACK_DIRNAME}/${entry}`,\n join(localFeedbackDir, entry),\n ));\n steps.push(`Copied feedback/${entry}`);\n } catch (error: any) {\n errors.push(`feedback/${entry}: ${error.message}`);\n }\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Copy workspace orchestration state (.pan/ and beads) to a remote VM.\n */\nasync function copyPlanningStateToRemote(\n provider: RemoteProvider,\n vmName: string,\n localPath: string,\n _issueId: string,\n projectConfig?: any\n): Promise<{ steps: string[]; errors: string[] }> {\n const workspacePanResult = await copyWorkspacePanStateToRemote(provider, vmName, localPath);\n const steps = [...workspacePanResult.steps];\n const errors = [...workspacePanResult.errors];\n\n const projectPath = projectConfig?.path;\n const beadsLocations = [\n join(localPath, '.beads'),\n projectPath ? join(projectPath, '.beads') : null,\n ].filter(Boolean) as string[];\n\n for (const beadsDir of beadsLocations) {\n if (existsSync(beadsDir)) {\n try {\n await Effect.runPromise(provider.ssh(vmName, 'mkdir -p /workspace/.beads'));\n const beadsFiles = readdirSync(beadsDir).filter(f => f.endsWith('.db') || f.endsWith('.jsonl'));\n for (const file of beadsFiles) {\n await Effect.runPromise(provider.copyToVm(vmName, join(beadsDir, file), `/workspace/.beads/${file}`));\n }\n steps.push('Copied beads database');\n break;\n } catch (error: any) {\n errors.push(`Beads: ${error.message}`);\n }\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Copy workspace orchestration state (.pan/ and beads) from a remote VM.\n */\nasync function copyPlanningStateFromRemote(\n provider: RemoteProvider,\n vmName: string,\n localPath: string,\n _issueId: string,\n _projectConfig?: any\n): Promise<{ steps: string[]; errors: string[] }> {\n const workspacePanResult = await copyWorkspacePanStateFromRemote(provider, vmName, localPath);\n const steps = [...workspacePanResult.steps];\n const errors = [...workspacePanResult.errors];\n\n const beadsCheck = await Effect.runPromise(provider.ssh(vmName, 'ls /workspace/.beads/ 2>/dev/null'));\n if (beadsCheck.exitCode === 0 && beadsCheck.stdout.trim()) {\n try {\n const localBeadsDir = join(localPath, '.beads');\n mkdirSync(localBeadsDir, { recursive: true });\n\n const files = beadsCheck.stdout.trim().split('\\n').filter(f => f.endsWith('.db') || f.endsWith('.jsonl'));\n for (const file of files) {\n try {\n await Effect.runPromise(provider.copyFromVm(vmName, `/workspace/.beads/${file}`, join(localBeadsDir, file)));\n } catch {\n // scp might not work for all files\n }\n }\n steps.push('Copied beads database');\n } catch (error: any) {\n errors.push(`Beads: ${error.message}`);\n }\n }\n\n return { steps, errors };\n}\n\n/**\n * Migrate local workspace to remote (Fly.io)\n */\nexport async function migrateLocalToRemote(\n issueId: string,\n options: MigrateOptions\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n success: false,\n message: '',\n errors: [],\n steps: [],\n };\n\n const spinner = ora('Preparing migration to remote...').start();\n\n try {\n // 1. Find local workspace\n const localPath = findLocalWorkspacePath(issueId);\n if (!localPath) {\n spinner.fail('Local workspace not found');\n result.errors.push(`No local workspace found for ${issueId}`);\n return result;\n }\n result.steps.push(`Found local workspace: ${localPath}`);\n\n // 2. Check if remote already exists\n const existingRemote = loadWorkspaceMetadataSync(issueId);\n if (existingRemote && !options.force) {\n spinner.fail('Remote workspace already exists');\n result.errors.push(`Remote workspace already exists for ${issueId}. Use --force to overwrite.`);\n return result;\n }\n\n // 3. Check remote provider availability\n spinner.text = 'Checking remote provider...';\n const config = loadConfigSync();\n if (!config.remote?.enabled) {\n spinner.fail('Remote workspaces not enabled');\n result.errors.push('Remote workspaces not enabled in config. Set remote.enabled = true');\n return result;\n }\n\n const provider = createFlyProviderFromConfig(config.remote);\n\n const isAuth = await Effect.runPromise(provider.isAuthenticated());\n if (!isAuth) {\n spinner.fail('Not authenticated with Fly.io');\n result.errors.push('Not authenticated with Fly.io. Run: flyctl auth login');\n return result;\n }\n result.steps.push('Authenticated with Fly.io');\n\n // 4. Polyrepo guard: the consolidated flow (shared remote-workspace module)\n // is single-repo. The old polyrepo path was already broken on fly (SSH\n // URLs on keyless VMs, macOS-only creds) — refuse honestly instead.\n const teamPrefix = extractTeamPrefix(issueId);\n const projectConfig: ProjectConfig | null = teamPrefix ? findProjectByTeamSync(teamPrefix) : null;\n const subRepos = readdirSync(localPath, { withFileTypes: true })\n .filter(d => d.isDirectory() && !d.name.startsWith('.') && existsSync(join(localPath, d.name, '.git')));\n if (subRepos.length > 0) {\n throw new Error('Polyrepo workspaces are not yet supported for remote migration');\n }\n if (!existsSync(join(localPath, '.git'))) {\n throw new Error(`${localPath} is not a git worktree`);\n }\n\n // 5. Quiesce the local agent so nothing writes mid-migration, and pause\n // it so deacon auto-resume can't respawn a local duplicate while the\n // issue runs remotely. `pan start <id> --remote --force` clears the gate.\n const agentId = `agent-${issueId.toLowerCase()}`;\n if (sessionExistsSync(agentId)) {\n spinner.text = 'Stopping local agent...';\n stopAgentSync(agentId);\n result.steps.push(`Stopped local agent ${agentId}`);\n }\n setAgentPausedSync(agentId, 'migrated to remote (fly.io)');\n result.steps.push('Paused local agent (deacon resume gate)');\n\n // 6. Make sure ALL local work reaches origin before anything else:\n // commit a checkpoint if the tree is dirty, then push the branch.\n spinner.text = 'Committing and pushing local work...';\n const { stdout: branchOut } = await execAsync('git branch --show-current', { cwd: localPath });\n const branchName = branchOut.trim();\n const expectedBranch = `feature/${issueId.toLowerCase()}`;\n if (branchName !== expectedBranch && !options.force) {\n throw new Error(\n `Workspace is on '${branchName}', expected '${expectedBranch}' — worktree drift. Use --force to migrate anyway.`\n );\n }\n const { stdout: porcelain } = await execAsync('git status --porcelain', { cwd: localPath });\n if (porcelain.trim()) {\n await execAsync('git add -A', { cwd: localPath });\n await execAsync(\n `git commit -m \"chore: wip checkpoint before remote migration (${issueId.toLowerCase()})\"`,\n { cwd: localPath }\n );\n result.steps.push('Committed WIP checkpoint (tree was dirty)');\n }\n await execAsync(`git push -u origin ${branchName}`, { cwd: localPath });\n result.steps.push(`Pushed ${branchName} to origin`);\n\n // 7. If --force and a remote workspace already exists, tear it down first\n // so the shared creation path starts clean.\n if (existingRemote && options.force) {\n spinner.text = `Destroying existing VM ${existingRemote.vmName}...`;\n try {\n await Effect.runPromise(provider.deleteVm(existingRemote.vmName));\n result.steps.push(`Destroyed existing VM ${existingRemote.vmName}`);\n } catch (error: any) {\n result.steps.push(`Warning: could not destroy old VM: ${error.message}`);\n }\n deleteWorkspaceMetadataSync(issueId);\n }\n\n // 8. Create the remote workspace via the shared module: VM, credential\n // sync (gh token + Claude), https clone tracking the just-pushed branch,\n // Claude Code config, beads install, and continue.json/beads sync from\n // the local workspace.\n spinner.text = 'Creating remote workspace...';\n const { createRemoteWorkspace } = await import('../../lib/remote-workspace.js');\n const metadata = await Effect.runPromise(createRemoteWorkspace(issueId, { spinner }));\n result.steps.push(`Remote workspace ready on ${metadata.vmName}`);\n\n // 9. Copy remaining workspace .pan state (feature context, feedback,\n // legacy workspace spec) and beads databases not covered by creation.\n spinner.text = 'Copying workspace state...';\n const planningResult = await copyPlanningStateToRemote(provider, metadata.vmName, localPath, issueId, projectConfig);\n result.steps.push(...planningResult.steps);\n if (planningResult.errors.length > 0) {\n // Non-fatal - planning state might not exist\n result.steps.push(`Warning: ${planningResult.errors.join(', ')}`);\n }\n\n // Safety: never delete the local workspace if the continue state failed\n // to reach the VM — it's gitignored, so the local copy is the only one.\n const localContinue = join(localPath, PAN_DIRNAME, PAN_CONTINUE_FILENAME);\n let keepLocal = options.keep;\n if (!keepLocal && existsSync(localContinue)) {\n const check = await Effect.runPromise(\n provider.ssh(metadata.vmName, `[ -f /workspace/${PAN_DIRNAME}/${PAN_CONTINUE_FILENAME} ] && echo present`)\n );\n if (!check.stdout.trim()) {\n result.steps.push('Warning: continue.json did not reach the VM — keeping local workspace');\n keepLocal = true;\n }\n }\n\n // 10. Cleanup local (unless --keep)\n // Docker containers are stopped by removeWorkspace() via stopWorkspaceDocker()\n if (!keepLocal) {\n spinner.text = 'Cleaning up local workspace...';\n try {\n if (projectConfig) {\n const removeResult = await Effect.runPromise(removeWorkspace({\n projectConfig,\n featureName: issueId.toLowerCase(),\n }));\n result.steps.push(...removeResult.steps);\n if (removeResult.errors.length > 0) {\n result.errors.push(...removeResult.errors);\n }\n }\n } catch (error: any) {\n result.errors.push(`Cleanup warning: ${error.message}`);\n }\n } else {\n result.steps.push('Local workspace kept (--keep flag)');\n }\n\n spinner.succeed(`Migrated ${issueId} to remote`);\n result.success = true;\n result.message = `Workspace migrated to ${metadata.vmName}`;\n\n } catch (error: any) {\n spinner.fail('Migration failed');\n result.errors.push(error.message);\n }\n\n return result;\n}\n\n/**\n * Migrate remote workspace to local\n */\nexport async function migrateRemoteToLocal(\n issueId: string,\n options: MigrateOptions\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n success: false,\n message: '',\n errors: [],\n steps: [],\n };\n\n const spinner = ora('Preparing migration to local...').start();\n\n try {\n // 1. Load remote workspace metadata\n const remoteMetadata = loadWorkspaceMetadataSync(issueId);\n if (!remoteMetadata) {\n spinner.fail('Remote workspace not found');\n result.errors.push(`No remote workspace found for ${issueId}`);\n return result;\n }\n result.steps.push(`Found remote workspace: ${remoteMetadata.vmName}`);\n\n // 2. Check if local already exists\n const existingLocal = findLocalWorkspacePath(issueId);\n if (existingLocal && !options.force) {\n spinner.fail('Local workspace already exists');\n result.errors.push(`Local workspace already exists at ${existingLocal}. Use --force to overwrite.`);\n return result;\n }\n\n // 3. Get provider\n const config = loadConfigSync();\n const provider = createFlyProviderFromConfig(config.remote);\n\n // 4. Verify VM is accessible\n spinner.text = 'Checking remote VM...';\n const vmStatus = await Effect.runPromise(provider.getStatus(remoteMetadata.vmName));\n if (vmStatus === 'unknown') {\n spinner.fail('Remote VM not found');\n result.errors.push(`VM ${remoteMetadata.vmName} not found on Fly.io`);\n return result;\n }\n result.steps.push(`VM status: ${vmStatus}`);\n\n // 5. Pull git changes from remote\n spinner.text = 'Pulling git changes...';\n // TODO: Implement git pull/fetch from remote branches\n result.steps.push('Git changes pulled');\n\n // 6. Create local workspace\n spinner.text = 'Creating local workspace...';\n const resolved = resolveProjectFromIssueSync(issueId, []);\n const teamPrefix = extractTeamPrefix(issueId);\n const projectConfig: ProjectConfig | null = teamPrefix ? findProjectByTeamSync(teamPrefix) : null;\n if (!projectConfig) {\n spinner.fail('Cannot resolve project config');\n result.errors.push(`Cannot resolve project config for ${issueId}`);\n return result;\n }\n\n const workspaceResult = await Effect.runPromise(createWorkspace({\n projectConfig,\n featureName: issueId.toLowerCase(),\n startDocker: !options.noDocker,\n }));\n\n if (!workspaceResult.success) {\n spinner.fail('Failed to create local workspace');\n result.errors.push(...workspaceResult.errors);\n return result;\n }\n result.steps.push(`Created local workspace: ${workspaceResult.workspacePath}`);\n result.steps.push(...workspaceResult.steps);\n\n // 7. Copy planning state from remote\n spinner.text = 'Copying planning state from remote...';\n const planningResult = await copyPlanningStateFromRemote(\n provider,\n remoteMetadata.vmName,\n workspaceResult.workspacePath,\n issueId,\n projectConfig\n );\n result.steps.push(...planningResult.steps);\n if (planningResult.errors.length > 0) {\n // Non-fatal\n result.steps.push(`Warning: ${planningResult.errors.join(', ')}`);\n }\n\n // 8. Cleanup remote (unless --keep)\n if (!options.keep) {\n spinner.text = 'Cleaning up remote workspace...';\n try {\n await Effect.runPromise(provider.deleteVm(remoteMetadata.vmName));\n result.steps.push(`Deleted VM: ${remoteMetadata.vmName}`);\n } catch (error: any) {\n result.errors.push(`Warning: Failed to delete VM: ${error.message}`);\n }\n deleteWorkspaceMetadataSync(issueId);\n result.steps.push('Deleted workspace metadata');\n } else {\n result.steps.push('Remote workspace kept (--keep flag)');\n }\n\n spinner.succeed(`Migrated ${issueId} to local`);\n result.success = true;\n result.message = `Workspace migrated to ${workspaceResult.workspacePath}`;\n\n } catch (error: any) {\n spinner.fail('Migration failed');\n result.errors.push(error.message);\n }\n\n return result;\n}\n\n/**\n * Main migrate command handler\n */\nexport async function migrateWorkspace(\n issueId: string,\n options: MigrateOptions\n): Promise<void> {\n console.log(chalk.bold(`\\nMigrating workspace: ${issueId}\\n`));\n\n // Validate options\n if (options.toRemote && options.toLocal) {\n console.error(chalk.red('Error: Cannot specify both --to-remote and --to-local'));\n process.exit(1);\n }\n\n // Detect current location\n const currentLocation = detectWorkspaceLocation(issueId);\n\n if (currentLocation === 'none') {\n console.error(chalk.red(`Error: No workspace found for ${issueId}`));\n console.error(chalk.dim('Create a workspace first with: pan workspace <issue-id>'));\n process.exit(1);\n }\n\n // Determine migration direction\n let direction: 'to-remote' | 'to-local';\n\n if (options.toRemote) {\n direction = 'to-remote';\n } else if (options.toLocal) {\n direction = 'to-local';\n } else {\n // Auto-detect based on current location\n direction = currentLocation === 'local' ? 'to-remote' : 'to-local';\n console.log(chalk.dim(`Auto-detected direction: ${direction} (current: ${currentLocation})`));\n }\n\n // Validate direction makes sense\n if (direction === 'to-remote' && currentLocation === 'remote') {\n console.error(chalk.red('Error: Workspace is already remote'));\n process.exit(1);\n }\n if (direction === 'to-local' && currentLocation === 'local') {\n console.error(chalk.red('Error: Workspace is already local'));\n process.exit(1);\n }\n\n // Execute migration\n let result: MigrationResult;\n\n if (direction === 'to-remote') {\n result = await migrateLocalToRemote(issueId, options);\n } else {\n result = await migrateRemoteToLocal(issueId, options);\n }\n\n // Print results\n console.log();\n\n if (result.steps.length > 0) {\n console.log(chalk.bold('Steps completed:'));\n for (const step of result.steps) {\n console.log(chalk.green(` ✓ ${step}`));\n }\n console.log();\n }\n\n if (result.errors.length > 0) {\n console.log(chalk.bold('Errors:'));\n for (const error of result.errors) {\n console.log(chalk.red(` ✗ ${error}`));\n }\n console.log();\n }\n\n if (result.success) {\n console.log(chalk.green(`✓ ${result.message}`));\n } else {\n console.error(chalk.red('Migration failed'));\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAuBqD;eAC6E;cASa;wBAC7D;aACV;WAClB;AAItD,MAAM,YAAY,UAAU,KAAK;;;;AAoBjC,SAAS,wBAAwB,SAA8C;CAC7E,MAAM,eAAe,QAAQ,aAAa;AAI1C,KADuB,0BAA0B,QAC/B,CAChB,QAAO;CAIT,MAAM,WAAW,4BAA4B,SAAS,EAAE,CAAC;AACzD,KAAI;MAEE,WADkB,KAAK,SAAS,aAAa,cAAc,WAAW,eAC9C,CAAC,CAC3B,QAAO;;AAIX,QAAO;;;;;AAMT,SAAS,uBAAuB,SAAgC;CAC9D,MAAM,eAAe,QAAQ,aAAa;CAC1C,MAAM,WAAW,4BAA4B,SAAS,EAAE,CAAC;AAEzD,KAAI,UAAU;EACZ,MAAM,gBAAgB,KAAK,SAAS,aAAa,cAAc,WAAW,eAAe;AACzF,MAAI,WAAW,cAAc,CAC3B,QAAO;;AAIX,QAAO;;AAYT,SAAS,yBAAyB,eAAkD;CAClF,MAAM,SAAS,KAAK,eAAe,YAAY;AAC/C,KAAI,CAAC,WAAW,OAAO,CAAE,QAAO,EAAE;CAElC,MAAM,QAAmC,EAAE;CAC3C,MAAM,cAAc;EAClB;GAAE,MAAM;GAAmB,OAAO;GAAoB;EACtD;GAAE,MAAM;GAAuB,OAAO;GAAkB;EACxD;GAAE,MAAM;GAAsB,OAAO;GAAmB;EACzD;AAED,MAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,YAAY,KAAK,QAAQ,KAAK,KAAK;AACzC,MAAI,WAAW,UAAU,IAAI,SAAS,UAAU,CAAC,QAAQ,CACvD,OAAM,KAAK;GACT;GACA,YAAY,cAAc,YAAY,GAAG,KAAK;GAC9C,OAAO,KAAK;GACb,CAAC;;CAIN,MAAM,cAAc,KAAK,QAAQ,qBAAqB;AACtD,KAAI,WAAW,YAAY,IAAI,SAAS,YAAY,CAAC,aAAa,CAChE,MAAK,MAAM,SAAS,YAAY,YAAY,EAAE;EAC5C,MAAM,YAAY,KAAK,aAAa,MAAM;AAC1C,MAAI,CAAC,SAAS,UAAU,CAAC,QAAQ,CAAE;AACnC,QAAM,KAAK;GACT;GACA,YAAY,cAAc,YAAY,GAAG,qBAAqB,GAAG;GACjE,OAAO,YAAY;GACpB,CAAC;;AAIN,QAAO;;AAGT,eAAe,8BACb,UACA,QACA,eACgD;CAChD,MAAM,QAAkB,EAAE;CAC1B,MAAM,SAAmB,EAAE;CAC3B,MAAM,QAAQ,yBAAyB,cAAc;AAErD,KAAI,MAAM,WAAW,GAAG;AACtB,QAAM,KAAK,0CAA0C;AACrD,SAAO;GAAE;GAAO;GAAQ;;AAG1B,KAAI;AACF,QAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,uBAAuB,YAAY,GAAG,uBAAuB,CAAC;AAC3G,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,QAAQ,KAAK,WAAW;AAC1C,SAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,YAAY,YAAY,CAAC;AACtE,SAAM,OAAO,WAAW,SAAS,SAAS,QAAQ,KAAK,WAAW,KAAK,WAAW,CAAC;AACnF,SAAM,KAAK,UAAU,KAAK,QAAQ;;UAE7B,OAAY;AACnB,SAAO,KAAK,yBAAyB,MAAM,UAAU;;AAGvD,QAAO;EAAE;EAAO;EAAQ;;AAG1B,eAAe,gCACb,UACA,QACA,eACgD;CAChD,MAAM,QAAkB,EAAE;CAC1B,MAAM,SAAmB,EAAE;CAE3B,MAAM,eAAe,cAAc;CACnC,MAAM,WAAW,MAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,QAAQ,aAAa,oBAAoB,CAAC;AACxG,KAAI,SAAS,aAAa,KAAK,CAAC,SAAS,OAAO,MAAM,EAAE;AACtD,QAAM,KAAK,8CAA8C;AACzD,SAAO;GAAE;GAAO;GAAQ;;CAG1B,MAAM,cAAc,KAAK,eAAe,YAAY;CACpD,MAAM,mBAAmB,KAAK,aAAa,qBAAqB;AAChE,WAAU,aAAa,EAAE,WAAW,MAAM,CAAC;AAC3C,WAAU,kBAAkB,EAAE,WAAW,MAAM,CAAC;CAEhD,MAAM,cAAc;EAClB;GAAE,MAAM;GAAmB,OAAO;GAAoB;EACtD;GAAE,MAAM;GAAuB,OAAO;GAAkB;EACxD;GAAE,MAAM;GAAsB,OAAO;GAAmB;EACzD;AAED,MAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,aAAa,GAAG,aAAa,GAAG,KAAK;EAC3C,MAAM,eAAe,MAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,QAAQ,WAAW,oBAAoB,CAAC;AAC1G,MAAI,aAAa,aAAa,KAAK,aAAa,OAAO,MAAM,CAC3D,KAAI;AACF,SAAM,OAAO,WAAW,SAAS,WAAW,QAAQ,YAAY,KAAK,aAAa,KAAK,KAAK,CAAC,CAAC;AAC9F,SAAM,KAAK,UAAU,KAAK,QAAQ;WAC3B,OAAY;AACnB,UAAO,KAAK,GAAG,KAAK,MAAM,IAAI,MAAM,UAAU;;;CAKpD,MAAM,eAAe,MAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,MAAM,aAAa,GAAG,qBAAqB,cAAc,CAAC;AAC5H,KAAI,aAAa,aAAa,KAAK,aAAa,OAAO,MAAM,CAC3D,MAAK,MAAM,SAAS,aAAa,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,OAAO,QAAQ,CACxE,KAAI;AACF,QAAM,OAAO,WAAW,SAAS,WAC/B,QACA,GAAG,aAAa,GAAG,qBAAqB,GAAG,SAC3C,KAAK,kBAAkB,MAAM,CAC9B,CAAC;AACF,QAAM,KAAK,mBAAmB,QAAQ;UAC/B,OAAY;AACnB,SAAO,KAAK,YAAY,MAAM,IAAI,MAAM,UAAU;;AAKxD,QAAO;EAAE;EAAO;EAAQ;;;;;AAM1B,eAAe,0BACb,UACA,QACA,WACA,UACA,eACgD;CAChD,MAAM,qBAAqB,MAAM,8BAA8B,UAAU,QAAQ,UAAU;CAC3F,MAAM,QAAQ,CAAC,GAAG,mBAAmB,MAAM;CAC3C,MAAM,SAAS,CAAC,GAAG,mBAAmB,OAAO;CAE7C,MAAM,cAAc,eAAe;CACnC,MAAM,iBAAiB,CACrB,KAAK,WAAW,SAAS,EACzB,cAAc,KAAK,aAAa,SAAS,GAAG,KAC7C,CAAC,OAAO,QAAQ;AAEjB,MAAK,MAAM,YAAY,eACrB,KAAI,WAAW,SAAS,CACtB,KAAI;AACF,QAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,6BAA6B,CAAC;EAC3E,MAAM,aAAa,YAAY,SAAS,CAAC,QAAO,MAAK,EAAE,SAAS,MAAM,IAAI,EAAE,SAAS,SAAS,CAAC;AAC/F,OAAK,MAAM,QAAQ,WACjB,OAAM,OAAO,WAAW,SAAS,SAAS,QAAQ,KAAK,UAAU,KAAK,EAAE,qBAAqB,OAAO,CAAC;AAEvG,QAAM,KAAK,wBAAwB;AACnC;UACO,OAAY;AACnB,SAAO,KAAK,UAAU,MAAM,UAAU;;AAK5C,QAAO;EAAE;EAAO;EAAQ;;;;;AAM1B,eAAe,4BACb,UACA,QACA,WACA,UACA,gBACgD;CAChD,MAAM,qBAAqB,MAAM,gCAAgC,UAAU,QAAQ,UAAU;CAC7F,MAAM,QAAQ,CAAC,GAAG,mBAAmB,MAAM;CAC3C,MAAM,SAAS,CAAC,GAAG,mBAAmB,OAAO;CAE7C,MAAM,aAAa,MAAM,OAAO,WAAW,SAAS,IAAI,QAAQ,oCAAoC,CAAC;AACrG,KAAI,WAAW,aAAa,KAAK,WAAW,OAAO,MAAM,CACvD,KAAI;EACF,MAAM,gBAAgB,KAAK,WAAW,SAAS;AAC/C,YAAU,eAAe,EAAE,WAAW,MAAM,CAAC;EAE7C,MAAM,QAAQ,WAAW,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,QAAO,MAAK,EAAE,SAAS,MAAM,IAAI,EAAE,SAAS,SAAS,CAAC;AACzG,OAAK,MAAM,QAAQ,MACjB,KAAI;AACF,SAAM,OAAO,WAAW,SAAS,WAAW,QAAQ,qBAAqB,QAAQ,KAAK,eAAe,KAAK,CAAC,CAAC;UACtG;AAIV,QAAM,KAAK,wBAAwB;UAC5B,OAAY;AACnB,SAAO,KAAK,UAAU,MAAM,UAAU;;AAI1C,QAAO;EAAE;EAAO;EAAQ;;;;;AAM1B,eAAsB,qBACpB,SACA,SAC0B;CAC1B,MAAM,SAA0B;EAC9B,SAAS;EACT,SAAS;EACT,QAAQ,EAAE;EACV,OAAO,EAAE;EACV;CAED,MAAM,UAAU,IAAI,mCAAmC,CAAC,OAAO;AAE/D,KAAI;EAEF,MAAM,YAAY,uBAAuB,QAAQ;AACjD,MAAI,CAAC,WAAW;AACd,WAAQ,KAAK,4BAA4B;AACzC,UAAO,OAAO,KAAK,gCAAgC,UAAU;AAC7D,UAAO;;AAET,SAAO,MAAM,KAAK,0BAA0B,YAAY;EAGxD,MAAM,iBAAiB,0BAA0B,QAAQ;AACzD,MAAI,kBAAkB,CAAC,QAAQ,OAAO;AACpC,WAAQ,KAAK,kCAAkC;AAC/C,UAAO,OAAO,KAAK,uCAAuC,QAAQ,6BAA6B;AAC/F,UAAO;;AAIT,UAAQ,OAAO;EACf,MAAM,SAAS,gBAAgB;AAC/B,MAAI,CAAC,OAAO,QAAQ,SAAS;AAC3B,WAAQ,KAAK,gCAAgC;AAC7C,UAAO,OAAO,KAAK,qEAAqE;AACxF,UAAO;;EAGT,MAAM,WAAW,4BAA4B,OAAO,OAAO;AAG3D,MAAI,CAAC,MADgB,OAAO,WAAW,SAAS,iBAAiB,CAAC,EACrD;AACX,WAAQ,KAAK,gCAAgC;AAC7C,UAAO,OAAO,KAAK,wDAAwD;AAC3E,UAAO;;AAET,SAAO,MAAM,KAAK,4BAA4B;EAK9C,MAAM,aAAa,kBAAkB,QAAQ;EAC7C,MAAM,gBAAsC,aAAa,sBAAsB,WAAW,GAAG;AAG7F,MAFiB,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC,CAC7D,QAAO,MAAK,EAAE,aAAa,IAAI,CAAC,EAAE,KAAK,WAAW,IAAI,IAAI,WAAW,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,CAC5F,CAAC,SAAS,EACpB,OAAM,IAAI,MAAM,iEAAiE;AAEnF,MAAI,CAAC,WAAW,KAAK,WAAW,OAAO,CAAC,CACtC,OAAM,IAAI,MAAM,GAAG,UAAU,wBAAwB;EAMvD,MAAM,UAAU,SAAS,QAAQ,aAAa;AAC9C,MAAI,kBAAkB,QAAQ,EAAE;AAC9B,WAAQ,OAAO;AACf,iBAAc,QAAQ;AACtB,UAAO,MAAM,KAAK,uBAAuB,UAAU;;AAErD,qBAAmB,SAAS,8BAA8B;AAC1D,SAAO,MAAM,KAAK,0CAA0C;AAI5D,UAAQ,OAAO;EACf,MAAM,EAAE,QAAQ,cAAc,MAAM,UAAU,6BAA6B,EAAE,KAAK,WAAW,CAAC;EAC9F,MAAM,aAAa,UAAU,MAAM;EACnC,MAAM,iBAAiB,WAAW,QAAQ,aAAa;AACvD,MAAI,eAAe,kBAAkB,CAAC,QAAQ,MAC5C,OAAM,IAAI,MACR,oBAAoB,WAAW,eAAe,eAAe,oDAC9D;EAEH,MAAM,EAAE,QAAQ,cAAc,MAAM,UAAU,0BAA0B,EAAE,KAAK,WAAW,CAAC;AAC3F,MAAI,UAAU,MAAM,EAAE;AACpB,SAAM,UAAU,cAAc,EAAE,KAAK,WAAW,CAAC;AACjD,SAAM,UACJ,iEAAiE,QAAQ,aAAa,CAAC,KACvF,EAAE,KAAK,WAAW,CACnB;AACD,UAAO,MAAM,KAAK,4CAA4C;;AAEhE,QAAM,UAAU,sBAAsB,cAAc,EAAE,KAAK,WAAW,CAAC;AACvE,SAAO,MAAM,KAAK,UAAU,WAAW,YAAY;AAInD,MAAI,kBAAkB,QAAQ,OAAO;AACnC,WAAQ,OAAO,0BAA0B,eAAe,OAAO;AAC/D,OAAI;AACF,UAAM,OAAO,WAAW,SAAS,SAAS,eAAe,OAAO,CAAC;AACjE,WAAO,MAAM,KAAK,yBAAyB,eAAe,SAAS;YAC5D,OAAY;AACnB,WAAO,MAAM,KAAK,sCAAsC,MAAM,UAAU;;AAE1E,+BAA4B,QAAQ;;AAOtC,UAAQ,OAAO;EACf,MAAM,EAAE,0BAA0B,MAAM,OAAO;EAC/C,MAAM,WAAW,MAAM,OAAO,WAAW,sBAAsB,SAAS,EAAE,SAAS,CAAC,CAAC;AACrF,SAAO,MAAM,KAAK,6BAA6B,SAAS,SAAS;AAIjE,UAAQ,OAAO;EACf,MAAM,iBAAiB,MAAM,0BAA0B,UAAU,SAAS,QAAQ,WAAW,SAAS,cAAc;AACpH,SAAO,MAAM,KAAK,GAAG,eAAe,MAAM;AAC1C,MAAI,eAAe,OAAO,SAAS,EAEjC,QAAO,MAAM,KAAK,YAAY,eAAe,OAAO,KAAK,KAAK,GAAG;EAKnE,MAAM,gBAAgB,KAAK,WAAW,aAAa,sBAAsB;EACzE,IAAI,YAAY,QAAQ;AACxB,MAAI,CAAC,aAAa,WAAW,cAAc;OAIrC,EAAC,MAHe,OAAO,WACzB,SAAS,IAAI,SAAS,QAAQ,uDAA4E,CAC3G,EACU,OAAO,MAAM,EAAE;AACxB,WAAO,MAAM,KAAK,wEAAwE;AAC1F,gBAAY;;;AAMhB,MAAI,CAAC,WAAW;AACd,WAAQ,OAAO;AACf,OAAI;AACF,QAAI,eAAe;KACjB,MAAM,eAAe,MAAM,OAAO,WAAW,gBAAgB;MAC3D;MACA,aAAa,QAAQ,aAAa;MACnC,CAAC,CAAC;AACH,YAAO,MAAM,KAAK,GAAG,aAAa,MAAM;AACxC,SAAI,aAAa,OAAO,SAAS,EAC/B,QAAO,OAAO,KAAK,GAAG,aAAa,OAAO;;YAGvC,OAAY;AACnB,WAAO,OAAO,KAAK,oBAAoB,MAAM,UAAU;;QAGzD,QAAO,MAAM,KAAK,qCAAqC;AAGzD,UAAQ,QAAQ,YAAY,QAAQ,YAAY;AAChD,SAAO,UAAU;AACjB,SAAO,UAAU,yBAAyB,SAAS;UAE5C,OAAY;AACnB,UAAQ,KAAK,mBAAmB;AAChC,SAAO,OAAO,KAAK,MAAM,QAAQ;;AAGnC,QAAO;;;;;AAMT,eAAsB,qBACpB,SACA,SAC0B;CAC1B,MAAM,SAA0B;EAC9B,SAAS;EACT,SAAS;EACT,QAAQ,EAAE;EACV,OAAO,EAAE;EACV;CAED,MAAM,UAAU,IAAI,kCAAkC,CAAC,OAAO;AAE9D,KAAI;EAEF,MAAM,iBAAiB,0BAA0B,QAAQ;AACzD,MAAI,CAAC,gBAAgB;AACnB,WAAQ,KAAK,6BAA6B;AAC1C,UAAO,OAAO,KAAK,iCAAiC,UAAU;AAC9D,UAAO;;AAET,SAAO,MAAM,KAAK,2BAA2B,eAAe,SAAS;EAGrE,MAAM,gBAAgB,uBAAuB,QAAQ;AACrD,MAAI,iBAAiB,CAAC,QAAQ,OAAO;AACnC,WAAQ,KAAK,iCAAiC;AAC9C,UAAO,OAAO,KAAK,qCAAqC,cAAc,6BAA6B;AACnG,UAAO;;EAKT,MAAM,WAAW,4BADF,gBACoC,CAAC,OAAO;AAG3D,UAAQ,OAAO;EACf,MAAM,WAAW,MAAM,OAAO,WAAW,SAAS,UAAU,eAAe,OAAO,CAAC;AACnF,MAAI,aAAa,WAAW;AAC1B,WAAQ,KAAK,sBAAsB;AACnC,UAAO,OAAO,KAAK,MAAM,eAAe,OAAO,sBAAsB;AACrE,UAAO;;AAET,SAAO,MAAM,KAAK,cAAc,WAAW;AAG3C,UAAQ,OAAO;AAEf,SAAO,MAAM,KAAK,qBAAqB;AAGvC,UAAQ,OAAO;AACE,8BAA4B,SAAS,EAAE,CAAC;EACzD,MAAM,aAAa,kBAAkB,QAAQ;EAC7C,MAAM,gBAAsC,aAAa,sBAAsB,WAAW,GAAG;AAC7F,MAAI,CAAC,eAAe;AAClB,WAAQ,KAAK,gCAAgC;AAC7C,UAAO,OAAO,KAAK,qCAAqC,UAAU;AAClE,UAAO;;EAGT,MAAM,kBAAkB,MAAM,OAAO,WAAW,gBAAgB;GAC9D;GACA,aAAa,QAAQ,aAAa;GAClC,aAAa,CAAC,QAAQ;GACvB,CAAC,CAAC;AAEH,MAAI,CAAC,gBAAgB,SAAS;AAC5B,WAAQ,KAAK,mCAAmC;AAChD,UAAO,OAAO,KAAK,GAAG,gBAAgB,OAAO;AAC7C,UAAO;;AAET,SAAO,MAAM,KAAK,4BAA4B,gBAAgB,gBAAgB;AAC9E,SAAO,MAAM,KAAK,GAAG,gBAAgB,MAAM;AAG3C,UAAQ,OAAO;EACf,MAAM,iBAAiB,MAAM,4BAC3B,UACA,eAAe,QACf,gBAAgB,eAChB,SACA,cACD;AACD,SAAO,MAAM,KAAK,GAAG,eAAe,MAAM;AAC1C,MAAI,eAAe,OAAO,SAAS,EAEjC,QAAO,MAAM,KAAK,YAAY,eAAe,OAAO,KAAK,KAAK,GAAG;AAInE,MAAI,CAAC,QAAQ,MAAM;AACjB,WAAQ,OAAO;AACf,OAAI;AACF,UAAM,OAAO,WAAW,SAAS,SAAS,eAAe,OAAO,CAAC;AACjE,WAAO,MAAM,KAAK,eAAe,eAAe,SAAS;YAClD,OAAY;AACnB,WAAO,OAAO,KAAK,iCAAiC,MAAM,UAAU;;AAEtE,+BAA4B,QAAQ;AACpC,UAAO,MAAM,KAAK,6BAA6B;QAE/C,QAAO,MAAM,KAAK,sCAAsC;AAG1D,UAAQ,QAAQ,YAAY,QAAQ,WAAW;AAC/C,SAAO,UAAU;AACjB,SAAO,UAAU,yBAAyB,gBAAgB;UAEnD,OAAY;AACnB,UAAQ,KAAK,mBAAmB;AAChC,SAAO,OAAO,KAAK,MAAM,QAAQ;;AAGnC,QAAO;;;;;AAMT,eAAsB,iBACpB,SACA,SACe;AACf,SAAQ,IAAI,MAAM,KAAK,0BAA0B,QAAQ,IAAI,CAAC;AAG9D,KAAI,QAAQ,YAAY,QAAQ,SAAS;AACvC,UAAQ,MAAM,MAAM,IAAI,wDAAwD,CAAC;AACjF,UAAQ,KAAK,EAAE;;CAIjB,MAAM,kBAAkB,wBAAwB,QAAQ;AAExD,KAAI,oBAAoB,QAAQ;AAC9B,UAAQ,MAAM,MAAM,IAAI,iCAAiC,UAAU,CAAC;AACpE,UAAQ,MAAM,MAAM,IAAI,0DAA0D,CAAC;AACnF,UAAQ,KAAK,EAAE;;CAIjB,IAAI;AAEJ,KAAI,QAAQ,SACV,aAAY;UACH,QAAQ,QACjB,aAAY;MACP;AAEL,cAAY,oBAAoB,UAAU,cAAc;AACxD,UAAQ,IAAI,MAAM,IAAI,4BAA4B,UAAU,aAAa,gBAAgB,GAAG,CAAC;;AAI/F,KAAI,cAAc,eAAe,oBAAoB,UAAU;AAC7D,UAAQ,MAAM,MAAM,IAAI,qCAAqC,CAAC;AAC9D,UAAQ,KAAK,EAAE;;AAEjB,KAAI,cAAc,cAAc,oBAAoB,SAAS;AAC3D,UAAQ,MAAM,MAAM,IAAI,oCAAoC,CAAC;AAC7D,UAAQ,KAAK,EAAE;;CAIjB,IAAI;AAEJ,KAAI,cAAc,YAChB,UAAS,MAAM,qBAAqB,SAAS,QAAQ;KAErD,UAAS,MAAM,qBAAqB,SAAS,QAAQ;AAIvD,SAAQ,KAAK;AAEb,KAAI,OAAO,MAAM,SAAS,GAAG;AAC3B,UAAQ,IAAI,MAAM,KAAK,mBAAmB,CAAC;AAC3C,OAAK,MAAM,QAAQ,OAAO,MACxB,SAAQ,IAAI,MAAM,MAAM,OAAO,OAAO,CAAC;AAEzC,UAAQ,KAAK;;AAGf,KAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,UAAQ,IAAI,MAAM,KAAK,UAAU,CAAC;AAClC,OAAK,MAAM,SAAS,OAAO,OACzB,SAAQ,IAAI,MAAM,IAAI,OAAO,QAAQ,CAAC;AAExC,UAAQ,KAAK;;AAGf,KAAI,OAAO,QACT,SAAQ,IAAI,MAAM,MAAM,KAAK,OAAO,UAAU,CAAC;MAC1C;AACL,UAAQ,MAAM,MAAM,IAAI,mBAAmB,CAAC;AAC5C,UAAQ,KAAK,EAAE"}
@@ -1,5 +1,5 @@
1
1
  import { $ as getOverdeckHome, et as init_paths } from "./paths-3R6b989k.js";
2
- import { Jt as getAgentStateSync, t as init_agents } from "./agents-CQHK0CSA.js";
2
+ import { Jt as getAgentStateSync, t as init_agents } from "./agents-CgVr77Qq.js";
3
3
  import { existsSync, readdirSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { execFile } from "child_process";
@@ -211,4 +211,4 @@ async function workspaceReapCommand(options = {}) {
211
211
  //#endregion
212
212
  export { workspaceReapCommand };
213
213
 
214
- //# sourceMappingURL=workspace-reap-DLCIoJmn.js.map
214
+ //# sourceMappingURL=workspace-reap-12O9354k.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-reap-DLCIoJmn.js","names":[],"sources":["../src/cli/commands/workspace-reap.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { existsSync, readdirSync } from 'fs';\nimport { createInterface } from 'readline/promises';\nimport { promisify } from 'util';\nimport { join } from 'path';\nimport chalk from 'chalk';\nimport { getOverdeckHome } from '../../lib/paths.js';\nimport { getAgentStateSync } from '../../lib/agents.js';\n\nconst execFileAsync = promisify(execFile);\nconst WORKSPACE_PROJECT_PREFIX = 'overdeck-feature-';\nconst ACTIVE_AGENT_STATUSES = new Set(['running', 'starting']);\n\nexport interface WorkspaceReapOptions {\n days?: string;\n apply?: boolean;\n yes?: boolean;\n}\n\ninterface DockerInspectContainer {\n Id?: string;\n Name?: string;\n Created?: string;\n Config?: {\n Labels?: Record<string, string>;\n };\n State?: {\n Status?: string;\n ExitCode?: number;\n };\n}\n\nexport interface ReapCandidate {\n project: string;\n issueId: string;\n reason: string;\n ageDays: number;\n containers: Array<{\n id: string;\n name: string;\n status: string;\n exitCode: number;\n createdAt: string;\n }>;\n composeFiles: string[];\n workingDir?: string;\n}\n\ninterface CandidateGroup {\n project: string;\n issueId: string;\n containers: ReapCandidate['containers'];\n composeFiles: Set<string>;\n workingDir?: string;\n reasons: string[];\n oldestBadCreatedMs: number;\n}\n\nexport function parseReapDays(value: string | undefined): number {\n const raw = value ?? '7';\n const days = Number.parseInt(raw, 10);\n if (!Number.isInteger(days) || days < 0 || String(days) !== raw.trim()) {\n throw new Error(`--days must be a non-negative integer, got ${JSON.stringify(raw)}`);\n }\n return days;\n}\n\nfunction normalizeIssueId(value: string): string {\n return value.toLowerCase().replace(/^feature-/, '').replace(/[^a-z0-9-]/g, '-');\n}\n\nfunction issueIdFromProject(project: string): string {\n const lower = project.toLowerCase();\n if (lower.startsWith(WORKSPACE_PROJECT_PREFIX)) {\n return normalizeIssueId(lower.slice(WORKSPACE_PROJECT_PREFIX.length));\n }\n return normalizeIssueId(lower);\n}\n\nfunction parseComposeFiles(value: string | undefined): string[] {\n if (!value) return [];\n return value\n .split(',')\n .map(file => file.trim())\n .filter(Boolean);\n}\n\nfunction isBadContainer(status: string, exitCode: number): string | null {\n if (status === 'created') return 'Created';\n if (status === 'exited' && exitCode !== 0) return `Exited (${exitCode})`;\n return null;\n}\n\nexport function collectWorkspaceReapCandidatesFromInspect(\n containers: DockerInspectContainer[],\n cutoffMs: number,\n nowMs: number = Date.now(),\n): ReapCandidate[] {\n const groups = new Map<string, CandidateGroup>();\n\n for (const container of containers) {\n const labels = container.Config?.Labels ?? {};\n const name = (container.Name ?? '').replace(/^\\//, '');\n const project = labels['com.docker.compose.project'] ?? name.split('-').slice(0, -2).join('-');\n if (!name.includes(WORKSPACE_PROJECT_PREFIX) && !project.startsWith(WORKSPACE_PROJECT_PREFIX)) {\n continue;\n }\n\n const status = (container.State?.Status ?? '').toLowerCase();\n const exitCode = container.State?.ExitCode ?? 0;\n const reason = isBadContainer(status, exitCode);\n if (!reason) continue;\n\n const createdMs = Date.parse(container.Created ?? '');\n if (!Number.isFinite(createdMs) || createdMs > cutoffMs) continue;\n\n const id = container.Id ?? '';\n if (!id) continue;\n\n let group = groups.get(project);\n if (!group) {\n group = {\n project,\n issueId: issueIdFromProject(project),\n containers: [],\n composeFiles: new Set(),\n workingDir: labels['com.docker.compose.project.working_dir'],\n reasons: [],\n oldestBadCreatedMs: createdMs,\n };\n groups.set(project, group);\n }\n\n for (const file of parseComposeFiles(labels['com.docker.compose.project.config_files'])) {\n if (existsSync(file)) group.composeFiles.add(file);\n }\n if (!group.workingDir && labels['com.docker.compose.project.working_dir']) {\n group.workingDir = labels['com.docker.compose.project.working_dir'];\n }\n group.oldestBadCreatedMs = Math.min(group.oldestBadCreatedMs, createdMs);\n group.containers.push({\n id,\n name,\n status,\n exitCode,\n createdAt: container.Created ?? '',\n });\n group.reasons.push(`${name}: ${reason}`);\n }\n\n return [...groups.values()]\n .map(group => ({\n project: group.project,\n issueId: group.issueId,\n reason: group.reasons.join(', '),\n ageDays: Math.floor((nowMs - group.oldestBadCreatedMs) / 86_400_000),\n containers: group.containers,\n composeFiles: [...group.composeFiles],\n workingDir: group.workingDir,\n }))\n .sort((a, b) => b.ageDays - a.ageDays || a.project.localeCompare(b.project));\n}\n\nfunction collectActiveAgentIssueIds(): Set<string> {\n const activeIssues = new Set<string>();\n const agentsDir = join(getOverdeckHome(), 'agents');\n if (!existsSync(agentsDir)) return activeIssues;\n\n for (const dir of readdirSync(agentsDir)) {\n try {\n const state = getAgentStateSync(dir);\n if (!state?.issueId || !state.status || !ACTIVE_AGENT_STATUSES.has(state.status)) continue;\n activeIssues.add(normalizeIssueId(state.issueId));\n } catch {}\n }\n\n return activeIssues;\n}\n\nasync function listMatchingContainerIds(): Promise<string[]> {\n const { stdout } = await execFileAsync('docker', [\n 'ps',\n '-a',\n '--filter',\n `name=${WORKSPACE_PROJECT_PREFIX}`,\n '--format',\n '{{.ID}}',\n ], { encoding: 'utf-8' });\n return stdout.trim().split('\\n').map(line => line.trim()).filter(Boolean);\n}\n\nasync function inspectContainers(ids: string[]): Promise<DockerInspectContainer[]> {\n if (ids.length === 0) return [];\n const { stdout } = await execFileAsync('docker', [\n 'inspect',\n ...ids,\n '--format',\n '{{json .}}',\n ], { encoding: 'utf-8', maxBuffer: 20 * 1024 * 1024 });\n return stdout.trim().split('\\n').filter(Boolean).map(line => JSON.parse(line));\n}\n\nasync function confirmApply(candidates: ReapCandidate[], yes: boolean | undefined): Promise<boolean> {\n if (yes) {\n console.log(chalk.dim(' --yes given; skipping interactive confirmation.'));\n return true;\n }\n\n if (!process.stdin.isTTY) {\n console.error(chalk.red('✗ --apply requires an interactive terminal, or pass --yes for non-interactive use.'));\n return false;\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const expected = String(candidates.length);\n const answer = (\n await rl.question(chalk.bold(`Type \"${expected}\" to reap these workspace stack(s), anything else to cancel: `))\n ).trim();\n return answer === expected;\n } finally {\n rl.close();\n }\n}\n\nasync function composeDown(candidate: ReapCandidate): Promise<void> {\n const args = [\n 'compose',\n ...candidate.composeFiles.flatMap(file => ['-f', file]),\n '-p',\n candidate.project,\n 'down',\n '-v',\n '--remove-orphans',\n ];\n await execFileAsync('docker', args, {\n cwd: candidate.workingDir && existsSync(candidate.workingDir) ? candidate.workingDir : process.cwd(),\n encoding: 'utf-8',\n timeout: 60_000,\n maxBuffer: 10 * 1024 * 1024,\n });\n}\n\nfunction printCandidates(candidates: ReapCandidate[], apply: boolean | undefined): void {\n if (candidates.length === 0) {\n console.log(chalk.green('No orphaned workspace Docker stacks found.'));\n return;\n }\n\n console.log(chalk.bold(`${apply ? 'Will reap' : 'Would reap'} ${candidates.length} workspace Docker stack(s):`));\n console.log('');\n for (const candidate of candidates) {\n console.log(`${chalk.cyan(candidate.project)} (${candidate.issueId.toUpperCase()}, ${candidate.ageDays}d old)`);\n console.log(` ${candidate.reason}`);\n if (candidate.composeFiles.length > 0) {\n console.log(chalk.dim(` compose: ${candidate.composeFiles.join(', ')}`));\n } else {\n console.log(chalk.yellow(' compose: no existing config file label found; using project-name cleanup'));\n }\n }\n console.log('');\n if (!apply) {\n console.log(chalk.dim('Dry run only. Re-run with --apply to run docker compose down -v --remove-orphans.'));\n }\n}\n\nexport async function workspaceReapCommand(options: WorkspaceReapOptions = {}): Promise<void> {\n let days: number;\n try {\n days = parseReapDays(options.days);\n } catch (error: any) {\n console.error(chalk.red(`✗ ${error.message}`));\n process.exit(1);\n }\n\n const cutoffMs = Date.now() - days * 86_400_000;\n let containers: DockerInspectContainer[];\n try {\n const ids = await listMatchingContainerIds();\n containers = await inspectContainers(ids);\n } catch (error: any) {\n console.error(chalk.red(`✗ Failed to query Docker containers: ${error.message ?? error}`));\n process.exit(1);\n }\n\n const activeAgentIssueIds = collectActiveAgentIssueIds();\n const candidates = collectWorkspaceReapCandidatesFromInspect(containers, cutoffMs)\n .filter(candidate => {\n if (!activeAgentIssueIds.has(candidate.issueId)) return true;\n console.log(chalk.yellow(`Skipping ${candidate.project}: active agent state exists for ${candidate.issueId.toUpperCase()}`));\n return false;\n });\n\n printCandidates(candidates, options.apply);\n if (!options.apply || candidates.length === 0) return;\n\n const confirmed = await confirmApply(candidates, options.yes);\n if (!confirmed) {\n console.log(chalk.green('Cancelled — no Docker stacks were reaped.'));\n return;\n }\n\n for (const candidate of candidates) {\n try {\n await composeDown(candidate);\n console.log(chalk.green(`✓ Reaped ${candidate.project}`));\n } catch (error: any) {\n console.error(chalk.red(`✗ Failed to reap ${candidate.project}: ${error.message ?? error}`));\n process.exitCode = 1;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace-reap-12O9354k.js","names":[],"sources":["../src/cli/commands/workspace-reap.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { existsSync, readdirSync } from 'fs';\nimport { createInterface } from 'readline/promises';\nimport { promisify } from 'util';\nimport { join } from 'path';\nimport chalk from 'chalk';\nimport { getOverdeckHome } from '../../lib/paths.js';\nimport { getAgentStateSync } from '../../lib/agents.js';\n\nconst execFileAsync = promisify(execFile);\nconst WORKSPACE_PROJECT_PREFIX = 'overdeck-feature-';\nconst ACTIVE_AGENT_STATUSES = new Set(['running', 'starting']);\n\nexport interface WorkspaceReapOptions {\n days?: string;\n apply?: boolean;\n yes?: boolean;\n}\n\ninterface DockerInspectContainer {\n Id?: string;\n Name?: string;\n Created?: string;\n Config?: {\n Labels?: Record<string, string>;\n };\n State?: {\n Status?: string;\n ExitCode?: number;\n };\n}\n\nexport interface ReapCandidate {\n project: string;\n issueId: string;\n reason: string;\n ageDays: number;\n containers: Array<{\n id: string;\n name: string;\n status: string;\n exitCode: number;\n createdAt: string;\n }>;\n composeFiles: string[];\n workingDir?: string;\n}\n\ninterface CandidateGroup {\n project: string;\n issueId: string;\n containers: ReapCandidate['containers'];\n composeFiles: Set<string>;\n workingDir?: string;\n reasons: string[];\n oldestBadCreatedMs: number;\n}\n\nexport function parseReapDays(value: string | undefined): number {\n const raw = value ?? '7';\n const days = Number.parseInt(raw, 10);\n if (!Number.isInteger(days) || days < 0 || String(days) !== raw.trim()) {\n throw new Error(`--days must be a non-negative integer, got ${JSON.stringify(raw)}`);\n }\n return days;\n}\n\nfunction normalizeIssueId(value: string): string {\n return value.toLowerCase().replace(/^feature-/, '').replace(/[^a-z0-9-]/g, '-');\n}\n\nfunction issueIdFromProject(project: string): string {\n const lower = project.toLowerCase();\n if (lower.startsWith(WORKSPACE_PROJECT_PREFIX)) {\n return normalizeIssueId(lower.slice(WORKSPACE_PROJECT_PREFIX.length));\n }\n return normalizeIssueId(lower);\n}\n\nfunction parseComposeFiles(value: string | undefined): string[] {\n if (!value) return [];\n return value\n .split(',')\n .map(file => file.trim())\n .filter(Boolean);\n}\n\nfunction isBadContainer(status: string, exitCode: number): string | null {\n if (status === 'created') return 'Created';\n if (status === 'exited' && exitCode !== 0) return `Exited (${exitCode})`;\n return null;\n}\n\nexport function collectWorkspaceReapCandidatesFromInspect(\n containers: DockerInspectContainer[],\n cutoffMs: number,\n nowMs: number = Date.now(),\n): ReapCandidate[] {\n const groups = new Map<string, CandidateGroup>();\n\n for (const container of containers) {\n const labels = container.Config?.Labels ?? {};\n const name = (container.Name ?? '').replace(/^\\//, '');\n const project = labels['com.docker.compose.project'] ?? name.split('-').slice(0, -2).join('-');\n if (!name.includes(WORKSPACE_PROJECT_PREFIX) && !project.startsWith(WORKSPACE_PROJECT_PREFIX)) {\n continue;\n }\n\n const status = (container.State?.Status ?? '').toLowerCase();\n const exitCode = container.State?.ExitCode ?? 0;\n const reason = isBadContainer(status, exitCode);\n if (!reason) continue;\n\n const createdMs = Date.parse(container.Created ?? '');\n if (!Number.isFinite(createdMs) || createdMs > cutoffMs) continue;\n\n const id = container.Id ?? '';\n if (!id) continue;\n\n let group = groups.get(project);\n if (!group) {\n group = {\n project,\n issueId: issueIdFromProject(project),\n containers: [],\n composeFiles: new Set(),\n workingDir: labels['com.docker.compose.project.working_dir'],\n reasons: [],\n oldestBadCreatedMs: createdMs,\n };\n groups.set(project, group);\n }\n\n for (const file of parseComposeFiles(labels['com.docker.compose.project.config_files'])) {\n if (existsSync(file)) group.composeFiles.add(file);\n }\n if (!group.workingDir && labels['com.docker.compose.project.working_dir']) {\n group.workingDir = labels['com.docker.compose.project.working_dir'];\n }\n group.oldestBadCreatedMs = Math.min(group.oldestBadCreatedMs, createdMs);\n group.containers.push({\n id,\n name,\n status,\n exitCode,\n createdAt: container.Created ?? '',\n });\n group.reasons.push(`${name}: ${reason}`);\n }\n\n return [...groups.values()]\n .map(group => ({\n project: group.project,\n issueId: group.issueId,\n reason: group.reasons.join(', '),\n ageDays: Math.floor((nowMs - group.oldestBadCreatedMs) / 86_400_000),\n containers: group.containers,\n composeFiles: [...group.composeFiles],\n workingDir: group.workingDir,\n }))\n .sort((a, b) => b.ageDays - a.ageDays || a.project.localeCompare(b.project));\n}\n\nfunction collectActiveAgentIssueIds(): Set<string> {\n const activeIssues = new Set<string>();\n const agentsDir = join(getOverdeckHome(), 'agents');\n if (!existsSync(agentsDir)) return activeIssues;\n\n for (const dir of readdirSync(agentsDir)) {\n try {\n const state = getAgentStateSync(dir);\n if (!state?.issueId || !state.status || !ACTIVE_AGENT_STATUSES.has(state.status)) continue;\n activeIssues.add(normalizeIssueId(state.issueId));\n } catch {}\n }\n\n return activeIssues;\n}\n\nasync function listMatchingContainerIds(): Promise<string[]> {\n const { stdout } = await execFileAsync('docker', [\n 'ps',\n '-a',\n '--filter',\n `name=${WORKSPACE_PROJECT_PREFIX}`,\n '--format',\n '{{.ID}}',\n ], { encoding: 'utf-8' });\n return stdout.trim().split('\\n').map(line => line.trim()).filter(Boolean);\n}\n\nasync function inspectContainers(ids: string[]): Promise<DockerInspectContainer[]> {\n if (ids.length === 0) return [];\n const { stdout } = await execFileAsync('docker', [\n 'inspect',\n ...ids,\n '--format',\n '{{json .}}',\n ], { encoding: 'utf-8', maxBuffer: 20 * 1024 * 1024 });\n return stdout.trim().split('\\n').filter(Boolean).map(line => JSON.parse(line));\n}\n\nasync function confirmApply(candidates: ReapCandidate[], yes: boolean | undefined): Promise<boolean> {\n if (yes) {\n console.log(chalk.dim(' --yes given; skipping interactive confirmation.'));\n return true;\n }\n\n if (!process.stdin.isTTY) {\n console.error(chalk.red('✗ --apply requires an interactive terminal, or pass --yes for non-interactive use.'));\n return false;\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const expected = String(candidates.length);\n const answer = (\n await rl.question(chalk.bold(`Type \"${expected}\" to reap these workspace stack(s), anything else to cancel: `))\n ).trim();\n return answer === expected;\n } finally {\n rl.close();\n }\n}\n\nasync function composeDown(candidate: ReapCandidate): Promise<void> {\n const args = [\n 'compose',\n ...candidate.composeFiles.flatMap(file => ['-f', file]),\n '-p',\n candidate.project,\n 'down',\n '-v',\n '--remove-orphans',\n ];\n await execFileAsync('docker', args, {\n cwd: candidate.workingDir && existsSync(candidate.workingDir) ? candidate.workingDir : process.cwd(),\n encoding: 'utf-8',\n timeout: 60_000,\n maxBuffer: 10 * 1024 * 1024,\n });\n}\n\nfunction printCandidates(candidates: ReapCandidate[], apply: boolean | undefined): void {\n if (candidates.length === 0) {\n console.log(chalk.green('No orphaned workspace Docker stacks found.'));\n return;\n }\n\n console.log(chalk.bold(`${apply ? 'Will reap' : 'Would reap'} ${candidates.length} workspace Docker stack(s):`));\n console.log('');\n for (const candidate of candidates) {\n console.log(`${chalk.cyan(candidate.project)} (${candidate.issueId.toUpperCase()}, ${candidate.ageDays}d old)`);\n console.log(` ${candidate.reason}`);\n if (candidate.composeFiles.length > 0) {\n console.log(chalk.dim(` compose: ${candidate.composeFiles.join(', ')}`));\n } else {\n console.log(chalk.yellow(' compose: no existing config file label found; using project-name cleanup'));\n }\n }\n console.log('');\n if (!apply) {\n console.log(chalk.dim('Dry run only. Re-run with --apply to run docker compose down -v --remove-orphans.'));\n }\n}\n\nexport async function workspaceReapCommand(options: WorkspaceReapOptions = {}): Promise<void> {\n let days: number;\n try {\n days = parseReapDays(options.days);\n } catch (error: any) {\n console.error(chalk.red(`✗ ${error.message}`));\n process.exit(1);\n }\n\n const cutoffMs = Date.now() - days * 86_400_000;\n let containers: DockerInspectContainer[];\n try {\n const ids = await listMatchingContainerIds();\n containers = await inspectContainers(ids);\n } catch (error: any) {\n console.error(chalk.red(`✗ Failed to query Docker containers: ${error.message ?? error}`));\n process.exit(1);\n }\n\n const activeAgentIssueIds = collectActiveAgentIssueIds();\n const candidates = collectWorkspaceReapCandidatesFromInspect(containers, cutoffMs)\n .filter(candidate => {\n if (!activeAgentIssueIds.has(candidate.issueId)) return true;\n console.log(chalk.yellow(`Skipping ${candidate.project}: active agent state exists for ${candidate.issueId.toUpperCase()}`));\n return false;\n });\n\n printCandidates(candidates, options.apply);\n if (!options.apply || candidates.length === 0) return;\n\n const confirmed = await confirmApply(candidates, options.yes);\n if (!confirmed) {\n console.log(chalk.green('Cancelled — no Docker stacks were reaped.'));\n return;\n }\n\n for (const candidate of candidates) {\n try {\n await composeDown(candidate);\n console.log(chalk.green(`✓ Reaped ${candidate.project}`));\n } catch (error: any) {\n console.error(chalk.red(`✗ Failed to reap ${candidate.project}: ${error.message ?? error}`));\n process.exitCode = 1;\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overdeck/core",
3
- "version": "0.45.10",
3
+ "version": "0.45.11",
4
4
  "description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -17,7 +17,7 @@
17
17
  1321 src/dashboard/server/ws-rpc.ts
18
18
  1035 src/lib/cloister/deacon-auto-resume.ts
19
19
  1179 src/lib/cloister/deacon-swarm.ts
20
- 3627 src/lib/cloister/deacon.ts
20
+ 3632 src/lib/cloister/deacon.ts
21
21
  1409 src/lib/cloister/merge-agent.ts
22
22
  2135 src/lib/cloister/service.ts
23
23
  1749 src/lib/cloister/specialists.ts
@@ -1,3 +0,0 @@
1
- import { i as init_cli, t as createResetMarker } from "./cli-Cewlu3yU.js";
2
- init_cli();
3
- export { createResetMarker };
@@ -1,3 +0,0 @@
1
- import { r as init_cli, t as createResetMarker } from "./cli-CvK9xuSS.js";
2
- init_cli();
3
- export { createResetMarker };
@@ -1,2 +0,0 @@
1
- import { r as coordinateSwarmSlots } from "./deacon-swarm-DadWNi5J.js";
2
- export { coordinateSwarmSlots };
@@ -1,2 +0,0 @@
1
- import { n as onInspectComplete } from "./inspect-agent-BKKc0XnB.js";
2
- export { onInspectComplete };
@@ -1,2 +0,0 @@
1
- import { c as triggerMerge } from "./workspaces-CRU9_vRn.js";
2
- export { triggerMerge };
@@ -1,3 +0,0 @@
1
- import { E as resumeAgent, T as init_resume } from "./agents-DHvsjsuP.js";
2
- init_resume();
3
- export { resumeAgent };
@@ -1,3 +0,0 @@
1
- import { a as killAllReviewSessions, c as resolveReviewMode, d as handleReviewDiscoveryReady, f as resolveReReviewScope, i as isReviewStaleSync, l as spawnReviewRoleForIssue, m as reviewResumeDecision, n as isExtendedReviewEnabled, o as killAllReviewerSessions, p as spawnReviewSubRoleForIssue, r as isReviewSessionForIssue, s as purgeReviewAgentsForIssue, t as init_review_agent, u as buildConvoyPrompt } from "./review-agent-phzqNqIs.js";
2
- init_review_agent();
3
- export { buildConvoyPrompt, handleReviewDiscoveryReady, isExtendedReviewEnabled, isReviewSessionForIssue, isReviewStaleSync, killAllReviewSessions, killAllReviewerSessions, purgeReviewAgentsForIssue, resolveReReviewScope, resolveReviewMode, reviewResumeDecision, spawnReviewRoleForIssue, spawnReviewSubRoleForIssue };