@overdeck/core 0.45.10 → 0.45.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
  2. package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
  3. package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
  4. package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
  5. package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
  6. package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
  7. package/dist/cli/index.js +45 -45
  8. package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
  9. package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
  10. package/dist/cli-MTB9IOoR.js +3 -0
  11. package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
  12. package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
  13. package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
  14. package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
  15. package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
  16. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
  17. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
  18. package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
  19. package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
  20. package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
  21. package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
  22. package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
  23. package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
  24. package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
  25. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
  26. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
  27. package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
  28. package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
  29. package/dist/dashboard/cli-DCqy_wFa.js +3 -0
  30. package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
  31. package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
  32. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
  33. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
  34. package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
  35. package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
  36. package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
  37. package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
  38. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
  39. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
  40. package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
  41. package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
  42. package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
  43. package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
  44. package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
  45. package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
  46. package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
  47. package/dist/dashboard/deacon.js +4 -4
  48. package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
  49. package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
  50. package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
  51. package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
  52. package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
  53. package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
  54. package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
  55. package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
  56. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
  57. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
  58. package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
  59. package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
  60. package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
  61. package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
  62. package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
  63. package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
  64. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
  65. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
  66. package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
  67. package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
  68. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
  69. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
  70. package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
  71. package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
  72. package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
  73. package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
  74. package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
  75. package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
  76. package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
  77. package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
  78. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
  79. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
  80. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
  81. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
  82. package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
  83. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
  84. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
  85. package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
  86. package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
  87. package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
  88. package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
  89. package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
  90. package/dist/dashboard/review-agent-DTINEffB.js +3 -0
  91. package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
  92. package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
  93. package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
  94. package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
  95. package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
  96. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
  97. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
  98. package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
  99. package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
  100. package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
  101. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
  102. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
  103. package/dist/dashboard/server.js +66 -55
  104. package/dist/dashboard/server.js.map +1 -1
  105. package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
  106. package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
  107. package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
  108. package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
  109. package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
  110. package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
  111. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
  112. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
  113. package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
  114. package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
  115. package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
  116. package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
  117. package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
  118. package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
  119. package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
  120. package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
  121. package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
  122. package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
  123. package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
  124. package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
  125. package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
  126. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
  127. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
  128. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
  129. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
  130. package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
  131. package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
  132. package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
  133. package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
  134. package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
  135. package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
  136. package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
  137. package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
  138. package/dist/dashboard/workflows-DVwXauef.js +2 -0
  139. package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
  140. package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
  141. package/dist/docs-index.sqlite +0 -0
  142. package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
  143. package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
  144. package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
  145. package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
  146. package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
  147. package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
  148. package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
  149. package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
  150. package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
  151. package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
  152. package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
  153. package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
  154. package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
  155. package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
  156. package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
  157. package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
  158. package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
  159. package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
  160. package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
  161. package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
  162. package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
  163. package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
  164. package/dist/records-backfill-BQcmTmwF.js +2 -0
  165. package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
  166. package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
  167. package/dist/resume-Bn4kDk9x.js +3 -0
  168. package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
  169. package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
  170. package/dist/review-agent-hLbRRxZF.js +3 -0
  171. package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
  172. package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
  173. package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
  174. package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
  175. package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
  176. package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
  177. package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
  178. package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
  179. package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
  180. package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
  181. package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
  182. package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
  183. package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
  184. package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
  185. package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
  186. package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
  187. package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
  188. package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
  189. package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
  190. package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
  191. package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
  192. package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
  193. package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
  194. package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
  195. package/dist/sync-CylE_SjB.js +2 -0
  196. package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
  197. package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
  198. package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
  199. package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
  200. package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
  201. package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
  202. package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
  203. package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
  204. package/package.json +1 -1
  205. package/scripts/file-size-baseline.txt +1 -1
  206. package/dist/cli-DBWG12XK.js +0 -3
  207. package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
  208. package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
  209. package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
  210. package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
  211. package/dist/dashboard/resume-DnIsmIgC.js +0 -3
  212. package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
  213. package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
  214. package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
  215. package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
  216. package/dist/records-backfill-DEgtRyKL.js +0 -2
  217. package/dist/resume-DGymuXZM.js +0 -3
  218. package/dist/review-agent-DjVadURa.js +0 -3
  219. package/dist/sync-4jqukkxz.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"specialists-BYi0k8CZ.js","names":[],"sources":["../../src/lib/cost-parsers/jsonl-parser.ts","../../src/lib/cloister/specialists.ts"],"sourcesContent":["/**\n * Claude Code JSONL Parser\n *\n * Parse token usage from Claude Code session files.\n * Session files are stored at: ~/.claude/projects/<project-path-hash>/<session-id>.jsonl\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { Effect } from 'effect';\nimport { encodeClaudeProjectDir } from '../paths.js';\nimport { TokenUsage, calculateCostSync, getPricingSync, AIProvider, logCostSync, CostEntry } from '../cost.js';\nimport { FsError } from '../errors.js';\n\n// Claude Code JSONL message format\nexport interface ClaudeMessage {\n sessionId?: string;\n timestamp?: string;\n parentMessageId?: string;\n // Claude Code per-API-request id. Stable across the multiple JSONL lines of one response.\n requestId?: string;\n message?: {\n id?: string;\n role?: 'user' | 'assistant';\n model?: string;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation?: {\n ephemeral_1h_input_tokens?: number;\n ephemeral_5m_input_tokens?: number;\n };\n };\n };\n // Some messages have usage at top level\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation?: {\n ephemeral_1h_input_tokens?: number;\n ephemeral_5m_input_tokens?: number;\n };\n };\n model?: string;\n}\n\n// Parsed session usage\nexport interface SessionUsage {\n sessionId: string;\n sessionFile: string;\n startTime: string;\n endTime: string;\n model: string; // Display name (normalized). Shows \"sonnet-4-6 → opus-4-6\" for upgrades\n usage: TokenUsage; // Total tokens across all models\n cost: number; // DEPRECATED: Uses first-model pricing (kept for backward compatibility)\n cost_v2?: number; // NEW: Accurate per-message pricing\n cwd?: string;\n messageCount: number;\n modelBreakdown?: Record<string, { // NEW: Cost/token breakdown by exact model ID\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n // Optional per-model cache token counts (ohmypi/omp sessions; absent for Claude Code sessions)\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }>;\n}\n\n// Claude projects directory\nfunction getClaudeProjectsDir(): string {\n return process.env.CLAUDE_PROJECTS_DIR || join(homedir(), '.claude', 'projects');\n}\n\n/**\n * Get all Claude Code project directories\n */\nexport function getProjectDirsSync(): string[] {\n const claudeProjectsDir = getClaudeProjectsDir();\n if (!existsSync(claudeProjectsDir)) {\n return [];\n }\n\n return readdirSync(claudeProjectsDir)\n .map(name => join(claudeProjectsDir, name))\n .filter(path => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Get session JSONL files for a project directory\n */\nexport function getSessionFilesSync(projectDir: string): string[] {\n if (!existsSync(projectDir)) {\n return [];\n }\n\n return readdirSync(projectDir)\n .filter(name => name.endsWith('.jsonl'))\n .map(name => join(projectDir, name))\n .sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Get all session files across all projects\n */\nexport function getAllSessionFilesSync(): string[] {\n const files: string[] = [];\n\n for (const projectDir of getProjectDirsSync()) {\n files.push(...getSessionFilesSync(projectDir));\n }\n\n return files.sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Normalize model name for pricing lookup\n */\nexport function normalizeModelName(model: string): { provider: AIProvider; model: string } {\n // Claude models\n if (model.includes('claude')) {\n let normalizedModel = model;\n\n // Map full model IDs to pricing model names\n // Order matters - check more specific patterns first\n\n // Fable models (Mythos-class flagship)\n if (model.includes('fable-5') || model.includes('fable.5') || model.includes('fable')) {\n normalizedModel = 'claude-fable-5';\n }\n\n // Opus models\n if (model.includes('opus-4-8') || model.includes('opus-4.8')) {\n normalizedModel = 'claude-opus-4-8';\n } else if (model.includes('opus-4-7') || model.includes('opus-4.7')) {\n normalizedModel = 'claude-opus-4-7';\n } else if (model.includes('opus-4-6') || model.includes('opus-4.6')) {\n normalizedModel = 'claude-opus-4-6';\n } else if (model.includes('opus-4-1') || model.includes('opus-4.1')) {\n normalizedModel = 'claude-opus-4-1';\n } else if (model.includes('opus-4') || model.includes('opus')) {\n normalizedModel = 'claude-opus-4';\n }\n\n // Sonnet models\n if (model.includes('sonnet-5') || model.includes('sonnet.5')) {\n normalizedModel = 'claude-sonnet-5';\n } else if (model.includes('sonnet-4-6') || model.includes('sonnet-4.6')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4-5') || model.includes('sonnet-4.5')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4') || model.includes('sonnet')) {\n normalizedModel = 'claude-sonnet-4';\n }\n\n // Haiku models - default to 4.5 (current), support 3 for legacy\n if (model.includes('haiku-4-5') || model.includes('haiku-4.5')) {\n normalizedModel = 'claude-haiku-4-5';\n } else if (model.includes('haiku-3')) {\n normalizedModel = 'claude-haiku-3';\n } else if (model.includes('haiku')) {\n normalizedModel = 'claude-haiku-4-5'; // Default to current model\n }\n\n return { provider: 'anthropic', model: normalizedModel };\n }\n\n // OpenAI models\n if (model.includes('gpt')) {\n return { provider: 'openai', model };\n }\n\n // Google models\n if (model.includes('gemini')) {\n return { provider: 'google', model };\n }\n\n // MiniMax models\n if (model.includes('minimax')) {\n return { provider: 'custom', model };\n }\n\n // Default to anthropic/claude\n return { provider: 'anthropic', model: 'claude-sonnet-4' };\n}\n\n/**\n * Parse a Claude Code session JSONL file and extract usage with per-message cost calculation\n *\n * This function calculates costs accurately for sessions that span multiple models\n * (e.g., when Claude Max auto-upgrades from Sonnet to Opus mid-conversation).\n *\n * Cost Calculation:\n * - `cost_v2`: Accurate per-message pricing. Each message is costed using its own model's pricing.\n * - `cost`: DEPRECATED. Uses first model's pricing for all messages (kept for backward compatibility).\n *\n * Model Display:\n * - Single model: \"claude-sonnet-4-6\" (normalized name)\n * - Multiple models: \"claude-sonnet-4-6 → claude-opus-4-6\" (progression of normalized names)\n *\n * Model Breakdown:\n * - Keys are exact model IDs (e.g., \"claude-sonnet-4-5-20250929\")\n * - Values contain per-model cost, token counts, and message count\n *\n * @param sessionFile - Path to the .jsonl session file\n * @returns Session usage summary with accurate multi-model costing, or null if no usage found\n */\nexport function parseClaudeSessionSync(sessionFile: string): SessionUsage | null {\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n const content = readFileSync(sessionFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n let sessionId = '';\n let startTime = '';\n let endTime = '';\n let primaryModel = '';\n let messageCount = 0;\n\n const totalUsage: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n\n // NEW: Per-message cost tracking\n const modelBreakdown: Record<string, {\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }> = {};\n let totalCostV2 = 0;\n\n // Claude Code repeats the same `usage` on every JSONL line of one API response\n // (text line, each tool_use line, …). Dedup on requestId/message.id so a multi-block\n // turn is counted once instead of inflating tokens/cost ~2-3×.\n const countedUsageIds = new Set<string>();\n\n for (const line of lines) {\n try {\n const msg: ClaudeMessage = JSON.parse(line);\n\n // Extract session ID from first message\n if (msg.sessionId && !sessionId) {\n sessionId = msg.sessionId;\n }\n\n // Track timestamps\n if (msg.timestamp) {\n if (!startTime || msg.timestamp < startTime) {\n startTime = msg.timestamp;\n }\n if (!endTime || msg.timestamp > endTime) {\n endTime = msg.timestamp;\n }\n }\n\n // Extract usage - can be in message.usage or top-level usage\n const usage = msg.message?.usage || msg.usage;\n const modelId = msg.message?.model || msg.model; // Exact model ID\n const usageId = msg.requestId ?? msg.message?.id;\n\n if (usage && (usageId === undefined || !countedUsageIds.has(usageId))) {\n if (usageId !== undefined) countedUsageIds.add(usageId);\n // Accumulate total tokens (existing behavior)\n totalUsage.inputTokens += usage.input_tokens || 0;\n totalUsage.outputTokens += usage.output_tokens || 0;\n totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);\n totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);\n messageCount++;\n\n // NEW: Calculate cost for THIS message using THIS message's model\n if (modelId) {\n // Normalize model name for pricing lookup\n const { provider, model: normalizedModel } = normalizeModelName(modelId);\n const pricing = getPricingSync(provider, normalizedModel);\n\n if (pricing) {\n // Create message-specific usage object\n const msgUsage: TokenUsage = {\n inputTokens: usage.input_tokens || 0,\n outputTokens: usage.output_tokens || 0,\n cacheReadTokens: usage.cache_read_input_tokens || 0,\n cacheWriteTokens: usage.cache_creation_input_tokens || 0,\n cacheTTL: usage.cache_creation?.ephemeral_1h_input_tokens\n && !usage.cache_creation?.ephemeral_5m_input_tokens\n ? '1h'\n : '5m',\n };\n\n // Calculate cost for this message\n const msgCost = calculateCostSync(msgUsage, pricing);\n totalCostV2 += msgCost;\n\n // Track breakdown by exact model ID\n if (!modelBreakdown[modelId]) {\n modelBreakdown[modelId] = {\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n messageCount: 0,\n };\n }\n modelBreakdown[modelId].cost += msgCost;\n modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;\n modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;\n modelBreakdown[modelId].cacheReadTokens = (modelBreakdown[modelId].cacheReadTokens || 0)\n + (msgUsage.cacheReadTokens || 0);\n modelBreakdown[modelId].cacheWriteTokens = (modelBreakdown[modelId].cacheWriteTokens || 0)\n + (msgUsage.cacheWriteTokens || 0);\n modelBreakdown[modelId].messageCount++;\n }\n }\n }\n\n // Track primary model (first model found - for backward compatibility)\n if (modelId && !primaryModel) {\n primaryModel = modelId;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // If no usage found, return null\n if (totalUsage.inputTokens === 0 && totalUsage.outputTokens === 0) {\n return null;\n }\n\n // Use filename as session ID if not found in messages\n if (!sessionId) {\n sessionId = basename(sessionFile, '.jsonl');\n }\n\n // Default model if not found\n if (!primaryModel) {\n primaryModel = 'claude-sonnet-4';\n }\n\n // Generate model display string (normalized names)\n // For multi-model sessions: \"claude-sonnet-4-6 → claude-opus-4-6\"\n // For single-model sessions: \"claude-sonnet-4-6\"\n const normalizedModels = Object.keys(modelBreakdown)\n .map(id => normalizeModelName(id).model);\n const modelDisplay = normalizedModels.length > 0\n ? (normalizedModels.length > 1\n ? normalizedModels.join(' → ')\n : normalizedModels[0])\n : normalizeModelName(primaryModel).model;\n\n // DEPRECATED: Calculate cost using first model (for backward compatibility)\n const { provider, model } = normalizeModelName(primaryModel);\n const pricing = getPricingSync(provider, model);\n const cost = pricing ? calculateCostSync(totalUsage, pricing) : 0;\n\n return {\n sessionId,\n sessionFile,\n startTime: startTime || new Date().toISOString(),\n endTime: endTime || new Date().toISOString(),\n model: modelDisplay,\n usage: totalUsage,\n cost, // DEPRECATED: First-model pricing\n cost_v2: totalCostV2 > 0 ? totalCostV2 : undefined, // NEW: Accurate per-message pricing\n messageCount,\n modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : undefined, // NEW: Cost breakdown by model\n };\n}\n\n/**\n * Parse all sessions and return usage summaries\n */\nexport function parseAllSessionsSync(maxAge?: number): SessionUsage[] {\n const sessions: SessionUsage[] = [];\n const cutoffTime = maxAge ? Date.now() - maxAge : 0;\n\n for (const file of getAllSessionFilesSync()) {\n try {\n const stat = statSync(file);\n if (cutoffTime && stat.mtime.getTime() < cutoffTime) {\n continue;\n }\n\n const usage = parseClaudeSessionSync(file);\n if (usage) {\n sessions.push(usage);\n }\n } catch {\n // Skip files that can't be read\n }\n }\n\n return sessions;\n}\n\n/**\n * Get recent sessions (last N days)\n */\nexport function getRecentSessionsSync(days: number = 7): SessionUsage[] {\n const maxAge = days * 24 * 60 * 60 * 1000;\n return parseAllSessionsSync(maxAge);\n}\n\n/**\n * Get the active session model for a workspace\n * Returns the full model ID (e.g., \"claude-sonnet-4-5-20250929\") from the most recent session file\n *\n * NOTE: Claude Max can auto-upgrade models mid-session (e.g., Sonnet → Opus).\n * We read from the END of the file to get the CURRENT model, not the initial one.\n */\nexport function getActiveSessionModelSync(workspacePath: string): string | null {\n try {\n // Convert workspace path to Claude project dir name\n // e.g., /home/user/projects/myn/workspaces/feature-min-664\n // -> -home-user-projects-myn-workspaces-feature-min-664\n // NOTE: The directory name KEEPS the leading dash\n const projectDirName = encodeClaudeProjectDir(workspacePath);\n const projectDir = join(getClaudeProjectsDir(), projectDirName);\n\n // Find most recently modified session file\n const sessions = getSessionFilesSync(projectDir);\n if (sessions.length === 0) {\n return null;\n }\n\n // Parse the most recent session file to find model\n const mostRecentSession = sessions[0]; // Already sorted by mtime\n const content = readFileSync(mostRecentSession, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n // Read from END of file to get CURRENT model (may have been auto-upgraded by Claude Max)\n // Look at last 100 lines to find the most recent model entry\n const searchLines = lines.slice(-100);\n for (let i = searchLines.length - 1; i >= 0; i--) {\n try {\n const msg: ClaudeMessage = JSON.parse(searchLines[i]);\n const model = msg.message?.model || msg.model;\n // Skip synthetic/placeholder model values\n if (model && model !== '<synthetic>') {\n return model; // Return full model ID\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // Fallback: check first few lines if nothing found at end\n for (let i = 0; i < Math.min(lines.length, 10); i++) {\n try {\n const msg: ClaudeMessage = JSON.parse(lines[i]);\n const model = msg.message?.model || msg.model;\n if (model && model !== '<synthetic>') {\n return model;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return null;\n } catch (error) {\n console.warn('Failed to get active session model:', error);\n return null;\n }\n}\n\n/**\n * Import session usage to cost log\n */\nexport function importSessionToCostLog(\n session: SessionUsage,\n options: {\n issueId?: string;\n agentId?: string;\n operation?: string;\n } = {}\n): CostEntry | null {\n const { provider, model } = normalizeModelName(session.model);\n const pricing = getPricingSync(provider, model);\n\n if (!pricing) {\n console.warn(`No pricing found for ${session.model}`);\n return null;\n }\n\n return logCostSync({\n provider,\n model,\n usage: session.usage,\n cost: session.cost,\n currency: 'USD',\n operation: options.operation || 'claude_session',\n issueId: options.issueId,\n agentId: options.agentId,\n metadata: {\n sessionId: session.sessionId,\n sessionFile: session.sessionFile,\n startTime: session.startTime,\n endTime: session.endTime,\n messageCount: session.messageCount,\n },\n });\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** Effect variant of getProjectDirs. */\nexport const getProjectDirs = (): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getProjectDirsSync(),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getProjectDirs', cause }),\n });\n\n/** Effect variant of getSessionFiles. */\nexport const getSessionFiles = (\n projectDir: string,\n): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getSessionFilesSync(projectDir),\n catch: (cause) => new FsError({ path: projectDir, operation: 'getSessionFiles', cause }),\n });\n\n/** Effect variant of getAllSessionFiles. */\nexport const getAllSessionFiles = (): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getAllSessionFilesSync(),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getAllSessionFiles', cause }),\n });\n\n/** Effect variant of parseClaudeSession. */\nexport const parseClaudeSession = (\n sessionFile: string,\n): Effect.Effect<SessionUsage | null, FsError> =>\n Effect.try({\n try: () => parseClaudeSessionSync(sessionFile),\n catch: (cause) => new FsError({ path: sessionFile, operation: 'parseClaudeSession', cause }),\n });\n\n/** Effect variant of parseAllSessions. */\nexport const parseAllSessions = (\n maxAge?: number,\n): Effect.Effect<SessionUsage[], FsError> =>\n Effect.try({\n try: () => parseAllSessionsSync(maxAge),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'parseAllSessions', cause }),\n });\n\n/** Effect variant of getRecentSessions. */\nexport const getRecentSessions = (\n days: number = 7,\n): Effect.Effect<SessionUsage[], FsError> =>\n Effect.try({\n try: () => getRecentSessionsSync(days),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getRecentSessions', cause }),\n });\n\n/** Effect variant of getActiveSessionModel. */\nexport const getActiveSessionModel = (\n workspacePath: string,\n): Effect.Effect<string | null, FsError> =>\n Effect.try({\n try: () => getActiveSessionModelSync(workspacePath),\n catch: (cause) => new FsError({ path: workspacePath, operation: 'getActiveSessionModel', cause }),\n });\n","/**\n * Cloister Specialist Agents\n *\n * Manages long-running specialist agents that can be woken up on demand.\n * Specialists maintain context across invocations via session files.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { randomUUID, createHash } from 'crypto';\nimport { Effect } from 'effect';\nimport { AGENTS_DIR, OVERDECK_HOME } from '../paths.js';\nimport { getDevrootPathSync } from '../config.js';\nimport { getClaudePermissionFlagsStringSync } from '../claude-permissions.js';\nimport { getProjectSync } from '../projects.js';\nimport { getAllSessionFilesSync, parseClaudeSessionSync } from '../cost-parsers/jsonl-parser.js';\nimport { createSpecialistHandoff, logSpecialistHandoff } from './specialist-handoff-logger.js';\nimport type { ModelId } from '../settings.js';\nimport { loadCloisterConfigSync } from './config.js';\nimport { readCavemanVariant } from '../caveman/workspace.js';\nimport { getProviderForModelSync, setupCredentialFileAuthSync, clearCredentialFileAuthSync } from '../providers.js';\nimport { getProviderEnvForModel } from '../agents.js';\nimport { generateLauncherScriptSync, generateLauncherWrapperSync } from '../launcher-generator.js';\nimport { resolveHarness } from '../harness-resolve.js';\nimport { killSession, listPaneValues, sessionExists } from '../tmux.js';\nimport { notifyPipelineSync } from '../pipeline-notifier.js';\nimport { isTaskReadySync } from './task-readiness.js';\n\nconst execAsync = promisify(exec);\n\nfunction roleForSpecialistModel(specialistType: string): { role: 'plan' | 'work' | 'review' | 'test' | 'ship'; subRole?: string } {\n const normalized = specialistType.replace(/-agent$/, '');\n if (normalized === 'inspect') return { role: 'work', subRole: 'inspect' };\n if (normalized === 'inspect-deep') return { role: 'work', subRole: 'inspect-deep' };\n if (normalized === 'review') return { role: 'review' };\n if (normalized === 'test' || normalized === 'uat') return { role: 'test' };\n if (normalized === 'merge' || normalized === 'ship') return { role: 'ship' };\n if (normalized === 'planning' || normalized === 'plan') return { role: 'plan' };\n return { role: 'work' };\n}\n\n\n/**\n * Resolve git directories and branch name from a workspace path.\n * Handles both monorepo (single .git at root) and polyrepo (multiple .git in subdirs).\n * When task.branch is missing, detects it from the checked-out branch in git repos.\n */\nasync function resolveWorkspaceGitInfo(workspace: string | undefined, taskBranch: string | undefined): Promise<{\n gitDirs: string[];\n branch: string;\n isPolyrepo: boolean;\n}> {\n const gitDirs: string[] = [];\n let branch = taskBranch || 'unknown';\n\n if (!workspace || workspace === 'unknown') {\n return { gitDirs, branch, isPolyrepo: false };\n }\n\n // Detect git directories\n if (existsSync(join(workspace, '.git'))) {\n gitDirs.push(workspace);\n } else {\n try {\n const entries = readdirSync(workspace, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && existsSync(join(workspace, entry.name, '.git'))) {\n gitDirs.push(join(workspace, entry.name));\n }\n }\n } catch {}\n }\n\n // Auto-resolve branch from git when not provided\n if (branch === 'unknown' && gitDirs.length > 0) {\n try {\n const { stdout } = await execAsync(\n `cd \"${gitDirs[0]}\" && git branch --show-current`,\n { encoding: 'utf-8', timeout: 5000 }\n );\n const detected = stdout.trim();\n if (detected) {\n branch = detected;\n }\n } catch {}\n }\n\n return { gitDirs, branch, isPolyrepo: gitDirs.length > 1 };\n}\n\n/**\n * Shell fragment that unsets every provider-routing env var a parent tmux server\n * may have leaked into its child sessions. The overdeck tmux server is long-lived\n * and inherits whatever env existed when it was spawned — so fresh Anthropic-model\n * agents can still see a stale ANTHROPIC_BASE_URL pointing at cliproxy, which\n * responds with \"unknown provider for model claude-*\" (PAN-705). Every launcher\n * script must run this before exec'ing claude.\n */\nconst PROVIDER_ENV_UNSETS = [\n 'ANTHROPIC_BASE_URL',\n 'ANTHROPIC_AUTH_TOKEN',\n 'OPENAI_API_KEY',\n 'GEMINI_API_KEY',\n 'API_TIMEOUT_MS',\n 'CLAUDE_CODE_API_KEY_HELPER_TTL_MS',\n];\nconst PROVIDER_UNSET_CMD = `unset ${PROVIDER_ENV_UNSETS.join(' ')}`;\n\n/**\n * Convert a providerEnv dict to bash export lines.\n * Non-Anthropic models (e.g. gpt-5.4 via cliproxy) need ANTHROPIC_BASE_URL set in the\n * script body after provider env vars are unset.\n */\nfunction buildProviderExportLines(providerEnv: Record<string, string>): string {\n const entries = Object.entries(providerEnv);\n if (entries.length === 0) return '';\n return entries.map(([k, v]) => `export ${k}=\"${v}\"`).join('\\n') + '\\n';\n}\n\n/**\n * Build tmux -e flags for environment variables\n */\nfunction buildTmuxEnvFlags(env: Record<string, string>): string {\n let flags = '';\n for (const [key, value] of Object.entries(env)) {\n flags += ` -e ${key}=\"${value.replace(/\"/g, '\\\\\"')}\"`;\n }\n return flags;\n}\n\n\nexport async function buildSpecialistBaseCommand(\n specialistType: string,\n model: string,\n sessionName?: string,\n): Promise<string> {\n const { getAgentRuntimeBaseCommand } = await import('../agents.js');\n const role = roleForSpecialistModel(specialistType).role;\n const harness = await resolveHarness({ model, role });\n const agentDefinition = specialistType.startsWith('pan-')\n ? specialistType\n : `pan-${specialistType.endsWith('-agent')\n ? specialistType.slice(0, -'-agent'.length)\n : specialistType}-agent`;\n return getAgentRuntimeBaseCommand(model, sessionName, agentDefinition, harness);\n}\n\nfunction readRecordedClaudeSessionId(tmuxSession: string): string | null {\n const sessionFile = join(AGENTS_DIR, tmuxSession, 'session.id');\n if (!existsSync(sessionFile)) return null;\n try {\n const sessionId = readFileSync(sessionFile, 'utf-8').trim();\n return sessionId || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Build shell export lines for caveman compression for specialist agents.\n *\n * Excluded: inspect-agent (its INSPECTION PASSED/BLOCKED sentinels are parsed by Cloister).\n * Uses per-specialist-type intensity from config.\n *\n * @param specialistType The specialist type (review-agent, test-agent, etc.)\n * @param workspacePath Workspace path to read the A/B variant from (may be undefined)\n * @param config Normalized caveman config\n * @returns Shell export lines to inject into the inner script\n */\nexport async function buildSpecialistCavemanExports(\n specialistType: string,\n workspacePath: string | undefined,\n config: import('../config-yaml.js').NormalizedCavemanConfig\n): Promise<string> {\n // inspect-agent: never compress — output contains sentinel strings parsed by Cloister\n if (specialistType === 'inspect-agent' || !config.enabled) return '';\n\n // Read the workspace's A/B variant if we have a workspace path\n const variant = workspacePath ? await Effect.runPromise(readCavemanVariant(workspacePath)) : 'off';\n if (variant === 'off') return '';\n if (variant === 'disabled') {\n return `export OVERDECK_CAVEMAN_VARIANT=\"${variant}\"\\n`;\n }\n\n // Map specialist type to caveman intensity mode\n const modeMap: Record<string, keyof typeof config.modes> = {\n 'review-agent': 'review',\n 'test-agent': 'test',\n 'merge-agent': 'merge',\n };\n const modeKey = modeMap[specialistType];\n if (!modeKey) return '';\n\n const mode = config.modes[modeKey];\n if (mode === 'off' || mode === 'disabled') return '';\n\n return `export CAVEMAN_DEFAULT_MODE=\"${mode}\"\\nexport OVERDECK_CAVEMAN_VARIANT=\"${variant}\"\\n`;\n}\n\nconst SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\nconst REGISTRY_FILE = join(SPECIALISTS_DIR, 'registry.json');\n\nconst SPECIALIST_AGENT_NAMES = ['merge-agent', 'review-agent', 'test-agent', 'inspect-agent', 'uat-agent'] as const;\nexport type SpecialistAgentName = typeof SPECIALIST_AGENT_NAMES[number];\n\ntype SpecialistLifecycleState = 'sleeping' | 'active' | 'uninitialized';\n\nexport interface LegacySpecialistDefinition {\n name: SpecialistAgentName;\n displayName: string;\n description: string;\n enabled: boolean;\n autoWake: boolean;\n sessionId?: string;\n lastWake?: string; // ISO 8601 timestamp\n contextTokens?: number;\n}\n\nexport interface LegacySpecialistRuntimeStatus extends LegacySpecialistDefinition {\n state: SpecialistLifecycleState;\n isRunning: boolean;\n tmuxSession?: string;\n currentIssue?: string; // Issue ID currently being worked on\n}\n\n/**\n * One step in the model resolution trace (PAN-754)\n */\nexport interface ResolutionStep {\n source: 'explicit-param' | 'role-config' | 'cloister-config' | 'fallback';\n workTypeId?: string;\n configKey?: string;\n resolvedAlias?: string;\n resolvedModel: string;\n matched: boolean;\n}\n\n/**\n * Per-project specialist metadata\n */\nexport interface ProjectSpecialistMetadata {\n runCount: number;\n lastRunAt: string | null;\n lastRunStatus: 'passed' | 'failed' | 'blocked' | null;\n currentRun: string | null; // Run ID if active\n sessionId?: string; // Legacy session ID for transition period\n // Identity fields (PAN-754)\n issueId?: string;\n tmuxSession?: string; // Stored at spawn time so we can look it up without recomputing\n role?: string; // For convoy members: 'correctness' | 'performance' | etc.\n // Activity visibility (PAN-754)\n currentActivity?: string | null;\n model?: string | null;\n resolutionTrace?: ResolutionStep[] | null;\n // Write-scope (PAN-754)\n writeScope?: 'full' | 'readonly-plus-output';\n outputPath?: string | null;\n workspace?: string | null; // workspace path for write-scope conflict detection\n}\n\nexport function isProjectSpecialistActivelyRunning(\n runtimeState?: { state?: 'active' | 'idle' | 'suspended' | 'stopped' | 'uninitialized' | 'waiting-on-human' } | null,\n fallbackRunning: boolean = false\n): boolean {\n if (runtimeState?.state === 'active') return true;\n if (\n runtimeState?.state === 'idle'\n || runtimeState?.state === 'suspended'\n || runtimeState?.state === 'stopped'\n || runtimeState?.state === 'waiting-on-human'\n ) {\n return false;\n }\n return fallbackRunning;\n}\n\n/**\n * Registry of all specialist agents (per-project structure)\n */\nexport interface SpecialistRegistry {\n version: string;\n // Global defaults for specialist configuration\n defaults: {\n contextRuns: number;\n digestModel: string | null;\n retention: { maxDays: number; maxRuns: number };\n };\n // Per-project specialist metadata\n projects: {\n [projectKey: string]: {\n [specialistType: string]: ProjectSpecialistMetadata;\n };\n };\n // Legacy: Global specialists list (for backward compatibility)\n specialists?: LegacySpecialistDefinition[];\n lastUpdated: string; // ISO 8601 timestamp\n}\n\n/**\n * PAN-1048 review feedback 003 (REQ-16): default specialist definitions are\n * gone. The role primitive replaces the specialist identity model, so\n * recreating registry.json with a hard-coded list of specialist names would\n * just resurrect what startup cleanup just deleted.\n */\nconst DEFAULT_SPECIALISTS: LegacySpecialistDefinition[] = [];\n\n/**\n * PAN-1048 review feedback 003 (REQ-16): initSpecialistsDirectory is a no-op.\n *\n * The cleanup at Cloister startup (service.ts cleanupLegacySpecialistsDirectory)\n * removes ~/.overdeck/specialists/ on every boot. The previous body of this\n * function would unconditionally re-create the directory and seed\n * registry.json from DEFAULT_SPECIALISTS the next time anything called\n * loadRegistry(), undoing the cleanup and resurrecting the legacy identity\n * model. The stub keeps the call sites alive (callers that still loadRegistry\n * get an in-memory default registry) without recreating any disk artifacts.\n */\nexport function initSpecialistsDirectory(): void {\n // Intentionally empty. See block comment above.\n}\n\n/**\n * Migrate old registry format to new per-project structure (PAN-754: compound-key aware).\n *\n * v1.0 → v2.0: flat specialist list → projects[projectKey][specialistType]\n * v2.0 → v3.0: projects[projectKey][specialistType] → projects[projectKey][compoundKey]\n * Legacy v2.0 plain-type keys are left as-is (still readable by compat wrappers).\n * getProjectSpecialistMetadata() and updateProjectSpecialistMetadata() handle both formats.\n */\nfunction migrateRegistryIfNeeded(): void {\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n const registry = JSON.parse(content) as SpecialistRegistry;\n\n // v2.0 already migrated (or registry.projects exists for fresh installs)\n if (registry.version === '2.0' && registry.projects) {\n // No additional migration needed — legacy plain-type keys are handled by compat wrappers.\n return;\n }\n\n if (!registry.projects) {\n // v1.0 → v2.0: add projects map\n console.log('[specialists] Migrating registry v1.0 → v2.0...');\n\n const migratedRegistry: SpecialistRegistry = {\n version: '2.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: {\n maxDays: 30,\n maxRuns: 50,\n },\n },\n projects: {},\n specialists: registry.specialists,\n lastUpdated: new Date().toISOString(),\n };\n\n saveRegistry(migratedRegistry);\n console.log('[specialists] Registry migration v1.0 → v2.0 complete');\n }\n } catch (error) {\n console.error('[specialists] Failed to migrate registry:', error);\n }\n}\n\n/**\n * Load the specialist registry\n *\n * @returns Specialist registry\n */\nexport function loadRegistry(): SpecialistRegistry {\n // PAN-1048 review feedback 003 (REQ-16): do not recreate the directory.\n // If registry.json is missing (typical after Cloister startup cleanup),\n // return an empty in-memory registry instead of seeding disk.\n if (!existsSync(REGISTRY_FILE)) {\n return {\n version: '3.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: { maxDays: 30, maxRuns: 50 },\n },\n projects: {},\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n }\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n return JSON.parse(content);\n } catch (error) {\n console.error('Failed to load specialist registry:', error);\n return {\n version: '3.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: { maxDays: 30, maxRuns: 50 },\n },\n projects: {},\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Save the specialist registry\n *\n * @param registry - Registry to save\n */\nexport function saveRegistry(registry: SpecialistRegistry): void {\n // PAN-1048 review feedback 003 (REQ-16): only persist when the legacy\n // directory already exists. Cloister startup cleanup deletes the directory\n // on every boot, so writing here without a pre-existing dir would re-create\n // the legacy identity model that the cleanup is meant to remove. The\n // in-memory registry from loadRegistry() is enough for the few residual\n // call sites that still consult specialist metadata.\n if (!existsSync(SPECIALISTS_DIR)) {\n return;\n }\n\n registry.lastUpdated = new Date().toISOString();\n\n try {\n const content = JSON.stringify(registry, null, 2);\n writeFileSync(REGISTRY_FILE, content, 'utf-8');\n } catch (error) {\n console.error('Failed to save specialist registry:', error);\n throw error;\n }\n}\n\n/**\n * Generate a deterministic UUID from a string.\n * Uses SHA-256 hash formatted as a UUID v4-compatible string.\n * This ensures the same specialist+project always gets the same session ID\n * while satisfying Claude Code's UUID format requirement.\n */\nfunction deterministicUUID(input: string): string {\n const hash = createHash('sha256').update(input).digest('hex');\n // Format as UUID: 8-4-4-4-12\n return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;\n}\n\n/**\n * Get metadata for a specific specialist\n *\n * @param name - Specialist name\n * @returns Specialist metadata or null if not found\n */\nexport function getSpecialistMetadata(name: SpecialistAgentName): LegacySpecialistDefinition | null {\n const registry = loadRegistry();\n return (registry.specialists ?? []).find((s) => s.name === name) || null;\n}\n\n/**\n * Update specialist metadata\n *\n * @param name - Specialist name\n * @param updates - Partial metadata to update\n */\nexport function updateSpecialistMetadata(\n name: SpecialistAgentName,\n updates: Partial<LegacySpecialistDefinition>\n): void {\n const registry = loadRegistry();\n\n const specialists = registry.specialists ?? [];\n const index = specialists.findIndex((s) => s.name === name);\n\n if (index === -1) {\n throw new Error(`Specialist ${name} not found in registry`);\n }\n\n specialists[index] = {\n ...specialists[index],\n ...updates,\n name, // Ensure name doesn't change\n };\n registry.specialists = specialists;\n\n saveRegistry(registry);\n}\n\n/**\n * Get all specialist metadata\n *\n * @returns Array of all specialists\n */\nexport function getAllSpecialists(): LegacySpecialistDefinition[] {\n const registry = loadRegistry();\n return registry.specialists ?? [];\n}\n\n/**\n * Check if a legacy specialist has a recorded Claude session.\n *\n * @param name - Specialist name\n * @returns True if the specialist has a recorded session id in its agent directory\n */\nexport function isInitialized(name: SpecialistAgentName): boolean {\n return readRecordedClaudeSessionId(getTmuxSessionName(name)) !== null;\n}\n\n/**\n * Get the state of a specialist from recorded agent metadata.\n *\n * Note: This only checks whether a recorded Claude session exists, not if it's actually running.\n * Use getSpecialistStatus() for runtime state.\n *\n * @param name - Specialist name\n * @returns Specialist state\n */\nexport function getSpecialistState(name: SpecialistAgentName): Exclude<SpecialistLifecycleState, 'active'> {\n return isInitialized(name) ? 'sleeping' : 'uninitialized';\n}\n\n/**\n * Get tmux session name for a specialist\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Expected tmux session name\n */\nexport function getTmuxSessionName(name: SpecialistAgentName, projectKey?: string, issueId?: string): string {\n if (projectKey && issueId) {\n return `specialist-${projectKey}-${issueId}-${name}`;\n }\n if (projectKey) {\n return `specialist-${projectKey}-${name}`;\n }\n // Legacy format for backward compatibility\n return `specialist-${name}`;\n}\n\n/**\n * The five canonical reviewer roles plus synthesis. One tmux session per role\n * per issue, alive for the lifetime of the issue across all review rounds.\n */\nexport type ReviewerRole =\n | 'correctness'\n | 'security'\n | 'performance'\n | 'requirements'\n | 'synthesis';\n\nexport const REVIEWER_ROLES: readonly ReviewerRole[] = [\n 'correctness',\n 'security',\n 'performance',\n 'requirements',\n 'synthesis',\n] as const;\n\n/**\n * Get the canonical reviewer tmux session name (PAN-830 / PAN-1048).\n *\n * Pattern: `agent-<issueId>-review-<role>`. One tmux session per role per\n * issue — sessions are reused across review rounds via the agent message delivery path.\n * resumption. Round 2 of `review-correctness` does NOT spawn a new session;\n * it injects a follow-up prompt into the existing pane.\n *\n * @param role - One of the canonical reviewer roles\n * @param _projectKey - Unused (kept for signature compatibility with existing callers)\n * @param issueId - Issue identifier (e.g. `pan-540` or `PAN-540`)\n * @returns Canonical tmux session name\n */\nexport function getReviewerSessionName(\n role: ReviewerRole,\n _projectKey: string,\n issueId: string,\n): string {\n return `agent-${issueId.toLowerCase()}-review-${role}`;\n}\n\n/**\n * Parse a canonical reviewer session name back into role + issue.\n * Returns null if the name does not match either pattern.\n * Supports both current `agent-*` format and legacy `specialist-*` format.\n */\nexport function parseReviewerSessionName(name: string): {\n role: ReviewerRole;\n issueId: string;\n} | null {\n // Current PAN-1048+ pattern: agent-<issueId>-review-<role>\n const agentMatch = name.match(/^agent-([a-z0-9]+-\\d+)-review-(correctness|security|performance|requirements|synthesis)$/i);\n if (agentMatch) {\n return { issueId: agentMatch[1]!.toUpperCase(), role: agentMatch[2] as ReviewerRole };\n }\n // Legacy PAN-830 pattern: specialist-<projectKey>-<issueId>-review-<role>\n const legacyMatch = name.match(/^specialist-([\\w.-]+?)-([\\w.-]+?)-review-(correctness|security|performance|requirements|synthesis)$/);\n if (legacyMatch) {\n return { issueId: legacyMatch[2], role: legacyMatch[3] as ReviewerRole };\n }\n return null;\n}\n\n/**\n * Construct the compound registry key for a per-issue specialist (PAN-754).\n * Format: `${specialistType}:${issueId}` or `${specialistType}:${issueId}:${role}` for convoy.\n */\nexport function makeSpecialistRegistryKey(specialistType: string, issueId: string, role?: string): string {\n return role ? `${specialistType}:${issueId}:${role}` : `${specialistType}:${issueId}`;\n}\n\n/**\n * Remove every project-specialist registry entry whose compound key references\n * the given issueId (case-insensitive match on the second segment). Returns\n * the number of entries removed. Called from teardown so closed issues do not\n * leave behind metadata that the deacon keeps inspecting and force-killing.\n */\nexport function pruneSpecialistRegistryEntriesForIssue(issueId: string): number {\n const issueLower = issueId.toLowerCase();\n const issueUpper = issueId.toUpperCase();\n const registry = loadRegistry();\n let removed = 0;\n for (const projectKey of Object.keys(registry.projects ?? {})) {\n const bucket = registry.projects![projectKey] ?? {};\n for (const key of Object.keys(bucket)) {\n const { issueId: keyIssue } = parseSpecialistRegistryKey(key);\n if (!keyIssue) continue;\n if (keyIssue === issueLower || keyIssue === issueUpper) {\n delete bucket[key];\n removed++;\n }\n }\n }\n if (removed > 0) {\n saveRegistry(registry);\n }\n return removed;\n}\n\n/**\n * Parse a compound registry key back into its parts.\n */\nexport function parseSpecialistRegistryKey(key: string): { specialistType: string; issueId?: string; role?: string } {\n const parts = key.split(':');\n if (parts.length === 1) return { specialistType: parts[0] };\n if (parts.length === 2) return { specialistType: parts[0], issueId: parts[1] };\n return { specialistType: parts[0], issueId: parts[1], role: parts[2] };\n}\n\n/**\n * Record wake event in metadata\n *\n * @param name - Specialist name\n * @param sessionId - New session ID (if changed)\n */\nexport function recordWake(name: SpecialistAgentName, sessionId?: string): void {\n const updates: Partial<LegacySpecialistDefinition> = {\n lastWake: new Date().toISOString(),\n };\n\n if (sessionId) {\n updates.sessionId = sessionId;\n }\n\n updateSpecialistMetadata(name, updates);\n}\n\n/**\n * ===========================================================================\n * Ephemeral Lifecycle Management\n * ===========================================================================\n */\n\n/**\n * Grace period state for a specialist\n */\nexport interface GracePeriodState {\n active: boolean;\n startedAt: string;\n duration: number; // milliseconds\n paused: boolean;\n pausedAt?: string;\n remainingTime?: number; // milliseconds when paused\n}\n\nconst gracePeriodStates = new Map<string, GracePeriodState>();\n\n/**\n * Task context interface for specialist tasks.\n */\nexport interface TaskContext {\n prUrl?: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n reason?: string;\n targetModel?: string;\n additionalInstructions?: string;\n [key: string]: string | string[] | undefined;\n}\n\n/**\n * PAN-1048 R1: spawnEphemeralSpecialist removed.\n *\n * The function was the generic dispatcher that took an arbitrary\n * SpecialistAgentName ('review-agent' | 'test-agent' | 'merge-agent' |\n * 'inspect-agent' | …) and shelled out a launcher specific to each. Under\n * the role primitive, those flavours are first-class roles\n * (review/test/ship) plus a single work sub-role (inspect / inspect-deep).\n *\n * Replacements:\n * - Review/test/ship runs: spawnRun(issueId, role, opts) in src/lib/agents.ts.\n * Reactive Cloister fires these on lifecycle transitions; the manual\n * re-dispatch in routes/workspaces.ts also uses spawnRun.\n * - Inspect runs: spawnInspectAgent() in cloister/inspect-agent.ts owns\n * its own minimal launcher path (single-bead-scoped, ephemeral).\n *\n * The specialist registry/run-log/grace-period machinery stays in this\n * file because the dashboard read-model and reset/init/grace endpoints\n * still consume it for legacy run history; nothing writes new entries\n * once the dispatcher is gone.\n */\n\n\n/**\n * Start grace period for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param duration - Grace period duration in milliseconds (default: 60000)\n */\nexport function startGracePeriod(\n projectKey: string,\n specialistType: SpecialistAgentName,\n duration: number = 60000\n): void {\n const key = `${projectKey}-${specialistType}`;\n\n gracePeriodStates.set(key, {\n active: true,\n startedAt: new Date().toISOString(),\n duration,\n paused: false,\n });\n\n console.log(`[specialist] Grace period started for ${projectKey}/${specialistType} (${duration}ms)`);\n\n // Schedule termination after grace period\n setTimeout(() => {\n const state = gracePeriodStates.get(key);\n if (state && state.active && !state.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, duration);\n}\n\n/**\n * Pause grace period countdown\n */\nexport function pauseGracePeriod(projectKey: string, specialistType: SpecialistAgentName): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active) {\n return false;\n }\n\n const elapsed = Date.now() - new Date(state.startedAt).getTime();\n const remaining = state.duration - elapsed;\n\n state.paused = true;\n state.pausedAt = new Date().toISOString();\n state.remainingTime = remaining;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period paused for ${projectKey}/${specialistType}`);\n\n return true;\n}\n\n/**\n * Resume grace period countdown\n */\nexport function resumeGracePeriod(projectKey: string, specialistType: SpecialistAgentName): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active || !state.paused) {\n return false;\n }\n\n state.paused = false;\n state.startedAt = new Date().toISOString();\n state.pausedAt = undefined;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period resumed for ${projectKey}/${specialistType}`);\n\n // Schedule termination for remaining time\n setTimeout(() => {\n const currentState = gracePeriodStates.get(key);\n if (currentState && currentState.active && !currentState.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, state.remainingTime || 0);\n\n return true;\n}\n\n/**\n * Exit grace period immediately (terminate now)\n */\nexport function exitGracePeriod(projectKey: string, specialistType: SpecialistAgentName): void {\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n terminateSpecialist(projectKey, specialistType);\n}\n\n/**\n * Get grace period state\n */\nexport function getGracePeriodState(\n projectKey: string,\n specialistType: SpecialistAgentName\n): GracePeriodState | null {\n const key = `${projectKey}-${specialistType}`;\n return gracePeriodStates.get(key) || null;\n}\n\n/**\n * Find the active registry key for (projectKey, specialistType).\n * Searches compound keys; falls back to plain specialistType key.\n * Returns undefined if nothing is currently active.\n */\nexport function findActiveRegistryKey(projectKey: string, specialistType: SpecialistAgentName): string | undefined {\n const registry = loadRegistry();\n const bucket = registry.projects[projectKey] ?? {};\n\n // Check compound keys first (new format: \"type:issueId\")\n const prefix = `${specialistType}:`;\n const activeCompound = Object.keys(bucket).find(k =>\n k.startsWith(prefix) && bucket[k].currentRun !== null\n );\n if (activeCompound) return activeCompound;\n\n // Check legacy plain key\n if (bucket[specialistType]?.currentRun !== null) return specialistType;\n\n // Return most recently touched key even if not active\n const allMatching = Object.keys(bucket).filter(k =>\n k === specialistType || k.startsWith(prefix)\n ).sort((a, b) =>\n (bucket[b].lastRunAt ?? '').localeCompare(bucket[a].lastRunAt ?? '')\n );\n return allMatching[0];\n}\n\n/**\n * Signal that a specialist has completed its task\n *\n * This should be called when the specialist finishes its work.\n * It updates the run status and starts the grace period.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param result - Task result\n * @param issueId - Optional: issue being handled (used to compute compound registry key)\n */\nexport async function signalSpecialistCompletion(\n projectKey: string,\n specialistType: SpecialistAgentName,\n result: {\n status: 'passed' | 'failed' | 'blocked';\n notes?: string;\n },\n issueId?: string\n): Promise<void> {\n const registryKey = issueId\n ? makeSpecialistRegistryKey(specialistType, issueId)\n : (findActiveRegistryKey(projectKey, specialistType) ?? specialistType);\n const metadata = getRunMetadata(projectKey, registryKey);\n\n // Derive tmuxSession: use stored field when available, recompute as fallback\n const resolvedTmuxSession = metadata.tmuxSession ?? getTmuxSessionName(specialistType, projectKey, issueId);\n\n // Update status\n updateRunStatus(projectKey, registryKey, result.status);\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n try {\n const { finalizeRunLogSync } = await import('./specialist-logs.js');\n finalizeRunLogSync(projectKey, specialistType, metadata.currentRun, {\n status: result.status,\n notes: result.notes,\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n }\n\n // Completion means the run itself is over, even if the tmux session stays alive\n // during the grace period for inspection or manual termination.\n setCurrentRun(projectKey, registryKey, null);\n updateRunMetadata(projectKey, registryKey, { currentActivity: null });\n import('../agents.js')\n .then(({ saveAgentRuntimeState }) => {\n saveAgentRuntimeState(resolvedTmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n })\n .catch((error) => {\n console.error(`[specialist] Failed to mark ${projectKey}/${specialistType} idle:`, error);\n });\n\n // Start grace period (60 seconds)\n startGracePeriod(projectKey, specialistType, 60000);\n\n console.log(`[specialist] ${specialistType} completed for ${projectKey} (status: ${result.status})`);\n}\n\n/**\n * Terminate a specialist session\n *\n * Kills the tmux session, finalizes logs, and schedules digest generation.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport async function terminateSpecialist(\n projectKey: string,\n specialistType: SpecialistAgentName,\n issueId?: string\n): Promise<void> {\n const registryKey = issueId\n ? makeSpecialistRegistryKey(specialistType, issueId)\n : (findActiveRegistryKey(projectKey, specialistType) ?? specialistType);\n const metadata = getRunMetadata(projectKey, registryKey);\n\n // Derive tmuxSession: use stored field, or recompute\n const tmuxSession = metadata.tmuxSession ?? getTmuxSessionName(specialistType, projectKey, issueId);\n\n try {\n // Kill tmux session\n await Effect.runPromise(killSession(tmuxSession));\n console.log(`[specialist] Terminated ${projectKey}/${specialistType}`);\n } catch (error) {\n console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);\n }\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLogSync } = await import('./specialist-logs.js');\n\n try {\n finalizeRunLogSync(projectKey, specialistType, metadata.currentRun, {\n status: metadata.lastRunStatus || 'incomplete',\n notes: 'Specialist terminated',\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n\n // Clear current run\n setCurrentRun(projectKey, registryKey, null);\n }\n\n updateRunMetadata(projectKey, registryKey, { currentActivity: null });\n\n // Clear grace period state\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n // Update runtime state\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'suspended',\n lastActivity: new Date().toISOString(),\n });\n\n // Schedule digest generation (async, fire-and-forget)\n const { scheduleDigestGeneration } = await import('./specialist-context.js');\n scheduleDigestGeneration(projectKey, specialistType);\n\n // Run log cleanup for this project/specialist (async, fire-and-forget)\n scheduleLogCleanup(projectKey, specialistType);\n}\n\n/**\n * Schedule log cleanup for a project's specialist (async, fire-and-forget)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nfunction scheduleLogCleanup(projectKey: string, specialistType: SpecialistAgentName): void {\n // Run async without awaiting\n Promise.resolve().then(async () => {\n try {\n const { cleanupOldLogsSync } = await import('./specialist-logs.js');\n const { getSpecialistRetention } = await import('../projects.js');\n\n const retention = getSpecialistRetention(projectKey);\n const deleted = cleanupOldLogsSync(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });\n\n if (deleted > 0) {\n console.log(`[specialist] Cleaned up ${deleted} old logs for ${projectKey}/${specialistType}`);\n }\n } catch (error) {\n console.error(`[specialist] Log cleanup failed for ${projectKey}/${specialistType}:`, error);\n }\n });\n}\n\n/**\n * ===========================================================================\n * Per-Project Specialist Functions\n * ===========================================================================\n */\n\n/**\n * Get the directory for a project's specialist\n */\nexport function getProjectSpecialistDir(projectKey: string, specialistType: SpecialistAgentName): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType);\n}\n\n/**\n * Ensure per-project specialist directory structure exists\n */\nexport function ensureProjectSpecialistDir(projectKey: string, specialistType: SpecialistAgentName): void {\n const specialistDir = getProjectSpecialistDir(projectKey, specialistType);\n const runsDir = join(specialistDir, 'runs');\n const contextDir = join(specialistDir, 'context');\n\n if (!existsSync(runsDir)) {\n mkdirSync(runsDir, { recursive: true });\n }\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Get metadata for a specific (projectKey, registryKey) pair.\n * registryKey is either a plain specialistType (legacy) or a compound key from makeSpecialistRegistryKey().\n */\nexport function getRunMetadata(\n projectKey: string,\n registryKey: string,\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][registryKey]) {\n registry.projects[projectKey][registryKey] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n saveRegistry(registry);\n }\n\n return registry.projects[projectKey][registryKey];\n}\n\n/**\n * Update metadata for a specific (projectKey, registryKey) pair.\n */\nexport function updateRunMetadata(\n projectKey: string,\n registryKey: string,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][registryKey]) {\n registry.projects[projectKey][registryKey] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n }\n\n registry.projects[projectKey][registryKey] = {\n ...registry.projects[projectKey][registryKey],\n ...updates,\n };\n\n saveRegistry(registry);\n}\n\n/**\n * Get per-project specialist metadata — backward-compat wrapper.\n * Searches compound-key entries for this project+type; returns the active run, or most recent.\n */\nexport function getProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistAgentName\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n const projectBucket = registry.projects[projectKey] ?? {};\n\n // Check for exact legacy key first\n if (projectBucket[specialistType]) {\n return projectBucket[specialistType];\n }\n\n // Search compound keys for the most relevant entry\n const prefix = `${specialistType}:`;\n const candidates = Object.entries(projectBucket)\n .filter(([k]) => k.startsWith(prefix))\n .map(([, v]) => v);\n\n // Prefer active run, then most recently started\n const active = candidates.find(c => c.currentRun !== null);\n if (active) return active;\n const sorted = candidates.sort((a, b) =>\n (b.lastRunAt ?? '').localeCompare(a.lastRunAt ?? '')\n );\n if (sorted.length > 0) return sorted[0];\n\n // No entry found — return blank default (don't save it)\n return { runCount: 0, lastRunAt: null, lastRunStatus: null, currentRun: null };\n}\n\n/**\n * Update per-project specialist metadata — backward-compat wrapper.\n * Updates the active compound-key entry, or the legacy plain-key entry.\n */\nexport function updateProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistAgentName,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n const projectBucket = registry.projects[projectKey] ?? {};\n\n // Try legacy key first\n if (projectBucket[specialistType]) {\n updateRunMetadata(projectKey, specialistType, updates);\n return;\n }\n\n // Find the active compound-key entry for this type\n const prefix = `${specialistType}:`;\n const activeKey = Object.keys(projectBucket).find(k =>\n k.startsWith(prefix) && projectBucket[k].currentRun !== null\n );\n if (activeKey) {\n updateRunMetadata(projectKey, activeKey, updates);\n return;\n }\n\n // Fall back to most recent\n const latestKey = Object.keys(projectBucket)\n .filter(k => k.startsWith(prefix))\n .sort((a, b) => (projectBucket[b].lastRunAt ?? '').localeCompare(projectBucket[a].lastRunAt ?? ''))\n .shift();\n if (latestKey) {\n updateRunMetadata(projectKey, latestKey, updates);\n }\n}\n\n/**\n * Increment run count for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function incrementProjectRunCount(projectKey: string, registryKey: string): void {\n const metadata = getRunMetadata(projectKey, registryKey);\n updateRunMetadata(projectKey, registryKey, {\n runCount: metadata.runCount + 1,\n lastRunAt: new Date().toISOString(),\n });\n}\n\n/**\n * Set current run for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function setCurrentRun(\n projectKey: string,\n registryKey: string,\n runId: string | null\n): void {\n updateRunMetadata(projectKey, registryKey, { currentRun: runId });\n}\n\n/**\n * Update run status for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function updateRunStatus(\n projectKey: string,\n registryKey: string,\n status: 'passed' | 'failed' | 'blocked' | null\n): void {\n updateRunMetadata(projectKey, registryKey, { lastRunStatus: status });\n}\n\n/**\n * List all projects that have specialists configured\n */\nexport function listProjectsWithSpecialists(): string[] {\n const registry = loadRegistry();\n return Object.keys(registry.projects);\n}\n\n/**\n * List all specialist types for a project\n */\nexport function listSpecialistsForProject(projectKey: string): SpecialistAgentName[] {\n const registry = loadRegistry();\n const project = registry.projects[projectKey];\n\n if (!project) {\n return [];\n }\n\n return Object.keys(project) as SpecialistAgentName[];\n}\n\n/**\n * Get all per-project specialist statuses (PAN-754: compound-key aware).\n * Walks registry including compound keys (type:issueId[:role]) and returns\n * enriched entries with issueId, model, currentActivity for the Agents page.\n */\nexport async function getAllProjectSpecialistStatuses(): Promise<Array<{\n projectKey: string;\n specialistType: SpecialistAgentName;\n registryKey: string;\n issueId?: string;\n role?: string;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n}>> {\n const registry = loadRegistry();\n const { getAgentRuntimeStateSync } = await import('../agents.js');\n\n const results: Array<{\n projectKey: string;\n specialistType: SpecialistAgentName;\n registryKey: string;\n issueId?: string;\n role?: string;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n }> = [];\n\n for (const [projectKey, specialists] of Object.entries(registry.projects)) {\n for (const [registryKey, metadata] of Object.entries(specialists)) {\n const { specialistType, issueId, role } = parseSpecialistRegistryKey(registryKey);\n\n // Determine tmux session: use stored field when available\n const tmuxSession = metadata.tmuxSession\n ?? getTmuxSessionName(specialistType as SpecialistAgentName, projectKey, issueId);\n\n const runtimeState = getAgentRuntimeStateSync(tmuxSession);\n const sessionRunning = await isRunning(specialistType as SpecialistAgentName, projectKey).catch(() => false);\n const running = isProjectSpecialistActivelyRunning(runtimeState, sessionRunning);\n const effectiveMetadata = running ? metadata : { ...metadata, currentRun: null };\n\n results.push({\n projectKey,\n specialistType: specialistType as SpecialistAgentName,\n registryKey,\n issueId,\n role,\n metadata: effectiveMetadata,\n isRunning: running,\n tmuxSession,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Update context token count for a specialist\n *\n * @param name - Specialist name\n * @param tokens - Total context tokens\n */\nexport function updateContextTokens(name: SpecialistAgentName, tokens: number): void {\n updateSpecialistMetadata(name, { contextTokens: tokens });\n}\n\n/**\n * Enable a specialist\n *\n * @param name - Specialist name\n */\nexport function enableSpecialist(name: SpecialistAgentName): void {\n updateSpecialistMetadata(name, { enabled: true });\n}\n\n/**\n * Disable a specialist\n *\n * @param name - Specialist name\n */\nexport function disableSpecialist(name: SpecialistAgentName): void {\n updateSpecialistMetadata(name, { enabled: false });\n}\n\n/**\n * Check if a specialist is enabled\n *\n * @param name - Specialist name\n * @returns True if specialist is enabled\n */\nexport function isEnabled(name: SpecialistAgentName): boolean {\n const metadata = getSpecialistMetadata(name);\n return metadata?.enabled ?? false;\n}\n\n/**\n * Get all enabled specialists\n *\n * @returns Array of enabled specialists\n */\nexport function getEnabledSpecialists(): LegacySpecialistDefinition[] {\n return getAllSpecialists().filter((s) => s.enabled);\n}\n\n/**\n * Find JSONL file for a session ID\n *\n * Searches through Claude Code project directories to find the JSONL file.\n *\n * @param sessionId - Session ID to find\n * @returns Path to JSONL file or null if not found\n */\nexport function findSessionFile(sessionId: string): string | null {\n try {\n const allFiles = getAllSessionFilesSync();\n\n for (const file of allFiles) {\n const fileSessionId = basename(file, '.jsonl');\n if (fileSessionId === sessionId) {\n return file;\n }\n }\n } catch {\n // Session files not available\n }\n\n return null;\n}\n\n/**\n * Count context tokens for a specialist session\n *\n * Reads the JSONL file for the specialist's session and sums all token usage.\n * This gives an approximate count of context size.\n *\n * @param name - Specialist name\n * @returns Total token count or null if session not found\n */\nexport function countContextTokens(name: SpecialistAgentName): number | null {\n const sessionId = readRecordedClaudeSessionId(getTmuxSessionName(name));\n\n if (!sessionId) {\n return null;\n }\n\n const sessionFile = findSessionFile(sessionId);\n\n if (!sessionFile) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionFile);\n\n if (!sessionUsage) {\n return null;\n }\n\n // Sum all token types for total context\n return (\n sessionUsage.usage.inputTokens +\n sessionUsage.usage.outputTokens +\n (sessionUsage.usage.cacheReadTokens || 0) +\n (sessionUsage.usage.cacheWriteTokens || 0)\n );\n}\n\n/**\n * Check if a specialist is currently running in tmux\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns True if specialist has an active tmux session\n */\nexport async function isRunning(name: SpecialistAgentName, projectKey?: string): Promise<boolean> {\n const tmuxSession = getTmuxSessionName(name, projectKey);\n\n try {\n const exists = await Effect.runPromise(sessionExists(tmuxSession));\n if (!exists) return false;\n // Session exists — but check if the pane actually has a running process.\n // When Claude Code crashes, the pane's process exits but the tmux session persists,\n // making has-session return success even though nothing is running.\n const panePid = (await Effect.runPromise(listPaneValues(tmuxSession, '#{pane_pid}')))[0]?.trim() ?? '';\n if (!panePid) return false;\n // Check if the pane's process has any child processes (Claude Code / bash)\n const { stdout: children } = await execAsync(\n `ps --ppid ${panePid} --no-headers 2>/dev/null || echo \"\"`,\n { encoding: 'utf-8' }\n );\n return children.trim().length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Get complete status for a specialist\n *\n * Combines metadata, session info, and runtime state (PAN-80: uses hook-based state).\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Complete specialist status\n */\nexport async function getSpecialistStatus(\n name: SpecialistAgentName,\n projectKey?: string\n): Promise<LegacySpecialistRuntimeStatus> {\n const metadata = getSpecialistMetadata(name) || {\n name,\n displayName: name,\n description: '',\n enabled: false,\n autoWake: false,\n };\n\n const sessionId = readRecordedClaudeSessionId(getTmuxSessionName(name, projectKey));\n const running = await isRunning(name, projectKey);\n const contextTokens = countContextTokens(name);\n\n // Determine state from hook-based runtime state (PAN-80)\n const { getAgentRuntimeStateSync } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name, projectKey);\n const runtimeState = getAgentRuntimeStateSync(tmuxSession);\n\n let state: SpecialistLifecycleState;\n if (runtimeState) {\n // Map runtime state to specialist state\n switch (runtimeState.state) {\n case 'active':\n state = 'active';\n break;\n case 'idle':\n state = 'sleeping'; // Idle = at prompt waiting\n break;\n case 'suspended':\n state = 'sleeping'; // Suspended = session saved, not running\n break;\n case 'uninitialized':\n default:\n state = 'uninitialized';\n break;\n }\n } else {\n // Fallback if no runtime state available\n if (running && sessionId) {\n state = 'sleeping';\n } else if (sessionId) {\n state = 'sleeping';\n } else {\n state = 'uninitialized';\n }\n }\n\n return {\n ...metadata,\n sessionId: sessionId || undefined,\n contextTokens: contextTokens || undefined,\n state,\n isRunning: running,\n tmuxSession: getTmuxSessionName(name, projectKey),\n currentIssue: running ? runtimeState?.currentIssue : undefined,\n };\n}\n\n/**\n * Get status for all specialists\n *\n * @returns Array of specialist statuses\n */\nexport async function getAllSpecialistStatus(): Promise<LegacySpecialistRuntimeStatus[]> {\n const specialists = getAllSpecialists();\n return Promise.all(specialists.map((metadata) => getSpecialistStatus(metadata.name)));\n}\n\n\n/**\n * Initialize all enabled but uninitialized specialists\n *\n * Called during Cloister startup to ensure specialists are ready.\n *\n * @returns Promise with array of initialization results\n */\nexport async function initializeEnabledSpecialists(): Promise<Array<{\n name: SpecialistAgentName;\n success: boolean;\n message: string;\n}>> {\n const enabled = getEnabledSpecialists();\n const results: Array<{ name: SpecialistAgentName; success: boolean; message: string }> = [];\n\n for (const specialist of enabled) {\n results.push({\n name: specialist.name,\n success: true,\n message: 'Legacy global specialist initialization removed; role flows spawn agents on demand.',\n });\n }\n\n return results;\n}\n\n/**\n * ===========================================================================\n * Specialist Feedback System\n * ===========================================================================\n *\n * Specialists accumulate context and expertise. This system allows them to\n * share learnings back to issue agents, creating a feedback loop that\n * improves the overall system over time.\n */\n\n/**\n * Feedback from a specialist to an issue agent\n */\nexport interface SpecialistFeedback {\n id: string;\n timestamp: string;\n fromSpecialist: SpecialistAgentName;\n toIssueId: string;\n feedbackType: 'success' | 'failure' | 'warning' | 'insight';\n category: 'merge' | 'test' | 'review' | 'general';\n summary: string;\n details: string;\n actionItems?: string[];\n patterns?: string[]; // Patterns the specialist noticed\n suggestions?: string[]; // Suggestions for the issue agent\n}\n\nconst FEEDBACK_DIR = join(OVERDECK_HOME, 'specialists', 'feedback');\nconst FEEDBACK_LOG = join(FEEDBACK_DIR, 'feedback.jsonl');\n\n/**\n * Send feedback from a specialist to an issue agent\n *\n * This is the key mechanism for specialists to share their accumulated\n * expertise back to the issue agents that spawned the work.\n *\n * @param feedback - The feedback to send\n * @returns True if feedback was sent successfully\n */\nexport async function sendFeedbackToAgent(\n feedback: Omit<SpecialistFeedback, 'id' | 'timestamp'>\n): Promise<boolean> {\n const { fromSpecialist, toIssueId, summary, details } = feedback;\n\n // Ensure feedback directory exists\n if (!existsSync(FEEDBACK_DIR)) {\n mkdirSync(FEEDBACK_DIR, { recursive: true });\n }\n\n // Create full feedback record\n const fullFeedback: SpecialistFeedback = {\n ...feedback,\n id: `feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n timestamp: new Date().toISOString(),\n };\n\n // Log feedback to JSONL\n try {\n const line = JSON.stringify(fullFeedback) + '\\n';\n appendFileSync(FEEDBACK_LOG, line, 'utf-8');\n } catch (error) {\n console.error(`[specialist] Failed to log feedback:`, error);\n }\n\n // Try to send feedback to the issue agent\n const agentSession = `agent-${toIssueId.toLowerCase()}`;\n\n // Format feedback message for the agent\n const feedbackMessage = formatFeedbackForAgent(fullFeedback);\n\n // Write feedback to workspace file\n const { writeFeedbackFile } = await import('./feedback-writer.js');\n const specialistMap: Record<string, 'review-agent' | 'test-agent' | 'merge-agent'> = {\n 'review-agent': 'review-agent',\n 'test-agent': 'test-agent',\n 'merge-agent': 'merge-agent',\n };\n const specialist = specialistMap[fromSpecialist] || 'review-agent';\n const outcome = feedback.feedbackType === 'success' ? 'approved' : feedback.feedbackType === 'failure' ? 'failed' : feedback.feedbackType;\n\n const fileResult = await Effect.runPromise(writeFeedbackFile({\n issueId: toIssueId,\n specialist,\n outcome,\n summary: summary.slice(0, 100),\n markdownBody: feedbackMessage,\n }));\n\n if (!fileResult.success) {\n console.error(`[specialist] Failed to write feedback file for ${toIssueId}: ${fileResult.error}`);\n return false;\n }\n\n // Send a short, explicit message with the ABSOLUTE path.\n try {\n const { messageAgent } = await import('../agents.js');\n const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.\\n\\nMUST READ: ${fileResult.filePath}\\n\\nUse your Read tool to open this file, read every line, then address the feedback and continue working. Do NOT stop at the prompt.`;\n await messageAgent(agentSession, msg);\n console.log(`[specialist] Sent feedback from ${fromSpecialist} to ${agentSession} (file: ${fileResult.filePath})`);\n return true;\n } catch (err) {\n // Agent may be gone — feedback file is still in the workspace for crash recovery\n console.log(`[specialist] Could not send reference to ${agentSession} (file written): ${err}`);\n return true; // File was written successfully, that's the important part\n }\n}\n\n/**\n * Format feedback for display to an agent\n */\nfunction formatFeedbackForAgent(feedback: SpecialistFeedback): string {\n const { fromSpecialist, feedbackType, category, summary, details, actionItems, patterns, suggestions } = feedback;\n\n const typeEmoji = {\n success: '✅',\n failure: '❌',\n warning: '⚠️',\n insight: '💡',\n }[feedbackType];\n\n let message = `\\n${typeEmoji} **Feedback from ${fromSpecialist}** (${category})\\n\\n`;\n message += `**Summary:** ${summary}\\n\\n`;\n message += `**Details:**\\n${details}\\n`;\n\n if (actionItems?.length) {\n message += `\\n**Action Items:**\\n`;\n actionItems.forEach((item, i) => {\n message += `${i + 1}. ${item}\\n`;\n });\n }\n\n if (patterns?.length) {\n message += `\\n**Patterns Noticed:**\\n`;\n patterns.forEach(pattern => {\n message += `- ${pattern}\\n`;\n });\n }\n\n if (suggestions?.length) {\n message += `\\n**Suggestions:**\\n`;\n suggestions.forEach(suggestion => {\n message += `- ${suggestion}\\n`;\n });\n }\n\n return message;\n}\n\n/**\n * Get pending feedback for an issue that hasn't been delivered yet\n *\n * @param issueId - Issue ID to get feedback for\n * @returns Array of feedback records\n */\nexport function getPendingFeedback(issueId: string): SpecialistFeedback[] {\n if (!existsSync(FEEDBACK_LOG)) {\n return [];\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n const allFeedback = lines.map(line => JSON.parse(line) as SpecialistFeedback);\n\n // Filter to this issue\n return allFeedback.filter(f => f.toIssueId.toLowerCase() === issueId.toLowerCase());\n } catch (error) {\n console.error(`[specialist] Failed to read feedback log:`, error);\n return [];\n }\n}\n\n/**\n * Get feedback statistics for all specialists\n *\n * @returns Feedback stats by specialist and type\n */\nexport function getFeedbackStats(): {\n bySpecialist: Record<SpecialistAgentName, number>;\n byType: Record<string, number>;\n total: number;\n} {\n const stats = {\n bySpecialist: {\n 'merge-agent': 0,\n 'review-agent': 0,\n 'test-agent': 0,\n } as Record<SpecialistAgentName, number>,\n byType: {} as Record<string, number>,\n total: 0,\n };\n\n if (!existsSync(FEEDBACK_LOG)) {\n return stats;\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n\n for (const line of lines) {\n const feedback = JSON.parse(line) as SpecialistFeedback;\n stats.bySpecialist[feedback.fromSpecialist] = (stats.bySpecialist[feedback.fromSpecialist] || 0) + 1;\n stats.byType[feedback.feedbackType] = (stats.byType[feedback.feedbackType] || 0) + 1;\n stats.total++;\n }\n } catch (error) {\n console.error(`[specialist] Failed to read feedback stats:`, error);\n }\n\n return stats;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"specialists-nZ0mRwpZ.js","names":[],"sources":["../../src/lib/cost-parsers/jsonl-parser.ts","../../src/lib/cloister/specialists.ts"],"sourcesContent":["/**\n * Claude Code JSONL Parser\n *\n * Parse token usage from Claude Code session files.\n * Session files are stored at: ~/.claude/projects/<project-path-hash>/<session-id>.jsonl\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { Effect } from 'effect';\nimport { encodeClaudeProjectDir } from '../paths.js';\nimport { TokenUsage, calculateCostSync, getPricingSync, AIProvider, logCostSync, CostEntry } from '../cost.js';\nimport { FsError } from '../errors.js';\n\n// Claude Code JSONL message format\nexport interface ClaudeMessage {\n sessionId?: string;\n timestamp?: string;\n parentMessageId?: string;\n // Claude Code per-API-request id. Stable across the multiple JSONL lines of one response.\n requestId?: string;\n message?: {\n id?: string;\n role?: 'user' | 'assistant';\n model?: string;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation?: {\n ephemeral_1h_input_tokens?: number;\n ephemeral_5m_input_tokens?: number;\n };\n };\n };\n // Some messages have usage at top level\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation?: {\n ephemeral_1h_input_tokens?: number;\n ephemeral_5m_input_tokens?: number;\n };\n };\n model?: string;\n}\n\n// Parsed session usage\nexport interface SessionUsage {\n sessionId: string;\n sessionFile: string;\n startTime: string;\n endTime: string;\n model: string; // Display name (normalized). Shows \"sonnet-4-6 → opus-4-6\" for upgrades\n usage: TokenUsage; // Total tokens across all models\n cost: number; // DEPRECATED: Uses first-model pricing (kept for backward compatibility)\n cost_v2?: number; // NEW: Accurate per-message pricing\n cwd?: string;\n messageCount: number;\n modelBreakdown?: Record<string, { // NEW: Cost/token breakdown by exact model ID\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n // Optional per-model cache token counts (ohmypi/omp sessions; absent for Claude Code sessions)\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }>;\n}\n\n// Claude projects directory\nfunction getClaudeProjectsDir(): string {\n return process.env.CLAUDE_PROJECTS_DIR || join(homedir(), '.claude', 'projects');\n}\n\n/**\n * Get all Claude Code project directories\n */\nexport function getProjectDirsSync(): string[] {\n const claudeProjectsDir = getClaudeProjectsDir();\n if (!existsSync(claudeProjectsDir)) {\n return [];\n }\n\n return readdirSync(claudeProjectsDir)\n .map(name => join(claudeProjectsDir, name))\n .filter(path => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Get session JSONL files for a project directory\n */\nexport function getSessionFilesSync(projectDir: string): string[] {\n if (!existsSync(projectDir)) {\n return [];\n }\n\n return readdirSync(projectDir)\n .filter(name => name.endsWith('.jsonl'))\n .map(name => join(projectDir, name))\n .sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Get all session files across all projects\n */\nexport function getAllSessionFilesSync(): string[] {\n const files: string[] = [];\n\n for (const projectDir of getProjectDirsSync()) {\n files.push(...getSessionFilesSync(projectDir));\n }\n\n return files.sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Normalize model name for pricing lookup\n */\nexport function normalizeModelName(model: string): { provider: AIProvider; model: string } {\n // Claude models\n if (model.includes('claude')) {\n let normalizedModel = model;\n\n // Map full model IDs to pricing model names\n // Order matters - check more specific patterns first\n\n // Fable models (Mythos-class flagship)\n if (model.includes('fable-5') || model.includes('fable.5') || model.includes('fable')) {\n normalizedModel = 'claude-fable-5';\n }\n\n // Opus models\n if (model.includes('opus-4-8') || model.includes('opus-4.8')) {\n normalizedModel = 'claude-opus-4-8';\n } else if (model.includes('opus-4-7') || model.includes('opus-4.7')) {\n normalizedModel = 'claude-opus-4-7';\n } else if (model.includes('opus-4-6') || model.includes('opus-4.6')) {\n normalizedModel = 'claude-opus-4-6';\n } else if (model.includes('opus-4-1') || model.includes('opus-4.1')) {\n normalizedModel = 'claude-opus-4-1';\n } else if (model.includes('opus-4') || model.includes('opus')) {\n normalizedModel = 'claude-opus-4';\n }\n\n // Sonnet models\n if (model.includes('sonnet-5') || model.includes('sonnet.5')) {\n normalizedModel = 'claude-sonnet-5';\n } else if (model.includes('sonnet-4-6') || model.includes('sonnet-4.6')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4-5') || model.includes('sonnet-4.5')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4') || model.includes('sonnet')) {\n normalizedModel = 'claude-sonnet-4';\n }\n\n // Haiku models - default to 4.5 (current), support 3 for legacy\n if (model.includes('haiku-4-5') || model.includes('haiku-4.5')) {\n normalizedModel = 'claude-haiku-4-5';\n } else if (model.includes('haiku-3')) {\n normalizedModel = 'claude-haiku-3';\n } else if (model.includes('haiku')) {\n normalizedModel = 'claude-haiku-4-5'; // Default to current model\n }\n\n return { provider: 'anthropic', model: normalizedModel };\n }\n\n // OpenAI models\n if (model.includes('gpt')) {\n return { provider: 'openai', model };\n }\n\n // Google models\n if (model.includes('gemini')) {\n return { provider: 'google', model };\n }\n\n // MiniMax models\n if (model.includes('minimax')) {\n return { provider: 'custom', model };\n }\n\n // Default to anthropic/claude\n return { provider: 'anthropic', model: 'claude-sonnet-4' };\n}\n\n/**\n * Parse a Claude Code session JSONL file and extract usage with per-message cost calculation\n *\n * This function calculates costs accurately for sessions that span multiple models\n * (e.g., when Claude Max auto-upgrades from Sonnet to Opus mid-conversation).\n *\n * Cost Calculation:\n * - `cost_v2`: Accurate per-message pricing. Each message is costed using its own model's pricing.\n * - `cost`: DEPRECATED. Uses first model's pricing for all messages (kept for backward compatibility).\n *\n * Model Display:\n * - Single model: \"claude-sonnet-4-6\" (normalized name)\n * - Multiple models: \"claude-sonnet-4-6 → claude-opus-4-6\" (progression of normalized names)\n *\n * Model Breakdown:\n * - Keys are exact model IDs (e.g., \"claude-sonnet-4-5-20250929\")\n * - Values contain per-model cost, token counts, and message count\n *\n * @param sessionFile - Path to the .jsonl session file\n * @returns Session usage summary with accurate multi-model costing, or null if no usage found\n */\nexport function parseClaudeSessionSync(sessionFile: string): SessionUsage | null {\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n const content = readFileSync(sessionFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n let sessionId = '';\n let startTime = '';\n let endTime = '';\n let primaryModel = '';\n let messageCount = 0;\n\n const totalUsage: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n\n // NEW: Per-message cost tracking\n const modelBreakdown: Record<string, {\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }> = {};\n let totalCostV2 = 0;\n\n // Claude Code repeats the same `usage` on every JSONL line of one API response\n // (text line, each tool_use line, …). Dedup on requestId/message.id so a multi-block\n // turn is counted once instead of inflating tokens/cost ~2-3×.\n const countedUsageIds = new Set<string>();\n\n for (const line of lines) {\n try {\n const msg: ClaudeMessage = JSON.parse(line);\n\n // Extract session ID from first message\n if (msg.sessionId && !sessionId) {\n sessionId = msg.sessionId;\n }\n\n // Track timestamps\n if (msg.timestamp) {\n if (!startTime || msg.timestamp < startTime) {\n startTime = msg.timestamp;\n }\n if (!endTime || msg.timestamp > endTime) {\n endTime = msg.timestamp;\n }\n }\n\n // Extract usage - can be in message.usage or top-level usage\n const usage = msg.message?.usage || msg.usage;\n const modelId = msg.message?.model || msg.model; // Exact model ID\n const usageId = msg.requestId ?? msg.message?.id;\n\n if (usage && (usageId === undefined || !countedUsageIds.has(usageId))) {\n if (usageId !== undefined) countedUsageIds.add(usageId);\n // Accumulate total tokens (existing behavior)\n totalUsage.inputTokens += usage.input_tokens || 0;\n totalUsage.outputTokens += usage.output_tokens || 0;\n totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);\n totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);\n messageCount++;\n\n // NEW: Calculate cost for THIS message using THIS message's model\n if (modelId) {\n // Normalize model name for pricing lookup\n const { provider, model: normalizedModel } = normalizeModelName(modelId);\n const pricing = getPricingSync(provider, normalizedModel);\n\n if (pricing) {\n // Create message-specific usage object\n const msgUsage: TokenUsage = {\n inputTokens: usage.input_tokens || 0,\n outputTokens: usage.output_tokens || 0,\n cacheReadTokens: usage.cache_read_input_tokens || 0,\n cacheWriteTokens: usage.cache_creation_input_tokens || 0,\n cacheTTL: usage.cache_creation?.ephemeral_1h_input_tokens\n && !usage.cache_creation?.ephemeral_5m_input_tokens\n ? '1h'\n : '5m',\n };\n\n // Calculate cost for this message\n const msgCost = calculateCostSync(msgUsage, pricing);\n totalCostV2 += msgCost;\n\n // Track breakdown by exact model ID\n if (!modelBreakdown[modelId]) {\n modelBreakdown[modelId] = {\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n messageCount: 0,\n };\n }\n modelBreakdown[modelId].cost += msgCost;\n modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;\n modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;\n modelBreakdown[modelId].cacheReadTokens = (modelBreakdown[modelId].cacheReadTokens || 0)\n + (msgUsage.cacheReadTokens || 0);\n modelBreakdown[modelId].cacheWriteTokens = (modelBreakdown[modelId].cacheWriteTokens || 0)\n + (msgUsage.cacheWriteTokens || 0);\n modelBreakdown[modelId].messageCount++;\n }\n }\n }\n\n // Track primary model (first model found - for backward compatibility)\n if (modelId && !primaryModel) {\n primaryModel = modelId;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // If no usage found, return null\n if (totalUsage.inputTokens === 0 && totalUsage.outputTokens === 0) {\n return null;\n }\n\n // Use filename as session ID if not found in messages\n if (!sessionId) {\n sessionId = basename(sessionFile, '.jsonl');\n }\n\n // Default model if not found\n if (!primaryModel) {\n primaryModel = 'claude-sonnet-4';\n }\n\n // Generate model display string (normalized names)\n // For multi-model sessions: \"claude-sonnet-4-6 → claude-opus-4-6\"\n // For single-model sessions: \"claude-sonnet-4-6\"\n const normalizedModels = Object.keys(modelBreakdown)\n .map(id => normalizeModelName(id).model);\n const modelDisplay = normalizedModels.length > 0\n ? (normalizedModels.length > 1\n ? normalizedModels.join(' → ')\n : normalizedModels[0])\n : normalizeModelName(primaryModel).model;\n\n // DEPRECATED: Calculate cost using first model (for backward compatibility)\n const { provider, model } = normalizeModelName(primaryModel);\n const pricing = getPricingSync(provider, model);\n const cost = pricing ? calculateCostSync(totalUsage, pricing) : 0;\n\n return {\n sessionId,\n sessionFile,\n startTime: startTime || new Date().toISOString(),\n endTime: endTime || new Date().toISOString(),\n model: modelDisplay,\n usage: totalUsage,\n cost, // DEPRECATED: First-model pricing\n cost_v2: totalCostV2 > 0 ? totalCostV2 : undefined, // NEW: Accurate per-message pricing\n messageCount,\n modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : undefined, // NEW: Cost breakdown by model\n };\n}\n\n/**\n * Parse all sessions and return usage summaries\n */\nexport function parseAllSessionsSync(maxAge?: number): SessionUsage[] {\n const sessions: SessionUsage[] = [];\n const cutoffTime = maxAge ? Date.now() - maxAge : 0;\n\n for (const file of getAllSessionFilesSync()) {\n try {\n const stat = statSync(file);\n if (cutoffTime && stat.mtime.getTime() < cutoffTime) {\n continue;\n }\n\n const usage = parseClaudeSessionSync(file);\n if (usage) {\n sessions.push(usage);\n }\n } catch {\n // Skip files that can't be read\n }\n }\n\n return sessions;\n}\n\n/**\n * Get recent sessions (last N days)\n */\nexport function getRecentSessionsSync(days: number = 7): SessionUsage[] {\n const maxAge = days * 24 * 60 * 60 * 1000;\n return parseAllSessionsSync(maxAge);\n}\n\n/**\n * Get the active session model for a workspace\n * Returns the full model ID (e.g., \"claude-sonnet-4-5-20250929\") from the most recent session file\n *\n * NOTE: Claude Max can auto-upgrade models mid-session (e.g., Sonnet → Opus).\n * We read from the END of the file to get the CURRENT model, not the initial one.\n */\nexport function getActiveSessionModelSync(workspacePath: string): string | null {\n try {\n // Convert workspace path to Claude project dir name\n // e.g., /home/user/projects/myn/workspaces/feature-min-664\n // -> -home-user-projects-myn-workspaces-feature-min-664\n // NOTE: The directory name KEEPS the leading dash\n const projectDirName = encodeClaudeProjectDir(workspacePath);\n const projectDir = join(getClaudeProjectsDir(), projectDirName);\n\n // Find most recently modified session file\n const sessions = getSessionFilesSync(projectDir);\n if (sessions.length === 0) {\n return null;\n }\n\n // Parse the most recent session file to find model\n const mostRecentSession = sessions[0]; // Already sorted by mtime\n const content = readFileSync(mostRecentSession, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n // Read from END of file to get CURRENT model (may have been auto-upgraded by Claude Max)\n // Look at last 100 lines to find the most recent model entry\n const searchLines = lines.slice(-100);\n for (let i = searchLines.length - 1; i >= 0; i--) {\n try {\n const msg: ClaudeMessage = JSON.parse(searchLines[i]);\n const model = msg.message?.model || msg.model;\n // Skip synthetic/placeholder model values\n if (model && model !== '<synthetic>') {\n return model; // Return full model ID\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // Fallback: check first few lines if nothing found at end\n for (let i = 0; i < Math.min(lines.length, 10); i++) {\n try {\n const msg: ClaudeMessage = JSON.parse(lines[i]);\n const model = msg.message?.model || msg.model;\n if (model && model !== '<synthetic>') {\n return model;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return null;\n } catch (error) {\n console.warn('Failed to get active session model:', error);\n return null;\n }\n}\n\n/**\n * Import session usage to cost log\n */\nexport function importSessionToCostLog(\n session: SessionUsage,\n options: {\n issueId?: string;\n agentId?: string;\n operation?: string;\n } = {}\n): CostEntry | null {\n const { provider, model } = normalizeModelName(session.model);\n const pricing = getPricingSync(provider, model);\n\n if (!pricing) {\n console.warn(`No pricing found for ${session.model}`);\n return null;\n }\n\n return logCostSync({\n provider,\n model,\n usage: session.usage,\n cost: session.cost,\n currency: 'USD',\n operation: options.operation || 'claude_session',\n issueId: options.issueId,\n agentId: options.agentId,\n metadata: {\n sessionId: session.sessionId,\n sessionFile: session.sessionFile,\n startTime: session.startTime,\n endTime: session.endTime,\n messageCount: session.messageCount,\n },\n });\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/** Effect variant of getProjectDirs. */\nexport const getProjectDirs = (): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getProjectDirsSync(),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getProjectDirs', cause }),\n });\n\n/** Effect variant of getSessionFiles. */\nexport const getSessionFiles = (\n projectDir: string,\n): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getSessionFilesSync(projectDir),\n catch: (cause) => new FsError({ path: projectDir, operation: 'getSessionFiles', cause }),\n });\n\n/** Effect variant of getAllSessionFiles. */\nexport const getAllSessionFiles = (): Effect.Effect<string[], FsError> =>\n Effect.try({\n try: () => getAllSessionFilesSync(),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getAllSessionFiles', cause }),\n });\n\n/** Effect variant of parseClaudeSession. */\nexport const parseClaudeSession = (\n sessionFile: string,\n): Effect.Effect<SessionUsage | null, FsError> =>\n Effect.try({\n try: () => parseClaudeSessionSync(sessionFile),\n catch: (cause) => new FsError({ path: sessionFile, operation: 'parseClaudeSession', cause }),\n });\n\n/** Effect variant of parseAllSessions. */\nexport const parseAllSessions = (\n maxAge?: number,\n): Effect.Effect<SessionUsage[], FsError> =>\n Effect.try({\n try: () => parseAllSessionsSync(maxAge),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'parseAllSessions', cause }),\n });\n\n/** Effect variant of getRecentSessions. */\nexport const getRecentSessions = (\n days: number = 7,\n): Effect.Effect<SessionUsage[], FsError> =>\n Effect.try({\n try: () => getRecentSessionsSync(days),\n catch: (cause) => new FsError({ path: '~/.claude/projects', operation: 'getRecentSessions', cause }),\n });\n\n/** Effect variant of getActiveSessionModel. */\nexport const getActiveSessionModel = (\n workspacePath: string,\n): Effect.Effect<string | null, FsError> =>\n Effect.try({\n try: () => getActiveSessionModelSync(workspacePath),\n catch: (cause) => new FsError({ path: workspacePath, operation: 'getActiveSessionModel', cause }),\n });\n","/**\n * Cloister Specialist Agents\n *\n * Manages long-running specialist agents that can be woken up on demand.\n * Specialists maintain context across invocations via session files.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { randomUUID, createHash } from 'crypto';\nimport { Effect } from 'effect';\nimport { AGENTS_DIR, OVERDECK_HOME } from '../paths.js';\nimport { getDevrootPathSync } from '../config.js';\nimport { getClaudePermissionFlagsStringSync } from '../claude-permissions.js';\nimport { getProjectSync } from '../projects.js';\nimport { getAllSessionFilesSync, parseClaudeSessionSync } from '../cost-parsers/jsonl-parser.js';\nimport { createSpecialistHandoff, logSpecialistHandoff } from './specialist-handoff-logger.js';\nimport type { ModelId } from '../settings.js';\nimport { loadCloisterConfigSync } from './config.js';\nimport { readCavemanVariant } from '../caveman/workspace.js';\nimport { getProviderForModelSync, setupCredentialFileAuthSync, clearCredentialFileAuthSync } from '../providers.js';\nimport { getProviderEnvForModel } from '../agents.js';\nimport { generateLauncherScriptSync, generateLauncherWrapperSync } from '../launcher-generator.js';\nimport { resolveHarness } from '../harness-resolve.js';\nimport { killSession, listPaneValues, sessionExists } from '../tmux.js';\nimport { notifyPipelineSync } from '../pipeline-notifier.js';\nimport { isTaskReadySync } from './task-readiness.js';\n\nconst execAsync = promisify(exec);\n\nfunction roleForSpecialistModel(specialistType: string): { role: 'plan' | 'work' | 'review' | 'test' | 'ship'; subRole?: string } {\n const normalized = specialistType.replace(/-agent$/, '');\n if (normalized === 'inspect') return { role: 'work', subRole: 'inspect' };\n if (normalized === 'inspect-deep') return { role: 'work', subRole: 'inspect-deep' };\n if (normalized === 'review') return { role: 'review' };\n if (normalized === 'test' || normalized === 'uat') return { role: 'test' };\n if (normalized === 'merge' || normalized === 'ship') return { role: 'ship' };\n if (normalized === 'planning' || normalized === 'plan') return { role: 'plan' };\n return { role: 'work' };\n}\n\n\n/**\n * Resolve git directories and branch name from a workspace path.\n * Handles both monorepo (single .git at root) and polyrepo (multiple .git in subdirs).\n * When task.branch is missing, detects it from the checked-out branch in git repos.\n */\nasync function resolveWorkspaceGitInfo(workspace: string | undefined, taskBranch: string | undefined): Promise<{\n gitDirs: string[];\n branch: string;\n isPolyrepo: boolean;\n}> {\n const gitDirs: string[] = [];\n let branch = taskBranch || 'unknown';\n\n if (!workspace || workspace === 'unknown') {\n return { gitDirs, branch, isPolyrepo: false };\n }\n\n // Detect git directories\n if (existsSync(join(workspace, '.git'))) {\n gitDirs.push(workspace);\n } else {\n try {\n const entries = readdirSync(workspace, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && existsSync(join(workspace, entry.name, '.git'))) {\n gitDirs.push(join(workspace, entry.name));\n }\n }\n } catch {}\n }\n\n // Auto-resolve branch from git when not provided\n if (branch === 'unknown' && gitDirs.length > 0) {\n try {\n const { stdout } = await execAsync(\n `cd \"${gitDirs[0]}\" && git branch --show-current`,\n { encoding: 'utf-8', timeout: 5000 }\n );\n const detected = stdout.trim();\n if (detected) {\n branch = detected;\n }\n } catch {}\n }\n\n return { gitDirs, branch, isPolyrepo: gitDirs.length > 1 };\n}\n\n/**\n * Shell fragment that unsets every provider-routing env var a parent tmux server\n * may have leaked into its child sessions. The overdeck tmux server is long-lived\n * and inherits whatever env existed when it was spawned — so fresh Anthropic-model\n * agents can still see a stale ANTHROPIC_BASE_URL pointing at cliproxy, which\n * responds with \"unknown provider for model claude-*\" (PAN-705). Every launcher\n * script must run this before exec'ing claude.\n */\nconst PROVIDER_ENV_UNSETS = [\n 'ANTHROPIC_BASE_URL',\n 'ANTHROPIC_AUTH_TOKEN',\n 'OPENAI_API_KEY',\n 'GEMINI_API_KEY',\n 'API_TIMEOUT_MS',\n 'CLAUDE_CODE_API_KEY_HELPER_TTL_MS',\n];\nconst PROVIDER_UNSET_CMD = `unset ${PROVIDER_ENV_UNSETS.join(' ')}`;\n\n/**\n * Convert a providerEnv dict to bash export lines.\n * Non-Anthropic models (e.g. gpt-5.4 via cliproxy) need ANTHROPIC_BASE_URL set in the\n * script body after provider env vars are unset.\n */\nfunction buildProviderExportLines(providerEnv: Record<string, string>): string {\n const entries = Object.entries(providerEnv);\n if (entries.length === 0) return '';\n return entries.map(([k, v]) => `export ${k}=\"${v}\"`).join('\\n') + '\\n';\n}\n\n/**\n * Build tmux -e flags for environment variables\n */\nfunction buildTmuxEnvFlags(env: Record<string, string>): string {\n let flags = '';\n for (const [key, value] of Object.entries(env)) {\n flags += ` -e ${key}=\"${value.replace(/\"/g, '\\\\\"')}\"`;\n }\n return flags;\n}\n\n\nexport async function buildSpecialistBaseCommand(\n specialistType: string,\n model: string,\n sessionName?: string,\n): Promise<string> {\n const { getAgentRuntimeBaseCommand } = await import('../agents.js');\n const role = roleForSpecialistModel(specialistType).role;\n const harness = await resolveHarness({ model, role });\n const agentDefinition = specialistType.startsWith('pan-')\n ? specialistType\n : `pan-${specialistType.endsWith('-agent')\n ? specialistType.slice(0, -'-agent'.length)\n : specialistType}-agent`;\n return getAgentRuntimeBaseCommand(model, sessionName, agentDefinition, harness);\n}\n\nfunction readRecordedClaudeSessionId(tmuxSession: string): string | null {\n const sessionFile = join(AGENTS_DIR, tmuxSession, 'session.id');\n if (!existsSync(sessionFile)) return null;\n try {\n const sessionId = readFileSync(sessionFile, 'utf-8').trim();\n return sessionId || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Build shell export lines for caveman compression for specialist agents.\n *\n * Excluded: inspect-agent (its INSPECTION PASSED/BLOCKED sentinels are parsed by Cloister).\n * Uses per-specialist-type intensity from config.\n *\n * @param specialistType The specialist type (review-agent, test-agent, etc.)\n * @param workspacePath Workspace path to read the A/B variant from (may be undefined)\n * @param config Normalized caveman config\n * @returns Shell export lines to inject into the inner script\n */\nexport async function buildSpecialistCavemanExports(\n specialistType: string,\n workspacePath: string | undefined,\n config: import('../config-yaml.js').NormalizedCavemanConfig\n): Promise<string> {\n // inspect-agent: never compress — output contains sentinel strings parsed by Cloister\n if (specialistType === 'inspect-agent' || !config.enabled) return '';\n\n // Read the workspace's A/B variant if we have a workspace path\n const variant = workspacePath ? await Effect.runPromise(readCavemanVariant(workspacePath)) : 'off';\n if (variant === 'off') return '';\n if (variant === 'disabled') {\n return `export OVERDECK_CAVEMAN_VARIANT=\"${variant}\"\\n`;\n }\n\n // Map specialist type to caveman intensity mode\n const modeMap: Record<string, keyof typeof config.modes> = {\n 'review-agent': 'review',\n 'test-agent': 'test',\n 'merge-agent': 'merge',\n };\n const modeKey = modeMap[specialistType];\n if (!modeKey) return '';\n\n const mode = config.modes[modeKey];\n if (mode === 'off' || mode === 'disabled') return '';\n\n return `export CAVEMAN_DEFAULT_MODE=\"${mode}\"\\nexport OVERDECK_CAVEMAN_VARIANT=\"${variant}\"\\n`;\n}\n\nconst SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\nconst REGISTRY_FILE = join(SPECIALISTS_DIR, 'registry.json');\n\nconst SPECIALIST_AGENT_NAMES = ['merge-agent', 'review-agent', 'test-agent', 'inspect-agent', 'uat-agent'] as const;\nexport type SpecialistAgentName = typeof SPECIALIST_AGENT_NAMES[number];\n\ntype SpecialistLifecycleState = 'sleeping' | 'active' | 'uninitialized';\n\nexport interface LegacySpecialistDefinition {\n name: SpecialistAgentName;\n displayName: string;\n description: string;\n enabled: boolean;\n autoWake: boolean;\n sessionId?: string;\n lastWake?: string; // ISO 8601 timestamp\n contextTokens?: number;\n}\n\nexport interface LegacySpecialistRuntimeStatus extends LegacySpecialistDefinition {\n state: SpecialistLifecycleState;\n isRunning: boolean;\n tmuxSession?: string;\n currentIssue?: string; // Issue ID currently being worked on\n}\n\n/**\n * One step in the model resolution trace (PAN-754)\n */\nexport interface ResolutionStep {\n source: 'explicit-param' | 'role-config' | 'cloister-config' | 'fallback';\n workTypeId?: string;\n configKey?: string;\n resolvedAlias?: string;\n resolvedModel: string;\n matched: boolean;\n}\n\n/**\n * Per-project specialist metadata\n */\nexport interface ProjectSpecialistMetadata {\n runCount: number;\n lastRunAt: string | null;\n lastRunStatus: 'passed' | 'failed' | 'blocked' | null;\n currentRun: string | null; // Run ID if active\n sessionId?: string; // Legacy session ID for transition period\n // Identity fields (PAN-754)\n issueId?: string;\n tmuxSession?: string; // Stored at spawn time so we can look it up without recomputing\n role?: string; // For convoy members: 'correctness' | 'performance' | etc.\n // Activity visibility (PAN-754)\n currentActivity?: string | null;\n model?: string | null;\n resolutionTrace?: ResolutionStep[] | null;\n // Write-scope (PAN-754)\n writeScope?: 'full' | 'readonly-plus-output';\n outputPath?: string | null;\n workspace?: string | null; // workspace path for write-scope conflict detection\n}\n\nexport function isProjectSpecialistActivelyRunning(\n runtimeState?: { state?: 'active' | 'idle' | 'suspended' | 'stopped' | 'uninitialized' | 'waiting-on-human' } | null,\n fallbackRunning: boolean = false\n): boolean {\n if (runtimeState?.state === 'active') return true;\n if (\n runtimeState?.state === 'idle'\n || runtimeState?.state === 'suspended'\n || runtimeState?.state === 'stopped'\n || runtimeState?.state === 'waiting-on-human'\n ) {\n return false;\n }\n return fallbackRunning;\n}\n\n/**\n * Registry of all specialist agents (per-project structure)\n */\nexport interface SpecialistRegistry {\n version: string;\n // Global defaults for specialist configuration\n defaults: {\n contextRuns: number;\n digestModel: string | null;\n retention: { maxDays: number; maxRuns: number };\n };\n // Per-project specialist metadata\n projects: {\n [projectKey: string]: {\n [specialistType: string]: ProjectSpecialistMetadata;\n };\n };\n // Legacy: Global specialists list (for backward compatibility)\n specialists?: LegacySpecialistDefinition[];\n lastUpdated: string; // ISO 8601 timestamp\n}\n\n/**\n * PAN-1048 review feedback 003 (REQ-16): default specialist definitions are\n * gone. The role primitive replaces the specialist identity model, so\n * recreating registry.json with a hard-coded list of specialist names would\n * just resurrect what startup cleanup just deleted.\n */\nconst DEFAULT_SPECIALISTS: LegacySpecialistDefinition[] = [];\n\n/**\n * PAN-1048 review feedback 003 (REQ-16): initSpecialistsDirectory is a no-op.\n *\n * The cleanup at Cloister startup (service.ts cleanupLegacySpecialistsDirectory)\n * removes ~/.overdeck/specialists/ on every boot. The previous body of this\n * function would unconditionally re-create the directory and seed\n * registry.json from DEFAULT_SPECIALISTS the next time anything called\n * loadRegistry(), undoing the cleanup and resurrecting the legacy identity\n * model. The stub keeps the call sites alive (callers that still loadRegistry\n * get an in-memory default registry) without recreating any disk artifacts.\n */\nexport function initSpecialistsDirectory(): void {\n // Intentionally empty. See block comment above.\n}\n\n/**\n * Migrate old registry format to new per-project structure (PAN-754: compound-key aware).\n *\n * v1.0 → v2.0: flat specialist list → projects[projectKey][specialistType]\n * v2.0 → v3.0: projects[projectKey][specialistType] → projects[projectKey][compoundKey]\n * Legacy v2.0 plain-type keys are left as-is (still readable by compat wrappers).\n * getProjectSpecialistMetadata() and updateProjectSpecialistMetadata() handle both formats.\n */\nfunction migrateRegistryIfNeeded(): void {\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n const registry = JSON.parse(content) as SpecialistRegistry;\n\n // v2.0 already migrated (or registry.projects exists for fresh installs)\n if (registry.version === '2.0' && registry.projects) {\n // No additional migration needed — legacy plain-type keys are handled by compat wrappers.\n return;\n }\n\n if (!registry.projects) {\n // v1.0 → v2.0: add projects map\n console.log('[specialists] Migrating registry v1.0 → v2.0...');\n\n const migratedRegistry: SpecialistRegistry = {\n version: '2.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: {\n maxDays: 30,\n maxRuns: 50,\n },\n },\n projects: {},\n specialists: registry.specialists,\n lastUpdated: new Date().toISOString(),\n };\n\n saveRegistry(migratedRegistry);\n console.log('[specialists] Registry migration v1.0 → v2.0 complete');\n }\n } catch (error) {\n console.error('[specialists] Failed to migrate registry:', error);\n }\n}\n\n/**\n * Load the specialist registry\n *\n * @returns Specialist registry\n */\nexport function loadRegistry(): SpecialistRegistry {\n // PAN-1048 review feedback 003 (REQ-16): do not recreate the directory.\n // If registry.json is missing (typical after Cloister startup cleanup),\n // return an empty in-memory registry instead of seeding disk.\n if (!existsSync(REGISTRY_FILE)) {\n return {\n version: '3.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: { maxDays: 30, maxRuns: 50 },\n },\n projects: {},\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n }\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n return JSON.parse(content);\n } catch (error) {\n console.error('Failed to load specialist registry:', error);\n return {\n version: '3.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: { maxDays: 30, maxRuns: 50 },\n },\n projects: {},\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Save the specialist registry\n *\n * @param registry - Registry to save\n */\nexport function saveRegistry(registry: SpecialistRegistry): void {\n // PAN-1048 review feedback 003 (REQ-16): only persist when the legacy\n // directory already exists. Cloister startup cleanup deletes the directory\n // on every boot, so writing here without a pre-existing dir would re-create\n // the legacy identity model that the cleanup is meant to remove. The\n // in-memory registry from loadRegistry() is enough for the few residual\n // call sites that still consult specialist metadata.\n if (!existsSync(SPECIALISTS_DIR)) {\n return;\n }\n\n registry.lastUpdated = new Date().toISOString();\n\n try {\n const content = JSON.stringify(registry, null, 2);\n writeFileSync(REGISTRY_FILE, content, 'utf-8');\n } catch (error) {\n console.error('Failed to save specialist registry:', error);\n throw error;\n }\n}\n\n/**\n * Generate a deterministic UUID from a string.\n * Uses SHA-256 hash formatted as a UUID v4-compatible string.\n * This ensures the same specialist+project always gets the same session ID\n * while satisfying Claude Code's UUID format requirement.\n */\nfunction deterministicUUID(input: string): string {\n const hash = createHash('sha256').update(input).digest('hex');\n // Format as UUID: 8-4-4-4-12\n return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;\n}\n\n/**\n * Get metadata for a specific specialist\n *\n * @param name - Specialist name\n * @returns Specialist metadata or null if not found\n */\nexport function getSpecialistMetadata(name: SpecialistAgentName): LegacySpecialistDefinition | null {\n const registry = loadRegistry();\n return (registry.specialists ?? []).find((s) => s.name === name) || null;\n}\n\n/**\n * Update specialist metadata\n *\n * @param name - Specialist name\n * @param updates - Partial metadata to update\n */\nexport function updateSpecialistMetadata(\n name: SpecialistAgentName,\n updates: Partial<LegacySpecialistDefinition>\n): void {\n const registry = loadRegistry();\n\n const specialists = registry.specialists ?? [];\n const index = specialists.findIndex((s) => s.name === name);\n\n if (index === -1) {\n throw new Error(`Specialist ${name} not found in registry`);\n }\n\n specialists[index] = {\n ...specialists[index],\n ...updates,\n name, // Ensure name doesn't change\n };\n registry.specialists = specialists;\n\n saveRegistry(registry);\n}\n\n/**\n * Get all specialist metadata\n *\n * @returns Array of all specialists\n */\nexport function getAllSpecialists(): LegacySpecialistDefinition[] {\n const registry = loadRegistry();\n return registry.specialists ?? [];\n}\n\n/**\n * Check if a legacy specialist has a recorded Claude session.\n *\n * @param name - Specialist name\n * @returns True if the specialist has a recorded session id in its agent directory\n */\nexport function isInitialized(name: SpecialistAgentName): boolean {\n return readRecordedClaudeSessionId(getTmuxSessionName(name)) !== null;\n}\n\n/**\n * Get the state of a specialist from recorded agent metadata.\n *\n * Note: This only checks whether a recorded Claude session exists, not if it's actually running.\n * Use getSpecialistStatus() for runtime state.\n *\n * @param name - Specialist name\n * @returns Specialist state\n */\nexport function getSpecialistState(name: SpecialistAgentName): Exclude<SpecialistLifecycleState, 'active'> {\n return isInitialized(name) ? 'sleeping' : 'uninitialized';\n}\n\n/**\n * Get tmux session name for a specialist\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Expected tmux session name\n */\nexport function getTmuxSessionName(name: SpecialistAgentName, projectKey?: string, issueId?: string): string {\n if (projectKey && issueId) {\n return `specialist-${projectKey}-${issueId}-${name}`;\n }\n if (projectKey) {\n return `specialist-${projectKey}-${name}`;\n }\n // Legacy format for backward compatibility\n return `specialist-${name}`;\n}\n\n/**\n * The five canonical reviewer roles plus synthesis. One tmux session per role\n * per issue, alive for the lifetime of the issue across all review rounds.\n */\nexport type ReviewerRole =\n | 'correctness'\n | 'security'\n | 'performance'\n | 'requirements'\n | 'synthesis';\n\nexport const REVIEWER_ROLES: readonly ReviewerRole[] = [\n 'correctness',\n 'security',\n 'performance',\n 'requirements',\n 'synthesis',\n] as const;\n\n/**\n * Get the canonical reviewer tmux session name (PAN-830 / PAN-1048).\n *\n * Pattern: `agent-<issueId>-review-<role>`. One tmux session per role per\n * issue — sessions are reused across review rounds via the agent message delivery path.\n * resumption. Round 2 of `review-correctness` does NOT spawn a new session;\n * it injects a follow-up prompt into the existing pane.\n *\n * @param role - One of the canonical reviewer roles\n * @param _projectKey - Unused (kept for signature compatibility with existing callers)\n * @param issueId - Issue identifier (e.g. `pan-540` or `PAN-540`)\n * @returns Canonical tmux session name\n */\nexport function getReviewerSessionName(\n role: ReviewerRole,\n _projectKey: string,\n issueId: string,\n): string {\n return `agent-${issueId.toLowerCase()}-review-${role}`;\n}\n\n/**\n * Parse a canonical reviewer session name back into role + issue.\n * Returns null if the name does not match either pattern.\n * Supports both current `agent-*` format and legacy `specialist-*` format.\n */\nexport function parseReviewerSessionName(name: string): {\n role: ReviewerRole;\n issueId: string;\n} | null {\n // Current PAN-1048+ pattern: agent-<issueId>-review-<role>\n const agentMatch = name.match(/^agent-([a-z0-9]+-\\d+)-review-(correctness|security|performance|requirements|synthesis)$/i);\n if (agentMatch) {\n return { issueId: agentMatch[1]!.toUpperCase(), role: agentMatch[2] as ReviewerRole };\n }\n // Legacy PAN-830 pattern: specialist-<projectKey>-<issueId>-review-<role>\n const legacyMatch = name.match(/^specialist-([\\w.-]+?)-([\\w.-]+?)-review-(correctness|security|performance|requirements|synthesis)$/);\n if (legacyMatch) {\n return { issueId: legacyMatch[2], role: legacyMatch[3] as ReviewerRole };\n }\n return null;\n}\n\n/**\n * Construct the compound registry key for a per-issue specialist (PAN-754).\n * Format: `${specialistType}:${issueId}` or `${specialistType}:${issueId}:${role}` for convoy.\n */\nexport function makeSpecialistRegistryKey(specialistType: string, issueId: string, role?: string): string {\n return role ? `${specialistType}:${issueId}:${role}` : `${specialistType}:${issueId}`;\n}\n\n/**\n * Remove every project-specialist registry entry whose compound key references\n * the given issueId (case-insensitive match on the second segment). Returns\n * the number of entries removed. Called from teardown so closed issues do not\n * leave behind metadata that the deacon keeps inspecting and force-killing.\n */\nexport function pruneSpecialistRegistryEntriesForIssue(issueId: string): number {\n const issueLower = issueId.toLowerCase();\n const issueUpper = issueId.toUpperCase();\n const registry = loadRegistry();\n let removed = 0;\n for (const projectKey of Object.keys(registry.projects ?? {})) {\n const bucket = registry.projects![projectKey] ?? {};\n for (const key of Object.keys(bucket)) {\n const { issueId: keyIssue } = parseSpecialistRegistryKey(key);\n if (!keyIssue) continue;\n if (keyIssue === issueLower || keyIssue === issueUpper) {\n delete bucket[key];\n removed++;\n }\n }\n }\n if (removed > 0) {\n saveRegistry(registry);\n }\n return removed;\n}\n\n/**\n * Parse a compound registry key back into its parts.\n */\nexport function parseSpecialistRegistryKey(key: string): { specialistType: string; issueId?: string; role?: string } {\n const parts = key.split(':');\n if (parts.length === 1) return { specialistType: parts[0] };\n if (parts.length === 2) return { specialistType: parts[0], issueId: parts[1] };\n return { specialistType: parts[0], issueId: parts[1], role: parts[2] };\n}\n\n/**\n * Record wake event in metadata\n *\n * @param name - Specialist name\n * @param sessionId - New session ID (if changed)\n */\nexport function recordWake(name: SpecialistAgentName, sessionId?: string): void {\n const updates: Partial<LegacySpecialistDefinition> = {\n lastWake: new Date().toISOString(),\n };\n\n if (sessionId) {\n updates.sessionId = sessionId;\n }\n\n updateSpecialistMetadata(name, updates);\n}\n\n/**\n * ===========================================================================\n * Ephemeral Lifecycle Management\n * ===========================================================================\n */\n\n/**\n * Grace period state for a specialist\n */\nexport interface GracePeriodState {\n active: boolean;\n startedAt: string;\n duration: number; // milliseconds\n paused: boolean;\n pausedAt?: string;\n remainingTime?: number; // milliseconds when paused\n}\n\nconst gracePeriodStates = new Map<string, GracePeriodState>();\n\n/**\n * Task context interface for specialist tasks.\n */\nexport interface TaskContext {\n prUrl?: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n reason?: string;\n targetModel?: string;\n additionalInstructions?: string;\n [key: string]: string | string[] | undefined;\n}\n\n/**\n * PAN-1048 R1: spawnEphemeralSpecialist removed.\n *\n * The function was the generic dispatcher that took an arbitrary\n * SpecialistAgentName ('review-agent' | 'test-agent' | 'merge-agent' |\n * 'inspect-agent' | …) and shelled out a launcher specific to each. Under\n * the role primitive, those flavours are first-class roles\n * (review/test/ship) plus a single work sub-role (inspect / inspect-deep).\n *\n * Replacements:\n * - Review/test/ship runs: spawnRun(issueId, role, opts) in src/lib/agents.ts.\n * Reactive Cloister fires these on lifecycle transitions; the manual\n * re-dispatch in routes/workspaces.ts also uses spawnRun.\n * - Inspect runs: spawnInspectAgent() in cloister/inspect-agent.ts owns\n * its own minimal launcher path (single-bead-scoped, ephemeral).\n *\n * The specialist registry/run-log/grace-period machinery stays in this\n * file because the dashboard read-model and reset/init/grace endpoints\n * still consume it for legacy run history; nothing writes new entries\n * once the dispatcher is gone.\n */\n\n\n/**\n * Start grace period for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param duration - Grace period duration in milliseconds (default: 60000)\n */\nexport function startGracePeriod(\n projectKey: string,\n specialistType: SpecialistAgentName,\n duration: number = 60000\n): void {\n const key = `${projectKey}-${specialistType}`;\n\n gracePeriodStates.set(key, {\n active: true,\n startedAt: new Date().toISOString(),\n duration,\n paused: false,\n });\n\n console.log(`[specialist] Grace period started for ${projectKey}/${specialistType} (${duration}ms)`);\n\n // Schedule termination after grace period\n setTimeout(() => {\n const state = gracePeriodStates.get(key);\n if (state && state.active && !state.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, duration);\n}\n\n/**\n * Pause grace period countdown\n */\nexport function pauseGracePeriod(projectKey: string, specialistType: SpecialistAgentName): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active) {\n return false;\n }\n\n const elapsed = Date.now() - new Date(state.startedAt).getTime();\n const remaining = state.duration - elapsed;\n\n state.paused = true;\n state.pausedAt = new Date().toISOString();\n state.remainingTime = remaining;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period paused for ${projectKey}/${specialistType}`);\n\n return true;\n}\n\n/**\n * Resume grace period countdown\n */\nexport function resumeGracePeriod(projectKey: string, specialistType: SpecialistAgentName): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active || !state.paused) {\n return false;\n }\n\n state.paused = false;\n state.startedAt = new Date().toISOString();\n state.pausedAt = undefined;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period resumed for ${projectKey}/${specialistType}`);\n\n // Schedule termination for remaining time\n setTimeout(() => {\n const currentState = gracePeriodStates.get(key);\n if (currentState && currentState.active && !currentState.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, state.remainingTime || 0);\n\n return true;\n}\n\n/**\n * Exit grace period immediately (terminate now)\n */\nexport function exitGracePeriod(projectKey: string, specialistType: SpecialistAgentName): void {\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n terminateSpecialist(projectKey, specialistType);\n}\n\n/**\n * Get grace period state\n */\nexport function getGracePeriodState(\n projectKey: string,\n specialistType: SpecialistAgentName\n): GracePeriodState | null {\n const key = `${projectKey}-${specialistType}`;\n return gracePeriodStates.get(key) || null;\n}\n\n/**\n * Find the active registry key for (projectKey, specialistType).\n * Searches compound keys; falls back to plain specialistType key.\n * Returns undefined if nothing is currently active.\n */\nexport function findActiveRegistryKey(projectKey: string, specialistType: SpecialistAgentName): string | undefined {\n const registry = loadRegistry();\n const bucket = registry.projects[projectKey] ?? {};\n\n // Check compound keys first (new format: \"type:issueId\")\n const prefix = `${specialistType}:`;\n const activeCompound = Object.keys(bucket).find(k =>\n k.startsWith(prefix) && bucket[k].currentRun !== null\n );\n if (activeCompound) return activeCompound;\n\n // Check legacy plain key\n if (bucket[specialistType]?.currentRun !== null) return specialistType;\n\n // Return most recently touched key even if not active\n const allMatching = Object.keys(bucket).filter(k =>\n k === specialistType || k.startsWith(prefix)\n ).sort((a, b) =>\n (bucket[b].lastRunAt ?? '').localeCompare(bucket[a].lastRunAt ?? '')\n );\n return allMatching[0];\n}\n\n/**\n * Signal that a specialist has completed its task\n *\n * This should be called when the specialist finishes its work.\n * It updates the run status and starts the grace period.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param result - Task result\n * @param issueId - Optional: issue being handled (used to compute compound registry key)\n */\nexport async function signalSpecialistCompletion(\n projectKey: string,\n specialistType: SpecialistAgentName,\n result: {\n status: 'passed' | 'failed' | 'blocked';\n notes?: string;\n },\n issueId?: string\n): Promise<void> {\n const registryKey = issueId\n ? makeSpecialistRegistryKey(specialistType, issueId)\n : (findActiveRegistryKey(projectKey, specialistType) ?? specialistType);\n const metadata = getRunMetadata(projectKey, registryKey);\n\n // Derive tmuxSession: use stored field when available, recompute as fallback\n const resolvedTmuxSession = metadata.tmuxSession ?? getTmuxSessionName(specialistType, projectKey, issueId);\n\n // Update status\n updateRunStatus(projectKey, registryKey, result.status);\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n try {\n const { finalizeRunLogSync } = await import('./specialist-logs.js');\n finalizeRunLogSync(projectKey, specialistType, metadata.currentRun, {\n status: result.status,\n notes: result.notes,\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n }\n\n // Completion means the run itself is over, even if the tmux session stays alive\n // during the grace period for inspection or manual termination.\n setCurrentRun(projectKey, registryKey, null);\n updateRunMetadata(projectKey, registryKey, { currentActivity: null });\n import('../agents.js')\n .then(({ saveAgentRuntimeState }) => {\n saveAgentRuntimeState(resolvedTmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n })\n .catch((error) => {\n console.error(`[specialist] Failed to mark ${projectKey}/${specialistType} idle:`, error);\n });\n\n // Start grace period (60 seconds)\n startGracePeriod(projectKey, specialistType, 60000);\n\n console.log(`[specialist] ${specialistType} completed for ${projectKey} (status: ${result.status})`);\n}\n\n/**\n * Terminate a specialist session\n *\n * Kills the tmux session, finalizes logs, and schedules digest generation.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport async function terminateSpecialist(\n projectKey: string,\n specialistType: SpecialistAgentName,\n issueId?: string\n): Promise<void> {\n const registryKey = issueId\n ? makeSpecialistRegistryKey(specialistType, issueId)\n : (findActiveRegistryKey(projectKey, specialistType) ?? specialistType);\n const metadata = getRunMetadata(projectKey, registryKey);\n\n // Derive tmuxSession: use stored field, or recompute\n const tmuxSession = metadata.tmuxSession ?? getTmuxSessionName(specialistType, projectKey, issueId);\n\n try {\n // Kill tmux session\n await Effect.runPromise(killSession(tmuxSession));\n console.log(`[specialist] Terminated ${projectKey}/${specialistType}`);\n } catch (error) {\n console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);\n }\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLogSync } = await import('./specialist-logs.js');\n\n try {\n finalizeRunLogSync(projectKey, specialistType, metadata.currentRun, {\n status: metadata.lastRunStatus || 'incomplete',\n notes: 'Specialist terminated',\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n\n // Clear current run\n setCurrentRun(projectKey, registryKey, null);\n }\n\n updateRunMetadata(projectKey, registryKey, { currentActivity: null });\n\n // Clear grace period state\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n // Update runtime state\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'suspended',\n lastActivity: new Date().toISOString(),\n });\n\n // Schedule digest generation (async, fire-and-forget)\n const { scheduleDigestGeneration } = await import('./specialist-context.js');\n scheduleDigestGeneration(projectKey, specialistType);\n\n // Run log cleanup for this project/specialist (async, fire-and-forget)\n scheduleLogCleanup(projectKey, specialistType);\n}\n\n/**\n * Schedule log cleanup for a project's specialist (async, fire-and-forget)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nfunction scheduleLogCleanup(projectKey: string, specialistType: SpecialistAgentName): void {\n // Run async without awaiting\n Promise.resolve().then(async () => {\n try {\n const { cleanupOldLogsSync } = await import('./specialist-logs.js');\n const { getSpecialistRetention } = await import('../projects.js');\n\n const retention = getSpecialistRetention(projectKey);\n const deleted = cleanupOldLogsSync(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });\n\n if (deleted > 0) {\n console.log(`[specialist] Cleaned up ${deleted} old logs for ${projectKey}/${specialistType}`);\n }\n } catch (error) {\n console.error(`[specialist] Log cleanup failed for ${projectKey}/${specialistType}:`, error);\n }\n });\n}\n\n/**\n * ===========================================================================\n * Per-Project Specialist Functions\n * ===========================================================================\n */\n\n/**\n * Get the directory for a project's specialist\n */\nexport function getProjectSpecialistDir(projectKey: string, specialistType: SpecialistAgentName): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType);\n}\n\n/**\n * Ensure per-project specialist directory structure exists\n */\nexport function ensureProjectSpecialistDir(projectKey: string, specialistType: SpecialistAgentName): void {\n const specialistDir = getProjectSpecialistDir(projectKey, specialistType);\n const runsDir = join(specialistDir, 'runs');\n const contextDir = join(specialistDir, 'context');\n\n if (!existsSync(runsDir)) {\n mkdirSync(runsDir, { recursive: true });\n }\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Get metadata for a specific (projectKey, registryKey) pair.\n * registryKey is either a plain specialistType (legacy) or a compound key from makeSpecialistRegistryKey().\n */\nexport function getRunMetadata(\n projectKey: string,\n registryKey: string,\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][registryKey]) {\n registry.projects[projectKey][registryKey] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n saveRegistry(registry);\n }\n\n return registry.projects[projectKey][registryKey];\n}\n\n/**\n * Update metadata for a specific (projectKey, registryKey) pair.\n */\nexport function updateRunMetadata(\n projectKey: string,\n registryKey: string,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][registryKey]) {\n registry.projects[projectKey][registryKey] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n }\n\n registry.projects[projectKey][registryKey] = {\n ...registry.projects[projectKey][registryKey],\n ...updates,\n };\n\n saveRegistry(registry);\n}\n\n/**\n * Get per-project specialist metadata — backward-compat wrapper.\n * Searches compound-key entries for this project+type; returns the active run, or most recent.\n */\nexport function getProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistAgentName\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n const projectBucket = registry.projects[projectKey] ?? {};\n\n // Check for exact legacy key first\n if (projectBucket[specialistType]) {\n return projectBucket[specialistType];\n }\n\n // Search compound keys for the most relevant entry\n const prefix = `${specialistType}:`;\n const candidates = Object.entries(projectBucket)\n .filter(([k]) => k.startsWith(prefix))\n .map(([, v]) => v);\n\n // Prefer active run, then most recently started\n const active = candidates.find(c => c.currentRun !== null);\n if (active) return active;\n const sorted = candidates.sort((a, b) =>\n (b.lastRunAt ?? '').localeCompare(a.lastRunAt ?? '')\n );\n if (sorted.length > 0) return sorted[0];\n\n // No entry found — return blank default (don't save it)\n return { runCount: 0, lastRunAt: null, lastRunStatus: null, currentRun: null };\n}\n\n/**\n * Update per-project specialist metadata — backward-compat wrapper.\n * Updates the active compound-key entry, or the legacy plain-key entry.\n */\nexport function updateProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistAgentName,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n const projectBucket = registry.projects[projectKey] ?? {};\n\n // Try legacy key first\n if (projectBucket[specialistType]) {\n updateRunMetadata(projectKey, specialistType, updates);\n return;\n }\n\n // Find the active compound-key entry for this type\n const prefix = `${specialistType}:`;\n const activeKey = Object.keys(projectBucket).find(k =>\n k.startsWith(prefix) && projectBucket[k].currentRun !== null\n );\n if (activeKey) {\n updateRunMetadata(projectKey, activeKey, updates);\n return;\n }\n\n // Fall back to most recent\n const latestKey = Object.keys(projectBucket)\n .filter(k => k.startsWith(prefix))\n .sort((a, b) => (projectBucket[b].lastRunAt ?? '').localeCompare(projectBucket[a].lastRunAt ?? ''))\n .shift();\n if (latestKey) {\n updateRunMetadata(projectKey, latestKey, updates);\n }\n}\n\n/**\n * Increment run count for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function incrementProjectRunCount(projectKey: string, registryKey: string): void {\n const metadata = getRunMetadata(projectKey, registryKey);\n updateRunMetadata(projectKey, registryKey, {\n runCount: metadata.runCount + 1,\n lastRunAt: new Date().toISOString(),\n });\n}\n\n/**\n * Set current run for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function setCurrentRun(\n projectKey: string,\n registryKey: string,\n runId: string | null\n): void {\n updateRunMetadata(projectKey, registryKey, { currentRun: runId });\n}\n\n/**\n * Update run status for a project's specialist.\n * registryKey may be a plain specialistType (legacy) or a compound key.\n */\nexport function updateRunStatus(\n projectKey: string,\n registryKey: string,\n status: 'passed' | 'failed' | 'blocked' | null\n): void {\n updateRunMetadata(projectKey, registryKey, { lastRunStatus: status });\n}\n\n/**\n * List all projects that have specialists configured\n */\nexport function listProjectsWithSpecialists(): string[] {\n const registry = loadRegistry();\n return Object.keys(registry.projects);\n}\n\n/**\n * List all specialist types for a project\n */\nexport function listSpecialistsForProject(projectKey: string): SpecialistAgentName[] {\n const registry = loadRegistry();\n const project = registry.projects[projectKey];\n\n if (!project) {\n return [];\n }\n\n return Object.keys(project) as SpecialistAgentName[];\n}\n\n/**\n * Get all per-project specialist statuses (PAN-754: compound-key aware).\n * Walks registry including compound keys (type:issueId[:role]) and returns\n * enriched entries with issueId, model, currentActivity for the Agents page.\n */\nexport async function getAllProjectSpecialistStatuses(): Promise<Array<{\n projectKey: string;\n specialistType: SpecialistAgentName;\n registryKey: string;\n issueId?: string;\n role?: string;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n}>> {\n const registry = loadRegistry();\n const { getAgentRuntimeStateSync } = await import('../agents.js');\n\n const results: Array<{\n projectKey: string;\n specialistType: SpecialistAgentName;\n registryKey: string;\n issueId?: string;\n role?: string;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n }> = [];\n\n for (const [projectKey, specialists] of Object.entries(registry.projects)) {\n for (const [registryKey, metadata] of Object.entries(specialists)) {\n const { specialistType, issueId, role } = parseSpecialistRegistryKey(registryKey);\n\n // Determine tmux session: use stored field when available\n const tmuxSession = metadata.tmuxSession\n ?? getTmuxSessionName(specialistType as SpecialistAgentName, projectKey, issueId);\n\n const runtimeState = getAgentRuntimeStateSync(tmuxSession);\n const sessionRunning = await isRunning(specialistType as SpecialistAgentName, projectKey).catch(() => false);\n const running = isProjectSpecialistActivelyRunning(runtimeState, sessionRunning);\n const effectiveMetadata = running ? metadata : { ...metadata, currentRun: null };\n\n results.push({\n projectKey,\n specialistType: specialistType as SpecialistAgentName,\n registryKey,\n issueId,\n role,\n metadata: effectiveMetadata,\n isRunning: running,\n tmuxSession,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Update context token count for a specialist\n *\n * @param name - Specialist name\n * @param tokens - Total context tokens\n */\nexport function updateContextTokens(name: SpecialistAgentName, tokens: number): void {\n updateSpecialistMetadata(name, { contextTokens: tokens });\n}\n\n/**\n * Enable a specialist\n *\n * @param name - Specialist name\n */\nexport function enableSpecialist(name: SpecialistAgentName): void {\n updateSpecialistMetadata(name, { enabled: true });\n}\n\n/**\n * Disable a specialist\n *\n * @param name - Specialist name\n */\nexport function disableSpecialist(name: SpecialistAgentName): void {\n updateSpecialistMetadata(name, { enabled: false });\n}\n\n/**\n * Check if a specialist is enabled\n *\n * @param name - Specialist name\n * @returns True if specialist is enabled\n */\nexport function isEnabled(name: SpecialistAgentName): boolean {\n const metadata = getSpecialistMetadata(name);\n return metadata?.enabled ?? false;\n}\n\n/**\n * Get all enabled specialists\n *\n * @returns Array of enabled specialists\n */\nexport function getEnabledSpecialists(): LegacySpecialistDefinition[] {\n return getAllSpecialists().filter((s) => s.enabled);\n}\n\n/**\n * Find JSONL file for a session ID\n *\n * Searches through Claude Code project directories to find the JSONL file.\n *\n * @param sessionId - Session ID to find\n * @returns Path to JSONL file or null if not found\n */\nexport function findSessionFile(sessionId: string): string | null {\n try {\n const allFiles = getAllSessionFilesSync();\n\n for (const file of allFiles) {\n const fileSessionId = basename(file, '.jsonl');\n if (fileSessionId === sessionId) {\n return file;\n }\n }\n } catch {\n // Session files not available\n }\n\n return null;\n}\n\n/**\n * Count context tokens for a specialist session\n *\n * Reads the JSONL file for the specialist's session and sums all token usage.\n * This gives an approximate count of context size.\n *\n * @param name - Specialist name\n * @returns Total token count or null if session not found\n */\nexport function countContextTokens(name: SpecialistAgentName): number | null {\n const sessionId = readRecordedClaudeSessionId(getTmuxSessionName(name));\n\n if (!sessionId) {\n return null;\n }\n\n const sessionFile = findSessionFile(sessionId);\n\n if (!sessionFile) {\n return null;\n }\n\n const sessionUsage = parseClaudeSessionSync(sessionFile);\n\n if (!sessionUsage) {\n return null;\n }\n\n // Sum all token types for total context\n return (\n sessionUsage.usage.inputTokens +\n sessionUsage.usage.outputTokens +\n (sessionUsage.usage.cacheReadTokens || 0) +\n (sessionUsage.usage.cacheWriteTokens || 0)\n );\n}\n\n/**\n * Check if a specialist is currently running in tmux\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns True if specialist has an active tmux session\n */\nexport async function isRunning(name: SpecialistAgentName, projectKey?: string): Promise<boolean> {\n const tmuxSession = getTmuxSessionName(name, projectKey);\n\n try {\n const exists = await Effect.runPromise(sessionExists(tmuxSession));\n if (!exists) return false;\n // Session exists — but check if the pane actually has a running process.\n // When Claude Code crashes, the pane's process exits but the tmux session persists,\n // making has-session return success even though nothing is running.\n const panePid = (await Effect.runPromise(listPaneValues(tmuxSession, '#{pane_pid}')))[0]?.trim() ?? '';\n if (!panePid) return false;\n // Check if the pane's process has any child processes (Claude Code / bash)\n const { stdout: children } = await execAsync(\n `ps --ppid ${panePid} --no-headers 2>/dev/null || echo \"\"`,\n { encoding: 'utf-8' }\n );\n return children.trim().length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Get complete status for a specialist\n *\n * Combines metadata, session info, and runtime state (PAN-80: uses hook-based state).\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Complete specialist status\n */\nexport async function getSpecialistStatus(\n name: SpecialistAgentName,\n projectKey?: string\n): Promise<LegacySpecialistRuntimeStatus> {\n const metadata = getSpecialistMetadata(name) || {\n name,\n displayName: name,\n description: '',\n enabled: false,\n autoWake: false,\n };\n\n const sessionId = readRecordedClaudeSessionId(getTmuxSessionName(name, projectKey));\n const running = await isRunning(name, projectKey);\n const contextTokens = countContextTokens(name);\n\n // Determine state from hook-based runtime state (PAN-80)\n const { getAgentRuntimeStateSync } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name, projectKey);\n const runtimeState = getAgentRuntimeStateSync(tmuxSession);\n\n let state: SpecialistLifecycleState;\n if (runtimeState) {\n // Map runtime state to specialist state\n switch (runtimeState.state) {\n case 'active':\n state = 'active';\n break;\n case 'idle':\n state = 'sleeping'; // Idle = at prompt waiting\n break;\n case 'suspended':\n state = 'sleeping'; // Suspended = session saved, not running\n break;\n case 'uninitialized':\n default:\n state = 'uninitialized';\n break;\n }\n } else {\n // Fallback if no runtime state available\n if (running && sessionId) {\n state = 'sleeping';\n } else if (sessionId) {\n state = 'sleeping';\n } else {\n state = 'uninitialized';\n }\n }\n\n return {\n ...metadata,\n sessionId: sessionId || undefined,\n contextTokens: contextTokens || undefined,\n state,\n isRunning: running,\n tmuxSession: getTmuxSessionName(name, projectKey),\n currentIssue: running ? runtimeState?.currentIssue : undefined,\n };\n}\n\n/**\n * Get status for all specialists\n *\n * @returns Array of specialist statuses\n */\nexport async function getAllSpecialistStatus(): Promise<LegacySpecialistRuntimeStatus[]> {\n const specialists = getAllSpecialists();\n return Promise.all(specialists.map((metadata) => getSpecialistStatus(metadata.name)));\n}\n\n\n/**\n * Initialize all enabled but uninitialized specialists\n *\n * Called during Cloister startup to ensure specialists are ready.\n *\n * @returns Promise with array of initialization results\n */\nexport async function initializeEnabledSpecialists(): Promise<Array<{\n name: SpecialistAgentName;\n success: boolean;\n message: string;\n}>> {\n const enabled = getEnabledSpecialists();\n const results: Array<{ name: SpecialistAgentName; success: boolean; message: string }> = [];\n\n for (const specialist of enabled) {\n results.push({\n name: specialist.name,\n success: true,\n message: 'Legacy global specialist initialization removed; role flows spawn agents on demand.',\n });\n }\n\n return results;\n}\n\n/**\n * ===========================================================================\n * Specialist Feedback System\n * ===========================================================================\n *\n * Specialists accumulate context and expertise. This system allows them to\n * share learnings back to issue agents, creating a feedback loop that\n * improves the overall system over time.\n */\n\n/**\n * Feedback from a specialist to an issue agent\n */\nexport interface SpecialistFeedback {\n id: string;\n timestamp: string;\n fromSpecialist: SpecialistAgentName;\n toIssueId: string;\n feedbackType: 'success' | 'failure' | 'warning' | 'insight';\n category: 'merge' | 'test' | 'review' | 'general';\n summary: string;\n details: string;\n actionItems?: string[];\n patterns?: string[]; // Patterns the specialist noticed\n suggestions?: string[]; // Suggestions for the issue agent\n}\n\nconst FEEDBACK_DIR = join(OVERDECK_HOME, 'specialists', 'feedback');\nconst FEEDBACK_LOG = join(FEEDBACK_DIR, 'feedback.jsonl');\n\n/**\n * Send feedback from a specialist to an issue agent\n *\n * This is the key mechanism for specialists to share their accumulated\n * expertise back to the issue agents that spawned the work.\n *\n * @param feedback - The feedback to send\n * @returns True if feedback was sent successfully\n */\nexport async function sendFeedbackToAgent(\n feedback: Omit<SpecialistFeedback, 'id' | 'timestamp'>\n): Promise<boolean> {\n const { fromSpecialist, toIssueId, summary, details } = feedback;\n\n // Ensure feedback directory exists\n if (!existsSync(FEEDBACK_DIR)) {\n mkdirSync(FEEDBACK_DIR, { recursive: true });\n }\n\n // Create full feedback record\n const fullFeedback: SpecialistFeedback = {\n ...feedback,\n id: `feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n timestamp: new Date().toISOString(),\n };\n\n // Log feedback to JSONL\n try {\n const line = JSON.stringify(fullFeedback) + '\\n';\n appendFileSync(FEEDBACK_LOG, line, 'utf-8');\n } catch (error) {\n console.error(`[specialist] Failed to log feedback:`, error);\n }\n\n // Try to send feedback to the issue agent\n const agentSession = `agent-${toIssueId.toLowerCase()}`;\n\n // Format feedback message for the agent\n const feedbackMessage = formatFeedbackForAgent(fullFeedback);\n\n // Write feedback to workspace file\n const { writeFeedbackFile } = await import('./feedback-writer.js');\n const specialistMap: Record<string, 'review-agent' | 'test-agent' | 'merge-agent'> = {\n 'review-agent': 'review-agent',\n 'test-agent': 'test-agent',\n 'merge-agent': 'merge-agent',\n };\n const specialist = specialistMap[fromSpecialist] || 'review-agent';\n const outcome = feedback.feedbackType === 'success' ? 'approved' : feedback.feedbackType === 'failure' ? 'failed' : feedback.feedbackType;\n\n const fileResult = await Effect.runPromise(writeFeedbackFile({\n issueId: toIssueId,\n specialist,\n outcome,\n summary: summary.slice(0, 100),\n markdownBody: feedbackMessage,\n }));\n\n if (!fileResult.success) {\n console.error(`[specialist] Failed to write feedback file for ${toIssueId}: ${fileResult.error}`);\n return false;\n }\n\n // Send a short, explicit message with the ABSOLUTE path.\n try {\n const { messageAgent } = await import('../agents.js');\n const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.\\n\\nMUST READ: ${fileResult.filePath}\\n\\nUse your Read tool to open this file, read every line, then address the feedback and continue working. Do NOT stop at the prompt.`;\n await messageAgent(agentSession, msg);\n console.log(`[specialist] Sent feedback from ${fromSpecialist} to ${agentSession} (file: ${fileResult.filePath})`);\n return true;\n } catch (err) {\n // Agent may be gone — feedback file is still in the workspace for crash recovery\n console.log(`[specialist] Could not send reference to ${agentSession} (file written): ${err}`);\n return true; // File was written successfully, that's the important part\n }\n}\n\n/**\n * Format feedback for display to an agent\n */\nfunction formatFeedbackForAgent(feedback: SpecialistFeedback): string {\n const { fromSpecialist, feedbackType, category, summary, details, actionItems, patterns, suggestions } = feedback;\n\n const typeEmoji = {\n success: '✅',\n failure: '❌',\n warning: '⚠️',\n insight: '💡',\n }[feedbackType];\n\n let message = `\\n${typeEmoji} **Feedback from ${fromSpecialist}** (${category})\\n\\n`;\n message += `**Summary:** ${summary}\\n\\n`;\n message += `**Details:**\\n${details}\\n`;\n\n if (actionItems?.length) {\n message += `\\n**Action Items:**\\n`;\n actionItems.forEach((item, i) => {\n message += `${i + 1}. ${item}\\n`;\n });\n }\n\n if (patterns?.length) {\n message += `\\n**Patterns Noticed:**\\n`;\n patterns.forEach(pattern => {\n message += `- ${pattern}\\n`;\n });\n }\n\n if (suggestions?.length) {\n message += `\\n**Suggestions:**\\n`;\n suggestions.forEach(suggestion => {\n message += `- ${suggestion}\\n`;\n });\n }\n\n return message;\n}\n\n/**\n * Get pending feedback for an issue that hasn't been delivered yet\n *\n * @param issueId - Issue ID to get feedback for\n * @returns Array of feedback records\n */\nexport function getPendingFeedback(issueId: string): SpecialistFeedback[] {\n if (!existsSync(FEEDBACK_LOG)) {\n return [];\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n const allFeedback = lines.map(line => JSON.parse(line) as SpecialistFeedback);\n\n // Filter to this issue\n return allFeedback.filter(f => f.toIssueId.toLowerCase() === issueId.toLowerCase());\n } catch (error) {\n console.error(`[specialist] Failed to read feedback log:`, error);\n return [];\n }\n}\n\n/**\n * Get feedback statistics for all specialists\n *\n * @returns Feedback stats by specialist and type\n */\nexport function getFeedbackStats(): {\n bySpecialist: Record<SpecialistAgentName, number>;\n byType: Record<string, number>;\n total: number;\n} {\n const stats = {\n bySpecialist: {\n 'merge-agent': 0,\n 'review-agent': 0,\n 'test-agent': 0,\n } as Record<SpecialistAgentName, number>,\n byType: {} as Record<string, number>,\n total: 0,\n };\n\n if (!existsSync(FEEDBACK_LOG)) {\n return stats;\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n\n for (const line of lines) {\n const feedback = JSON.parse(line) as SpecialistFeedback;\n stats.bySpecialist[feedback.fromSpecialist] = (stats.bySpecialist[feedback.fromSpecialist] || 0) + 1;\n stats.byType[feedback.feedbackType] = (stats.byType[feedback.feedbackType] || 0) + 1;\n stats.total++;\n }\n } catch (error) {\n console.error(`[specialist] Failed to read feedback stats:`, error);\n }\n\n return stats;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import "./smart-compaction-CrP90OFE.js";
2
- import { a as generateFallbackSummary, l as init_summary_fork } from "./summary-fork-EjvOkEPl.js";
1
+ import "./smart-compaction-uke1npUz.js";
2
+ import { a as generateFallbackSummary, l as init_summary_fork } from "./summary-fork-Xn0QUNmb.js";
3
3
  init_summary_fork();
4
4
  export { generateFallbackSummary };
@@ -1,9 +1,9 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime-DNCzzKnI.js";
2
2
  import { Gr as sessionFilePath, Rr as getHandoffsDir, Ur as packageRoot, Vr as init_paths, cr as FsError, f as loadConfigSync, t as init_config_yaml, vr as init_errors } from "./config-yaml-DPAieL_G.js";
3
- import { St as deliverAgentMessage, t as init_agents } from "./agents-DHvsjsuP.js";
3
+ import { St as deliverAgentMessage, t as init_agents } from "./agents-C7V4odQ6.js";
4
4
  import { k as init_conversations } from "./conversations-DL_BAWVk.js";
5
5
  import { a as init_stack_health, i as getWorkspaceStackHealth } from "./stack-health-BJVh1TCg.js";
6
- import { a as runModelSummary, i as parseEntries, o as serializeConversation, r as init_smart_compaction, s as summarizeSerializedText, t as generateSmartSummary } from "./smart-compaction-CrP90OFE.js";
6
+ import { a as runModelSummary, i as parseEntries, o as serializeConversation, r as init_smart_compaction, s as summarizeSerializedText, t as generateSmartSummary } from "./smart-compaction-uke1npUz.js";
7
7
  import { Effect } from "effect";
8
8
  import { join } from "path";
9
9
  import { homedir } from "node:os";
@@ -521,4 +521,4 @@ var init_summary_fork = __esmMin((() => {
521
521
  //#endregion
522
522
  export { init_transcript_adapter as _, generateFallbackSummary as a, handoffPreconditionFallbackReason as c, prependFallbackFocus as d, requestHandoffFromAgent as f, getTranscriptAdapter as g, validateHandoffDoc as h, copySessionFromCompactBoundary as i, init_summary_fork as l, sanitizeHandoffDoc as m, HandoffValidationError as n, generateSummaryForFork as o, reserveSummaryForkSession as p, authorHandoffExternal as r, handoffFailureReason as s, HandoffStallError as t, logHandoffFallback as u };
523
523
 
524
- //# sourceMappingURL=summary-fork-EjvOkEPl.js.map
524
+ //# sourceMappingURL=summary-fork-Xn0QUNmb.js.map