@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,6 +1,6 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime-DNCzzKnI.js";
2
2
  import { r as isNoResumeValueEnabled, t as init_no_resume_mode } from "./no-resume-mode-yrPEQ_pC.js";
3
- import { a as spawnSequencerAgent, i as init_sequencer_agent } from "./sequencer-agent-D8pW2aU_.js";
3
+ import { a as spawnSequencerAgent, i as init_sequencer_agent } from "./sequencer-agent-ja-nkEc1.js";
4
4
  //#region ../../lib/backlog/backlog-auto-trigger.ts
5
5
  /**
6
6
  * PAN-2396: --no-resume is the operator's full-quiescence escape hatch —
@@ -37,4 +37,4 @@ __esmMin((() => {
37
37
  }))();
38
38
  export { triggerDebouncedIncrementalPass };
39
39
 
40
- //# sourceMappingURL=backlog-auto-trigger-COm_R96V.js.map
40
+ //# sourceMappingURL=backlog-auto-trigger-Cb8PR-5w.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"backlog-auto-trigger-COm_R96V.js","names":[],"sources":["../../src/lib/backlog/backlog-auto-trigger.ts"],"sourcesContent":["import { isNoResumeValueEnabled } from '../cloister/no-resume-mode.js';\nimport { spawnSequencerAgent } from './sequencer-agent.js';\n\n/**\n * PAN-2396: --no-resume is the operator's full-quiescence escape hatch —\n * \"human-driven only\". When the boot carries OVERDECK_NO_RESUME, no\n * autonomous sequencer pass may spawn; only explicit operator actions\n * (the dashboard's start-sequencer button, `pan` commands) remain allowed.\n * Checked at FIRE time so an already-scheduled timer respects the gate too.\n */\nfunction autonomousSpawnSuppressed(mode: 'incremental' | 'review'): boolean {\n if (!isNoResumeValueEnabled(process.env.OVERDECK_NO_RESUME)) return false;\n console.warn(`[backlog-auto-trigger] suppressed ${mode} sequencer pass: boot is --no-resume (human-driven only)`);\n return true;\n}\n\nlet _incrementalTimer: ReturnType<typeof setTimeout> | null = null;\nlet _reviewTimer: ReturnType<typeof setInterval> | null = null;\n\nconst DEBOUNCE_MS = 30_000;\n\n/**\n * PAN-1866: Call on every backlog delta (issue opened/closed/edited).\n * Debounces: multiple changes within DEBOUNCE_MS collapse into a single\n * incremental sequencer pass.\n */\nexport function triggerDebouncedIncrementalPass(projectRoot: string): void {\n if (_incrementalTimer !== null) clearTimeout(_incrementalTimer);\n _incrementalTimer = setTimeout(() => {\n _incrementalTimer = null;\n if (autonomousSpawnSuppressed('incremental')) return;\n spawnSequencerAgent('incremental', { projectRoot }).catch((err: unknown) =>\n console.warn('[backlog-auto-trigger] incremental pass failed:', err),\n );\n }, DEBOUNCE_MS);\n}\n\n/**\n * PAN-1866: Start a periodic review-pass cadence. Safe to call multiple times\n * (clears the previous interval first). Pass 0 to disable.\n */\nexport function startPeriodicReviewPass(projectRoot: string, intervalMs: number): void {\n if (_reviewTimer !== null) {\n clearInterval(_reviewTimer);\n _reviewTimer = null;\n }\n if (intervalMs <= 0) return;\n _reviewTimer = setInterval(() => {\n if (autonomousSpawnSuppressed('review')) return;\n spawnSequencerAgent('review', { projectRoot }).catch((err: unknown) =>\n console.warn('[backlog-auto-trigger] periodic review pass failed:', err),\n );\n }, intervalMs);\n}\n\nexport function stopPeriodicReviewPass(): void {\n if (_reviewTimer !== null) {\n clearInterval(_reviewTimer);\n _reviewTimer = null;\n }\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAS,0BAA0B,MAAyC;AAC1E,KAAI,CAAC,uBAAuB,QAAQ,IAAI,mBAAmB,CAAE,QAAO;AACpE,SAAQ,KAAK,qCAAqC,KAAK,0DAA0D;AACjH,QAAO;;;;;;;AAaT,SAAgB,gCAAgC,aAA2B;AACzE,KAAI,sBAAsB,KAAM,cAAa,kBAAkB;AAC/D,qBAAoB,iBAAiB;AACnC,sBAAoB;AACpB,MAAI,0BAA0B,cAAc,CAAE;AAC9C,sBAAoB,eAAe,EAAE,aAAa,CAAC,CAAC,OAAO,QACzD,QAAQ,KAAK,mDAAmD,IAAI,CACrE;IACA,YAAY;;;;;sBAlCsD;uBACZ;AAevD,qBAA0D;AAGxD,eAAc"}
1
+ {"version":3,"file":"backlog-auto-trigger-Cb8PR-5w.js","names":[],"sources":["../../src/lib/backlog/backlog-auto-trigger.ts"],"sourcesContent":["import { isNoResumeValueEnabled } from '../cloister/no-resume-mode.js';\nimport { spawnSequencerAgent } from './sequencer-agent.js';\n\n/**\n * PAN-2396: --no-resume is the operator's full-quiescence escape hatch —\n * \"human-driven only\". When the boot carries OVERDECK_NO_RESUME, no\n * autonomous sequencer pass may spawn; only explicit operator actions\n * (the dashboard's start-sequencer button, `pan` commands) remain allowed.\n * Checked at FIRE time so an already-scheduled timer respects the gate too.\n */\nfunction autonomousSpawnSuppressed(mode: 'incremental' | 'review'): boolean {\n if (!isNoResumeValueEnabled(process.env.OVERDECK_NO_RESUME)) return false;\n console.warn(`[backlog-auto-trigger] suppressed ${mode} sequencer pass: boot is --no-resume (human-driven only)`);\n return true;\n}\n\nlet _incrementalTimer: ReturnType<typeof setTimeout> | null = null;\nlet _reviewTimer: ReturnType<typeof setInterval> | null = null;\n\nconst DEBOUNCE_MS = 30_000;\n\n/**\n * PAN-1866: Call on every backlog delta (issue opened/closed/edited).\n * Debounces: multiple changes within DEBOUNCE_MS collapse into a single\n * incremental sequencer pass.\n */\nexport function triggerDebouncedIncrementalPass(projectRoot: string): void {\n if (_incrementalTimer !== null) clearTimeout(_incrementalTimer);\n _incrementalTimer = setTimeout(() => {\n _incrementalTimer = null;\n if (autonomousSpawnSuppressed('incremental')) return;\n spawnSequencerAgent('incremental', { projectRoot }).catch((err: unknown) =>\n console.warn('[backlog-auto-trigger] incremental pass failed:', err),\n );\n }, DEBOUNCE_MS);\n}\n\n/**\n * PAN-1866: Start a periodic review-pass cadence. Safe to call multiple times\n * (clears the previous interval first). Pass 0 to disable.\n */\nexport function startPeriodicReviewPass(projectRoot: string, intervalMs: number): void {\n if (_reviewTimer !== null) {\n clearInterval(_reviewTimer);\n _reviewTimer = null;\n }\n if (intervalMs <= 0) return;\n _reviewTimer = setInterval(() => {\n if (autonomousSpawnSuppressed('review')) return;\n spawnSequencerAgent('review', { projectRoot }).catch((err: unknown) =>\n console.warn('[backlog-auto-trigger] periodic review pass failed:', err),\n );\n }, intervalMs);\n}\n\nexport function stopPeriodicReviewPass(): void {\n if (_reviewTimer !== null) {\n clearInterval(_reviewTimer);\n _reviewTimer = null;\n }\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAS,0BAA0B,MAAyC;AAC1E,KAAI,CAAC,uBAAuB,QAAQ,IAAI,mBAAmB,CAAE,QAAO;AACpE,SAAQ,KAAK,qCAAqC,KAAK,0DAA0D;AACjH,QAAO;;;;;;;AAaT,SAAgB,gCAAgC,aAA2B;AACzE,KAAI,sBAAsB,KAAM,cAAa,kBAAkB;AAC/D,qBAAoB,iBAAiB;AACnC,sBAAoB;AACpB,MAAI,0BAA0B,cAAc,CAAE;AAC9C,sBAAoB,eAAe,EAAE,aAAa,CAAC,CAAC,OAAO,QACzD,QAAQ,KAAK,mDAAmD,IAAI,CACrE;IACA,YAAY;;;;;sBAlCsD;uBACZ;AAevD,qBAA0D;AAGxD,eAAc"}
@@ -1,7 +1,7 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime-DNCzzKnI.js";
2
2
  import { Vr as init_paths } from "./config-yaml-DPAieL_G.js";
3
3
  import { c as resolveMemoryRoot, i as init_paths$1, l as resolveObservationsFile, p as resolveSummariesDir, r as ensureParentDir } from "./paths-TE1GOZDr.js";
4
- import { t as init_agents } from "./agents-DHvsjsuP.js";
4
+ import { t as init_agents } from "./agents-C7V4odQ6.js";
5
5
  import { i as init_settings } from "./settings-D2wt4nku.js";
6
6
  import { r as init_health, t as init_providers } from "./providers-4-xOzHQu.js";
7
7
  import { a as withMemoryFtsDatabase, i as runMemoryFtsTransaction, n as init_fts_db } from "./fts-db-DPtxOAjp.js";
@@ -221,4 +221,4 @@ var init_cli = __esmMin((() => {
221
221
  //#endregion
222
222
  export { generateDailySummary as n, init_cli as r, createResetMarker as t };
223
223
 
224
- //# sourceMappingURL=cli-CvK9xuSS.js.map
224
+ //# sourceMappingURL=cli-BgzbB6h3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-CvK9xuSS.js","names":[],"sources":["../../src/lib/memory/cli.ts"],"sourcesContent":["import { randomUUID } from 'crypto';\nimport { readdir, readFile, rename, writeFile } from 'fs/promises';\nimport { dirname, join } from 'path';\nimport type { MemoryObservation, MemoryStatus, ResetMarker } from '@overdeck/contracts';\nimport { getOverdeckHome } from '../paths.js';\nimport { runMemoryFtsTransaction, withMemoryFtsDatabase } from './fts-db.js';\nimport { resolveExtractionProviderSelection } from './providers/index.js';\nimport { getMemoryRollupPendingThreshold, loadMemorySettings } from './settings.js';\nimport {\n ensureParentDir,\n resolveMemoryRoot,\n resolveObservationsFile,\n resolvePendingDir,\n resolveSummariesDir,\n} from './paths.js';\nimport { getMemoryHealthPath, type MemoryHealthSnapshot } from './health.js';\nimport { readCurrentStatus } from './rollup.js';\nimport { getAgentStateSync } from '../agents.js';\n\nconst DEFAULT_PROJECT_ID = 'overdeck';\nconst MIN_DAILY_SUMMARY_OBSERVATIONS = 3;\nconst DAILY_SUMMARY_REGENERATION_OBSERVATIONS = 20;\n\nexport interface MemorySearchOptions {\n project?: string;\n workspace?: string;\n issue?: string;\n tag?: string;\n sibling?: boolean;\n limit?: number;\n includeArchived?: boolean;\n}\n\nexport interface MemorySearchResult {\n observation: MemoryObservation;\n score: number;\n}\n\nexport type DailySummaryStatus = 'generated' | 'insufficient-data' | 'up-to-date';\n\nexport interface DailySummaryResult {\n status: DailySummaryStatus;\n path: string;\n markdown: string;\n observationCount: number;\n previousObservationCount: number | null;\n}\n\nexport interface MemoryDoctorOptions {\n project?: string;\n now?: Date;\n}\n\nexport interface MemoryDoctorResult {\n exitCode: number;\n provider: Awaited<ReturnType<typeof resolveExtractionProviderSelection>>;\n rollupPendingThreshold: number;\n issues: Array<{\n projectId: string;\n issueId: string;\n health: MemoryHealthSnapshot;\n pendingCount: number;\n lastObservation: string | null;\n }>;\n staleActiveAgents: Array<{ agentId: string; issueId: string; lastSuccess: string | null }>;\n}\n\nexport async function searchMemory(query: string, options: MemorySearchOptions = {}): Promise<MemorySearchResult[]> {\n const projectId = options.project ?? DEFAULT_PROJECT_ID;\n const observations = await readObservationScope(projectId, options.issue, options.sibling ?? false);\n const resetMarkers = options.includeArchived ? [] : await readResetMarkers(projectId);\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const results = observations\n .filter((observation) => !options.workspace || observation.workspaceId === options.workspace)\n .filter((observation) => !options.issue || options.sibling || observation.issueId === options.issue)\n .filter((observation) => options.includeArchived || isAfterLatestResetMarker(observation, resetMarkers))\n .filter((observation) => !options.tag || observation.tags.includes(options.tag))\n .map((observation) => ({ observation, score: scoreObservation(observation, terms) }))\n .filter((result) => terms.length === 0 || result.score > 0)\n .sort((a, b) => b.score - a.score || b.observation.timestamp.localeCompare(a.observation.timestamp));\n\n return results.slice(0, options.limit ?? 20);\n}\n\nexport async function getMemoryStatus(projectId: string, issueId: string): Promise<MemoryStatus | undefined> {\n return readCurrentStatus(projectId, issueId);\n}\n\nexport async function createResetMarker(input: {\n projectId?: string;\n scope: ResetMarker['scope'];\n scopeId: string;\n reason: string;\n fromTimestamp?: string;\n id?: string;\n createdAt?: string;\n emitResetMarkerCreated?: (marker: ResetMarker, timestamp: string) => void | Promise<void>;\n}): Promise<ResetMarker> {\n const projectId = input.projectId ?? DEFAULT_PROJECT_ID;\n const marker: ResetMarker = {\n id: input.id ?? randomUUID(),\n scope: input.scope,\n scopeId: input.scopeId,\n fromTimestamp: input.fromTimestamp ?? new Date().toISOString(),\n reason: input.reason,\n createdAt: input.createdAt ?? new Date().toISOString(),\n };\n const path = join(resolveMemoryRoot(projectId), 'reset-markers.json');\n const markers = await readJsonFile<ResetMarker[]>(path, []);\n await writeJsonAtomically(path, [...markers, marker]);\n await writeResetMarkerToFtsDb(projectId, marker);\n await (input.emitResetMarkerCreated ?? emitResetMarkerCreated)(marker, marker.createdAt);\n return marker;\n}\n\nexport async function generateDailySummary(input: {\n projectId?: string;\n issueId: string;\n date?: string;\n}): Promise<DailySummaryResult> {\n const projectId = input.projectId ?? DEFAULT_PROJECT_ID;\n const date = input.date ?? new Date().toISOString().slice(0, 10);\n const path = join(resolveSummariesDir(projectId, input.issueId), `${date}.md`);\n const observations = await readObservationsFile(resolveObservationsFile(projectId, input.issueId, date));\n const existingMarkdown = await readTextFile(path);\n const previousObservationCount = existingMarkdown ? parseSummaryObservationCount(existingMarkdown) : null;\n\n if (observations.length < MIN_DAILY_SUMMARY_OBSERVATIONS) {\n return { status: 'insufficient-data', path, markdown: existingMarkdown ?? '', observationCount: observations.length, previousObservationCount };\n }\n\n if (existingMarkdown && previousObservationCount !== null && observations.length - previousObservationCount < DAILY_SUMMARY_REGENERATION_OBSERVATIONS) {\n return { status: 'up-to-date', path, markdown: existingMarkdown, observationCount: observations.length, previousObservationCount };\n }\n\n const markdown = buildDailySummaryMarkdown(input.issueId, date, observations);\n await ensureParentDir(path);\n await writeFile(path, markdown, 'utf8');\n await indexDailySummary(projectId, input.issueId, date, observations, markdown);\n return { status: 'generated', path, markdown, observationCount: observations.length, previousObservationCount };\n}\n\nexport async function runMemoryDoctor(options: MemoryDoctorOptions = {}): Promise<MemoryDoctorResult> {\n const projectId = options.project ?? DEFAULT_PROJECT_ID;\n const now = options.now ?? new Date();\n const provider = await resolveExtractionProviderSelection();\n const rollupPendingThreshold = await getMemoryRollupPendingThreshold();\n const issues = await readIssueDoctorSnapshots(projectId);\n const activeAgents = await readActiveAgents();\n const staleActiveAgents = activeAgents\n .map((agent) => {\n const issue = issues.find((candidate) => candidate.issueId === agent.issueId);\n return { agentId: agent.id, issueId: agent.issueId, lastSuccess: issue?.health.last_success ?? null };\n })\n .filter((agent) => isStaleExtraction(agent.lastSuccess, now));\n\n return {\n exitCode: staleActiveAgents.length > 0 ? 1 : 0,\n provider,\n rollupPendingThreshold,\n issues,\n staleActiveAgents,\n };\n}\n\nexport async function readMemorySettingsSummary(): Promise<{\n rollupPendingThreshold: number;\n provider: Awaited<ReturnType<typeof resolveExtractionProviderSelection>>;\n}> {\n return {\n rollupPendingThreshold: (await loadMemorySettings()).rollupPendingThreshold,\n provider: await resolveExtractionProviderSelection(),\n };\n}\n\nasync function readObservationScope(projectId: string, issueId: string | undefined, sibling: boolean): Promise<MemoryObservation[]> {\n if (issueId && !sibling) return readIssueObservations(projectId, issueId);\n const issueIds = await listIssueIds(projectId);\n const selectedIssueIds = issueId && sibling ? issueIds.filter((candidate) => candidate !== issueId) : issueIds;\n const nested = await Promise.all(selectedIssueIds.map((candidate) => readIssueObservations(projectId, candidate)));\n return nested.flat();\n}\n\nasync function readIssueObservations(projectId: string, issueId: string): Promise<MemoryObservation[]> {\n const observationsDir = dirname(resolveObservationsFile(projectId, issueId, new Date()));\n const files = (await readdir(observationsDir).catch((error: unknown) => {\n if (isEnoent(error)) return [] as string[];\n throw error;\n })).filter((file) => file.endsWith('.jsonl')).sort();\n const nested = await Promise.all(files.map((file) => readObservationsFile(join(observationsDir, file))));\n return nested.flat();\n}\n\nasync function readResetMarkers(projectId: string): Promise<ResetMarker[]> {\n const jsonMarkers = await readJsonFile<ResetMarker[]>(join(resolveMemoryRoot(projectId), 'reset-markers.json'), []);\n const dbMarkers = await withMemoryFtsDatabase(projectId, (db) => db.prepare(`\n SELECT id, scope, scope_id, from_timestamp, reason, created_at\n FROM reset_markers\n `).all<{\n id: number;\n scope: ResetMarker['scope'];\n scope_id: string;\n from_timestamp: string;\n reason: string | null;\n created_at: string;\n }>()).catch((error: unknown) => {\n if (isNoSuchTable(error)) return [];\n throw error;\n });\n return dedupeResetMarkers([\n ...jsonMarkers,\n ...dbMarkers.map((marker) => ({\n id: `db-${marker.id}`,\n scope: marker.scope,\n scopeId: marker.scope_id,\n fromTimestamp: marker.from_timestamp,\n reason: marker.reason ?? '',\n createdAt: marker.created_at,\n })),\n ]);\n}\n\nfunction dedupeResetMarkers(markers: ResetMarker[]): ResetMarker[] {\n const seen = new Set<string>();\n const deduped: ResetMarker[] = [];\n for (const marker of markers) {\n const key = [marker.scope, marker.scopeId, marker.fromTimestamp, marker.reason, marker.createdAt].join('|');\n if (seen.has(key)) continue;\n seen.add(key);\n deduped.push(marker);\n }\n return deduped;\n}\n\nfunction isAfterLatestResetMarker(observation: MemoryObservation, markers: ResetMarker[]): boolean {\n const latest = markers\n .filter((marker) => appliesToObservation(marker, observation))\n .map((marker) => marker.fromTimestamp)\n .sort()\n .at(-1);\n return !latest || observation.timestamp > latest;\n}\n\nfunction appliesToObservation(marker: ResetMarker, observation: MemoryObservation): boolean {\n switch (marker.scope) {\n case 'project':\n return marker.scopeId === observation.projectId;\n case 'workspace':\n return marker.scopeId === observation.workspaceId;\n case 'issue':\n return marker.scopeId === observation.issueId;\n case 'session':\n return marker.scopeId === observation.sessionId;\n }\n}\n\nasync function readObservationsFile(path: string): Promise<MemoryObservation[]> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch (error) {\n if (isEnoent(error)) return [];\n throw error;\n }\n return raw.split('\\n')\n .filter((line) => line.trim().length > 0)\n .map((line) => JSON.parse(line) as MemoryObservation);\n}\n\nasync function readTextFile(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf8');\n } catch (error) {\n if (isEnoent(error)) return null;\n throw error;\n }\n}\n\nfunction buildDailySummaryMarkdown(issueId: string, date: string, observations: MemoryObservation[]): string {\n const actions = observations.filter((observation) => observation.actionStatus !== null);\n return [\n `# ${issueId} memory summary — ${date}`,\n '',\n `Observations: ${observations.length}`,\n `Action updates: ${actions.length}`,\n '',\n ...observations.map((observation) => [\n `## ${observation.timestamp}`,\n '',\n observation.actionStatus ? `**Action:** ${observation.actionStatus}` : '**Action:** none',\n '',\n observation.summary,\n '',\n observation.files.length > 0 ? `Files: ${observation.files.join(', ')}` : 'Files: none',\n observation.tags.length > 0 ? `Tags: ${observation.tags.join(', ')}` : 'Tags: none',\n '',\n ].join('\\n')),\n ].join('\\n');\n}\n\nfunction parseSummaryObservationCount(markdown: string): number | null {\n const match = markdown.match(/^Observations: (\\d+)$/m);\n return match ? Number(match[1]) : null;\n}\n\nasync function indexDailySummary(projectId: string, issueId: string, date: string, observations: MemoryObservation[], markdown: string): Promise<void> {\n const latest = observations.at(-1);\n const files = [...new Set(observations.flatMap((observation) => observation.files))].join(',');\n const tags = [...new Set(['memory', 'summary', ...observations.flatMap((observation) => observation.tags)])].join(',');\n const entryTime = latest?.timestamp.slice(11) ?? '00:00:00.000Z';\n await runMemoryFtsTransaction(projectId, [\n {\n method: 'run',\n sql: `\n DELETE FROM memory_fts\n WHERE project_id = ?\n AND issue_id = ?\n AND doc_type = 'summary'\n AND entry_date = ?\n `,\n params: [projectId, issueId, date],\n },\n {\n method: 'run',\n sql: `\n INSERT INTO memory_fts (\n content,\n display_content,\n source,\n branch,\n entry_date,\n entry_time,\n entry_type,\n files,\n tags,\n doc_type,\n scope,\n project_id,\n workspace_id,\n issue_id,\n run_id,\n session_id,\n agent_role,\n agent_harness\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n params: [\n markdown,\n markdown,\n 'summary',\n latest?.gitBranch ?? '',\n date,\n entryTime,\n 'memory-summary',\n files,\n tags,\n 'summary',\n 'issue',\n projectId,\n latest?.workspaceId ?? '',\n issueId,\n latest?.runId ?? '',\n latest?.sessionId ?? '',\n latest?.agentRole ?? '',\n latest?.agentHarness ?? '',\n ],\n },\n ]);\n}\n\nasync function listIssueIds(projectId: string): Promise<string[]> {\n const root = resolveMemoryRoot(projectId);\n const entries = await readdir(root, { withFileTypes: true }).catch((error: unknown) => {\n if (isEnoent(error)) return [];\n throw error;\n });\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();\n}\n\nfunction scoreObservation(observation: MemoryObservation, terms: string[]): number {\n const haystack = [\n observation.actionStatus ?? '',\n observation.summary,\n observation.narrative,\n observation.files.join(' '),\n observation.tags.join(' '),\n ].join(' ').toLowerCase();\n return terms.reduce((score, term) => score + occurrences(haystack, term), 0);\n}\n\nfunction occurrences(value: string, term: string): number {\n let count = 0;\n let index = value.indexOf(term);\n while (index !== -1) {\n count += 1;\n index = value.indexOf(term, index + term.length);\n }\n return count;\n}\n\nasync function readIssueDoctorSnapshots(projectId: string): Promise<MemoryDoctorResult['issues']> {\n const issueIds = await listIssueIds(projectId);\n return Promise.all(issueIds.map(async (issueId) => {\n const health = await readJsonFile<MemoryHealthSnapshot>(getMemoryHealthPath({ projectId, issueId }), emptyHealth());\n const pendingCount = await countJsonFiles(resolvePendingDir(projectId, issueId));\n const observations = await readIssueObservations(projectId, issueId);\n return {\n projectId,\n issueId,\n health,\n pendingCount,\n lastObservation: observations.at(-1)?.timestamp ?? null,\n };\n }));\n}\n\nasync function readActiveAgents(): Promise<Array<{ id: string; issueId: string }>> {\n const agentsDir = join(getOverdeckHome(), 'agents');\n const entries = await readdir(agentsDir, { withFileTypes: true }).catch((error: unknown) => {\n if (isEnoent(error)) return [];\n throw error;\n });\n const agents: Array<{ id: string; issueId: string }> = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const state = getAgentStateSync(entry.name);\n if ((state?.status === 'running' || state?.status === 'starting') && state?.issueId) {\n agents.push({ id: entry.name, issueId: state.issueId });\n }\n }\n return agents;\n}\n\nasync function countJsonFiles(path: string): Promise<number> {\n const entries = await readdir(path).catch((error: unknown) => {\n if (isEnoent(error)) return [] as string[];\n throw error;\n });\n return entries.filter((entry) => entry.endsWith('.json') && !entry.startsWith('.')).length;\n}\n\nasync function readJsonFile<T>(path: string, fallback: T): Promise<T> {\n try {\n return JSON.parse(await readFile(path, 'utf8')) as T;\n } catch (error) {\n if (isEnoent(error)) return fallback;\n throw error;\n }\n}\n\nasync function writeJsonAtomically(path: string, value: unknown): Promise<void> {\n await ensureParentDir(path);\n const tempPath = `${dirname(path)}/.${randomUUID()}.tmp`;\n await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\\n`, 'utf8');\n await rename(tempPath, path);\n}\n\nasync function writeResetMarkerToFtsDb(projectId: string, marker: ResetMarker): Promise<void> {\n await withMemoryFtsDatabase(projectId, async (db) => {\n await db.prepare(`\n INSERT INTO reset_markers (scope, scope_id, from_timestamp, reason, created_at)\n VALUES (?, ?, ?, ?, ?)\n `).run(marker.scope, marker.scopeId, marker.fromTimestamp, marker.reason, marker.createdAt);\n });\n}\n\nasync function emitResetMarkerCreated(marker: ResetMarker, timestamp: string): Promise<void> {\n const { initEventStore } = await import('../../dashboard/server/event-store.js');\n const store = await initEventStore();\n await store.appendAsync({\n type: 'memory.reset_marker_created',\n timestamp,\n payload: { marker },\n });\n}\n\nfunction isStaleExtraction(lastSuccess: string | null, now: Date): boolean {\n if (!lastSuccess) return true;\n const timestamp = Date.parse(lastSuccess);\n return Number.isNaN(timestamp) || now.getTime() - timestamp > 60 * 60 * 1000;\n}\n\nfunction emptyHealth(): MemoryHealthSnapshot {\n return {\n status: 'healthy',\n last_success: null,\n last_failure: null,\n extractions_attempted: 0,\n extractions_succeeded: 0,\n failed_by_reason: {},\n };\n}\n\nfunction isEnoent(error: unknown): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n\nfunction isNoSuchTable(error: unknown): boolean {\n return error instanceof Error && error.message.includes('no such table: reset_markers');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-BgzbB6h3.js","names":[],"sources":["../../src/lib/memory/cli.ts"],"sourcesContent":["import { randomUUID } from 'crypto';\nimport { readdir, readFile, rename, writeFile } from 'fs/promises';\nimport { dirname, join } from 'path';\nimport type { MemoryObservation, MemoryStatus, ResetMarker } from '@overdeck/contracts';\nimport { getOverdeckHome } from '../paths.js';\nimport { runMemoryFtsTransaction, withMemoryFtsDatabase } from './fts-db.js';\nimport { resolveExtractionProviderSelection } from './providers/index.js';\nimport { getMemoryRollupPendingThreshold, loadMemorySettings } from './settings.js';\nimport {\n ensureParentDir,\n resolveMemoryRoot,\n resolveObservationsFile,\n resolvePendingDir,\n resolveSummariesDir,\n} from './paths.js';\nimport { getMemoryHealthPath, type MemoryHealthSnapshot } from './health.js';\nimport { readCurrentStatus } from './rollup.js';\nimport { getAgentStateSync } from '../agents.js';\n\nconst DEFAULT_PROJECT_ID = 'overdeck';\nconst MIN_DAILY_SUMMARY_OBSERVATIONS = 3;\nconst DAILY_SUMMARY_REGENERATION_OBSERVATIONS = 20;\n\nexport interface MemorySearchOptions {\n project?: string;\n workspace?: string;\n issue?: string;\n tag?: string;\n sibling?: boolean;\n limit?: number;\n includeArchived?: boolean;\n}\n\nexport interface MemorySearchResult {\n observation: MemoryObservation;\n score: number;\n}\n\nexport type DailySummaryStatus = 'generated' | 'insufficient-data' | 'up-to-date';\n\nexport interface DailySummaryResult {\n status: DailySummaryStatus;\n path: string;\n markdown: string;\n observationCount: number;\n previousObservationCount: number | null;\n}\n\nexport interface MemoryDoctorOptions {\n project?: string;\n now?: Date;\n}\n\nexport interface MemoryDoctorResult {\n exitCode: number;\n provider: Awaited<ReturnType<typeof resolveExtractionProviderSelection>>;\n rollupPendingThreshold: number;\n issues: Array<{\n projectId: string;\n issueId: string;\n health: MemoryHealthSnapshot;\n pendingCount: number;\n lastObservation: string | null;\n }>;\n staleActiveAgents: Array<{ agentId: string; issueId: string; lastSuccess: string | null }>;\n}\n\nexport async function searchMemory(query: string, options: MemorySearchOptions = {}): Promise<MemorySearchResult[]> {\n const projectId = options.project ?? DEFAULT_PROJECT_ID;\n const observations = await readObservationScope(projectId, options.issue, options.sibling ?? false);\n const resetMarkers = options.includeArchived ? [] : await readResetMarkers(projectId);\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const results = observations\n .filter((observation) => !options.workspace || observation.workspaceId === options.workspace)\n .filter((observation) => !options.issue || options.sibling || observation.issueId === options.issue)\n .filter((observation) => options.includeArchived || isAfterLatestResetMarker(observation, resetMarkers))\n .filter((observation) => !options.tag || observation.tags.includes(options.tag))\n .map((observation) => ({ observation, score: scoreObservation(observation, terms) }))\n .filter((result) => terms.length === 0 || result.score > 0)\n .sort((a, b) => b.score - a.score || b.observation.timestamp.localeCompare(a.observation.timestamp));\n\n return results.slice(0, options.limit ?? 20);\n}\n\nexport async function getMemoryStatus(projectId: string, issueId: string): Promise<MemoryStatus | undefined> {\n return readCurrentStatus(projectId, issueId);\n}\n\nexport async function createResetMarker(input: {\n projectId?: string;\n scope: ResetMarker['scope'];\n scopeId: string;\n reason: string;\n fromTimestamp?: string;\n id?: string;\n createdAt?: string;\n emitResetMarkerCreated?: (marker: ResetMarker, timestamp: string) => void | Promise<void>;\n}): Promise<ResetMarker> {\n const projectId = input.projectId ?? DEFAULT_PROJECT_ID;\n const marker: ResetMarker = {\n id: input.id ?? randomUUID(),\n scope: input.scope,\n scopeId: input.scopeId,\n fromTimestamp: input.fromTimestamp ?? new Date().toISOString(),\n reason: input.reason,\n createdAt: input.createdAt ?? new Date().toISOString(),\n };\n const path = join(resolveMemoryRoot(projectId), 'reset-markers.json');\n const markers = await readJsonFile<ResetMarker[]>(path, []);\n await writeJsonAtomically(path, [...markers, marker]);\n await writeResetMarkerToFtsDb(projectId, marker);\n await (input.emitResetMarkerCreated ?? emitResetMarkerCreated)(marker, marker.createdAt);\n return marker;\n}\n\nexport async function generateDailySummary(input: {\n projectId?: string;\n issueId: string;\n date?: string;\n}): Promise<DailySummaryResult> {\n const projectId = input.projectId ?? DEFAULT_PROJECT_ID;\n const date = input.date ?? new Date().toISOString().slice(0, 10);\n const path = join(resolveSummariesDir(projectId, input.issueId), `${date}.md`);\n const observations = await readObservationsFile(resolveObservationsFile(projectId, input.issueId, date));\n const existingMarkdown = await readTextFile(path);\n const previousObservationCount = existingMarkdown ? parseSummaryObservationCount(existingMarkdown) : null;\n\n if (observations.length < MIN_DAILY_SUMMARY_OBSERVATIONS) {\n return { status: 'insufficient-data', path, markdown: existingMarkdown ?? '', observationCount: observations.length, previousObservationCount };\n }\n\n if (existingMarkdown && previousObservationCount !== null && observations.length - previousObservationCount < DAILY_SUMMARY_REGENERATION_OBSERVATIONS) {\n return { status: 'up-to-date', path, markdown: existingMarkdown, observationCount: observations.length, previousObservationCount };\n }\n\n const markdown = buildDailySummaryMarkdown(input.issueId, date, observations);\n await ensureParentDir(path);\n await writeFile(path, markdown, 'utf8');\n await indexDailySummary(projectId, input.issueId, date, observations, markdown);\n return { status: 'generated', path, markdown, observationCount: observations.length, previousObservationCount };\n}\n\nexport async function runMemoryDoctor(options: MemoryDoctorOptions = {}): Promise<MemoryDoctorResult> {\n const projectId = options.project ?? DEFAULT_PROJECT_ID;\n const now = options.now ?? new Date();\n const provider = await resolveExtractionProviderSelection();\n const rollupPendingThreshold = await getMemoryRollupPendingThreshold();\n const issues = await readIssueDoctorSnapshots(projectId);\n const activeAgents = await readActiveAgents();\n const staleActiveAgents = activeAgents\n .map((agent) => {\n const issue = issues.find((candidate) => candidate.issueId === agent.issueId);\n return { agentId: agent.id, issueId: agent.issueId, lastSuccess: issue?.health.last_success ?? null };\n })\n .filter((agent) => isStaleExtraction(agent.lastSuccess, now));\n\n return {\n exitCode: staleActiveAgents.length > 0 ? 1 : 0,\n provider,\n rollupPendingThreshold,\n issues,\n staleActiveAgents,\n };\n}\n\nexport async function readMemorySettingsSummary(): Promise<{\n rollupPendingThreshold: number;\n provider: Awaited<ReturnType<typeof resolveExtractionProviderSelection>>;\n}> {\n return {\n rollupPendingThreshold: (await loadMemorySettings()).rollupPendingThreshold,\n provider: await resolveExtractionProviderSelection(),\n };\n}\n\nasync function readObservationScope(projectId: string, issueId: string | undefined, sibling: boolean): Promise<MemoryObservation[]> {\n if (issueId && !sibling) return readIssueObservations(projectId, issueId);\n const issueIds = await listIssueIds(projectId);\n const selectedIssueIds = issueId && sibling ? issueIds.filter((candidate) => candidate !== issueId) : issueIds;\n const nested = await Promise.all(selectedIssueIds.map((candidate) => readIssueObservations(projectId, candidate)));\n return nested.flat();\n}\n\nasync function readIssueObservations(projectId: string, issueId: string): Promise<MemoryObservation[]> {\n const observationsDir = dirname(resolveObservationsFile(projectId, issueId, new Date()));\n const files = (await readdir(observationsDir).catch((error: unknown) => {\n if (isEnoent(error)) return [] as string[];\n throw error;\n })).filter((file) => file.endsWith('.jsonl')).sort();\n const nested = await Promise.all(files.map((file) => readObservationsFile(join(observationsDir, file))));\n return nested.flat();\n}\n\nasync function readResetMarkers(projectId: string): Promise<ResetMarker[]> {\n const jsonMarkers = await readJsonFile<ResetMarker[]>(join(resolveMemoryRoot(projectId), 'reset-markers.json'), []);\n const dbMarkers = await withMemoryFtsDatabase(projectId, (db) => db.prepare(`\n SELECT id, scope, scope_id, from_timestamp, reason, created_at\n FROM reset_markers\n `).all<{\n id: number;\n scope: ResetMarker['scope'];\n scope_id: string;\n from_timestamp: string;\n reason: string | null;\n created_at: string;\n }>()).catch((error: unknown) => {\n if (isNoSuchTable(error)) return [];\n throw error;\n });\n return dedupeResetMarkers([\n ...jsonMarkers,\n ...dbMarkers.map((marker) => ({\n id: `db-${marker.id}`,\n scope: marker.scope,\n scopeId: marker.scope_id,\n fromTimestamp: marker.from_timestamp,\n reason: marker.reason ?? '',\n createdAt: marker.created_at,\n })),\n ]);\n}\n\nfunction dedupeResetMarkers(markers: ResetMarker[]): ResetMarker[] {\n const seen = new Set<string>();\n const deduped: ResetMarker[] = [];\n for (const marker of markers) {\n const key = [marker.scope, marker.scopeId, marker.fromTimestamp, marker.reason, marker.createdAt].join('|');\n if (seen.has(key)) continue;\n seen.add(key);\n deduped.push(marker);\n }\n return deduped;\n}\n\nfunction isAfterLatestResetMarker(observation: MemoryObservation, markers: ResetMarker[]): boolean {\n const latest = markers\n .filter((marker) => appliesToObservation(marker, observation))\n .map((marker) => marker.fromTimestamp)\n .sort()\n .at(-1);\n return !latest || observation.timestamp > latest;\n}\n\nfunction appliesToObservation(marker: ResetMarker, observation: MemoryObservation): boolean {\n switch (marker.scope) {\n case 'project':\n return marker.scopeId === observation.projectId;\n case 'workspace':\n return marker.scopeId === observation.workspaceId;\n case 'issue':\n return marker.scopeId === observation.issueId;\n case 'session':\n return marker.scopeId === observation.sessionId;\n }\n}\n\nasync function readObservationsFile(path: string): Promise<MemoryObservation[]> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch (error) {\n if (isEnoent(error)) return [];\n throw error;\n }\n return raw.split('\\n')\n .filter((line) => line.trim().length > 0)\n .map((line) => JSON.parse(line) as MemoryObservation);\n}\n\nasync function readTextFile(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf8');\n } catch (error) {\n if (isEnoent(error)) return null;\n throw error;\n }\n}\n\nfunction buildDailySummaryMarkdown(issueId: string, date: string, observations: MemoryObservation[]): string {\n const actions = observations.filter((observation) => observation.actionStatus !== null);\n return [\n `# ${issueId} memory summary — ${date}`,\n '',\n `Observations: ${observations.length}`,\n `Action updates: ${actions.length}`,\n '',\n ...observations.map((observation) => [\n `## ${observation.timestamp}`,\n '',\n observation.actionStatus ? `**Action:** ${observation.actionStatus}` : '**Action:** none',\n '',\n observation.summary,\n '',\n observation.files.length > 0 ? `Files: ${observation.files.join(', ')}` : 'Files: none',\n observation.tags.length > 0 ? `Tags: ${observation.tags.join(', ')}` : 'Tags: none',\n '',\n ].join('\\n')),\n ].join('\\n');\n}\n\nfunction parseSummaryObservationCount(markdown: string): number | null {\n const match = markdown.match(/^Observations: (\\d+)$/m);\n return match ? Number(match[1]) : null;\n}\n\nasync function indexDailySummary(projectId: string, issueId: string, date: string, observations: MemoryObservation[], markdown: string): Promise<void> {\n const latest = observations.at(-1);\n const files = [...new Set(observations.flatMap((observation) => observation.files))].join(',');\n const tags = [...new Set(['memory', 'summary', ...observations.flatMap((observation) => observation.tags)])].join(',');\n const entryTime = latest?.timestamp.slice(11) ?? '00:00:00.000Z';\n await runMemoryFtsTransaction(projectId, [\n {\n method: 'run',\n sql: `\n DELETE FROM memory_fts\n WHERE project_id = ?\n AND issue_id = ?\n AND doc_type = 'summary'\n AND entry_date = ?\n `,\n params: [projectId, issueId, date],\n },\n {\n method: 'run',\n sql: `\n INSERT INTO memory_fts (\n content,\n display_content,\n source,\n branch,\n entry_date,\n entry_time,\n entry_type,\n files,\n tags,\n doc_type,\n scope,\n project_id,\n workspace_id,\n issue_id,\n run_id,\n session_id,\n agent_role,\n agent_harness\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n params: [\n markdown,\n markdown,\n 'summary',\n latest?.gitBranch ?? '',\n date,\n entryTime,\n 'memory-summary',\n files,\n tags,\n 'summary',\n 'issue',\n projectId,\n latest?.workspaceId ?? '',\n issueId,\n latest?.runId ?? '',\n latest?.sessionId ?? '',\n latest?.agentRole ?? '',\n latest?.agentHarness ?? '',\n ],\n },\n ]);\n}\n\nasync function listIssueIds(projectId: string): Promise<string[]> {\n const root = resolveMemoryRoot(projectId);\n const entries = await readdir(root, { withFileTypes: true }).catch((error: unknown) => {\n if (isEnoent(error)) return [];\n throw error;\n });\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();\n}\n\nfunction scoreObservation(observation: MemoryObservation, terms: string[]): number {\n const haystack = [\n observation.actionStatus ?? '',\n observation.summary,\n observation.narrative,\n observation.files.join(' '),\n observation.tags.join(' '),\n ].join(' ').toLowerCase();\n return terms.reduce((score, term) => score + occurrences(haystack, term), 0);\n}\n\nfunction occurrences(value: string, term: string): number {\n let count = 0;\n let index = value.indexOf(term);\n while (index !== -1) {\n count += 1;\n index = value.indexOf(term, index + term.length);\n }\n return count;\n}\n\nasync function readIssueDoctorSnapshots(projectId: string): Promise<MemoryDoctorResult['issues']> {\n const issueIds = await listIssueIds(projectId);\n return Promise.all(issueIds.map(async (issueId) => {\n const health = await readJsonFile<MemoryHealthSnapshot>(getMemoryHealthPath({ projectId, issueId }), emptyHealth());\n const pendingCount = await countJsonFiles(resolvePendingDir(projectId, issueId));\n const observations = await readIssueObservations(projectId, issueId);\n return {\n projectId,\n issueId,\n health,\n pendingCount,\n lastObservation: observations.at(-1)?.timestamp ?? null,\n };\n }));\n}\n\nasync function readActiveAgents(): Promise<Array<{ id: string; issueId: string }>> {\n const agentsDir = join(getOverdeckHome(), 'agents');\n const entries = await readdir(agentsDir, { withFileTypes: true }).catch((error: unknown) => {\n if (isEnoent(error)) return [];\n throw error;\n });\n const agents: Array<{ id: string; issueId: string }> = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const state = getAgentStateSync(entry.name);\n if ((state?.status === 'running' || state?.status === 'starting') && state?.issueId) {\n agents.push({ id: entry.name, issueId: state.issueId });\n }\n }\n return agents;\n}\n\nasync function countJsonFiles(path: string): Promise<number> {\n const entries = await readdir(path).catch((error: unknown) => {\n if (isEnoent(error)) return [] as string[];\n throw error;\n });\n return entries.filter((entry) => entry.endsWith('.json') && !entry.startsWith('.')).length;\n}\n\nasync function readJsonFile<T>(path: string, fallback: T): Promise<T> {\n try {\n return JSON.parse(await readFile(path, 'utf8')) as T;\n } catch (error) {\n if (isEnoent(error)) return fallback;\n throw error;\n }\n}\n\nasync function writeJsonAtomically(path: string, value: unknown): Promise<void> {\n await ensureParentDir(path);\n const tempPath = `${dirname(path)}/.${randomUUID()}.tmp`;\n await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\\n`, 'utf8');\n await rename(tempPath, path);\n}\n\nasync function writeResetMarkerToFtsDb(projectId: string, marker: ResetMarker): Promise<void> {\n await withMemoryFtsDatabase(projectId, async (db) => {\n await db.prepare(`\n INSERT INTO reset_markers (scope, scope_id, from_timestamp, reason, created_at)\n VALUES (?, ?, ?, ?, ?)\n `).run(marker.scope, marker.scopeId, marker.fromTimestamp, marker.reason, marker.createdAt);\n });\n}\n\nasync function emitResetMarkerCreated(marker: ResetMarker, timestamp: string): Promise<void> {\n const { initEventStore } = await import('../../dashboard/server/event-store.js');\n const store = await initEventStore();\n await store.appendAsync({\n type: 'memory.reset_marker_created',\n timestamp,\n payload: { marker },\n });\n}\n\nfunction isStaleExtraction(lastSuccess: string | null, now: Date): boolean {\n if (!lastSuccess) return true;\n const timestamp = Date.parse(lastSuccess);\n return Number.isNaN(timestamp) || now.getTime() - timestamp > 60 * 60 * 1000;\n}\n\nfunction emptyHealth(): MemoryHealthSnapshot {\n return {\n status: 'healthy',\n last_success: null,\n last_failure: null,\n extractions_attempted: 0,\n extractions_succeeded: 0,\n failed_by_reason: {},\n };\n}\n\nfunction isEnoent(error: unknown): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n\nfunction isNoSuchTable(error: unknown): boolean {\n return error instanceof Error && error.message.includes('no such table: reset_markers');\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { r as init_cli, t as createResetMarker } from "./cli-BgzbB6h3.js";
2
+ init_cli();
3
+ export { createResetMarker };
@@ -1,5 +1,5 @@
1
1
  import { $n as extractPrefixSync, Qn as extractNumberSync, er as init_issue_id, tr as normalizeIssueIdSync } from "./config-yaml-DPAieL_G.js";
2
- import { Mn as getAgentState, Vn as markAgentStoppedState, qn as saveAgentState, t as init_agents } from "./agents-DHvsjsuP.js";
2
+ import { Mn as getAgentState, Vn as markAgentStoppedState, qn as saveAgentState, t as init_agents } from "./agents-C7V4odQ6.js";
3
3
  import { a as stepSkipped, i as stepOk, n as init_types, r as stepFailed, t as getLinearApiKey } from "./types-B5u3NxMl.js";
4
4
  import { Effect } from "effect";
5
5
  import { exec } from "child_process";
@@ -327,4 +327,4 @@ async function applyLabelLinearImpl(ctx, apiKey) {
327
327
  //#endregion
328
328
  export { closeIssue as t };
329
329
 
330
- //# sourceMappingURL=close-issue-BnpErnD1.js.map
330
+ //# sourceMappingURL=close-issue-BWf3pxn4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"close-issue-BnpErnD1.js","names":[],"sources":["../../src/lib/lifecycle/close-issue.ts"],"sourcesContent":["/**\n * close-issue — Transition issue to closed/done state + label management.\n *\n * Uses the IssueTracker abstraction when available, with fallback to\n * direct API calls for contexts where the tracker isn't set up (e.g.,\n * standalone CLI).\n *\n * Operations:\n * 1. Transition issue to closed state\n * 2. Add 'closed-out' label\n * 3. Remove all current-phase workflow labels (WORKFLOW_LABELS)\n * 4. Add completion comment\n */\n\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport type { IssueTracker } from '../tracker/interface.js';\nimport type { LifecycleContext, StepResult } from './types.js';\nimport { stepOk, stepSkipped, stepFailed, getLinearApiKey } from './types.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from '../issue-id.js';\nimport { getAgentState, markAgentStoppedState, saveAgentState } from '../agents.js';\n\nconst execAsync = promisify(exec);\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';\n/**\n * Canonical set of current-phase pipeline labels. A closed/done issue is in no\n * phase, so close paths strip ALL of these. Single source of truth — close-out\n * imports this rather than keeping its own (previously divergent, missing\n * `planned`) list, which had left ~82 closed issues falsely labelled in-review/\n * in-progress/planned.\n */\nexport const WORKFLOW_LABELS = [\n 'in-progress', 'in-review', 'planned', 'in-planning',\n 'needs-close-out', 'verifying-on-main', 'ready-for-merge',\n];\n\nexport const POST_MERGE_RESIDUE_LABELS = ['merged', 'ready'];\nconst CLOSE_OUT_LABELS_TO_REMOVE = [...WORKFLOW_LABELS, ...POST_MERGE_RESIDUE_LABELS];\n\n/** Options for close-issue */\nexport interface CloseIssueOptions {\n /** IssueTracker instance (preferred — uses abstraction layer) */\n tracker?: IssueTracker;\n /** Reason for closing */\n reason?: string;\n /** Comment to add when closing */\n comment?: string;\n /** Apply the closed-out label. Default: true */\n applyLabel?: boolean;\n /** Only apply label (skip state transition). Default: false */\n labelOnly?: boolean;\n}\n\n/**\n * Close an issue and manage labels.\n *\n * If a tracker is provided, uses the abstraction layer.\n * Otherwise, falls back to direct gh CLI (GitHub) or Linear SDK calls.\n */\nasync function markWorkAgentStoppedForIssue(issueId: string): Promise<void> {\n const agentId = `agent-${normalizeIssueIdSync(issueId)}`;\n const state = await Effect.runPromise(getAgentState(agentId));\n if (!state) return;\n markAgentStoppedState(state);\n await Effect.runPromise(saveAgentState(state));\n}\n\nexport function closeIssue(\n ctx: LifecycleContext,\n opts: CloseIssueOptions = {},\n): Effect.Effect<StepResult[]> {\n return Effect.gen(function* () {\n const results: StepResult[] = [];\n const { applyLabel = true, labelOnly = false, comment } = opts;\n\n // Step 1: Transition to closed (unless labelOnly)\n if (!labelOnly) {\n const closeResult = opts.tracker\n ? yield* closeViaTracker(ctx, opts.tracker, comment)\n : yield* closeViaDirect(ctx, comment);\n results.push(closeResult);\n\n // If close failed, don't bother with labels\n if (!closeResult.success && !closeResult.skipped) {\n return results;\n }\n }\n\n // Step 2: Close any open PR for the feature branch (GitHub only)\n if (ctx.github) {\n const prResult = yield* closeGitHubPr(ctx);\n results.push(prResult);\n }\n\n // Step 3: Apply closed-out label + remove workflow labels\n if (applyLabel) {\n const labelResult = yield* applyClosedOutLabel(ctx, opts.tracker);\n results.push(labelResult);\n }\n\n return results;\n });\n}\n\n/**\n * Close via IssueTracker abstraction.\n */\nfunction closeViaTracker(\n ctx: LifecycleContext,\n tracker: IssueTracker,\n comment?: string,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:transition';\n return Effect.gen(function* () {\n yield* tracker.transitionIssue(ctx.issueId, 'closed');\n yield* Effect.promise(() => markWorkAgentStoppedForIssue(ctx.issueId));\n if (comment) {\n // Best-effort comment — swallow errors\n yield* tracker.addComment(ctx.issueId, comment).pipe(\n Effect.catch(() => Effect.void),\n );\n }\n return stepOk(step, [`Closed ${ctx.issueId} via ${tracker.name} tracker`]);\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed(step, `Failed to close via tracker: ${(err as Error).message ?? String(err)}`)),\n ),\n );\n}\n\n/**\n * Close via direct API calls (fallback when no tracker configured).\n * Determines issue type from context and uses appropriate method.\n */\nfunction closeViaDirect(\n ctx: LifecycleContext,\n comment?: string,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:transition';\n\n if (ctx.github) {\n return closeGitHubDirect(ctx, comment);\n }\n\n // Rally issue\n if (ctx.rally) {\n return closeRallyDirect(ctx);\n }\n\n // Try Linear\n return Effect.gen(function* () {\n const linearApiKey = yield* Effect.promise(() => getLinearApiKey());\n if (linearApiKey) {\n return yield* closeLinearDirect(ctx, linearApiKey);\n }\n return stepFailed(step, 'No tracker available and cannot determine issue type');\n });\n}\n\n/**\n * Close a GitHub issue via gh CLI.\n */\nfunction closeGitHubDirect(ctx: LifecycleContext, comment?: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeGitHubDirectImpl(ctx, comment),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `gh issue close failed: ${(err as Error).message}`)),\n ),\n );\n}\n\nasync function closeGitHubDirectImpl(ctx: LifecycleContext, comment?: string): Promise<StepResult> {\n const step = 'close-issue:transition';\n if (!ctx.github) {\n return stepFailed(step, 'GitHub config not provided');\n }\n const { owner, repo, number } = ctx.github;\n\n // Refuse to close a GitHub issue when its feature branch still has an open,\n // unmerged PR. PAN-1030 closed the issue while PR #1046 was open with 1152\n // lines of real changes — the close-out path trusted the operator without\n // verifying the merge. Fail loudly so the operator notices and either\n // merges the PR first or tells us why the issue should still close.\n const branchName = `feature/${ctx.issueId.toLowerCase()}`;\n try {\n const { stdout: prRaw } = await execAsync(\n `gh pr list --repo ${owner}/${repo} --head \"${branchName}\" --state open --json number,mergedAt,mergeCommit --jq '.[0]'`,\n { encoding: 'utf-8' },\n );\n const trimmed = prRaw.trim();\n if (trimmed) {\n try {\n const pr = JSON.parse(trimmed) as { number?: number; mergedAt?: string | null; mergeCommit?: unknown };\n if (pr.number && !pr.mergedAt && !pr.mergeCommit) {\n return stepFailed(\n step,\n `Refusing to close issue #${number}: open PR #${pr.number} on ${branchName} has not been merged. Merge or close the PR first, or rename the branch if it is unrelated.`,\n );\n }\n } catch {\n // PR JSON unparseable — continue with the close (don't block on parser error).\n }\n }\n } catch {\n // gh query failed — don't block close-out on a flaky network call.\n }\n\n try {\n const commentArg = comment ? ` --comment \"${comment.replace(/\"/g, '\\\\\"')}\"` : '';\n await execAsync(\n `gh issue close ${number} --repo ${owner}/${repo}${commentArg}`,\n { encoding: 'utf-8' },\n );\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Closed GitHub issue #${number} on ${owner}/${repo}`]);\n } catch (err) {\n return stepFailed(step, `gh issue close failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Close any open GitHub PR for the feature branch.\n */\nfunction closeGitHubPr(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeGitHubPrImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:close-pr', [`PR close failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function closeGitHubPrImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:close-pr';\n if (!ctx.github) {\n return stepSkipped(step, ['Not a GitHub issue']);\n }\n const { owner, repo } = ctx.github;\n const issueLower = ctx.issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n // Pull number + merge status. Closing without checking can land us in the\n // PAN-1030 state: issue closed + PR left OPEN with real unmerged code,\n // or worse — close a not-yet-merged PR with a \"Merged via Overdeck\n // lifecycle\" comment that lies about what happened.\n const { stdout: prListRaw } = await execAsync(\n `gh pr list --repo ${owner}/${repo} --head \"${branchName}\" --state open --json number,mergedAt,mergeCommit --jq '.[0]'`,\n { encoding: 'utf-8' },\n );\n const prRaw = prListRaw.trim();\n if (!prRaw) {\n return stepSkipped(step, ['No open PR found for branch']);\n }\n let pr: { number?: number; mergedAt?: string | null; mergeCommit?: unknown };\n try {\n pr = JSON.parse(prRaw);\n } catch {\n return stepSkipped(step, [`PR JSON parse failed: ${prRaw.slice(0, 80)}`]);\n }\n if (!pr.number) {\n return stepSkipped(step, ['Open PR query returned no number']);\n }\n if (!pr.mergedAt && !pr.mergeCommit) {\n // PR is open but NOT merged — refuse to close. The close-out flow must\n // not pretend an unmerged PR was merged. Fail loudly so the operator\n // notices the inconsistency rather than the PR silently disappearing.\n return stepFailed(\n step,\n `Refusing to close PR #${pr.number} on ${owner}/${repo}: PR is not merged. Investigate why close-out fired without a merge.`,\n );\n }\n await execAsync(\n `gh pr close ${pr.number} --repo ${owner}/${repo} --comment \"Merged via Overdeck lifecycle\"`,\n { encoding: 'utf-8' },\n );\n return stepOk(step, [`Closed PR #${pr.number} on ${owner}/${repo}`]);\n } catch (err) {\n return stepSkipped(step, [`PR close failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n\n/**\n * Rate limit circuit breaker for Linear API.\n * After hitting a rate limit, stop all Linear API calls for COOLDOWN_MS.\n * This prevents the 24,626-call storm that exhausted Linear's 5000 req/hr limit (PAN-328).\n */\nlet _linearRateLimitUntil = 0;\nconst LINEAR_RATE_LIMIT_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour (matches Linear's 5000/hr window)\n\n/**\n * Close a Linear issue via SDK (find by identifier, transition to Done).\n */\nfunction closeLinearDirect(ctx: LifecycleContext, apiKey: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeLinearDirectImpl(ctx, apiKey),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `Linear close failed: ${(err as Error).message}`)),\n ),\n );\n}\n\nasync function closeLinearDirectImpl(ctx: LifecycleContext, apiKey: string): Promise<StepResult> {\n const step = 'close-issue:transition';\n\n // Circuit breaker: if we recently hit a rate limit, fail fast without making API calls\n if (Date.now() < _linearRateLimitUntil) {\n const remainingMin = Math.ceil((_linearRateLimitUntil - Date.now()) / 60000);\n return stepFailed(step, `Linear rate limit cooldown active (${remainingMin}min remaining). Issue will be closed during close-out ceremony.`);\n }\n\n try {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey });\n\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n return stepFailed(step, `Could not parse issue ID: ${ctx.issueId}`);\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n\n if (results.nodes.length === 0) {\n return stepFailed(step, `Issue ${ctx.issueId} not found in Linear`);\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Moved Linear issue ${ctx.issueId} to Done`]);\n } catch (err) {\n const message = (err as Error).message;\n\n // Detect rate limit errors and activate circuit breaker\n if (message.includes('Rate limit') || message.includes('rate limit') || message.includes('429')) {\n _linearRateLimitUntil = Date.now() + LINEAR_RATE_LIMIT_COOLDOWN_MS;\n console.warn(`[close-issue] Linear rate limit hit — circuit breaker activated for 1 hour`);\n return stepFailed(step, `Linear rate limit exceeded. Circuit breaker activated — no Linear API calls for 1 hour. Issue will be closed during close-out ceremony.`);\n }\n\n return stepFailed(step, `Linear close failed: ${message}`);\n }\n}\n\n/**\n * Close a Rally issue via RallyTracker.\n */\nfunction closeRallyDirect(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeRallyDirectImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `Rally close failed: ${(err as Error).message ?? String(err)}`)),\n ),\n );\n}\n\nasync function closeRallyDirectImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:transition';\n if (!ctx.rally) {\n return stepFailed(step, 'Rally config not provided');\n }\n const { RallyTracker } = await import('../tracker/rally.js');\n const tracker = new RallyTracker({\n apiKey: ctx.rally.apiKey,\n server: ctx.rally.server,\n workspace: ctx.rally.workspace,\n project: ctx.rally.project,\n });\n // RallyTracker.transitionIssue returns Effect (migrated in PAN-1249).\n await Effect.runPromise(tracker.transitionIssue(ctx.issueId, 'closed'));\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Closed Rally issue ${ctx.issueId}`]);\n}\n\n/**\n * Apply 'closed-out' label and remove workflow labels.\n * Uses tracker if available, falls back to direct calls.\n */\nfunction applyClosedOutLabel(\n ctx: LifecycleContext,\n tracker?: IssueTracker,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:label';\n\n if (tracker) {\n return applyLabelViaTracker(ctx, tracker);\n }\n\n if (ctx.github) {\n return applyLabelGitHub(ctx);\n }\n\n return Effect.gen(function* () {\n const linearApiKey = yield* Effect.promise(() => getLinearApiKey());\n if (linearApiKey) {\n return yield* applyLabelLinear(ctx, linearApiKey);\n }\n return stepSkipped(step, ['No tracker available for label management']);\n });\n}\n\nfunction applyLabelViaTracker(\n ctx: LifecycleContext,\n tracker: IssueTracker,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:label';\n return Effect.gen(function* () {\n const issue = yield* tracker.getIssue(ctx.issueId);\n const newLabels = issue.labels.filter((l: string) => !CLOSE_OUT_LABELS_TO_REMOVE.includes(l));\n if (!newLabels.includes(CLOSED_OUT_LABEL)) {\n newLabels.push(CLOSED_OUT_LABEL);\n }\n yield* tracker.updateIssue(ctx.issueId, { labels: newLabels });\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label via ${tracker.name} tracker`]);\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped(step, [`Label management failed (non-fatal): ${(err as Error).message ?? String(err)}`])),\n ),\n );\n}\n\nfunction applyLabelGitHub(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => applyLabelGitHubImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:label', [`Label management failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function applyLabelGitHubImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:label';\n if (!ctx.github) return stepSkipped(step);\n const { owner, repo, number } = ctx.github;\n\n try {\n // Ensure label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${owner}/${repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' },\n );\n // Only remove labels actually on the issue — `gh issue edit` fails the\n // whole edit if any --remove-label names a label missing from the repo.\n const { stdout: labelsJson } = await execAsync(\n `gh issue view ${number} --repo ${owner}/${repo} --json labels --jq '[.labels[].name]'`,\n { encoding: 'utf-8' },\n );\n const currentLabels: string[] = JSON.parse(labelsJson.trim() || '[]');\n const removeLabelArgs = CLOSE_OUT_LABELS_TO_REMOVE\n .filter(label => currentLabels.includes(label))\n .map(label => `--remove-label \"${label}\"`)\n .join(' ');\n await execAsync(\n `gh issue edit ${number} --repo ${owner}/${repo} --add-label \"${CLOSED_OUT_LABEL}\" ${removeLabelArgs}`.trim(),\n { encoding: 'utf-8' },\n );\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label on GitHub`]);\n } catch (err) {\n return stepSkipped(step, [`Label management failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n\nfunction applyLabelLinear(ctx: LifecycleContext, apiKey: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => applyLabelLinearImpl(ctx, apiKey),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:label', [`Linear label management failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function applyLabelLinearImpl(ctx: LifecycleContext, apiKey: string): Promise<StepResult> {\n const step = 'close-issue:label';\n try {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey });\n\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum === null || teamKey === null) {\n return stepFailed(step, `Could not parse issue ID: ${ctx.issueId}`);\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n return stepSkipped(step, ['Issue not found for label management']);\n }\n\n const issue = results.nodes[0];\n\n // Find or create closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n\n if (labelId) {\n const existingLabels = await issue.labels();\n const labelIds = existingLabels.nodes\n .filter(l => !CLOSE_OUT_LABELS_TO_REMOVE.includes(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n }\n await issue.update({ labelIds });\n }\n\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label on Linear`]);\n } catch (err) {\n return stepSkipped(step, [`Linear label management failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"close-issue-BWf3pxn4.js","names":[],"sources":["../../src/lib/lifecycle/close-issue.ts"],"sourcesContent":["/**\n * close-issue — Transition issue to closed/done state + label management.\n *\n * Uses the IssueTracker abstraction when available, with fallback to\n * direct API calls for contexts where the tracker isn't set up (e.g.,\n * standalone CLI).\n *\n * Operations:\n * 1. Transition issue to closed state\n * 2. Add 'closed-out' label\n * 3. Remove all current-phase workflow labels (WORKFLOW_LABELS)\n * 4. Add completion comment\n */\n\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport type { IssueTracker } from '../tracker/interface.js';\nimport type { LifecycleContext, StepResult } from './types.js';\nimport { stepOk, stepSkipped, stepFailed, getLinearApiKey } from './types.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from '../issue-id.js';\nimport { getAgentState, markAgentStoppedState, saveAgentState } from '../agents.js';\n\nconst execAsync = promisify(exec);\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';\n/**\n * Canonical set of current-phase pipeline labels. A closed/done issue is in no\n * phase, so close paths strip ALL of these. Single source of truth — close-out\n * imports this rather than keeping its own (previously divergent, missing\n * `planned`) list, which had left ~82 closed issues falsely labelled in-review/\n * in-progress/planned.\n */\nexport const WORKFLOW_LABELS = [\n 'in-progress', 'in-review', 'planned', 'in-planning',\n 'needs-close-out', 'verifying-on-main', 'ready-for-merge',\n];\n\nexport const POST_MERGE_RESIDUE_LABELS = ['merged', 'ready'];\nconst CLOSE_OUT_LABELS_TO_REMOVE = [...WORKFLOW_LABELS, ...POST_MERGE_RESIDUE_LABELS];\n\n/** Options for close-issue */\nexport interface CloseIssueOptions {\n /** IssueTracker instance (preferred — uses abstraction layer) */\n tracker?: IssueTracker;\n /** Reason for closing */\n reason?: string;\n /** Comment to add when closing */\n comment?: string;\n /** Apply the closed-out label. Default: true */\n applyLabel?: boolean;\n /** Only apply label (skip state transition). Default: false */\n labelOnly?: boolean;\n}\n\n/**\n * Close an issue and manage labels.\n *\n * If a tracker is provided, uses the abstraction layer.\n * Otherwise, falls back to direct gh CLI (GitHub) or Linear SDK calls.\n */\nasync function markWorkAgentStoppedForIssue(issueId: string): Promise<void> {\n const agentId = `agent-${normalizeIssueIdSync(issueId)}`;\n const state = await Effect.runPromise(getAgentState(agentId));\n if (!state) return;\n markAgentStoppedState(state);\n await Effect.runPromise(saveAgentState(state));\n}\n\nexport function closeIssue(\n ctx: LifecycleContext,\n opts: CloseIssueOptions = {},\n): Effect.Effect<StepResult[]> {\n return Effect.gen(function* () {\n const results: StepResult[] = [];\n const { applyLabel = true, labelOnly = false, comment } = opts;\n\n // Step 1: Transition to closed (unless labelOnly)\n if (!labelOnly) {\n const closeResult = opts.tracker\n ? yield* closeViaTracker(ctx, opts.tracker, comment)\n : yield* closeViaDirect(ctx, comment);\n results.push(closeResult);\n\n // If close failed, don't bother with labels\n if (!closeResult.success && !closeResult.skipped) {\n return results;\n }\n }\n\n // Step 2: Close any open PR for the feature branch (GitHub only)\n if (ctx.github) {\n const prResult = yield* closeGitHubPr(ctx);\n results.push(prResult);\n }\n\n // Step 3: Apply closed-out label + remove workflow labels\n if (applyLabel) {\n const labelResult = yield* applyClosedOutLabel(ctx, opts.tracker);\n results.push(labelResult);\n }\n\n return results;\n });\n}\n\n/**\n * Close via IssueTracker abstraction.\n */\nfunction closeViaTracker(\n ctx: LifecycleContext,\n tracker: IssueTracker,\n comment?: string,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:transition';\n return Effect.gen(function* () {\n yield* tracker.transitionIssue(ctx.issueId, 'closed');\n yield* Effect.promise(() => markWorkAgentStoppedForIssue(ctx.issueId));\n if (comment) {\n // Best-effort comment — swallow errors\n yield* tracker.addComment(ctx.issueId, comment).pipe(\n Effect.catch(() => Effect.void),\n );\n }\n return stepOk(step, [`Closed ${ctx.issueId} via ${tracker.name} tracker`]);\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed(step, `Failed to close via tracker: ${(err as Error).message ?? String(err)}`)),\n ),\n );\n}\n\n/**\n * Close via direct API calls (fallback when no tracker configured).\n * Determines issue type from context and uses appropriate method.\n */\nfunction closeViaDirect(\n ctx: LifecycleContext,\n comment?: string,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:transition';\n\n if (ctx.github) {\n return closeGitHubDirect(ctx, comment);\n }\n\n // Rally issue\n if (ctx.rally) {\n return closeRallyDirect(ctx);\n }\n\n // Try Linear\n return Effect.gen(function* () {\n const linearApiKey = yield* Effect.promise(() => getLinearApiKey());\n if (linearApiKey) {\n return yield* closeLinearDirect(ctx, linearApiKey);\n }\n return stepFailed(step, 'No tracker available and cannot determine issue type');\n });\n}\n\n/**\n * Close a GitHub issue via gh CLI.\n */\nfunction closeGitHubDirect(ctx: LifecycleContext, comment?: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeGitHubDirectImpl(ctx, comment),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `gh issue close failed: ${(err as Error).message}`)),\n ),\n );\n}\n\nasync function closeGitHubDirectImpl(ctx: LifecycleContext, comment?: string): Promise<StepResult> {\n const step = 'close-issue:transition';\n if (!ctx.github) {\n return stepFailed(step, 'GitHub config not provided');\n }\n const { owner, repo, number } = ctx.github;\n\n // Refuse to close a GitHub issue when its feature branch still has an open,\n // unmerged PR. PAN-1030 closed the issue while PR #1046 was open with 1152\n // lines of real changes — the close-out path trusted the operator without\n // verifying the merge. Fail loudly so the operator notices and either\n // merges the PR first or tells us why the issue should still close.\n const branchName = `feature/${ctx.issueId.toLowerCase()}`;\n try {\n const { stdout: prRaw } = await execAsync(\n `gh pr list --repo ${owner}/${repo} --head \"${branchName}\" --state open --json number,mergedAt,mergeCommit --jq '.[0]'`,\n { encoding: 'utf-8' },\n );\n const trimmed = prRaw.trim();\n if (trimmed) {\n try {\n const pr = JSON.parse(trimmed) as { number?: number; mergedAt?: string | null; mergeCommit?: unknown };\n if (pr.number && !pr.mergedAt && !pr.mergeCommit) {\n return stepFailed(\n step,\n `Refusing to close issue #${number}: open PR #${pr.number} on ${branchName} has not been merged. Merge or close the PR first, or rename the branch if it is unrelated.`,\n );\n }\n } catch {\n // PR JSON unparseable — continue with the close (don't block on parser error).\n }\n }\n } catch {\n // gh query failed — don't block close-out on a flaky network call.\n }\n\n try {\n const commentArg = comment ? ` --comment \"${comment.replace(/\"/g, '\\\\\"')}\"` : '';\n await execAsync(\n `gh issue close ${number} --repo ${owner}/${repo}${commentArg}`,\n { encoding: 'utf-8' },\n );\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Closed GitHub issue #${number} on ${owner}/${repo}`]);\n } catch (err) {\n return stepFailed(step, `gh issue close failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Close any open GitHub PR for the feature branch.\n */\nfunction closeGitHubPr(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeGitHubPrImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:close-pr', [`PR close failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function closeGitHubPrImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:close-pr';\n if (!ctx.github) {\n return stepSkipped(step, ['Not a GitHub issue']);\n }\n const { owner, repo } = ctx.github;\n const issueLower = ctx.issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n // Pull number + merge status. Closing without checking can land us in the\n // PAN-1030 state: issue closed + PR left OPEN with real unmerged code,\n // or worse — close a not-yet-merged PR with a \"Merged via Overdeck\n // lifecycle\" comment that lies about what happened.\n const { stdout: prListRaw } = await execAsync(\n `gh pr list --repo ${owner}/${repo} --head \"${branchName}\" --state open --json number,mergedAt,mergeCommit --jq '.[0]'`,\n { encoding: 'utf-8' },\n );\n const prRaw = prListRaw.trim();\n if (!prRaw) {\n return stepSkipped(step, ['No open PR found for branch']);\n }\n let pr: { number?: number; mergedAt?: string | null; mergeCommit?: unknown };\n try {\n pr = JSON.parse(prRaw);\n } catch {\n return stepSkipped(step, [`PR JSON parse failed: ${prRaw.slice(0, 80)}`]);\n }\n if (!pr.number) {\n return stepSkipped(step, ['Open PR query returned no number']);\n }\n if (!pr.mergedAt && !pr.mergeCommit) {\n // PR is open but NOT merged — refuse to close. The close-out flow must\n // not pretend an unmerged PR was merged. Fail loudly so the operator\n // notices the inconsistency rather than the PR silently disappearing.\n return stepFailed(\n step,\n `Refusing to close PR #${pr.number} on ${owner}/${repo}: PR is not merged. Investigate why close-out fired without a merge.`,\n );\n }\n await execAsync(\n `gh pr close ${pr.number} --repo ${owner}/${repo} --comment \"Merged via Overdeck lifecycle\"`,\n { encoding: 'utf-8' },\n );\n return stepOk(step, [`Closed PR #${pr.number} on ${owner}/${repo}`]);\n } catch (err) {\n return stepSkipped(step, [`PR close failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n\n/**\n * Rate limit circuit breaker for Linear API.\n * After hitting a rate limit, stop all Linear API calls for COOLDOWN_MS.\n * This prevents the 24,626-call storm that exhausted Linear's 5000 req/hr limit (PAN-328).\n */\nlet _linearRateLimitUntil = 0;\nconst LINEAR_RATE_LIMIT_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour (matches Linear's 5000/hr window)\n\n/**\n * Close a Linear issue via SDK (find by identifier, transition to Done).\n */\nfunction closeLinearDirect(ctx: LifecycleContext, apiKey: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeLinearDirectImpl(ctx, apiKey),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `Linear close failed: ${(err as Error).message}`)),\n ),\n );\n}\n\nasync function closeLinearDirectImpl(ctx: LifecycleContext, apiKey: string): Promise<StepResult> {\n const step = 'close-issue:transition';\n\n // Circuit breaker: if we recently hit a rate limit, fail fast without making API calls\n if (Date.now() < _linearRateLimitUntil) {\n const remainingMin = Math.ceil((_linearRateLimitUntil - Date.now()) / 60000);\n return stepFailed(step, `Linear rate limit cooldown active (${remainingMin}min remaining). Issue will be closed during close-out ceremony.`);\n }\n\n try {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey });\n\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n return stepFailed(step, `Could not parse issue ID: ${ctx.issueId}`);\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n\n if (results.nodes.length === 0) {\n return stepFailed(step, `Issue ${ctx.issueId} not found in Linear`);\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Moved Linear issue ${ctx.issueId} to Done`]);\n } catch (err) {\n const message = (err as Error).message;\n\n // Detect rate limit errors and activate circuit breaker\n if (message.includes('Rate limit') || message.includes('rate limit') || message.includes('429')) {\n _linearRateLimitUntil = Date.now() + LINEAR_RATE_LIMIT_COOLDOWN_MS;\n console.warn(`[close-issue] Linear rate limit hit — circuit breaker activated for 1 hour`);\n return stepFailed(step, `Linear rate limit exceeded. Circuit breaker activated — no Linear API calls for 1 hour. Issue will be closed during close-out ceremony.`);\n }\n\n return stepFailed(step, `Linear close failed: ${message}`);\n }\n}\n\n/**\n * Close a Rally issue via RallyTracker.\n */\nfunction closeRallyDirect(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => closeRallyDirectImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepFailed('close-issue:transition', `Rally close failed: ${(err as Error).message ?? String(err)}`)),\n ),\n );\n}\n\nasync function closeRallyDirectImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:transition';\n if (!ctx.rally) {\n return stepFailed(step, 'Rally config not provided');\n }\n const { RallyTracker } = await import('../tracker/rally.js');\n const tracker = new RallyTracker({\n apiKey: ctx.rally.apiKey,\n server: ctx.rally.server,\n workspace: ctx.rally.workspace,\n project: ctx.rally.project,\n });\n // RallyTracker.transitionIssue returns Effect (migrated in PAN-1249).\n await Effect.runPromise(tracker.transitionIssue(ctx.issueId, 'closed'));\n await markWorkAgentStoppedForIssue(ctx.issueId);\n return stepOk(step, [`Closed Rally issue ${ctx.issueId}`]);\n}\n\n/**\n * Apply 'closed-out' label and remove workflow labels.\n * Uses tracker if available, falls back to direct calls.\n */\nfunction applyClosedOutLabel(\n ctx: LifecycleContext,\n tracker?: IssueTracker,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:label';\n\n if (tracker) {\n return applyLabelViaTracker(ctx, tracker);\n }\n\n if (ctx.github) {\n return applyLabelGitHub(ctx);\n }\n\n return Effect.gen(function* () {\n const linearApiKey = yield* Effect.promise(() => getLinearApiKey());\n if (linearApiKey) {\n return yield* applyLabelLinear(ctx, linearApiKey);\n }\n return stepSkipped(step, ['No tracker available for label management']);\n });\n}\n\nfunction applyLabelViaTracker(\n ctx: LifecycleContext,\n tracker: IssueTracker,\n): Effect.Effect<StepResult> {\n const step = 'close-issue:label';\n return Effect.gen(function* () {\n const issue = yield* tracker.getIssue(ctx.issueId);\n const newLabels = issue.labels.filter((l: string) => !CLOSE_OUT_LABELS_TO_REMOVE.includes(l));\n if (!newLabels.includes(CLOSED_OUT_LABEL)) {\n newLabels.push(CLOSED_OUT_LABEL);\n }\n yield* tracker.updateIssue(ctx.issueId, { labels: newLabels });\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label via ${tracker.name} tracker`]);\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped(step, [`Label management failed (non-fatal): ${(err as Error).message ?? String(err)}`])),\n ),\n );\n}\n\nfunction applyLabelGitHub(ctx: LifecycleContext): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => applyLabelGitHubImpl(ctx),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:label', [`Label management failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function applyLabelGitHubImpl(ctx: LifecycleContext): Promise<StepResult> {\n const step = 'close-issue:label';\n if (!ctx.github) return stepSkipped(step);\n const { owner, repo, number } = ctx.github;\n\n try {\n // Ensure label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${owner}/${repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' },\n );\n // Only remove labels actually on the issue — `gh issue edit` fails the\n // whole edit if any --remove-label names a label missing from the repo.\n const { stdout: labelsJson } = await execAsync(\n `gh issue view ${number} --repo ${owner}/${repo} --json labels --jq '[.labels[].name]'`,\n { encoding: 'utf-8' },\n );\n const currentLabels: string[] = JSON.parse(labelsJson.trim() || '[]');\n const removeLabelArgs = CLOSE_OUT_LABELS_TO_REMOVE\n .filter(label => currentLabels.includes(label))\n .map(label => `--remove-label \"${label}\"`)\n .join(' ');\n await execAsync(\n `gh issue edit ${number} --repo ${owner}/${repo} --add-label \"${CLOSED_OUT_LABEL}\" ${removeLabelArgs}`.trim(),\n { encoding: 'utf-8' },\n );\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label on GitHub`]);\n } catch (err) {\n return stepSkipped(step, [`Label management failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n\nfunction applyLabelLinear(ctx: LifecycleContext, apiKey: string): Effect.Effect<StepResult> {\n return Effect.tryPromise({\n try: () => applyLabelLinearImpl(ctx, apiKey),\n catch: (err) => err,\n }).pipe(\n Effect.catch((err) =>\n Effect.succeed(stepSkipped('close-issue:label', [`Linear label management failed (non-fatal): ${(err as Error).message}`])),\n ),\n );\n}\n\nasync function applyLabelLinearImpl(ctx: LifecycleContext, apiKey: string): Promise<StepResult> {\n const step = 'close-issue:label';\n try {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey });\n\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum === null || teamKey === null) {\n return stepFailed(step, `Could not parse issue ID: ${ctx.issueId}`);\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n return stepSkipped(step, ['Issue not found for label management']);\n }\n\n const issue = results.nodes[0];\n\n // Find or create closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n\n if (labelId) {\n const existingLabels = await issue.labels();\n const labelIds = existingLabels.nodes\n .filter(l => !CLOSE_OUT_LABELS_TO_REMOVE.includes(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n }\n await issue.update({ labelIds });\n }\n\n return stepOk(step, [`Applied '${CLOSED_OUT_LABEL}' label on Linear`]);\n } catch (err) {\n return stepSkipped(step, [`Linear label management failed (non-fatal): ${(err as Error).message}`]);\n }\n}\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { Ft as resolveProjectForIssue, Ln as listProjectsSync, Nn as init_projects, Ot as init_record, Vr as init_paths, er as init_issue_id, yr as AGENTS_DIR } from "./config-yaml-DPAieL_G.js";
2
2
  import { a as init_activity_logger, n as emitActivityEntrySync } from "./activity-logger-pcbf-e30.js";
3
3
  import { O as listSessionNames, w as killSession, x as init_tmux } from "./tmux-ePNhMIMU.js";
4
- import { c as init_review_status, u as loadReviewStatuses } from "./review-status-BQjqQXZG.js";
5
- import { K as stopAgent, Z as listRunningAgents, t as init_agents } from "./agents-DHvsjsuP.js";
4
+ import { c as init_review_status, u as loadReviewStatuses } from "./review-status-B5ZASOo4.js";
5
+ import { K as stopAgent, Z as listRunningAgents, t as init_agents } from "./agents-C7V4odQ6.js";
6
6
  import { i as teardownWorkspaceDockerByNamePromise, n as init_docker } from "./docker-Q4bphryk.js";
7
7
  import { l as init_prd_locations, o as init_archive_planning } from "./archive-planning-zTvWWHG_.js";
8
8
  import { n as init_issue_closed, r as isIssueClosed } from "./issue-closed-BO84g03t.js";
9
- import "./close-issue-BnpErnD1.js";
9
+ import "./close-issue-BWf3pxn4.js";
10
10
  import { Data, Effect } from "effect";
11
11
  import "fs";
12
12
  import "path";
@@ -342,4 +342,4 @@ async function reconcileClosedIssueAgents() {
342
342
  //#endregion
343
343
  export { reconcileClosedIssueAgents as n, handleIssueStatusChangedClosed as t };
344
344
 
345
- //# sourceMappingURL=closed-issue-reaper-DRqK5k39.js.map
345
+ //# sourceMappingURL=closed-issue-reaper-D7XPq5pY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"closed-issue-reaper-DRqK5k39.js","names":["execAsync","promisify","exec","join","existsSync","existsSync","readdirSync","join"],"sources":["../../src/lib/close-out.ts","../../src/lib/cloister/reap-issue-residue.ts","../../src/lib/cloister/closed-issue-reaper.ts"],"sourcesContent":["/**\n * Close-Out Ceremony — Human-gated verification and cleanup after merge.\n *\n * Verifies PRD is preserved, branch is merged, then cleans up agent state,\n * closes the issue on the tracker, and applies a `closed-out` label.\n */\n\nimport { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect, Data } from 'effect';\nimport {\n OVERDECK_HOME,\n AGENTS_DIR,\n PROJECT_DOCS_SUBDIR,\n PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR,\n} from './paths.js';\nimport { findPrdAtStatusSync, canonicalPrdSubdirSync } from './prd-locations.js';\nimport { killSession, sessionExistsSync, listSessionNames } from './tmux.js';\nimport { loadReviewStatuses } from './review-status.js';\nimport { getLinearApiKey } from './lifecycle/types.js';\nimport { POST_MERGE_RESIDUE_LABELS, WORKFLOW_LABELS } from './lifecycle/close-issue.js';\nimport { findWorkspacePath } from './lifecycle/archive-planning.js';\nimport { teardownWorkspaceDockerByNamePromise } from './workspace-manager/docker.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from './issue-id.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Check if a feature branch has been merged into main.\n *\n * Uses `git merge-base --is-ancestor` for regular merges, plus a\n * code-diff fallback to detect squash merges where the branch still exists.\n * Also checks review-status.json as authoritative — the merge specialist\n * validates the merge before setting mergeStatus to 'merged'.\n */\nexport async function isBranchMerged(\n branchName: string,\n projectPath: string,\n): Promise<{ status: 'merged' | 'unmerged' | 'no-branch'; message: string }> {\n // Check review-status as a hint — but NEVER trust it over the live git state.\n // An issue that merged once can be re-activated and accumulate new unmerged\n // commits; honoring a stale mergeStatus here would let close-out tear down a\n // workspace with un-landed work. Only honor it when the branch has nothing\n // beyond main; otherwise fall through to the robust git checks below (which\n // still recognise squash-merges via the code-diff check).\n try {\n const issueId = branchName.replace('feature/', '').toUpperCase();\n const statuses = loadReviewStatuses();\n if (statuses[issueId]?.mergeStatus === 'merged') {\n const { stdout: aheadOfMain } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!aheadOfMain.trim()) {\n return { status: 'merged', message: 'Merge specialist confirmed merge completed' };\n }\n }\n } catch {\n // review-status.json may not exist, continue with git checks\n }\n\n // Check if branch exists locally\n const { stdout: branchExists } = await execAsync(\n `git branch --list \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (branchExists.trim()) {\n // Use merge-base --is-ancestor: checks if the branch tip is reachable from main\n // This works for regular merges, squash merges, and cherry-picks\n try {\n await execAsync(\n `git merge-base --is-ancestor ${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'All commits merged to main' };\n } catch {\n // --is-ancestor fails for squash merges where the branch still exists.\n // Check if the code diff (excluding planning artifacts) is empty — if so,\n // the code was squash-merged and only planning files remain on the branch.\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...${branchName} -- ':!.planning' ':!docs/prds' ':!.overdeck/prompts' 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through to unmerged report\n }\n\n const { stdout: unmerged } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = unmerged.trim() ? unmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on ${branchName}. Merge before closing out.`,\n };\n }\n }\n\n // Check remote\n const { stdout: remoteBranch } = await execAsync(\n `git ls-remote --heads origin \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (remoteBranch.trim()) {\n await execAsync(`git fetch origin ${branchName}`, { cwd: projectPath }).catch(() => {});\n try {\n await execAsync(\n `git merge-base --is-ancestor origin/${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'Remote branch fully merged' };\n } catch {\n // Squash-merge detection for remote branch\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...origin/${branchName} -- ':!.planning' ':!docs/prds' ':!.overdeck/prompts' 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Remote code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through\n }\n\n const { stdout: remoteUnmerged } = await execAsync(\n `git log main..origin/${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = remoteUnmerged.trim() ? remoteUnmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on remote ${branchName}.`,\n };\n }\n }\n\n // No branch at all — assume squash-merged and branch deleted\n return { status: 'no-branch', message: 'Branch already cleaned up (squash-merged)' };\n}\n\n\nexport interface CloseOutStep {\n name: string;\n status: 'passed' | 'failed' | 'skipped';\n message?: string;\n}\n\nexport interface CloseOutResult {\n success: boolean;\n issueId: string;\n steps: CloseOutStep[];\n error?: string;\n}\n\nexport interface CloseOutContext {\n issueId: string;\n projectPath: string;\n isGitHub: boolean;\n owner?: string;\n repo?: string;\n number?: number;\n}\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';async function executeCloseOutPromise(ctx: CloseOutContext): Promise<CloseOutResult> {\n const steps: CloseOutStep[] = [];\n const issueLower = ctx.issueId.toLowerCase();\n\n // Step 1: Verify PRD preserved.\n // Tolerates all four legacy formats (subdir/flat × lowercase/uppercase) via findPrdAtStatus.\n try {\n if (findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'completed')) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'PRD in completed/' });\n } else {\n const source = findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'active');\n if (!source) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: 'No PRD found (may not have had one)' });\n } else {\n const completedSubdir = canonicalPrdSubdirSync(ctx.projectPath, ctx.issueId, 'completed');\n const completedFlat = join(\n ctx.projectPath, PROJECT_DOCS_SUBDIR, PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR, `${issueLower}-plan.md`,\n );\n const dest = source.format === 'subdir' ? completedSubdir : completedFlat;\n const destParent = dirname(dest);\n if (!existsSync(destParent)) {\n mkdirSync(destParent, { recursive: true });\n }\n try {\n await execAsync(`git mv \"${source.path}\" \"${dest}\"`, { cwd: ctx.projectPath });\n await execAsync(`git commit -m \"Move ${ctx.issueId} PRD to completed (close-out)\"`, { cwd: ctx.projectPath });\n await execAsync(`git push`, { cwd: ctx.projectPath });\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Moved PRD from active/ to completed/' });\n } catch {\n try {\n cpSync(source.path, dest, { recursive: true });\n if (existsSync(dest)) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Copied PRD to completed/ (git mv failed, plain copy succeeded)' });\n } else {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: 'PRD copy appeared to succeed but file not found at destination' });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — file not at destination after copy' };\n }\n } catch (cpErr) {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: `Failed to copy PRD: ${(cpErr as Error).message}` });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — both git mv and copy failed' };\n }\n }\n }\n }\n } catch (err) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 2: Verify branch merged (hard fail)\n try {\n const branchName = `feature/${issueLower}`;\n const merged = await isBranchMerged(branchName, ctx.projectPath);\n\n if (merged.status === 'merged') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else if (merged.status === 'no-branch') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: merged.message,\n });\n return { success: false, issueId: ctx.issueId, steps, error: merged.message };\n }\n } catch (err) {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: `Could not verify merge: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: 'Could not verify branch merge status' };\n }\n\n // Step 2.5: Refuse close-out while the work agent session is still alive.\n // postMergeLifecycle pauses the work agent at merge, so a live agent-<issue>\n // session means the issue was re-activated and is doing in-progress work that\n // tearing down the workspace would destroy (the verifying_on_main tag can be\n // stale — it is set at first merge and not cleared when work resumes).\n const liveAgentSession = `agent-${issueLower}`;\n if (sessionExistsSync(liveAgentSession)) {\n const activeMsg = `${liveAgentSession} is still running — pause/stop it before close-out (it was re-activated after merge; closing now would kill in-progress work).`;\n steps.push({ name: 'No active work agent', status: 'failed', message: activeMsg });\n return { success: false, issueId: ctx.issueId, steps, error: activeMsg };\n }\n\n // Step 3: Clean up workspace\n try {\n const workspacePath = findWorkspacePath(ctx.projectPath, issueLower);\n const agentSession = `agent-${issueLower}`;\n let cleaned = false;\n\n // Kill tmux sessions for this issue\n const exactPatterns = [agentSession, `test-${issueLower}`, `merge-${issueLower}`];\n for (const session of exactPatterns) {\n if (sessionExistsSync(session)) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n }\n\n // Review sessions use timestamped names: review-<issue>-<timestamp>-<role>\n try {\n const allSessions = await Effect.runPromise(listSessionNames());\n const reviewRegex = new RegExp(`^review-${issueLower.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}-\\\\d+`);\n const reviewSessions = allSessions.filter(s => reviewRegex.test(s));\n for (const session of reviewSessions) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n } catch { /* tmux server may not be running */ }\n\n // Stop Docker containers (name-based, works even if workspace dir is gone)\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n cleaned = true;\n steps.push({\n name: 'Docker stack removed',\n status: 'passed',\n message: `Stopped and removed Docker stack for ${issueLower}`,\n });\n } else {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker network overdeck-feature-${issueLower}_devnet is still present; the closed-issue reaper will retry`,\n });\n }\n } catch (err) {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker teardown failed: ${(err as Error).message}; the closed-issue reaper will retry`,\n });\n }\n\n // Remove git worktree\n if (workspacePath && existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch {\n // Try direct removal if worktree remove fails\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n cleaned = true;\n } catch { /* Already gone */ }\n }\n }\n\n // Delete local and remote feature branches (safe — we verified merge in step 2)\n const branchName = `feature/${issueLower}`;\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist locally */ }\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist on remote */ }\n\n steps.push({\n name: 'Clean up workspace',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Workspace cleaned up' : 'No workspace to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up workspace', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 4: Prune checkpoint refs\n // Primary: delete all refs/pan/turn/<agentId>/* for this issue's agents.\n // Global: also sweep any checkpoint refs older than 30 days (safety net for\n // abandoned workspaces whose postMergeLifecycle was never called or skipped).\n try {\n const { pruneCheckpointRefsForAgents, pruneStaleCheckpointRefs } = await import('./checkpoint/checkpoint-manager.js');\n const issueLower = ctx.issueId.toLowerCase();\n const agentIds = [`agent-${issueLower}`, `planning-${issueLower}`];\n await Effect.runPromise(pruneCheckpointRefsForAgents(ctx.projectPath, agentIds));\n const stalePruned = await Effect.runPromise(pruneStaleCheckpointRefs(ctx.projectPath, 30));\n steps.push({\n name: 'Prune checkpoint refs',\n status: 'passed',\n message: stalePruned > 0\n ? `Issue refs removed; ${stalePruned} stale ref(s) >30 days also pruned`\n : 'Issue checkpoint refs removed',\n });\n } catch (err) {\n steps.push({ name: 'Prune checkpoint refs', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 5: Clean up agent state\n try {\n let cleaned = false;\n const agentDir = join(AGENTS_DIR, `agent-${issueLower}`);\n const planningDir = join(AGENTS_DIR, `planning-${issueLower}`);\n\n if (existsSync(agentDir)) {\n rmSync(agentDir, { recursive: true, force: true });\n cleaned = true;\n }\n if (existsSync(planningDir)) {\n rmSync(planningDir, { recursive: true, force: true });\n cleaned = true;\n }\n\n steps.push({\n name: 'Clean up agent state',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Agent state directories removed' : 'No agent state to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up agent state', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 6: Close issue on tracker (hard fail)\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n await execAsync(\n `gh issue close ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --comment \"Closed via close-out ceremony\"`,\n { encoding: 'utf-8' }\n );\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `GitHub issue #${ctx.number} closed` });\n } else {\n // Linear issue\n const linearApiKey = await getLinearApiKey();\n if (!linearApiKey) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: 'LINEAR_API_KEY not configured' });\n return { success: false, issueId: ctx.issueId, steps, error: 'LINEAR_API_KEY not configured' };\n }\n\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n\n // Find the issue by identifier using issues filter (searchIssues returns\n // IssueSearchResult which lacks .update(); client.issue() needs the UUID)\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Could not parse issue ID: ${ctx.issueId}` });\n return { success: false, issueId: ctx.issueId, steps, error: `Could not parse issue ID: ${ctx.issueId}` };\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Issue ${ctx.issueId} not found in Linear` });\n return { success: false, issueId: ctx.issueId, steps, error: `Issue ${ctx.issueId} not found in Linear` };\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `Linear issue ${ctx.issueId} moved to Done` });\n }\n } catch (err) {\n steps.push({\n name: 'Close issue on tracker',\n status: 'failed',\n message: `Failed to close: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: `Failed to close issue: ${(err as Error).message}` };\n }\n\n // Step 7: Apply closed-out label\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n // Ensure the label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${ctx.owner}/${ctx.repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n // Add the label\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --add-label \"${CLOSED_OUT_LABEL}\"`,\n { encoding: 'utf-8' }\n );\n // Remove all current-phase workflow labels (single source: WORKFLOW_LABELS)\n for (const label of WORKFLOW_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n for (const label of POST_MERGE_RESIDUE_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: `Added '${CLOSED_OUT_LABEL}' label` });\n } else {\n // Linear: add label if possible\n try {\n const linearApiKey = await getLinearApiKey();\n if (linearApiKey) {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum !== null && teamKey !== null) {\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length > 0) {\n const issue = results.nodes[0];\n // Find or create the closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n if (labelId) {\n const existingLabels = await issue.labels();\n const residueLabelNames = new Set(POST_MERGE_RESIDUE_LABELS);\n const labelIds = existingLabels.nodes\n .filter(l => !residueLabelNames.has(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n await issue.update({ labelIds });\n }\n }\n }\n }\n }\n } catch { /* Non-fatal for Linear */ }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: 'Label applied' });\n }\n } catch (err) {\n steps.push({ name: 'Apply closed-out label', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 8: Mark the durable pipeline journal terminal before clearing the DB cache.\n try {\n const { markRecordPipelineClosedOutSync } = await import('./pan-dir/records.js');\n markRecordPipelineClosedOutSync({ name: 'inferred', path: ctx.projectPath }, ctx.issueId.toUpperCase());\n steps.push({ name: 'Mark pipeline terminal', status: 'passed', message: 'Pipeline journal marked closed-out' });\n } catch (err) {\n steps.push({ name: 'Mark pipeline terminal', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 9: Clear review status\n try {\n // Dynamically import to avoid circular dependency with server\n const { clearReviewStatus } = await import('./review-status.js');\n clearReviewStatus(ctx.issueId.toUpperCase());\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared' });\n } catch {\n // review-status module may not be available in CLI context\n // Try cleaning the file directly\n try {\n const statusFile = join(OVERDECK_HOME, 'review-status.json');\n if (existsSync(statusFile)) {\n const data = JSON.parse(readFileSync(statusFile, 'utf-8'));\n const upperKey = ctx.issueId.toUpperCase();\n if (data[upperKey]) {\n delete data[upperKey];\n const { writeFileSync } = await import('fs');\n writeFileSync(statusFile, JSON.stringify(data, null, 2));\n }\n }\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared (direct)' });\n } catch (innerErr) {\n steps.push({ name: 'Clear review status', status: 'skipped', message: `Warning: ${(innerErr as Error).message}` });\n }\n }\n\n return { success: true, issueId: ctx.issueId, steps };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** A close-out ceremony step failed unexpectedly outside its recovery branches. */\nexport class CloseOutError extends Data.TaggedError('CloseOutError')<{\n readonly issueId: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect-native executeCloseOut. The underlying ceremony already aggregates\n * per-step results into the returned payload (success boolean + steps). The\n * Effect variant only fails with CloseOutError if the call itself throws\n * (e.g. dynamic import of @linear/sdk explodes). Step-level failures stay\n * in the returned `success/steps` shape so callers can display them.\n */\nexport const executeCloseOut = (\n ctx: CloseOutContext,\n): Effect.Effect<CloseOutResult, CloseOutError> =>\n Effect.tryPromise({\n try: () => executeCloseOutPromise(ctx),\n catch: (cause) =>\n new CloseOutError({\n issueId: ctx.issueId,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","import { existsSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nimport { isBranchMerged } from '../close-out.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { killSession, listSessionNames } from '../tmux.js';\nimport { teardownWorkspaceDockerByNamePromise } from '../workspace-manager/docker.js';\n\nconst execAsync = promisify(exec);\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport async function reapIssueResidue(projectPath: string, issueId: string): Promise<string[]> {\n const actions: string[] = [];\n const issueLower = issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n const sessions = await Effect.runPromise(listSessionNames());\n const exactSessions = new Set([\n `agent-${issueLower}`,\n `test-${issueLower}`,\n `merge-${issueLower}`,\n ]);\n const reviewRegex = new RegExp(`^review-${escapeRegex(issueLower)}-\\\\d+`);\n for (const session of sessions) {\n if (!exactSessions.has(session) && !reviewRegex.test(session)) continue;\n try {\n await Effect.runPromise(killSession(session));\n actions.push(`killed tmux session ${session}`);\n } catch {\n // Session may already be gone.\n }\n }\n } catch {\n // tmux server may not be running.\n }\n\n const merged = await isBranchMerged(branchName, projectPath);\n if (merged.status === 'unmerged') {\n actions.push(`skipped reap for ${issueId} — branch unmerged`);\n return actions;\n }\n\n // Remove Docker stack by name, independent of whether the workspace dir still exists.\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n actions.push(`removed Docker stack overdeck-feature-${issueLower}`);\n } else {\n actions.push(`Docker network overdeck-feature-${issueLower}_devnet still present`);\n }\n } catch (err) {\n actions.push(`Docker teardown failed for ${issueId}: ${(err as Error).message}`);\n }\n\n const workspacePath = join(projectPath, 'workspaces', `feature-${issueLower}`);\n if (existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: projectPath });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n }\n\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted local branch ${branchName}`);\n } catch {\n // Branch may not exist locally.\n }\n\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted remote branch ${branchName}`);\n } catch {\n // Branch may not exist remotely.\n }\n\n for (const agentDirName of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n const agentDir = join(AGENTS_DIR, agentDirName);\n if (!existsSync(agentDir)) continue;\n try {\n rmSync(agentDir, { recursive: true, force: true });\n actions.push(`removed agent state ${agentDirName}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n\n return actions;\n}\n","import { Effect } from 'effect';\nimport { existsSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { listRunningAgents, stopAgent } from '../agents.js';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { listProjectsSync } from '../projects.js';\nimport { resolveProjectForIssue } from '../pan-dir/record.js';\nimport { listSessionNames } from '../tmux.js';\nimport { isIssueClosed } from './issue-closed.js';\nimport { reapIssueResidue } from './reap-issue-residue.js';\n\n// Sessions reaped by NAME as a backstop: inspect sessions never have agent\n// state, and strike sessions can outlive their state entry (e.g. state already\n// stopped or removed while the tmux session idles — PAN-1721).\nfunction issueIdFromStatelessSession(sessionName: string): string | null {\n const match = sessionName.match(/^(?:inspect|strike)-([a-z0-9]+-\\d+)(?:-|$)/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromFeatureWorkspace(entryName: string): string | null {\n const match = entryName.match(/^feature-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromAgentDir(entryName: string): string | null {\n const match = entryName.match(/^agent-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nasync function isClosedIssue(\n issueId: string,\n closedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = closedChecks.get(issueId);\n if (!promise) {\n promise = isIssueClosed(issueId);\n closedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nasync function reapClosedIssueResidue(\n projectPath: string,\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const key = `${projectPath}:${issueId}`;\n if (reapedIssueKeys.has(key)) return;\n reapedIssueKeys.add(key);\n actions.push(...await reapIssueResidue(projectPath, issueId));\n}\n\nasync function reapResolvedIssueResidue(\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const project = resolveProjectForIssue(issueId);\n if (!project) return;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n}\n\nfunction listConfiguredProjects(): Array<{ path: string }> {\n try {\n return listProjectsSync().map(({ config }) => ({ path: config.path }));\n } catch {\n return [];\n }\n}\n\nfunction listDirectoryNames(path: string): string[] {\n if (!existsSync(path)) return [];\n try {\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n}\n\nasync function stopClosedAgent(agentId: string, issueId: string, actions: string[]): Promise<void> {\n await Effect.runPromise(stopAgent(agentId));\n const action = `Reaped ${agentId} — parent issue ${issueId} is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] reaped ${agentId} — parent issue ${issueId} is closed`,\n });\n}\n\n/**\n * PAN-1908: reactive closed-issue reaper. When an issue is closed, stop all\n * running agents and stateless sessions belonging to it without scanning the\n * full agent table or tmux session list.\n */\nexport async function handleIssueStatusChangedClosed(issueId: string): Promise<string[]> {\n const upperIssueId = issueId.trim().toUpperCase();\n if (!upperIssueId) return [];\n\n // Guard against spurious events: only act when the issue is actually closed.\n if (!await isIssueClosed(upperIssueId)) return [];\n\n const actions: string[] = [];\n const reapedAgentIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n\n // Stop running agents for this issue.\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const agentIssueId = (agent.issueId ?? '').trim().toUpperCase();\n if (agentIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(agent.id, upperIssueId, actions);\n reapedAgentIds.add(agent.id);\n }\n\n // Stop stateless inspect/strike sessions for this issue.\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n const sessionIssueId = issueIdFromStatelessSession(sessionName);\n if (sessionIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(sessionName, upperIssueId, actions);\n reapedAgentIds.add(sessionName);\n }\n\n await reapResolvedIssueResidue(upperIssueId, actions, reapedIssueKeys);\n return actions;\n}\n\n/**\n * PAN-1908: keep a thin table+session safety net for dropped closed-issue\n * events. The primary path is reactive via handleIssueStatusChangedClosed.\n */\nexport async function reconcileClosedIssueAgents(): Promise<string[]> {\n const actions: string[] = [];\n const closedChecks = new Map<string, Promise<boolean>>();\n const reapedAgentIds = new Set<string>();\n const closedIssueIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n const agents = await Effect.runPromise(listRunningAgents());\n\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(agent.id, issueId, actions);\n reapedAgentIds.add(agent.id);\n closedIssueIds.add(issueId);\n }\n\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n\n const issueId = issueIdFromStatelessSession(sessionName);\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(sessionName, issueId, actions);\n reapedAgentIds.add(sessionName);\n closedIssueIds.add(issueId);\n }\n\n for (const issueId of closedIssueIds) {\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n for (const project of listConfiguredProjects()) {\n const workspacesDir = join(project.path, 'workspaces');\n for (const entryName of listDirectoryNames(workspacesDir)) {\n const issueId = issueIdFromFeatureWorkspace(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n }\n }\n\n for (const entryName of listDirectoryNames(AGENTS_DIR)) {\n const issueId = issueIdFromAgentDir(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n return actions;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"closed-issue-reaper-D7XPq5pY.js","names":["execAsync","promisify","exec","join","existsSync","existsSync","readdirSync","join"],"sources":["../../src/lib/close-out.ts","../../src/lib/cloister/reap-issue-residue.ts","../../src/lib/cloister/closed-issue-reaper.ts"],"sourcesContent":["/**\n * Close-Out Ceremony — Human-gated verification and cleanup after merge.\n *\n * Verifies PRD is preserved, branch is merged, then cleans up agent state,\n * closes the issue on the tracker, and applies a `closed-out` label.\n */\n\nimport { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect, Data } from 'effect';\nimport {\n OVERDECK_HOME,\n AGENTS_DIR,\n PROJECT_DOCS_SUBDIR,\n PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR,\n} from './paths.js';\nimport { findPrdAtStatusSync, canonicalPrdSubdirSync } from './prd-locations.js';\nimport { killSession, sessionExistsSync, listSessionNames } from './tmux.js';\nimport { loadReviewStatuses } from './review-status.js';\nimport { getLinearApiKey } from './lifecycle/types.js';\nimport { POST_MERGE_RESIDUE_LABELS, WORKFLOW_LABELS } from './lifecycle/close-issue.js';\nimport { findWorkspacePath } from './lifecycle/archive-planning.js';\nimport { teardownWorkspaceDockerByNamePromise } from './workspace-manager/docker.js';\nimport { extractNumberSync, extractPrefixSync, normalizeIssueIdSync } from './issue-id.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Check if a feature branch has been merged into main.\n *\n * Uses `git merge-base --is-ancestor` for regular merges, plus a\n * code-diff fallback to detect squash merges where the branch still exists.\n * Also checks review-status.json as authoritative — the merge specialist\n * validates the merge before setting mergeStatus to 'merged'.\n */\nexport async function isBranchMerged(\n branchName: string,\n projectPath: string,\n): Promise<{ status: 'merged' | 'unmerged' | 'no-branch'; message: string }> {\n // Check review-status as a hint — but NEVER trust it over the live git state.\n // An issue that merged once can be re-activated and accumulate new unmerged\n // commits; honoring a stale mergeStatus here would let close-out tear down a\n // workspace with un-landed work. Only honor it when the branch has nothing\n // beyond main; otherwise fall through to the robust git checks below (which\n // still recognise squash-merges via the code-diff check).\n try {\n const issueId = branchName.replace('feature/', '').toUpperCase();\n const statuses = loadReviewStatuses();\n if (statuses[issueId]?.mergeStatus === 'merged') {\n const { stdout: aheadOfMain } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!aheadOfMain.trim()) {\n return { status: 'merged', message: 'Merge specialist confirmed merge completed' };\n }\n }\n } catch {\n // review-status.json may not exist, continue with git checks\n }\n\n // Check if branch exists locally\n const { stdout: branchExists } = await execAsync(\n `git branch --list \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (branchExists.trim()) {\n // Use merge-base --is-ancestor: checks if the branch tip is reachable from main\n // This works for regular merges, squash merges, and cherry-picks\n try {\n await execAsync(\n `git merge-base --is-ancestor ${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'All commits merged to main' };\n } catch {\n // --is-ancestor fails for squash merges where the branch still exists.\n // Check if the code diff (excluding planning artifacts) is empty — if so,\n // the code was squash-merged and only planning files remain on the branch.\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...${branchName} -- ':!.planning' ':!docs/prds' ':!.overdeck/prompts' 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through to unmerged report\n }\n\n const { stdout: unmerged } = await execAsync(\n `git log main..${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = unmerged.trim() ? unmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on ${branchName}. Merge before closing out.`,\n };\n }\n }\n\n // Check remote\n const { stdout: remoteBranch } = await execAsync(\n `git ls-remote --heads origin \"${branchName}\" 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n\n if (remoteBranch.trim()) {\n await execAsync(`git fetch origin ${branchName}`, { cwd: projectPath }).catch(() => {});\n try {\n await execAsync(\n `git merge-base --is-ancestor origin/${branchName} main`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n return { status: 'merged', message: 'Remote branch fully merged' };\n } catch {\n // Squash-merge detection for remote branch\n try {\n const { stdout: codeDiff } = await execAsync(\n `git diff main...origin/${branchName} -- ':!.planning' ':!docs/prds' ':!.overdeck/prompts' 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n if (!codeDiff.trim()) {\n return { status: 'merged', message: 'Remote code changes squash-merged to main (only planning artifacts remain on branch)' };\n }\n } catch {\n // diff failed — fall through\n }\n\n const { stdout: remoteUnmerged } = await execAsync(\n `git log main..origin/${branchName} --oneline 2>/dev/null || true`,\n { cwd: projectPath, encoding: 'utf-8' },\n );\n const count = remoteUnmerged.trim() ? remoteUnmerged.trim().split('\\n').length : 0;\n return {\n status: 'unmerged',\n message: `${count} unmerged commit(s) on remote ${branchName}.`,\n };\n }\n }\n\n // No branch at all — assume squash-merged and branch deleted\n return { status: 'no-branch', message: 'Branch already cleaned up (squash-merged)' };\n}\n\n\nexport interface CloseOutStep {\n name: string;\n status: 'passed' | 'failed' | 'skipped';\n message?: string;\n}\n\nexport interface CloseOutResult {\n success: boolean;\n issueId: string;\n steps: CloseOutStep[];\n error?: string;\n}\n\nexport interface CloseOutContext {\n issueId: string;\n projectPath: string;\n isGitHub: boolean;\n owner?: string;\n repo?: string;\n number?: number;\n}\n\nconst CLOSED_OUT_LABEL = 'closed-out';\nconst CLOSED_OUT_COLOR = '1d4ed8';async function executeCloseOutPromise(ctx: CloseOutContext): Promise<CloseOutResult> {\n const steps: CloseOutStep[] = [];\n const issueLower = ctx.issueId.toLowerCase();\n\n // Step 1: Verify PRD preserved.\n // Tolerates all four legacy formats (subdir/flat × lowercase/uppercase) via findPrdAtStatus.\n try {\n if (findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'completed')) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'PRD in completed/' });\n } else {\n const source = findPrdAtStatusSync(ctx.projectPath, ctx.issueId, 'active');\n if (!source) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: 'No PRD found (may not have had one)' });\n } else {\n const completedSubdir = canonicalPrdSubdirSync(ctx.projectPath, ctx.issueId, 'completed');\n const completedFlat = join(\n ctx.projectPath, PROJECT_DOCS_SUBDIR, PROJECT_PRDS_SUBDIR,\n PROJECT_PRDS_COMPLETED_SUBDIR, `${issueLower}-plan.md`,\n );\n const dest = source.format === 'subdir' ? completedSubdir : completedFlat;\n const destParent = dirname(dest);\n if (!existsSync(destParent)) {\n mkdirSync(destParent, { recursive: true });\n }\n try {\n await execAsync(`git mv \"${source.path}\" \"${dest}\"`, { cwd: ctx.projectPath });\n await execAsync(`git commit -m \"Move ${ctx.issueId} PRD to completed (close-out)\"`, { cwd: ctx.projectPath });\n await execAsync(`git push`, { cwd: ctx.projectPath });\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Moved PRD from active/ to completed/' });\n } catch {\n try {\n cpSync(source.path, dest, { recursive: true });\n if (existsSync(dest)) {\n steps.push({ name: 'Verify PRD preserved', status: 'passed', message: 'Copied PRD to completed/ (git mv failed, plain copy succeeded)' });\n } else {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: 'PRD copy appeared to succeed but file not found at destination' });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — file not at destination after copy' };\n }\n } catch (cpErr) {\n steps.push({ name: 'Verify PRD preserved', status: 'failed', message: `Failed to copy PRD: ${(cpErr as Error).message}` });\n return { success: false, issueId: ctx.issueId, steps, error: 'PRD preservation failed — both git mv and copy failed' };\n }\n }\n }\n }\n } catch (err) {\n steps.push({ name: 'Verify PRD preserved', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 2: Verify branch merged (hard fail)\n try {\n const branchName = `feature/${issueLower}`;\n const merged = await isBranchMerged(branchName, ctx.projectPath);\n\n if (merged.status === 'merged') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else if (merged.status === 'no-branch') {\n steps.push({ name: 'Verify branch merged', status: 'passed', message: merged.message });\n } else {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: merged.message,\n });\n return { success: false, issueId: ctx.issueId, steps, error: merged.message };\n }\n } catch (err) {\n steps.push({\n name: 'Verify branch merged',\n status: 'failed',\n message: `Could not verify merge: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: 'Could not verify branch merge status' };\n }\n\n // Step 2.5: Refuse close-out while the work agent session is still alive.\n // postMergeLifecycle pauses the work agent at merge, so a live agent-<issue>\n // session means the issue was re-activated and is doing in-progress work that\n // tearing down the workspace would destroy (the verifying_on_main tag can be\n // stale — it is set at first merge and not cleared when work resumes).\n const liveAgentSession = `agent-${issueLower}`;\n if (sessionExistsSync(liveAgentSession)) {\n const activeMsg = `${liveAgentSession} is still running — pause/stop it before close-out (it was re-activated after merge; closing now would kill in-progress work).`;\n steps.push({ name: 'No active work agent', status: 'failed', message: activeMsg });\n return { success: false, issueId: ctx.issueId, steps, error: activeMsg };\n }\n\n // Step 3: Clean up workspace\n try {\n const workspacePath = findWorkspacePath(ctx.projectPath, issueLower);\n const agentSession = `agent-${issueLower}`;\n let cleaned = false;\n\n // Kill tmux sessions for this issue\n const exactPatterns = [agentSession, `test-${issueLower}`, `merge-${issueLower}`];\n for (const session of exactPatterns) {\n if (sessionExistsSync(session)) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n }\n\n // Review sessions use timestamped names: review-<issue>-<timestamp>-<role>\n try {\n const allSessions = await Effect.runPromise(listSessionNames());\n const reviewRegex = new RegExp(`^review-${issueLower.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}-\\\\d+`);\n const reviewSessions = allSessions.filter(s => reviewRegex.test(s));\n for (const session of reviewSessions) {\n try {\n await Effect.runPromise(killSession(session));\n cleaned = true;\n } catch { /* session may already be dead */ }\n }\n } catch { /* tmux server may not be running */ }\n\n // Stop Docker containers (name-based, works even if workspace dir is gone)\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n cleaned = true;\n steps.push({\n name: 'Docker stack removed',\n status: 'passed',\n message: `Stopped and removed Docker stack for ${issueLower}`,\n });\n } else {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker network overdeck-feature-${issueLower}_devnet is still present; the closed-issue reaper will retry`,\n });\n }\n } catch (err) {\n steps.push({\n name: 'Docker stack removed',\n status: 'skipped',\n message: `Warning: Docker teardown failed: ${(err as Error).message}; the closed-issue reaper will retry`,\n });\n }\n\n // Remove git worktree\n if (workspacePath && existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch {\n // Try direct removal if worktree remove fails\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n cleaned = true;\n } catch { /* Already gone */ }\n }\n }\n\n // Delete local and remote feature branches (safe — we verified merge in step 2)\n const branchName = `feature/${issueLower}`;\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist locally */ }\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: ctx.projectPath });\n cleaned = true;\n } catch { /* Branch may not exist on remote */ }\n\n steps.push({\n name: 'Clean up workspace',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Workspace cleaned up' : 'No workspace to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up workspace', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 4: Prune checkpoint refs\n // Primary: delete all refs/pan/turn/<agentId>/* for this issue's agents.\n // Global: also sweep any checkpoint refs older than 30 days (safety net for\n // abandoned workspaces whose postMergeLifecycle was never called or skipped).\n try {\n const { pruneCheckpointRefsForAgents, pruneStaleCheckpointRefs } = await import('./checkpoint/checkpoint-manager.js');\n const issueLower = ctx.issueId.toLowerCase();\n const agentIds = [`agent-${issueLower}`, `planning-${issueLower}`];\n await Effect.runPromise(pruneCheckpointRefsForAgents(ctx.projectPath, agentIds));\n const stalePruned = await Effect.runPromise(pruneStaleCheckpointRefs(ctx.projectPath, 30));\n steps.push({\n name: 'Prune checkpoint refs',\n status: 'passed',\n message: stalePruned > 0\n ? `Issue refs removed; ${stalePruned} stale ref(s) >30 days also pruned`\n : 'Issue checkpoint refs removed',\n });\n } catch (err) {\n steps.push({ name: 'Prune checkpoint refs', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 5: Clean up agent state\n try {\n let cleaned = false;\n const agentDir = join(AGENTS_DIR, `agent-${issueLower}`);\n const planningDir = join(AGENTS_DIR, `planning-${issueLower}`);\n\n if (existsSync(agentDir)) {\n rmSync(agentDir, { recursive: true, force: true });\n cleaned = true;\n }\n if (existsSync(planningDir)) {\n rmSync(planningDir, { recursive: true, force: true });\n cleaned = true;\n }\n\n steps.push({\n name: 'Clean up agent state',\n status: cleaned ? 'passed' : 'skipped',\n message: cleaned ? 'Agent state directories removed' : 'No agent state to clean up',\n });\n } catch (err) {\n steps.push({ name: 'Clean up agent state', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 6: Close issue on tracker (hard fail)\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n await execAsync(\n `gh issue close ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --comment \"Closed via close-out ceremony\"`,\n { encoding: 'utf-8' }\n );\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `GitHub issue #${ctx.number} closed` });\n } else {\n // Linear issue\n const linearApiKey = await getLinearApiKey();\n if (!linearApiKey) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: 'LINEAR_API_KEY not configured' });\n return { success: false, issueId: ctx.issueId, steps, error: 'LINEAR_API_KEY not configured' };\n }\n\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n\n // Find the issue by identifier using issues filter (searchIssues returns\n // IssueSearchResult which lacks .update(); client.issue() needs the UUID)\n const issueNumber = extractNumberSync(ctx.issueId);\n const issuePrefix = extractPrefixSync(ctx.issueId);\n if (issueNumber === null || issuePrefix === null) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Could not parse issue ID: ${ctx.issueId}` });\n return { success: false, issueId: ctx.issueId, steps, error: `Could not parse issue ID: ${ctx.issueId}` };\n }\n const results = await client.issues({\n filter: {\n number: { eq: issueNumber },\n team: { key: { eq: issuePrefix } },\n },\n first: 1,\n });\n if (results.nodes.length === 0) {\n steps.push({ name: 'Close issue on tracker', status: 'failed', message: `Issue ${ctx.issueId} not found in Linear` });\n return { success: false, issueId: ctx.issueId, steps, error: `Issue ${ctx.issueId} not found in Linear` };\n }\n\n const issue = results.nodes[0];\n const team = await issue.team;\n if (team) {\n const states = await team.states();\n const doneState = states.nodes.find(s => s.name === 'Done') ||\n states.nodes.find(s => s.type === 'completed');\n if (doneState) {\n await issue.update({ stateId: doneState.id });\n }\n }\n\n steps.push({ name: 'Close issue on tracker', status: 'passed', message: `Linear issue ${ctx.issueId} moved to Done` });\n }\n } catch (err) {\n steps.push({\n name: 'Close issue on tracker',\n status: 'failed',\n message: `Failed to close: ${(err as Error).message}`,\n });\n return { success: false, issueId: ctx.issueId, steps, error: `Failed to close issue: ${(err as Error).message}` };\n }\n\n // Step 7: Apply closed-out label\n try {\n if (ctx.isGitHub && ctx.owner && ctx.repo && ctx.number) {\n // Ensure the label exists\n await execAsync(\n `gh label create \"${CLOSED_OUT_LABEL}\" --repo ${ctx.owner}/${ctx.repo} --color \"${CLOSED_OUT_COLOR}\" --description \"Verified and closed out\" --force 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n // Add the label\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --add-label \"${CLOSED_OUT_LABEL}\"`,\n { encoding: 'utf-8' }\n );\n // Remove all current-phase workflow labels (single source: WORKFLOW_LABELS)\n for (const label of WORKFLOW_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n for (const label of POST_MERGE_RESIDUE_LABELS) {\n await execAsync(\n `gh issue edit ${ctx.number} --repo ${ctx.owner}/${ctx.repo} --remove-label \"${label}\" 2>/dev/null || true`,\n { encoding: 'utf-8' }\n );\n }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: `Added '${CLOSED_OUT_LABEL}' label` });\n } else {\n // Linear: add label if possible\n try {\n const linearApiKey = await getLinearApiKey();\n if (linearApiKey) {\n const { LinearClient } = await import('@linear/sdk');\n const client = new LinearClient({ apiKey: linearApiKey });\n const issueNum = extractNumberSync(ctx.issueId);\n const teamKey = extractPrefixSync(ctx.issueId);\n if (issueNum !== null && teamKey !== null) {\n const results = await client.issues({\n filter: {\n number: { eq: issueNum },\n team: { key: { eq: teamKey } },\n },\n first: 1,\n });\n if (results.nodes.length > 0) {\n const issue = results.nodes[0];\n // Find or create the closed-out label\n const labels = await client.issueLabels({ filter: { name: { eq: CLOSED_OUT_LABEL } } });\n let labelId: string;\n if (labels.nodes.length > 0) {\n labelId = labels.nodes[0].id;\n } else {\n const created = await client.createIssueLabel({ name: CLOSED_OUT_LABEL, color: `#${CLOSED_OUT_COLOR}` });\n const createdLabel = await created.issueLabel;\n labelId = createdLabel ? createdLabel.id : '';\n }\n if (labelId) {\n const existingLabels = await issue.labels();\n const residueLabelNames = new Set(POST_MERGE_RESIDUE_LABELS);\n const labelIds = existingLabels.nodes\n .filter(l => !residueLabelNames.has(l.name))\n .map(l => l.id);\n if (!labelIds.includes(labelId)) {\n labelIds.push(labelId);\n await issue.update({ labelIds });\n }\n }\n }\n }\n }\n } catch { /* Non-fatal for Linear */ }\n steps.push({ name: 'Apply closed-out label', status: 'passed', message: 'Label applied' });\n }\n } catch (err) {\n steps.push({ name: 'Apply closed-out label', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 8: Mark the durable pipeline journal terminal before clearing the DB cache.\n try {\n const { markRecordPipelineClosedOutSync } = await import('./pan-dir/records.js');\n markRecordPipelineClosedOutSync({ name: 'inferred', path: ctx.projectPath }, ctx.issueId.toUpperCase());\n steps.push({ name: 'Mark pipeline terminal', status: 'passed', message: 'Pipeline journal marked closed-out' });\n } catch (err) {\n steps.push({ name: 'Mark pipeline terminal', status: 'skipped', message: `Warning: ${(err as Error).message}` });\n }\n\n // Step 9: Clear review status\n try {\n // Dynamically import to avoid circular dependency with server\n const { clearReviewStatus } = await import('./review-status.js');\n clearReviewStatus(ctx.issueId.toUpperCase());\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared' });\n } catch {\n // review-status module may not be available in CLI context\n // Try cleaning the file directly\n try {\n const statusFile = join(OVERDECK_HOME, 'review-status.json');\n if (existsSync(statusFile)) {\n const data = JSON.parse(readFileSync(statusFile, 'utf-8'));\n const upperKey = ctx.issueId.toUpperCase();\n if (data[upperKey]) {\n delete data[upperKey];\n const { writeFileSync } = await import('fs');\n writeFileSync(statusFile, JSON.stringify(data, null, 2));\n }\n }\n steps.push({ name: 'Clear review status', status: 'passed', message: 'Review status cleared (direct)' });\n } catch (innerErr) {\n steps.push({ name: 'Clear review status', status: 'skipped', message: `Warning: ${(innerErr as Error).message}` });\n }\n }\n\n return { success: true, issueId: ctx.issueId, steps };\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** A close-out ceremony step failed unexpectedly outside its recovery branches. */\nexport class CloseOutError extends Data.TaggedError('CloseOutError')<{\n readonly issueId: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect-native executeCloseOut. The underlying ceremony already aggregates\n * per-step results into the returned payload (success boolean + steps). The\n * Effect variant only fails with CloseOutError if the call itself throws\n * (e.g. dynamic import of @linear/sdk explodes). Step-level failures stay\n * in the returned `success/steps` shape so callers can display them.\n */\nexport const executeCloseOut = (\n ctx: CloseOutContext,\n): Effect.Effect<CloseOutResult, CloseOutError> =>\n Effect.tryPromise({\n try: () => executeCloseOutPromise(ctx),\n catch: (cause) =>\n new CloseOutError({\n issueId: ctx.issueId,\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","import { existsSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nimport { isBranchMerged } from '../close-out.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { killSession, listSessionNames } from '../tmux.js';\nimport { teardownWorkspaceDockerByNamePromise } from '../workspace-manager/docker.js';\n\nconst execAsync = promisify(exec);\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport async function reapIssueResidue(projectPath: string, issueId: string): Promise<string[]> {\n const actions: string[] = [];\n const issueLower = issueId.toLowerCase();\n const branchName = `feature/${issueLower}`;\n\n try {\n const sessions = await Effect.runPromise(listSessionNames());\n const exactSessions = new Set([\n `agent-${issueLower}`,\n `test-${issueLower}`,\n `merge-${issueLower}`,\n ]);\n const reviewRegex = new RegExp(`^review-${escapeRegex(issueLower)}-\\\\d+`);\n for (const session of sessions) {\n if (!exactSessions.has(session) && !reviewRegex.test(session)) continue;\n try {\n await Effect.runPromise(killSession(session));\n actions.push(`killed tmux session ${session}`);\n } catch {\n // Session may already be gone.\n }\n }\n } catch {\n // tmux server may not be running.\n }\n\n const merged = await isBranchMerged(branchName, projectPath);\n if (merged.status === 'unmerged') {\n actions.push(`skipped reap for ${issueId} — branch unmerged`);\n return actions;\n }\n\n // Remove Docker stack by name, independent of whether the workspace dir still exists.\n try {\n const teardownResult = await teardownWorkspaceDockerByNamePromise(issueLower);\n if (teardownResult.networkRemoved) {\n actions.push(`removed Docker stack overdeck-feature-${issueLower}`);\n } else {\n actions.push(`Docker network overdeck-feature-${issueLower}_devnet still present`);\n }\n } catch (err) {\n actions.push(`Docker teardown failed for ${issueId}: ${(err as Error).message}`);\n }\n\n const workspacePath = join(projectPath, 'workspaces', `feature-${issueLower}`);\n if (existsSync(workspacePath)) {\n try {\n await execAsync(`git worktree remove \"${workspacePath}\" --force`, { cwd: projectPath });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n try {\n rmSync(workspacePath, { recursive: true, force: true });\n actions.push(`removed workspace ${workspacePath}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n }\n\n try {\n await execAsync(`git branch -D \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted local branch ${branchName}`);\n } catch {\n // Branch may not exist locally.\n }\n\n try {\n await execAsync(`git push origin --delete \"${branchName}\"`, { cwd: projectPath });\n actions.push(`deleted remote branch ${branchName}`);\n } catch {\n // Branch may not exist remotely.\n }\n\n for (const agentDirName of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n const agentDir = join(AGENTS_DIR, agentDirName);\n if (!existsSync(agentDir)) continue;\n try {\n rmSync(agentDir, { recursive: true, force: true });\n actions.push(`removed agent state ${agentDirName}`);\n } catch {\n // Already gone or inaccessible.\n }\n }\n\n return actions;\n}\n","import { Effect } from 'effect';\nimport { existsSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { listRunningAgents, stopAgent } from '../agents.js';\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { AGENTS_DIR } from '../paths.js';\nimport { listProjectsSync } from '../projects.js';\nimport { resolveProjectForIssue } from '../pan-dir/record.js';\nimport { listSessionNames } from '../tmux.js';\nimport { isIssueClosed } from './issue-closed.js';\nimport { reapIssueResidue } from './reap-issue-residue.js';\n\n// Sessions reaped by NAME as a backstop: inspect sessions never have agent\n// state, and strike sessions can outlive their state entry (e.g. state already\n// stopped or removed while the tmux session idles — PAN-1721).\nfunction issueIdFromStatelessSession(sessionName: string): string | null {\n const match = sessionName.match(/^(?:inspect|strike)-([a-z0-9]+-\\d+)(?:-|$)/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromFeatureWorkspace(entryName: string): string | null {\n const match = entryName.match(/^feature-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nfunction issueIdFromAgentDir(entryName: string): string | null {\n const match = entryName.match(/^agent-([a-z]+-\\d+)$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\nasync function isClosedIssue(\n issueId: string,\n closedChecks: Map<string, Promise<boolean>>,\n): Promise<boolean> {\n let promise = closedChecks.get(issueId);\n if (!promise) {\n promise = isIssueClosed(issueId);\n closedChecks.set(issueId, promise);\n }\n return promise;\n}\n\nasync function reapClosedIssueResidue(\n projectPath: string,\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const key = `${projectPath}:${issueId}`;\n if (reapedIssueKeys.has(key)) return;\n reapedIssueKeys.add(key);\n actions.push(...await reapIssueResidue(projectPath, issueId));\n}\n\nasync function reapResolvedIssueResidue(\n issueId: string,\n actions: string[],\n reapedIssueKeys: Set<string>,\n): Promise<void> {\n const project = resolveProjectForIssue(issueId);\n if (!project) return;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n}\n\nfunction listConfiguredProjects(): Array<{ path: string }> {\n try {\n return listProjectsSync().map(({ config }) => ({ path: config.path }));\n } catch {\n return [];\n }\n}\n\nfunction listDirectoryNames(path: string): string[] {\n if (!existsSync(path)) return [];\n try {\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n}\n\nasync function stopClosedAgent(agentId: string, issueId: string, actions: string[]): Promise<void> {\n await Effect.runPromise(stopAgent(agentId));\n const action = `Reaped ${agentId} — parent issue ${issueId} is closed`;\n actions.push(action);\n console.log(`[deacon] ${action}`);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'info',\n issueId,\n message: `[deacon] reaped ${agentId} — parent issue ${issueId} is closed`,\n });\n}\n\n/**\n * PAN-1908: reactive closed-issue reaper. When an issue is closed, stop all\n * running agents and stateless sessions belonging to it without scanning the\n * full agent table or tmux session list.\n */\nexport async function handleIssueStatusChangedClosed(issueId: string): Promise<string[]> {\n const upperIssueId = issueId.trim().toUpperCase();\n if (!upperIssueId) return [];\n\n // Guard against spurious events: only act when the issue is actually closed.\n if (!await isIssueClosed(upperIssueId)) return [];\n\n const actions: string[] = [];\n const reapedAgentIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n\n // Stop running agents for this issue.\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const agentIssueId = (agent.issueId ?? '').trim().toUpperCase();\n if (agentIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(agent.id, upperIssueId, actions);\n reapedAgentIds.add(agent.id);\n }\n\n // Stop stateless inspect/strike sessions for this issue.\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n const sessionIssueId = issueIdFromStatelessSession(sessionName);\n if (sessionIssueId !== upperIssueId) continue;\n\n await stopClosedAgent(sessionName, upperIssueId, actions);\n reapedAgentIds.add(sessionName);\n }\n\n await reapResolvedIssueResidue(upperIssueId, actions, reapedIssueKeys);\n return actions;\n}\n\n/**\n * PAN-1908: keep a thin table+session safety net for dropped closed-issue\n * events. The primary path is reactive via handleIssueStatusChangedClosed.\n */\nexport async function reconcileClosedIssueAgents(): Promise<string[]> {\n const actions: string[] = [];\n const closedChecks = new Map<string, Promise<boolean>>();\n const reapedAgentIds = new Set<string>();\n const closedIssueIds = new Set<string>();\n const reapedIssueKeys = new Set<string>();\n const agents = await Effect.runPromise(listRunningAgents());\n\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(agent.id, issueId, actions);\n reapedAgentIds.add(agent.id);\n closedIssueIds.add(issueId);\n }\n\n const sessionNames = await Effect.runPromise(listSessionNames());\n for (const sessionName of sessionNames) {\n if (reapedAgentIds.has(sessionName)) continue;\n\n const issueId = issueIdFromStatelessSession(sessionName);\n if (!issueId) continue;\n\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n\n await stopClosedAgent(sessionName, issueId, actions);\n reapedAgentIds.add(sessionName);\n closedIssueIds.add(issueId);\n }\n\n for (const issueId of closedIssueIds) {\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n for (const project of listConfiguredProjects()) {\n const workspacesDir = join(project.path, 'workspaces');\n for (const entryName of listDirectoryNames(workspacesDir)) {\n const issueId = issueIdFromFeatureWorkspace(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapClosedIssueResidue(project.path, issueId, actions, reapedIssueKeys);\n }\n }\n\n for (const entryName of listDirectoryNames(AGENTS_DIR)) {\n const issueId = issueIdFromAgentDir(entryName);\n if (!issueId) continue;\n if (!await isClosedIssue(issueId, closedChecks)) continue;\n await reapResolvedIssueResidue(issueId, actions, reapedIssueKeys);\n }\n\n return actions;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { t as handleIssueStatusChangedClosed } from "./closed-issue-reaper-DRqK5k39.js";
1
+ import { t as handleIssueStatusChangedClosed } from "./closed-issue-reaper-D7XPq5pY.js";
2
2
  export { handleIssueStatusChangedClosed };
@@ -1,3 +1,3 @@
1
- import { a as resolveConflictGate, i as init_conflict_gate, t as buildRealConflictGateDeps } from "./conflict-gate-BhKvrPxn.js";
1
+ import { a as resolveConflictGate, i as init_conflict_gate, t as buildRealConflictGateDeps } from "./conflict-gate-FFEvTDVA.js";
2
2
  init_conflict_gate();
3
3
  export { buildRealConflictGateDeps, resolveConflictGate };
@@ -1,7 +1,7 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime-DNCzzKnI.js";
2
2
  import { a as init_activity_logger, n as emitActivityEntrySync } from "./activity-logger-pcbf-e30.js";
3
- import { c as init_review_status, s as getReviewStatusSync, y as setReviewStatusSync } from "./review-status-BQjqQXZG.js";
4
- import { W as spawnRun, j as messageAgent, t as init_agents } from "./agents-DHvsjsuP.js";
3
+ import { c as init_review_status, s as getReviewStatusSync, y as setReviewStatusSync } from "./review-status-B5ZASOo4.js";
4
+ import { W as spawnRun, j as messageAgent, t as init_agents } from "./agents-C7V4odQ6.js";
5
5
  import { exec } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
  //#region ../../lib/cloister/conflict-gate.ts
@@ -230,4 +230,4 @@ var init_conflict_gate = __esmMin((() => {
230
230
  //#endregion
231
231
  export { resolveConflictGate as a, init_conflict_gate as i, checkBranchMergeability as n, getCachedConflictGateMergeability as r, buildRealConflictGateDeps as t };
232
232
 
233
- //# sourceMappingURL=conflict-gate-BhKvrPxn.js.map
233
+ //# sourceMappingURL=conflict-gate-FFEvTDVA.js.map