@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.
- package/dist/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
- package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
- package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
- package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
- package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
- package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
- package/dist/cli/index.js +45 -45
- package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
- package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
- package/dist/cli-MTB9IOoR.js +3 -0
- package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
- package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
- package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
- package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
- package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
- package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
- package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
- package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
- package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
- package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
- package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
- package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
- package/dist/dashboard/cli-DCqy_wFa.js +3 -0
- package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
- package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
- package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
- package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
- package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
- package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
- package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
- package/dist/dashboard/deacon.js +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
- package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
- package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
- package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
- package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
- package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
- package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
- package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
- package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
- package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
- package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
- package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
- package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
- package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
- package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
- package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
- package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
- package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
- package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
- package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
- package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
- package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
- package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
- package/dist/dashboard/review-agent-DTINEffB.js +3 -0
- package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
- package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
- package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
- package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
- package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
- package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
- package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
- package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
- package/dist/dashboard/server.js +66 -55
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
- package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
- package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
- package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
- package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
- package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
- package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
- package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
- package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
- package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
- package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
- package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
- package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
- package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
- package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
- package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
- package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
- package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
- package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
- package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
- package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
- package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
- package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
- package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
- package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
- package/dist/dashboard/workflows-DVwXauef.js +2 -0
- package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
- package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
- package/dist/docs-index.sqlite +0 -0
- package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
- package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
- package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
- package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
- package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
- package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
- package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
- package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
- package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
- package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
- package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
- package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
- package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
- package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
- package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
- package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
- package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
- package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
- package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
- package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
- package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
- package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
- package/dist/records-backfill-BQcmTmwF.js +2 -0
- package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
- package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
- package/dist/resume-Bn4kDk9x.js +3 -0
- package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
- package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
- package/dist/review-agent-hLbRRxZF.js +3 -0
- package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
- package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
- package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
- package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
- package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
- package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
- package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
- package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
- package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
- package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
- package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
- package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
- package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
- package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
- package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
- package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
- package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
- package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
- package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
- package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
- package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
- package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
- package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
- package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
- package/dist/sync-CylE_SjB.js +2 -0
- package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
- package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
- package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
- package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
- package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
- package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
- package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
- package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
- package/package.json +1 -1
- package/scripts/file-size-baseline.txt +1 -1
- package/dist/cli-DBWG12XK.js +0 -3
- package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
- package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
- package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
- package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
- package/dist/dashboard/resume-DnIsmIgC.js +0 -3
- package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
- package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
- package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
- package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
- package/dist/records-backfill-DEgtRyKL.js +0 -2
- package/dist/resume-DGymuXZM.js +0 -3
- package/dist/review-agent-DjVadURa.js +0 -3
- package/dist/sync-4jqukkxz.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-CILd-uOt.js","names":["existsSync","readdirSync","join","execAsync","promisify","exec","execAsync","promisify","exec","getDailyTrends","execAsync","promisify","exec"],"sources":["../../src/lib/overdeck/cost.ts","../../src/lib/cloister/health.ts","../../src/lib/cloister/handoff-context.ts","../../src/lib/cloister/handoff.ts","../../src/lib/cloister/triggers.ts","../../src/lib/cloister/handoff-logger.ts","../../src/lib/cloister/fpp-violations.ts","../../src/lib/cloister/cost-monitor.ts","../../src/lib/cloister/pi-cost-reconciler.ts","../../src/lib/cloister/session-rotation.ts","../../src/lib/cloister/service.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { Context, Effect, Layer, Schema } from 'effect';\nimport { and, desc, eq, gte, like, sql } from 'drizzle-orm';\nimport { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';\nimport { HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nimport { CostArchive, CostArchiveLive, Db, DbLive, EventBus, EventBusLive } from './infra.js';\nimport { IssueId, type IssueId as IssueIdType } from './issues.js';\nimport {\n getAllBudgetsSync,\n checkBudgetSync,\n createBudgetSync,\n deleteBudgetSync,\n} from '../cost.js';\nimport type { CostBudget } from '../cost.js';\nimport { parseOhmypiSessionCostResultSync } from '../cost-parsers/ohmypi-parser.js';\nimport { parseCodexSessionCostEventsSync, parseCodexSessionSync } from '../cost-parsers/codex-parser.js';\nimport { getOverdeckHome } from '../paths.js';\nimport { deriveTieredAgentCostRole } from '../agents/tier-metrics.js';\n\n// ── Filesystem helpers ────────────────────────────────────────────────────────\n\nfunction walkJsonl(dir: string): string[] {\n if (!existsSync(dir)) return [];\n const result: string[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) result.push(...walkJsonl(full));\n else if (entry.name.endsWith('.jsonl')) result.push(full);\n }\n return result;\n}\n\nfunction readAgentRoleSync(agentDir: string): string {\n try {\n const raw = JSON.parse(readFileSync(join(agentDir, 'state.json'), 'utf8')) as { role?: unknown };\n return typeof raw.role === 'string' && raw.role.trim() ? raw.role : 'work';\n } catch {\n return 'work';\n }\n}\n\n// ── Local Drizzle table definition ───────────────────────────────────────────\n// Mirrors the locked schema (docs/overdeck-remodel/overdeck-schema.ts:173-195).\n// No FK or index definitions here — those live in the compiled schema only.\n\nconst costEventsTable = sqliteTable('cost_events', {\n id: integer('id').primaryKey({ autoIncrement: true }),\n ts: integer('ts', { mode: 'timestamp_ms' }).notNull(),\n issueId: text('issue_id'),\n agentId: text('agent_id'),\n sessionId: text('session_id'),\n sessionType: text('session_type'),\n provider: text('provider'),\n model: text('model'),\n input: integer('input'),\n output: integer('output'),\n cacheRead: integer('cache_read'),\n cacheWrite: integer('cache_write'),\n cost: real('cost'),\n requestId: text('request_id'),\n sourceFile: text('source_file'),\n});\n\n// ── Entities ─────────────────────────────────────────────────────────────────\n\nexport const Tokens = Schema.Struct({\n input: Schema.Number,\n output: Schema.Number,\n cacheRead: Schema.Number,\n cacheWrite: Schema.Number,\n});\nexport type Tokens = typeof Tokens.Type;\n\nexport const CostEvent = Schema.Struct({\n ts: Schema.Date,\n issueId: Schema.NullOr(IssueId),\n agentId: Schema.NullOr(Schema.String),\n sessionId: Schema.NullOr(Schema.String),\n sessionType: Schema.NullOr(Schema.String),\n provider: Schema.NullOr(Schema.String),\n model: Schema.NullOr(Schema.String),\n input: Schema.Number,\n output: Schema.Number,\n cacheRead: Schema.Number,\n cacheWrite: Schema.Number,\n cost: Schema.Number,\n requestId: Schema.NullOr(Schema.String),\n sourceFile: Schema.NullOr(Schema.String),\n warnings: Schema.optional(Schema.Array(Schema.Struct({\n type: Schema.String,\n provider: Schema.NullOr(Schema.String),\n model: Schema.String,\n reason: Schema.String,\n }))),\n});\nexport type CostEvent = typeof CostEvent.Type;\n\nexport interface SkippedCostSession {\n file: string;\n reason: string;\n}\n\nexport interface CostReconcileWarning {\n file: string;\n reason: string;\n provider: string | null;\n model: string;\n}\n\nexport const Rollup = Schema.Struct({\n key: Schema.String,\n role: Schema.optional(Schema.String),\n cost: Schema.Number,\n tokens: Tokens,\n});\nexport type Rollup = typeof Rollup.Type;\n\nexport const Window = Schema.Literals(['day', 'week', 'month']);\nexport type Window = typeof Window.Type;\n\nexport const WindowSummary = Schema.Struct({\n project: Schema.NullOr(Schema.String),\n window: Window,\n totalCost: Schema.Number,\n totalTokens: Schema.Number,\n entryCount: Schema.Number,\n byModel: Schema.Record(Schema.String, Schema.Number),\n});\nexport type WindowSummary = typeof WindowSummary.Type;\n\nexport const IssueCost = Schema.Struct({\n issueId: IssueId,\n totalCost: Schema.Number,\n tokens: Tokens,\n byModel: Schema.Record(Schema.String, Rollup),\n byStage: Schema.Record(Schema.String, Schema.Record(Schema.String, Tokens)),\n});\nexport type IssueCost = typeof IssueCost.Type;\n\nexport const BudgetSpec = Schema.Struct({\n name: Schema.String,\n type: Schema.Literals(['daily', 'monthly', 'project', 'issue', 'feature']),\n limit: Schema.Number,\n currency: Schema.String,\n alertThreshold: Schema.Number,\n});\nexport type BudgetSpec = typeof BudgetSpec.Type;\n\nexport const Budget = Schema.Struct({\n ...BudgetSpec.fields,\n id: Schema.String,\n spent: Schema.Number,\n enabled: Schema.Boolean,\n});\nexport type Budget = typeof Budget.Type;\n\nexport const BudgetStatus = Schema.Struct({\n budget: Budget,\n percentUsed: Schema.Number,\n remaining: Schema.Number,\n alert: Schema.Boolean,\n exceeded: Schema.Boolean,\n});\nexport type BudgetStatus = typeof BudgetStatus.Type;\n\nexport interface CostReconcileSummary {\n imported: number;\n skipped: SkippedCostSession[];\n sessionsScanned: number;\n eventsImported: number;\n duplicatesSkipped: number;\n errors: string[];\n earliestEventTs: string | null;\n latestEventTs: string | null;\n warnings: CostReconcileWarning[];\n}\n\nexport type CostReconcileExtraRoot =\n | { kind: 'codex-global'; root: string }\n | { kind: 'ohmypi-global'; root: string }\n | { kind: 'ohmypi-legacy-agents'; root: string };\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\nexport class CostIngestError extends Schema.TaggedErrorClass<CostIngestError>()(\n 'CostIngestError',\n { reason: Schema.String },\n) {}\n\nexport class BudgetNotFound extends Schema.TaggedErrorClass<BudgetNotFound>()(\n 'BudgetNotFound',\n { id: Schema.String },\n) {}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction mapBudget(b: CostBudget): Budget {\n return {\n id: b.id,\n name: b.name,\n type: b.type,\n limit: b.limit,\n currency: b.currency,\n alertThreshold: b.alertThreshold,\n spent: b.spent,\n enabled: b.enabled,\n };\n}\n\ntype TokenRow = {\n input: number | null;\n output: number | null;\n cacheRead: number | null;\n cacheWrite: number | null;\n};\n\nfunction toTokens(r: TokenRow): Tokens {\n return {\n input: r.input ?? 0,\n output: r.output ?? 0,\n cacheRead: r.cacheRead ?? 0,\n cacheWrite: r.cacheWrite ?? 0,\n };\n}\n\nfunction toRollup(key: string, r: TokenRow & { cost: number | null }, role?: string): Rollup {\n return { key, ...(role ? { role } : {}), cost: r.cost ?? 0, tokens: toTokens(r) };\n}\n\nfunction issueIdFromAgentName(agentName: string): IssueIdType | null {\n const match = agentName.match(/(?:^|-)((?:pan|min|aud|krux|cli)-\\d+)(?:-|$)/i);\n return match ? match[1]!.toUpperCase() as IssueIdType : null;\n}\n\nfunction inferIssueFromPath(path: string | undefined): IssueIdType | null {\n if (!path) return null;\n const match = path.match(/(?:feature[-/])?(pan|min|aud|krux|cli)[-/](\\d+)/i);\n return match ? `${match[1]!.toUpperCase()}-${match[2]}` as IssueIdType : null;\n}\n\nfunction inferIssueFromPiEncodedRootName(name: string): IssueIdType | null {\n return inferIssueFromPath(name.replace(/--+/g, '/'));\n}\n\nfunction inferPiProvider(model: string, provider: string | null): string | null {\n if (provider) return provider;\n const lower = model.toLowerCase();\n if (lower.includes('kimi') || lower.startsWith('minimax')) return 'custom';\n if (lower.includes('claude')) return 'anthropic';\n if (lower.includes('gpt')) return 'openai';\n if (lower.includes('gemini')) return 'google';\n return null;\n}\n\n// ── CostResolver — the read door ──────────────────────────────────────────────\n\nexport class CostResolver extends Context.Service<\n CostResolver,\n {\n // Windowed totals + per-model breakdown, optional project scope\n readonly summary: (window: Window, project?: string) => Effect.Effect<WindowSummary>;\n // All-issue rollup: GROUP BY issue_id\n readonly byIssue: () => Effect.Effect<ReadonlyArray<Rollup>>;\n // One issue's detail: total + per-model + per-stage\n readonly issueDetail: (id: IssueId) => Effect.Effect<IssueCost>;\n // Per-day trend over N days, optional issue filter\n readonly byDay: (days: number, issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per-model rollup, optional issue filter\n readonly byModel: (issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per-agent rollup, optional issue filter\n readonly byAgent: (issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per source_file, last N hours (background-AI costs)\n readonly byBackgroundSource: (hours: number) => Effect.Effect<ReadonlyArray<Rollup>>;\n // GROUP BY issue-id prefix (no project column — extracts from issue_id)\n readonly byProject: () => Effect.Effect<ReadonlyArray<Rollup>>;\n // Recent events, optional since filter\n readonly recent: (limit: number, since?: Date) => Effect.Effect<ReadonlyArray<CostEvent>>;\n // Budget reads (budgets.json — separate from cost_events)\n readonly listBudgets: () => Effect.Effect<ReadonlyArray<Budget>>;\n readonly checkBudget: (id: string) => Effect.Effect<BudgetStatus, BudgetNotFound>;\n }\n>()('overdeck/CostResolver') {}\n\nexport const CostResolverLive = Layer.effect(\n CostResolver,\n Effect.gen(function* () {\n const { q } = yield* Db;\n\n const groupBySelect = {\n cost: sql<number>`SUM(${costEventsTable.cost})`,\n input: sql<number>`SUM(${costEventsTable.input})`,\n output: sql<number>`SUM(${costEventsTable.output})`,\n cacheRead: sql<number>`SUM(${costEventsTable.cacheRead})`,\n cacheWrite: sql<number>`SUM(${costEventsTable.cacheWrite})`,\n } as const;\n\n const summary = (window: Window, project?: string) =>\n Effect.promise(async () => {\n const days = { day: 1, week: 7, month: 30 }[window];\n const cutoff = new Date(Date.now() - days * 86400_000);\n const where = project\n ? and(gte(costEventsTable.ts, cutoff), like(costEventsTable.issueId, `${project}-%`))\n : gte(costEventsTable.ts, cutoff);\n\n const [agg] = await q\n .select({\n totalCost: sql<number>`COALESCE(SUM(${costEventsTable.cost}), 0)`,\n totalTokens: sql<number>`COALESCE(SUM(COALESCE(${costEventsTable.input},0) + COALESCE(${costEventsTable.output},0) + COALESCE(${costEventsTable.cacheRead},0) + COALESCE(${costEventsTable.cacheWrite},0)), 0)`,\n entryCount: sql<number>`COUNT(*)`,\n })\n .from(costEventsTable)\n .where(where);\n\n const modelRows = await q\n .select({ model: costEventsTable.model, cost: sql<number>`COALESCE(SUM(${costEventsTable.cost}), 0)` })\n .from(costEventsTable)\n .where(where)\n .groupBy(costEventsTable.model);\n\n const byModel: Record<string, number> = {};\n for (const r of modelRows) byModel[r.model ?? 'unknown'] = r.cost ?? 0;\n\n return {\n project: project ?? null,\n window,\n totalCost: agg?.totalCost ?? 0,\n totalTokens: agg?.totalTokens ?? 0,\n entryCount: agg?.entryCount ?? 0,\n byModel,\n };\n });\n\n const byIssue = () =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.issueId, ...groupBySelect })\n .from(costEventsTable)\n .groupBy(costEventsTable.issueId);\n return rows.map((r) => toRollup(r.key ?? 'unattributed', r));\n });\n\n const issueDetail = (id: IssueId) =>\n Effect.promise(async () => {\n const modelRows = await q\n .select({ model: costEventsTable.model, ...groupBySelect })\n .from(costEventsTable)\n .where(eq(costEventsTable.issueId, id))\n .groupBy(costEventsTable.model);\n\n const byModel: Record<string, Rollup> = {};\n let totalCost = 0;\n let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;\n for (const r of modelRows) {\n const key = r.model ?? 'unknown';\n const t = toTokens(r);\n byModel[key] = { key, cost: r.cost ?? 0, tokens: t };\n totalCost += r.cost ?? 0;\n totalInput += t.input;\n totalOutput += t.output;\n totalCacheRead += t.cacheRead;\n totalCacheWrite += t.cacheWrite;\n }\n const tokens: Tokens = { input: totalInput, output: totalOutput, cacheRead: totalCacheRead, cacheWrite: totalCacheWrite };\n\n const stageRows = await q\n .select({\n stage: costEventsTable.sessionType,\n model: costEventsTable.model,\n input: sql<number>`SUM(${costEventsTable.input})`,\n output: sql<number>`SUM(${costEventsTable.output})`,\n cacheRead: sql<number>`SUM(${costEventsTable.cacheRead})`,\n cacheWrite: sql<number>`SUM(${costEventsTable.cacheWrite})`,\n })\n .from(costEventsTable)\n .where(eq(costEventsTable.issueId, id))\n .groupBy(costEventsTable.sessionType, costEventsTable.model);\n\n const byStage: Record<string, Record<string, Tokens>> = {};\n for (const r of stageRows) {\n const stage = r.stage ?? 'unknown';\n const model = r.model ?? 'unknown';\n if (!byStage[stage]) byStage[stage] = {};\n byStage[stage][model] = toTokens(r);\n }\n\n return { issueId: id, totalCost, tokens, byModel, byStage };\n });\n\n const byDay = (days: number, issue?: IssueId) =>\n Effect.promise(async () => {\n const cutoff = new Date(Date.now() - days * 86400_000);\n const dayExpr = sql<string>`DATE(${costEventsTable.ts}, 'unixepoch')`;\n const where = issue\n ? and(gte(costEventsTable.ts, cutoff), eq(costEventsTable.issueId, issue))\n : gte(costEventsTable.ts, cutoff);\n const rows = await q\n .select({ key: dayExpr, ...groupBySelect })\n .from(costEventsTable)\n .where(where)\n .groupBy(dayExpr)\n .orderBy(sql`DATE(${costEventsTable.ts}, 'unixepoch') DESC`);\n return rows.map((r) => toRollup(r.key ?? '', r));\n });\n\n const byModel = (issue?: IssueId) =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.model, ...groupBySelect })\n .from(costEventsTable)\n .where(issue ? eq(costEventsTable.issueId, issue) : undefined)\n .groupBy(costEventsTable.model);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const byAgent = (issue?: IssueId) =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.agentId, ...groupBySelect })\n .from(costEventsTable)\n .where(issue ? eq(costEventsTable.issueId, issue) : undefined)\n .groupBy(costEventsTable.agentId);\n return rows.map((r) => {\n const key = r.key ?? 'unattributed';\n return toRollup(key, r, deriveTieredAgentCostRole(key, issue));\n });\n });\n\n const byBackgroundSource = (hours: number) =>\n Effect.promise(async () => {\n const cutoff = new Date(Date.now() - hours * 3600_000);\n const rows = await q\n .select({ key: costEventsTable.sourceFile, ...groupBySelect })\n .from(costEventsTable)\n .where(and(gte(costEventsTable.ts, cutoff), sql`${costEventsTable.sourceFile} IS NOT NULL`))\n .groupBy(costEventsTable.sourceFile);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const byProject = () =>\n Effect.promise(async () => {\n const projExpr = sql<string>`UPPER(SUBSTR(${costEventsTable.issueId}, 1, INSTR(${costEventsTable.issueId}, '-') - 1))`;\n const rows = await q\n .select({ key: projExpr, ...groupBySelect })\n .from(costEventsTable)\n .where(sql`${costEventsTable.issueId} IS NOT NULL`)\n .groupBy(projExpr);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const recent = (limit: number, since?: Date) =>\n Effect.promise(async () => {\n const rows = await q\n .select()\n .from(costEventsTable)\n .where(since ? gte(costEventsTable.ts, since) : undefined)\n .orderBy(desc(costEventsTable.ts))\n .limit(limit);\n return rows.map((r) => ({\n ts: r.ts,\n issueId: r.issueId ?? null,\n agentId: r.agentId ?? null,\n sessionId: r.sessionId ?? null,\n sessionType: r.sessionType ?? null,\n provider: r.provider ?? null,\n model: r.model ?? null,\n input: r.input ?? 0,\n output: r.output ?? 0,\n cacheRead: r.cacheRead ?? 0,\n cacheWrite: r.cacheWrite ?? 0,\n cost: r.cost ?? 0,\n requestId: r.requestId ?? null,\n sourceFile: r.sourceFile ?? null,\n })) as ReadonlyArray<CostEvent>;\n });\n\n const listBudgets = () =>\n Effect.sync(() => getAllBudgetsSync().map(mapBudget));\n\n const checkBudget = (id: string) =>\n Effect.gen(function* () {\n const result = yield* Effect.sync(() => checkBudgetSync(id));\n if (!result.budget) return yield* Effect.fail(new BudgetNotFound({ id }));\n return {\n budget: mapBudget(result.budget),\n percentUsed: result.percentUsed,\n remaining: result.remaining,\n alert: result.alert,\n exceeded: result.exceeded,\n };\n });\n\n return CostResolver.of({\n summary, byIssue, issueDetail, byDay, byModel, byAgent,\n byBackgroundSource, byProject, recent, listBudgets, checkBudget,\n });\n }),\n);\n\n// ── CostWriter — the write door ───────────────────────────────────────────────\n\nexport class CostWriter extends Context.Service<\n CostWriter,\n {\n // The ONLY ingest primitive — owns archive fan-out and dedup\n readonly record: (event: CostEvent, opts?: { dryRun?: boolean }) => Effect.Effect<boolean, CostIngestError>;\n // Catch-up sweep (PAN-1935: pi/codex sweep lands here)\n readonly reconcile: (opts?: {\n source?: 'claude' | 'ohmypi' | 'codex' | 'wal';\n dryRun?: boolean;\n extraRoots?: string[];\n extraRootSpecs?: CostReconcileExtraRoot[];\n }) => Effect.Effect<CostReconcileSummary, CostIngestError>;\n // Full rebuild from archive; recomputes cost from tokens\n readonly rebuild: () => Effect.Effect<{ events: number }, CostIngestError>;\n // Budget writes (budgets.json — separate from cost_events)\n readonly createBudget: (spec: BudgetSpec) => Effect.Effect<Budget, CostIngestError>;\n readonly deleteBudget: (id: string) => Effect.Effect<void, BudgetNotFound>;\n }\n>()('overdeck/CostWriter') {}\n\nexport const CostWriterLive = Layer.effect(\n CostWriter,\n Effect.gen(function* () {\n const { q } = yield* Db;\n const archive = yield* CostArchive;\n const bus = yield* EventBus;\n\n // Dedup: prefer the precise requestId when present; fall back to sourceFile\n // only for events with no requestId (codex / background-AI dumps that carry\n // a file path but no request id). Using OR(requestId, sourceFile) is wrong —\n // it collapses every event that shares a sourceFile (e.g. all events\n // reconciled from one pi transcript) down to a single row, silently dropping\n // the rest (PAN-1935).\n const checkDuplicate = (e: CostEvent) =>\n Effect.gen(function* () {\n if (e.requestId == null && e.sourceFile == null) return false;\n const condition =\n e.requestId != null\n ? eq(costEventsTable.requestId, e.requestId)\n : eq(costEventsTable.sourceFile, e.sourceFile as string);\n const existing = yield* Effect.promise(() =>\n q\n .select({ id: costEventsTable.id })\n .from(costEventsTable)\n .where(condition)\n .limit(1),\n );\n return (existing as unknown[]).length > 0;\n });\n\n const record = (event: CostEvent, opts?: { dryRun?: boolean }) =>\n Effect.gen(function* () {\n if (opts?.dryRun) {\n if (yield* checkDuplicate(event)) return false;\n return true;\n }\n\n // 1. DURABLE ARCHIVE FIRST — events.jsonl. Archive deduplicates by\n // requestId/sourceFile so DB-only retries can repair the JSONL log.\n yield* archive.append(event);\n\n if (yield* checkDuplicate(event)) return false;\n\n // 2. Cache insert — UNIQUE(request_id) makes it idempotent on re-import.\n yield* Effect.promise(() =>\n q\n .insert(costEventsTable)\n .values({\n ts: event.ts,\n issueId: event.issueId,\n agentId: event.agentId,\n sessionId: event.sessionId,\n sessionType: event.sessionType,\n provider: event.provider,\n model: event.model,\n input: event.input,\n output: event.output,\n cacheRead: event.cacheRead,\n cacheWrite: event.cacheWrite,\n cost: event.cost,\n requestId: event.requestId,\n sourceFile: event.sourceFile,\n })\n .onConflictDoNothing(),\n );\n\n // 3. Announce — cost.subscribe + /api/costs/stream feed from this.\n yield* bus.emit({ type: 'cost.recorded', payload: { issueId: event.issueId, cost: event.cost } });\n return true;\n });\n\n // Catch-up sweep for pi/codex session files.\n // Walks OVERDECK_HOME/agents/<id>/sessions/**/*.jsonl (pi) or\n // OVERDECK_HOME/agents/<id>/codex-home/sessions/**/*.jsonl (codex),\n // parses each with the existing parsers, and feeds into record() (which deduplicates).\n const reconcile = (opts?: {\n source?: 'claude' | 'ohmypi' | 'codex' | 'wal';\n dryRun?: boolean;\n extraRoots?: string[];\n extraRootSpecs?: CostReconcileExtraRoot[];\n }) =>\n Effect.gen(function* () {\n const source = opts?.source ?? 'claude';\n const empty: CostReconcileSummary = {\n imported: 0,\n skipped: [],\n sessionsScanned: 0,\n eventsImported: 0,\n duplicatesSkipped: 0,\n errors: [],\n earliestEventTs: null,\n latestEventTs: null,\n warnings: [],\n };\n if (source !== 'ohmypi' && source !== 'codex') return empty;\n\n const agentsDir = join(getOverdeckHome(), 'agents');\n const agentNames = yield* Effect.sync(() => {\n if (!existsSync(agentsDir)) return [] as string[];\n return readdirSync(agentsDir, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => e.name);\n });\n\n let imported = 0;\n const skipped: SkippedCostSession[] = [];\n let duplicatesSkipped = 0;\n let sessionsScanned = 0;\n let earliestEventTs: string | null = null;\n let latestEventTs: string | null = null;\n const errors: string[] = [];\n const warnings: CostReconcileWarning[] = [];\n const markSkipped = (file: string, reason: string) => {\n skipped.push({ file, reason });\n console.warn(`[cost-reconcile] skipped ${file}: ${reason}`);\n };\n const markWarning = (warning: CostReconcileWarning) => {\n warnings.push(warning);\n console.warn(\n `[cost-reconcile] warning ${warning.file}: ${warning.reason} provider=${warning.provider ?? 'unknown'} model=${warning.model}`,\n );\n };\n\n const noteImportedTimestamp = (ts: Date) => {\n const iso = ts.toISOString();\n if (earliestEventTs == null || iso < earliestEventTs) earliestEventTs = iso;\n if (latestEventTs == null || iso > latestEventTs) latestEventTs = iso;\n };\n\n type ScanRoot = {\n root: string;\n agentName: string;\n issueId: IssueIdType | null;\n sessionType: 'ohmypi' | 'codex';\n inferIssueFromCwd?: boolean;\n };\n\n const roots: ScanRoot[] = agentNames.map((agentName) => ({\n root: source === 'ohmypi'\n ? join(agentsDir, agentName, 'sessions')\n : join(agentsDir, agentName, 'codex-home', 'sessions'),\n agentName,\n issueId: issueIdFromAgentName(agentName),\n sessionType: source,\n }));\n\n if (source === 'codex') {\n for (const root of opts?.extraRoots ?? []) {\n roots.push({\n root,\n agentName: 'codex-global',\n issueId: null,\n sessionType: 'codex',\n inferIssueFromCwd: true,\n });\n }\n }\n\n for (const extra of opts?.extraRootSpecs ?? []) {\n if (source === 'codex' && extra.kind === 'codex-global') {\n roots.push({\n root: extra.root,\n agentName: 'codex-global',\n issueId: null,\n sessionType: 'codex',\n inferIssueFromCwd: true,\n });\n }\n\n if (source === 'ohmypi' && extra.kind === 'ohmypi-global') {\n const encodedRoots = yield* Effect.sync(() => {\n if (!existsSync(extra.root)) return [] as ScanRoot[];\n return readdirSync(extra.root, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => ({\n root: join(extra.root, e.name),\n agentName: 'pi-global',\n issueId: inferIssueFromPiEncodedRootName(e.name) ?? 'UNKNOWN' as IssueIdType,\n sessionType: 'ohmypi' as const,\n }));\n });\n roots.push(...encodedRoots);\n }\n\n if (source === 'ohmypi' && extra.kind === 'ohmypi-legacy-agents') {\n const legacyRoots = yield* Effect.sync(() => {\n if (!existsSync(extra.root)) return [] as ScanRoot[];\n return readdirSync(extra.root, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => ({\n root: join(extra.root, e.name, 'sessions'),\n agentName: e.name,\n issueId: issueIdFromAgentName(e.name),\n sessionType: 'ohmypi' as const,\n }));\n });\n roots.push(...legacyRoots);\n }\n }\n\n for (const root of roots) {\n const sessionFiles = yield* Effect.sync(() => walkJsonl(root.root));\n\n for (const sessionFile of sessionFiles) {\n sessionsScanned++;\n if (source === 'ohmypi') {\n const parsed = yield* Effect.sync(() => {\n try {\n return parseOhmypiSessionCostResultSync(sessionFile);\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n errors.push(`${sessionFile}: ${reason}`);\n return { ok: false as const, reason };\n }\n });\n if (!parsed.ok) {\n markSkipped(sessionFile, parsed.reason ?? 'unreadable');\n continue;\n }\n\n const events = parsed.usageEvents ?? [];\n if (events.length === 0) {\n markSkipped(sessionFile, 'no-usage');\n continue;\n }\n const unpricedWarnings = parsed.unpricedModels ?? [];\n if (unpricedWarnings.length) markSkipped(sessionFile, 'unpriced-model');\n\n for (const usage of events) {\n const derivedWarnings = unpricedWarnings\n .filter(warning => warning.model === usage.model && warning.provider === usage.provider)\n .map(warning => ({ type: 'unpriced-model' as const, ...warning }));\n const eventWarnings = usage.warnings?.length ? usage.warnings : derivedWarnings;\n for (const warning of eventWarnings) {\n markWarning({ file: sessionFile, reason: warning.reason, provider: warning.provider, model: warning.model });\n }\n const ts = new Date(usage.timestamp);\n const event: CostEvent = {\n ts,\n issueId: root.issueId,\n agentId: root.agentName,\n sessionId: usage.sessionId,\n sessionType: root.sessionType,\n provider: inferPiProvider(usage.model, usage.provider),\n model: usage.model,\n input: usage.input,\n output: usage.output,\n cacheRead: usage.cacheRead,\n cacheWrite: usage.cacheWrite,\n cost: usage.cost,\n requestId: usage.requestId,\n sourceFile: sessionFile,\n warnings: eventWarnings.length > 0 ? eventWarnings : undefined,\n };\n\n if (yield* record(event, { dryRun: opts?.dryRun })) {\n imported++;\n noteImportedTimestamp(ts);\n } else {\n duplicatesSkipped++;\n }\n }\n continue;\n }\n\n const events = yield* Effect.sync(() => {\n try {\n return parseCodexSessionCostEventsSync(sessionFile);\n } catch (cause) {\n errors.push(`${sessionFile}: ${cause instanceof Error ? cause.message : String(cause)}`);\n return [];\n }\n });\n if (events.length === 0) {\n markSkipped(sessionFile, 'no-usage');\n continue;\n }\n if (events.some((event) => event.model === 'unknown')) {\n markSkipped(sessionFile, 'unknown-model');\n }\n const codexSession = root.inferIssueFromCwd\n ? yield* Effect.sync(() => parseCodexSessionSync(sessionFile))\n : null;\n const issueId = root.inferIssueFromCwd\n ? (inferIssueFromPath(codexSession?.cwd) ?? 'UNKNOWN' as IssueIdType)\n : root.issueId;\n for (const usage of events) {\n const ts = new Date(usage.timestamp);\n const event: CostEvent = {\n ts,\n issueId,\n agentId: root.agentName,\n sessionId: usage.sessionId,\n sessionType: root.sessionType,\n provider: usage.provider,\n model: usage.model,\n input: usage.input,\n output: usage.output,\n cacheRead: usage.cacheRead,\n cacheWrite: usage.cacheWrite,\n cost: usage.cost,\n requestId: usage.requestId,\n sourceFile: sessionFile,\n };\n\n if (yield* record(event, { dryRun: opts?.dryRun })) {\n imported++;\n noteImportedTimestamp(ts);\n } else {\n duplicatesSkipped++;\n }\n }\n }\n }\n\n return {\n imported,\n skipped,\n sessionsScanned,\n eventsImported: imported,\n duplicatesSkipped,\n errors,\n earliestEventTs,\n latestEventTs,\n warnings,\n };\n });\n\n // Stub — full rebuild (recomputes cost from tokens) deferred to\n // workspace-3zhmy.\n const rebuild = () =>\n Effect.gen(function* () {\n return { events: 0 };\n });\n\n const createBudget = (spec: BudgetSpec) =>\n Effect.gen(function* () {\n const created = yield* Effect.sync(() =>\n createBudgetSync({ ...spec, enabled: true }),\n );\n return mapBudget(created);\n });\n\n const deleteBudget = (id: string) =>\n Effect.gen(function* () {\n const deleted = yield* Effect.sync(() => deleteBudgetSync(id));\n if (!deleted) return yield* Effect.fail(new BudgetNotFound({ id }));\n });\n\n return CostWriter.of({ record, reconcile, rebuild, createBudget, deleteBudget });\n }),\n);\n\nexport const CostDoorLive = CostWriterLive.pipe(\n Layer.provide(Layer.mergeAll(DbLive, EventBusLive.pipe(Layer.provide(DbLive)), CostArchiveLive)),\n);\n\n// ── CostApi — the controller (HttpApiGroup, no handler wiring) ────────────────\n// Handler wiring (CostApiLive) is deferred to workspace-m9n4j (server bootstrap).\n\nexport const CostApi = HttpApiGroup.make('costs')\n // ── reads ──\n .add(\n HttpApiEndpoint.get('summary', '/costs/summary', {\n query: {\n window: Schema.optional(Window),\n project: Schema.optional(Schema.String),\n },\n success: WindowSummary,\n }),\n )\n .add(HttpApiEndpoint.get('byIssue', '/costs/by-issue', { success: Schema.Array(Rollup) }))\n .add(\n HttpApiEndpoint.get('issueDetail', '/costs/issue/:id', {\n params: { id: IssueId },\n success: IssueCost,\n }),\n )\n .add(\n HttpApiEndpoint.get('byDay', '/costs/trends', {\n query: {\n days: Schema.optional(Schema.NumberFromString),\n issueId: Schema.optional(IssueId),\n },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byModel', '/costs/by-model', {\n query: { issueId: Schema.optional(IssueId) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byAgent', '/costs/by-agent', {\n query: { issueId: Schema.optional(IssueId) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byBackgroundSource', '/costs/background', {\n query: { hours: Schema.optional(Schema.NumberFromString) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(HttpApiEndpoint.get('byProject', '/costs/by-project', { success: Schema.Array(Rollup) }))\n .add(\n HttpApiEndpoint.get('recent', '/costs/stream', {\n query: {\n limit: Schema.optional(Schema.NumberFromString),\n since: Schema.optional(Schema.String),\n },\n success: Schema.Array(CostEvent),\n }),\n )\n .add(HttpApiEndpoint.get('listBudgets', '/costs/budget', { success: Schema.Array(Budget) }))\n .add(\n HttpApiEndpoint.get('checkBudget', '/costs/budget/:id', {\n params: { id: Schema.String },\n success: BudgetStatus,\n error: BudgetNotFound,\n }),\n )\n // ── writes ──\n .add(\n HttpApiEndpoint.post('reconcile', '/costs/reconcile', {\n payload: Schema.Struct({\n source: Schema.optional(Schema.Literals(['claude', 'ohmypi', 'codex', 'wal'])),\n dryRun: Schema.optional(Schema.Boolean),\n extraRoots: Schema.optional(Schema.Array(Schema.String)),\n }),\n success: Schema.Struct({\n imported: Schema.Number,\n skipped: Schema.Array(Schema.Struct({ file: Schema.String, reason: Schema.String })),\n sessionsScanned: Schema.Number,\n eventsImported: Schema.Number,\n duplicatesSkipped: Schema.Number,\n errors: Schema.Array(Schema.String),\n earliestEventTs: Schema.NullOr(Schema.String),\n latestEventTs: Schema.NullOr(Schema.String),\n warnings: Schema.Array(Schema.Struct({\n file: Schema.String,\n reason: Schema.String,\n provider: Schema.NullOr(Schema.String),\n model: Schema.String,\n })),\n }),\n error: CostIngestError,\n }),\n )\n .add(\n HttpApiEndpoint.post('rebuild', '/costs/rebuild', {\n success: Schema.Struct({ events: Schema.Number }),\n error: CostIngestError,\n }),\n )\n .add(\n HttpApiEndpoint.post('createBudget', '/costs/budget', {\n payload: BudgetSpec,\n success: Budget,\n error: CostIngestError,\n }),\n )\n // HttpApiEndpoint exports 'del' as 'delete' (reserved word alias)\n .add(\n HttpApiEndpoint['delete']('deleteBudget', '/costs/budget/:id', {\n params: { id: Schema.String },\n error: BudgetNotFound,\n }),\n );\n","/**\n * Cloister Health Evaluator\n *\n * Evaluates agent health based on heartbeats and activity timestamps.\n */\n\nimport type { HealthState, Heartbeat, AgentRuntimeSync } from '../runtimes/types.js';\nimport { getAgentRuntimeStateSync } from '../agents.js';\nimport type { HealthThresholds } from './config.js';\nimport { getHealthThresholdsMs } from './config.js';\n\n/**\n * Agent health status\n */\nexport interface AgentHealth {\n agentId: string;\n state: HealthState;\n lastActivity: Date | null;\n timeSinceActivity: number | null; // milliseconds\n heartbeat: Heartbeat | null;\n isRunning: boolean;\n contextSaturatedAt?: string;\n}\n\n/**\n * Health summary for all agents\n */\nexport interface HealthSummary {\n active: number;\n stale: number;\n warning: number;\n stuck: number;\n wedged: number;\n total: number;\n}\n\n/**\n * Evaluate health state based on time since last activity\n *\n * @param timeSinceActivityMs - Milliseconds since last activity\n * @param thresholds - Health thresholds in milliseconds\n * @returns Health state\n */\nexport function evaluateHealthState(\n timeSinceActivityMs: number,\n thresholds: { stale: number; warning: number; stuck: number }\n): HealthState {\n if (timeSinceActivityMs < thresholds.stale) {\n return 'active';\n } else if (timeSinceActivityMs < thresholds.warning) {\n return 'stale';\n } else if (timeSinceActivityMs < thresholds.stuck) {\n return 'warning';\n } else {\n return 'stuck';\n }\n}\n\n/**\n * Get health status for a single agent\n *\n * @param agentId - Agent identifier\n * @param runtime - Runtime to query for heartbeat\n * @param thresholds - Health thresholds (optional, uses config if not provided)\n * @returns Agent health status\n */\nexport function getAgentHealth(\n agentId: string,\n runtime: AgentRuntimeSync,\n thresholds?: { stale: number; warning: number; stuck: number }\n): AgentHealth {\n const thresholdsMs = thresholds || getHealthThresholdsMs();\n\n // Check if agent is running\n const isRunning = runtime.isRunning(agentId);\n\n if (!isRunning) {\n return {\n agentId,\n state: 'stuck',\n lastActivity: null,\n timeSinceActivity: null,\n heartbeat: null,\n isRunning: false,\n };\n }\n\n // Get heartbeat\n const heartbeat = runtime.getHeartbeat(agentId);\n const contextSaturatedAt = getAgentRuntimeStateSync(agentId)?.contextSaturatedAt;\n\n if (!heartbeat) {\n // No heartbeat available - agent might be starting up\n return {\n agentId,\n state: contextSaturatedAt ? 'wedged' : 'active', // Assume active if no heartbeat yet\n lastActivity: null,\n timeSinceActivity: null,\n heartbeat: null,\n isRunning: true,\n contextSaturatedAt,\n };\n }\n\n if (contextSaturatedAt) {\n const timeSinceActivity = new Date().getTime() - heartbeat.timestamp.getTime();\n return {\n agentId,\n state: 'wedged',\n lastActivity: heartbeat.timestamp,\n timeSinceActivity,\n heartbeat,\n isRunning: true,\n contextSaturatedAt,\n };\n }\n\n // Calculate time since last activity\n const now = new Date();\n const timeSinceActivity = now.getTime() - heartbeat.timestamp.getTime();\n\n // Evaluate health state\n const state = evaluateHealthState(timeSinceActivity, thresholdsMs);\n\n return {\n agentId,\n state,\n lastActivity: heartbeat.timestamp,\n timeSinceActivity,\n heartbeat,\n isRunning: true,\n };\n}\n\n/**\n * Get health status for multiple agents\n *\n * @param agentIds - Array of agent identifiers\n * @param runtime - Runtime to query for heartbeats\n * @param thresholds - Health thresholds (optional, uses config if not provided)\n * @returns Array of agent health statuses\n */\nexport function getMultipleAgentHealth(\n agentIds: string[],\n runtime: AgentRuntimeSync,\n thresholds?: { stale: number; warning: number; stuck: number }\n): AgentHealth[] {\n return agentIds.map((agentId) => getAgentHealth(agentId, runtime, thresholds));\n}\n\n/**\n * Generate health summary from agent health statuses\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Health summary with counts by state\n */\nexport function generateHealthSummary(agentHealths: AgentHealth[]): HealthSummary {\n const summary: HealthSummary = {\n active: 0,\n stale: 0,\n warning: 0,\n stuck: 0,\n wedged: 0,\n total: agentHealths.length,\n };\n\n for (const health of agentHealths) {\n summary[health.state]++;\n }\n\n return summary;\n}\n\n/**\n * Check if an agent needs attention (warning or stuck)\n *\n * @param health - Agent health status\n * @returns True if agent needs attention\n */\nexport function needsAttention(health: AgentHealth): boolean {\n return health.state === 'warning' || health.state === 'stuck' || health.state === 'wedged';\n}\n\n/**\n * Check if an agent should be poked (warning state)\n *\n * @param health - Agent health status\n * @returns True if agent should be poked\n */\nexport function shouldPoke(health: AgentHealth): boolean {\n return health.state === 'warning';\n}\n\n/**\n * Check if an agent should be killed (stuck state)\n *\n * @param health - Agent health status\n * @returns True if agent should be killed\n */\nexport function shouldKill(health: AgentHealth): boolean {\n return health.state === 'stuck';\n}\n\n/**\n * Get agents that need attention\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents needing attention\n */\nexport function getAgentsNeedingAttention(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(needsAttention);\n}\n\n/**\n * Get agents that should be poked\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents to poke\n */\nexport function getAgentsToPoke(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(shouldPoke);\n}\n\n/**\n * Get agents that should be killed\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents to kill\n */\nexport function getAgentsToKill(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(shouldKill);\n}\n\n/**\n * Format time duration in human-readable format\n *\n * @param ms - Milliseconds\n * @returns Human-readable duration (e.g., \"5m\", \"2h\", \"1d\")\n */\nexport function formatDuration(ms: number | null): string {\n if (ms === null) {\n return 'unknown';\n }\n\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d`;\n } else if (hours > 0) {\n return `${hours}h`;\n } else if (minutes > 0) {\n return `${minutes}m`;\n } else {\n return `${seconds}s`;\n }\n}\n\n/**\n * Get health emoji for a health state\n *\n * @param state - Health state\n * @returns Emoji representing the state\n */\nexport function getHealthEmoji(state: HealthState): string {\n switch (state) {\n case 'active':\n return '🟢';\n case 'stale':\n return '🟡';\n case 'warning':\n return '🟠';\n case 'stuck':\n return '🔴';\n case 'wedged':\n return '🧱';\n }\n}\n\n/**\n * Get health label for a health state\n *\n * @param state - Health state\n * @returns Human-readable label\n */\nexport function getHealthLabel(state: HealthState): string {\n switch (state) {\n case 'active':\n return 'Active';\n case 'stale':\n return 'Stale';\n case 'warning':\n return 'Warning';\n case 'stuck':\n return 'Stuck';\n case 'wedged':\n return 'Wedged';\n }\n}\n","/**\n * Handoff Context Module\n *\n * Captures and serializes agent context for handoffs between models.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Data, Effect } from 'effect';\nimport type { TokenUsage, RuntimeName } from '../runtimes/types.js';\nimport type { ComplexityLevel } from './complexity.js';\nimport type { AgentState } from '../agents.js';\nimport { renderPrompt } from './prompts.js';\nimport type { ContinueState } from '../vbrief/continue-state.js';\nimport { getProjectConfigFromWorkspacePath, readRecordContinueViewSync, resolveProjectForIssue } from '../pan-dir/record.js';\nimport { withBdMutex } from '../bd-mutex.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Beads task snapshot for handoff\n */\nexport interface BeadsTask {\n id: string;\n title: string;\n description?: string;\n status: 'open' | 'in_progress' | 'closed';\n priority: number;\n labels?: string[];\n complexity?: ComplexityLevel;\n}\n\n/**\n * Handoff context - captures full state for agent transition\n */\nexport interface HandoffContext {\n // Agent identity\n issueId: string;\n agentId: string;\n workspace: string;\n\n // Source info\n previousModel: string;\n previousRuntime: RuntimeName;\n previousSessionId?: string;\n\n // Files\n /** Parsed scope continue file. */\n continueState?: ContinueState;\n claudeMd?: string; // CLAUDE.md content\n\n // Git state\n gitBranch?: string;\n uncommittedFiles?: string[];\n lastCommit?: string;\n\n // Beads state\n activeBeadsTasks?: BeadsTask[];\n remainingTasks?: BeadsTask[];\n completedTasks?: BeadsTask[];\n\n // AI summaries\n whatWasDone?: string;\n whatRemains?: string;\n blockers?: string[];\n decisions?: string[];\n\n // Metrics\n tokenUsage?: TokenUsage;\n costSoFar?: number;\n handoffCount?: number;\n\n // New agent target\n targetModel: string;\n reason: string;\n}async function captureHandoffContextPromise(\n agentState: AgentState,\n targetModel: string,\n reason: string\n): Promise<HandoffContext> {\n const context: HandoffContext = {\n issueId: agentState.issueId,\n agentId: agentState.id,\n workspace: agentState.workspace,\n previousModel: agentState.model,\n previousRuntime: agentState.harness ?? 'claude-code',\n previousSessionId: agentState.sessionId,\n targetModel,\n reason,\n handoffCount: 0,\n costSoFar: agentState.costSoFar || 0,\n };\n\n // Capture files (continue file, CLAUDE.md)\n await captureFiles(context, agentState.workspace, agentState.issueId);\n\n // Capture git state\n await captureGitState(context, agentState.workspace);\n\n // Capture beads tasks\n await captureBeadsTasks(context, agentState.issueId);\n\n return context;\n}\n\n/**\n * Capture workspace files (continue file, CLAUDE.md)\n */\nasync function captureFiles(\n context: HandoffContext,\n workspace: string,\n issueId: string,\n): Promise<void> {\n try {\n // Read continue context from the per-issue record (PAN-1919).\n try {\n const project = resolveProjectForIssue(issueId) ?? getProjectConfigFromWorkspacePath(workspace);\n const recordView = readRecordContinueViewSync(project, issueId);\n if (recordView) {\n context.continueState = recordView as unknown as ContinueState;\n }\n } catch { /* ignore */ }\n\n // Read CLAUDE.md if it exists\n const claudeMd = join(workspace, 'CLAUDE.md');\n if (existsSync(claudeMd)) {\n context.claudeMd = readFileSync(claudeMd, 'utf-8');\n }\n } catch (error) {\n console.error('Error capturing files:', error);\n }\n}\n\n/**\n * Capture git state\n */\nasync function captureGitState(context: HandoffContext, workspace: string): Promise<void> {\n try {\n // Get current branch\n const { stdout: branch } = await execAsync('git branch --show-current', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.gitBranch = branch.trim();\n\n // Get uncommitted files\n const { stdout: status } = await execAsync('git status --porcelain', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.uncommittedFiles = status\n .split('\\n')\n .filter(line => line.trim())\n .map(line => line.substring(3)); // Remove status prefix\n\n // Get last commit\n const { stdout: lastCommit } = await execAsync('git log -1 --oneline', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.lastCommit = lastCommit.trim();\n } catch (error) {\n console.error('Error capturing git state:', error);\n }\n}\n\n/**\n * Capture beads tasks state\n */\nasync function captureBeadsTasks(context: HandoffContext, issueId: string): Promise<void> {\n try {\n // List all tasks with this issue's label\n const label = issueId.toLowerCase();\n const { stdout: output } = await execAsync(`bd list --json -l ${label}`, {\n encoding: 'utf-8',\n });\n\n const tasks: BeadsTask[] = JSON.parse(output);\n\n // Categorize tasks\n context.activeBeadsTasks = tasks.filter(t => t.status === 'in_progress');\n context.remainingTasks = tasks.filter(t => t.status === 'open');\n context.completedTasks = tasks.filter(t => t.status === 'closed');\n } catch (error) {\n console.error('Error capturing beads tasks:', error);\n context.activeBeadsTasks = [];\n context.remainingTasks = [];\n context.completedTasks = [];\n }\n}\n\n/**\n * Serialize handoff context to markdown for agent prompt\n *\n * @param context - Handoff context\n * @returns Markdown representation\n */\nexport function serializeHandoffContext(context: HandoffContext): string {\n const lines: string[] = [];\n\n lines.push('# Handoff Context');\n lines.push('');\n lines.push(`**Reason:** ${context.reason}`);\n lines.push(`**From:** ${context.previousModel}`);\n lines.push(`**To:** ${context.targetModel}`);\n lines.push(`**Handoff Count:** ${context.handoffCount}`);\n if (context.costSoFar) {\n lines.push(`**Cost So Far:** $${context.costSoFar.toFixed(4)}`);\n }\n lines.push('');\n\n // Git state\n if (context.gitBranch) {\n lines.push('## Git State');\n lines.push('');\n lines.push(`**Branch:** ${context.gitBranch}`);\n if (context.lastCommit) {\n lines.push(`**Last Commit:** ${context.lastCommit}`);\n }\n if (context.uncommittedFiles && context.uncommittedFiles.length > 0) {\n lines.push(`**Uncommitted Files:** ${context.uncommittedFiles.length}`);\n lines.push('```');\n context.uncommittedFiles.forEach(file => lines.push(file));\n lines.push('```');\n }\n lines.push('');\n }\n\n // Beads tasks\n if (context.completedTasks && context.completedTasks.length > 0) {\n lines.push('## Completed Tasks');\n lines.push('');\n context.completedTasks.forEach(task => {\n lines.push(`- [x] ${task.title} (${task.id})`);\n });\n lines.push('');\n }\n\n if (context.activeBeadsTasks && context.activeBeadsTasks.length > 0) {\n lines.push('## Active Tasks');\n lines.push('');\n context.activeBeadsTasks.forEach(task => {\n lines.push(`- [ ] ${task.title} (${task.id}) - IN PROGRESS`);\n });\n lines.push('');\n }\n\n if (context.remainingTasks && context.remainingTasks.length > 0) {\n lines.push('## Remaining Tasks');\n lines.push('');\n context.remainingTasks.forEach(task => {\n lines.push(`- [ ] ${task.title} (${task.id})`);\n });\n lines.push('');\n }\n\n // Continue file content (structured planning state)\n if (context.continueState) {\n lines.push('## Current State (continue.vbrief.json)');\n lines.push('');\n lines.push('```json');\n lines.push(JSON.stringify(context.continueState, null, 2));\n lines.push('```');\n lines.push('');\n }\n\n // AI summaries (if available)\n if (context.whatWasDone) {\n lines.push('## What Was Done');\n lines.push('');\n lines.push(context.whatWasDone);\n lines.push('');\n }\n\n if (context.whatRemains) {\n lines.push('## What Remains');\n lines.push('');\n lines.push(context.whatRemains);\n lines.push('');\n }\n\n if (context.blockers && context.blockers.length > 0) {\n lines.push('## Blockers');\n lines.push('');\n context.blockers.forEach(blocker => lines.push(`- ${blocker}`));\n lines.push('');\n }\n\n if (context.decisions && context.decisions.length > 0) {\n lines.push('## Decisions Made');\n lines.push('');\n context.decisions.forEach(decision => lines.push(`- ${decision}`));\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Build handoff prompt for new agent\n *\n * @param context - Handoff context\n * @param additionalInstructions - Optional additional instructions\n * @returns Prompt for new agent\n */\nexport function buildHandoffPrompt(\n context: HandoffContext,\n additionalInstructions?: string\n): string {\n return Effect.runSync(renderPrompt({\n name: 'handoff-to-work',\n vars: {\n ISSUE_ID: context.issueId,\n PREVIOUS_MODEL: context.previousModel,\n REASON: context.reason,\n HANDOFF_CONTEXT: serializeHandoffContext(context),\n ADDITIONAL_INSTRUCTIONS_BLOCK: additionalInstructions || '',\n },\n }));\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel variants. The async variants above stay so existing\n// callers keep working; Effect-based callers can compose without runPromise.\n\n/** Tagged error for handoff-context Effect variants. */\nexport class HandoffContextError extends Data.TaggedError('HandoffContextError')<{\n readonly issueId: string;\n readonly stage: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/** Effect variant of `captureHandoffContext`. */\nexport const captureHandoffContext = (\n agentState: AgentState,\n targetModel: string,\n reason: string,\n): Effect.Effect<HandoffContext, HandoffContextError> =>\n Effect.tryPromise({\n try: () => captureHandoffContextPromise(agentState, targetModel, reason),\n catch: (cause) =>\n new HandoffContextError({\n issueId: agentState.issueId,\n stage: 'captureHandoffContext',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","/**\n * Handoff Manager\n *\n * Orchestrates model handoffs for running agents using two methods:\n * 1. Kill & Spawn: For general agents (clean handoff with context preservation)\n * 2. Legacy specialist wake has been removed; all handoffs use role-based respawn.\n */\n\nimport { existsSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { Data, Effect } from 'effect';\nimport type { AgentState } from '../agents.js';\nimport { getAgentStateSync, saveAgentStateSync, stopAgentSync, spawnAgent, spawnRun, getAgentDir } from '../agents.js';\nimport type { HandoffContext } from './handoff-context.js';\nimport { captureHandoffContext, buildHandoffPrompt } from './handoff-context.js';\nimport { sessionExistsSync } from '../tmux.js';\nimport { requireModelOverrideSync } from '../model-validation.js';\n\n/**\n * Handoff method type\n */\nexport type HandoffMethod = 'kill-spawn' | 'specialist-wake';\n\n/**\n * Handoff result\n */\nexport interface HandoffResult {\n success: boolean;\n method: HandoffMethod;\n newAgentId?: string;\n newSessionId?: string;\n context?: HandoffContext;\n error?: string;\n}\n\n/**\n * Handoff options\n */\nexport interface HandoffOptions {\n targetModel: string;\n reason: string;\n method?: HandoffMethod; // Auto-detect if not specified\n waitForIdle?: boolean; // Wait for agent to be idle before killing (default: true)\n idleTimeoutMs?: number; // How long to wait for idle (default: 30000)\n additionalInstructions?: string; // Extra instructions for new agent\n}async function performHandoffPromise(\n agentId: string,\n options: HandoffOptions\n): Promise<HandoffResult> {\n // Get current agent state\n const state = getAgentStateSync(agentId);\n if (!state) {\n return {\n success: false,\n method: 'kill-spawn',\n error: `Agent ${agentId} not found`,\n };\n }\n\n let targetModel: string;\n try {\n targetModel = requireModelOverrideSync(options.targetModel);\n } catch (error) {\n return {\n success: false,\n method: 'kill-spawn',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n\n // Legacy specialist wake has been deleted; normalize all handoffs to role respawn.\n const method = options.method === 'specialist-wake' ? 'kill-spawn' : (options.method || detectHandoffMethod(agentId));\n\n return await performKillAndSpawn(state, { ...options, targetModel, method });\n}\n\n/**\n * Detect appropriate handoff method for an agent\n *\n * @param agentId - Agent ID\n * @returns Handoff method\n */\nfunction detectHandoffMethod(_agentId: string): HandoffMethod {\n return 'kill-spawn';\n}\n\n/**\n * Kill & Spawn handoff method\n *\n * Process:\n * 1. Signal agent to save state (update continue file)\n * 2. Wait for idle (check for activity)\n * 3. Capture handoff context\n * 4. Kill current agent\n * 5. Build handoff prompt\n * 6. Spawn new agent with target model\n *\n * @param state - Current agent state\n * @param options - Handoff options\n * @returns Handoff result\n */\nasync function performKillAndSpawn(\n state: AgentState,\n options: HandoffOptions\n): Promise<HandoffResult> {\n try {\n // Step 1: Signal agent to save state\n // TODO: Send message to agent asking to update the continue file\n // For now, we'll capture what's there\n\n // Step 2: Wait for idle if requested\n if (options.waitForIdle !== false) {\n const timeout = options.idleTimeoutMs || 30000;\n const idle = await waitForIdle(state.id, timeout);\n if (!idle) {\n console.warn(`Agent ${state.id} did not become idle within ${timeout}ms`);\n }\n }\n\n // Step 3: Capture handoff context\n const context = await Effect.runPromise(captureHandoffContext(state, options.targetModel, options.reason));\n\n // Step 4: Kill current agent\n stopAgentSync(state.id);\n\n // Step 5: Build handoff prompt\n const prompt = buildHandoffPrompt(context, options.additionalInstructions);\n\n // Save handoff prompt for debugging\n const handoffDir = join(getAgentDir(state.id), 'handoffs');\n mkdirSync(handoffDir, { recursive: true });\n const handoffFile = join(handoffDir, `handoff-${Date.now()}.md`);\n writeFileSync(handoffFile, prompt);\n\n // Step 6: Spawn new agent with target model\n // Use same agent ID to preserve identity\n const newState = await spawnAgent({\n issueId: state.issueId,\n workspace: state.workspace,\n harness: state.harness,\n model: options.targetModel,\n role: 'work',\n prompt,\n allowHost: state.hostOverride === true,\n });\n\n // Preserve accumulated cost without reintroducing legacy phase/complexity routing fields.\n newState.costSoFar = state.costSoFar || 0;\n saveAgentStateSync(newState);\n\n return {\n success: true,\n method: 'kill-spawn',\n newAgentId: newState.id,\n context,\n };\n } catch (error) {\n return {\n success: false,\n method: 'kill-spawn',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n/**\n * Wait for agent to become idle\n *\n * @param agentId - Agent ID\n * @param timeoutMs - Timeout in milliseconds\n * @returns True if agent became idle, false if timeout\n */\nasync function waitForIdle(agentId: string, timeoutMs: number): Promise<boolean> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n // Check if agent session still exists\n if (!sessionExistsSync(agentId)) {\n return true; // Agent is gone, consider it idle\n }\n\n // Check for recent activity\n // TODO: Implement proper activity detection\n // For now, just wait a bit\n await sleep(1000);\n }\n\n return false; // Timeout\n}\n\n/**\n * Sleep helper\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Check if handoff is recommended for an agent\n *\n * This is a placeholder for future trigger logic.\n * Triggers will be implemented in Phase C.\n *\n * @param agentId - Agent ID\n * @returns True if handoff is recommended\n */\nexport function shouldHandoff(agentId: string): boolean {\n // TODO: Implement trigger logic in Phase C\n return false;\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// The handoff orchestration touches multiple subsystems (model validation,\n// agent state, tmux session detection, prompt persistence) — failures from any\n// of them surface here. The Effect variant collapses these into a single typed\n// error channel so callers can `Effect.catchTag` the relevant case.\n\n/** Tagged error for `performHandoffProgram` failures. */\nexport class HandoffError extends Data.TaggedError('HandoffError')<{\n readonly agentId: string;\n readonly stage: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect variant of `performHandoff`. Wraps the Promise-based implementation\n * and maps any thrown error into a typed `HandoffError`. The success result is\n * the same `HandoffResult` shape as the sync variant — including `success:\n * false` results when the agent isn't found or the model override is invalid.\n */\nexport const performHandoff = (\n agentId: string,\n options: HandoffOptions,\n): Effect.Effect<HandoffResult, HandoffError> =>\n Effect.tryPromise({\n try: () => performHandoffPromise(agentId, options),\n catch: (cause) =>\n new HandoffError({\n agentId,\n stage: 'performHandoff',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","/**\n * Handoff Triggers\n *\n * Detects conditions that should trigger model handoffs:\n * 1. Stuck escalation - Agent inactive for too long\n * 2. Planning complete - Planning phase finished, ready for implementation\n * 3. Test failure - Tests failing, need more powerful model\n * 4. Task completion - Implementation done, ready for specialist testing\n */\n\nimport { existsSync, statSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport type { AgentHealth } from './health.js';\nimport type { CloisterConfig } from './config.js';\nimport { loadCloisterConfigSync } from './config.js';\nimport { withBdMutex } from '../bd-mutex.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Cache for checkTaskCompletion keyed by `${workspace}::${issueId}`.\n * Invalidated by mtime of `.beads/issues.jsonl` — same trick as\n * computePlanningState (PAN-1024 hotfix). The handoff-suggestion endpoint\n * is polled every 30s per agent panel; without this cache, every poll\n * fires two `bd list --json` invocations which each take ~1.87s wall-clock\n * and 1+ MB of disk I/O, causing sustained disk thrashing across N agents.\n *\n * Single-flight via a Promise map prevents stampedes when the page first\n * loads multiple agent panels concurrently.\n */\ninterface TaskCompletionCacheEntry {\n mtimeMs: number;\n result: TriggerDetection;\n}\nconst taskCompletionCache = new Map<string, TaskCompletionCacheEntry>();\nconst taskCompletionInflight = new Map<string, Promise<TriggerDetection>>();\n\n/**\n * Trigger type\n */\nexport type TriggerType =\n | 'stuck_escalation'\n | 'test_failure'\n | 'task_complete'\n | 'manual';\n\n/**\n * Trigger detection result\n */\nexport interface TriggerDetection {\n triggered: boolean;\n type: TriggerType;\n reason: string;\n suggestedModel?: string;\n confidence: 'high' | 'medium' | 'low';\n}\n\n/**\n * Check if agent should be handed off due to stuck escalation\n *\n * Model-specific thresholds:\n * - Haiku: Stuck after 10 minutes → escalate to Sonnet\n * - Sonnet: Stuck after 20 minutes → escalate to Opus\n * - Opus: Stuck after 30 minutes → alert user (no auto-escalation)\n *\n * @param health - Agent health state\n * @param currentModel - Current model\n * @param config - Cloister configuration\n * @returns Trigger detection result\n */\nexport function checkStuckEscalation(\n health: AgentHealth,\n currentModel: string,\n config?: CloisterConfig\n): TriggerDetection {\n const conf = config || loadCloisterConfigSync();\n\n // Get stuck escalation config\n const stuckConfig = conf.handoffs?.auto_triggers?.stuck_escalation;\n if (!stuckConfig?.enabled) {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'Stuck escalation disabled in config',\n confidence: 'high',\n };\n }\n\n // Check if agent is stuck based on health state\n if (health.state !== 'stuck') {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: `Agent is ${health.state}, not stuck`,\n confidence: 'high',\n };\n }\n\n // Get minutes since last activity\n if (!health.lastActivity) {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'No last activity timestamp available',\n confidence: 'low',\n };\n }\n const minutesSinceActivity = (Date.now() - health.lastActivity.getTime()) / (1000 * 60);\n\n // Check model-specific thresholds\n if (currentModel === 'haiku' && minutesSinceActivity >= stuckConfig.haiku_to_sonnet_minutes) {\n return {\n triggered: true,\n type: 'stuck_escalation',\n reason: `Haiku agent stuck for ${Math.round(minutesSinceActivity)} minutes`,\n suggestedModel: 'sonnet',\n confidence: 'high',\n };\n }\n\n if (currentModel === 'sonnet' && minutesSinceActivity >= stuckConfig.sonnet_to_opus_minutes) {\n return {\n triggered: true,\n type: 'stuck_escalation',\n reason: `Sonnet agent stuck for ${Math.round(minutesSinceActivity)} minutes`,\n suggestedModel: 'opus',\n confidence: 'high',\n };\n }\n\n if (currentModel === 'opus') {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'Opus agent stuck - no higher model available, manual intervention needed',\n confidence: 'high',\n };\n }\n\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: `Agent stuck but threshold not reached (${Math.round(minutesSinceActivity)} minutes)`,\n confidence: 'medium',\n };\n}\n\n/**\n * Check if test failures should trigger escalation\n *\n * Aggressive escalation: Any test failure from Haiku escalates to Sonnet\n * Reasoning: Haiku is for simple tasks - if tests fail, the task isn't simple\n *\n * @param workspace - Workspace path\n * @param currentModel - Current model\n * @param config - Cloister configuration\n * @returns Trigger detection result\n */\nexport function checkTestFailure(\n workspace: string,\n currentModel: string,\n config?: CloisterConfig\n): TriggerDetection {\n const conf = config || loadCloisterConfigSync();\n\n // Get test failure config\n const testConfig = conf.handoffs?.auto_triggers?.test_failure;\n if (!testConfig?.enabled) {\n return {\n triggered: false,\n type: 'test_failure',\n reason: 'Test failure escalation disabled in config',\n confidence: 'high',\n };\n }\n\n // Only escalate from Haiku or configured from_model\n if (currentModel !== testConfig.from_model) {\n return {\n triggered: false,\n type: 'test_failure',\n reason: `Test failure escalation only applies to ${testConfig.from_model} model`,\n confidence: 'high',\n };\n }\n\n // Check for test failures\n // Look for common test result files/patterns\n const testFailure = detectTestFailure(workspace);\n\n if (testFailure.failed) {\n return {\n triggered: true,\n type: 'test_failure',\n reason: `Test failures detected: ${testFailure.reason}`,\n suggestedModel: testConfig.to_model,\n confidence: testFailure.confidence,\n };\n }\n\n return {\n triggered: false,\n type: 'test_failure',\n reason: 'No test failures detected',\n confidence: 'medium',\n };\n}\n\n/**\n * Detect test failures in workspace\n *\n * Checks for:\n * - npm test output\n * - Jest results\n * - pytest results\n * - cargo test results\n *\n * @param workspace - Workspace path\n * @returns Test failure detection\n */\nfunction detectTestFailure(workspace: string): {\n failed: boolean;\n reason: string;\n confidence: 'high' | 'medium' | 'low';\n} {\n // TODO: Implement proper test result detection\n // For now, this is a placeholder that checks for common test result files\n\n // Check for .test-results or similar\n const commonTestPaths = [\n '.test-results',\n 'test-results',\n 'junit.xml',\n 'coverage',\n '.nyc_output',\n ];\n\n // This is a simplified check - in reality we'd parse test output\n return {\n failed: false,\n reason: 'Test result detection not yet implemented',\n confidence: 'low',\n };\n}async function checkTaskCompletionPromise(\n issueId: string,\n config?: CloisterConfig,\n workspace?: string,\n): Promise<TriggerDetection> {\n const conf = config || loadCloisterConfigSync();\n\n // Get task completion config\n const completionConfig = conf.handoffs?.auto_triggers?.implementation_complete;\n if (!completionConfig?.enabled) {\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'Task completion detection disabled in config',\n confidence: 'high',\n };\n }\n\n // mtime-based cache\n const cacheKey = `${workspace ?? ''}::${issueId.toLowerCase()}`;\n let mtimeMs = 0;\n if (workspace) {\n const beadsFile = join(workspace, '.beads', 'issues.jsonl');\n try {\n mtimeMs = statSync(beadsFile).mtimeMs;\n } catch {\n // No beads file — fall through to compute (cheap when JSONL absent).\n }\n if (mtimeMs > 0) {\n const cached = taskCompletionCache.get(cacheKey);\n if (cached && cached.mtimeMs === mtimeMs) {\n return cached.result;\n }\n // Single-flight: if another request is computing this same key, await\n // its result instead of firing another pair of `bd list` invocations.\n const inflight = taskCompletionInflight.get(cacheKey);\n if (inflight) return inflight;\n }\n }\n\n const computePromise = (async (): Promise<TriggerDetection> => {\n // PAN-1812: the workspace beads DB must be queried from the agent's own\n // workspace. Without cwd the query can run against the wrong directory and\n // falsely report no open beads.\n if (!workspace) {\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'No workspace available to check task completion',\n confidence: 'high',\n };\n }\n\n const bdOptions = { encoding: 'utf-8' as const, cwd: workspace };\n const issueLower = issueId.toLowerCase();\n\n try {\n const { stdout: output } = await Effect.runPromise(withBdMutex(() => Effect.tryPromise({\n try: () => execAsync(`bd list --json --title-contains ${issueLower} --status closed`, bdOptions),\n catch: (cause) => cause,\n })));\n const tasks = JSON.parse(output);\n const implementTask = tasks.find((t: any) =>\n t.title.toLowerCase().includes('implement') ||\n t.labels?.includes('implementation')\n );\n\n if (implementTask) {\n const { stdout: openOutput } = await execAsync(`bd list --json --title-contains ${issueLower} --status open`, bdOptions);\n const openTasks = JSON.parse(openOutput);\n\n if (openTasks.length === 0) {\n return {\n triggered: true,\n type: 'task_complete',\n reason: 'Implementation task closed, no remaining tasks',\n suggestedModel: completionConfig.to_specialist,\n confidence: 'high',\n };\n }\n return {\n triggered: false,\n type: 'task_complete',\n reason: `Implementation task closed but ${openTasks.length} tasks remain`,\n confidence: 'medium',\n };\n }\n } catch {\n // Beads not available or error querying — fall through to default.\n }\n\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'No implementation completion signals detected',\n confidence: 'high',\n };\n })();\n\n if (workspace && mtimeMs > 0) {\n taskCompletionInflight.set(cacheKey, computePromise);\n try {\n const result = await computePromise;\n taskCompletionCache.set(cacheKey, { mtimeMs, result });\n return result;\n } finally {\n taskCompletionInflight.delete(cacheKey);\n }\n }\n return computePromise;\n}async function checkAllTriggersPromise(\n agentId: string,\n workspace: string,\n issueId: string,\n currentModel: string,\n health: AgentHealth,\n config?: CloisterConfig\n): Promise<TriggerDetection[]> {\n const triggers: TriggerDetection[] = [];\n\n // Check each trigger type\n const stuckCheck = checkStuckEscalation(health, currentModel, config);\n if (stuckCheck.triggered) triggers.push(stuckCheck);\n\n const testCheck = checkTestFailure(workspace, currentModel, config);\n if (testCheck.triggered) triggers.push(testCheck);\n\n const completionCheck = await Effect.runPromise(checkTaskCompletion(issueId, config, workspace));\n if (completionCheck.triggered) triggers.push(completionCheck);\n\n return triggers;\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n\n/**\n * Effect-typed variant of {@link checkTaskCompletion}. Wraps the Promise-based\n * implementation; never fails (the underlying function swallows errors and\n * returns a \"not triggered\" detection on failure).\n */\nexport function checkTaskCompletion(\n issueId: string,\n config?: CloisterConfig,\n workspace?: string,\n): Effect.Effect<TriggerDetection> {\n return Effect.promise(() => checkTaskCompletionPromise(issueId, config, workspace));\n}\n\n/**\n * Effect-typed variant of {@link checkAllTriggers}. Never fails — uses\n * `Effect.promise` because the underlying async path absorbs bd / fs errors.\n */\nexport function checkAllTriggers(\n agentId: string,\n workspace: string,\n issueId: string,\n currentModel: string,\n health: AgentHealth,\n config?: CloisterConfig,\n): Effect.Effect<TriggerDetection[]> {\n return Effect.promise(() => checkAllTriggersPromise(agentId, workspace, issueId, currentModel, health, config));\n}\n","/**\n * Handoff Event Logger\n *\n * Logs handoff events to JSONL file for tracking and analysis.\n */\n\nimport { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs';\nimport { appendFile, mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport type { HandoffContext } from './handoff-context.js';\nimport type { TriggerType } from './triggers.js';\n\n/**\n * Handoff event structure\n */\nexport interface HandoffEvent {\n timestamp: string;\n agentId: string;\n issueId: string;\n\n // Model transition\n from: {\n model: string;\n runtime: string;\n sessionId?: string;\n };\n to: {\n model: string;\n runtime: string;\n sessionId?: string;\n };\n\n // Trigger information\n trigger: TriggerType | 'manual';\n reason: string;\n\n // Context\n context: {\n beadsTaskCompleted?: string;\n stuckMinutes?: number;\n costAtHandoff?: number;\n handoffCount?: number;\n };\n\n // Result - handoff OPERATION success (did we spawn new agent?)\n success: boolean;\n errorMessage?: string;\n\n // Recovery outcome - did the agent ACTUALLY recover?\n // This is verified after the handoff, not at handoff time\n outcome?: {\n verified: boolean; // Has recovery been checked?\n agentRecovered: boolean; // Did the agent start making progress?\n verifiedAt?: string; // When was this verified?\n verificationMethod?: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n };\n}\n\n/**\n * Handoff log file path\n */\nconst HANDOFF_LOG_FILE = join(OVERDECK_HOME, 'logs', 'handoffs.jsonl');\n\n/**\n * Ensure log directory exists\n */\nfunction ensureLogDir(): void {\n const logDir = join(OVERDECK_HOME, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n}\n\n/**\n * Log a handoff event\n *\n * @param event - Handoff event to log\n */\nexport function logHandoffEventSync(event: HandoffEvent): void {\n ensureLogDir();\n\n const line = JSON.stringify(event) + '\\n';\n appendFileSync(HANDOFF_LOG_FILE, line, 'utf-8');\n}\n\n/**\n * Create a handoff event from handoff result\n *\n * @param agentId - Agent ID\n * @param issueId - Issue ID\n * @param context - Handoff context\n * @param trigger - Trigger type\n * @param success - Whether handoff succeeded\n * @param errorMessage - Error message if failed\n * @returns Handoff event\n */\nexport function createHandoffEvent(\n agentId: string,\n issueId: string,\n context: HandoffContext,\n trigger: TriggerType | 'manual',\n success: boolean,\n errorMessage?: string\n): HandoffEvent {\n // Calculate stuck minutes if applicable\n let stuckMinutes: number | undefined;\n if (trigger === 'stuck_escalation') {\n // This would be calculated from health state\n // For now, we'll omit it unless available in context\n }\n\n return {\n timestamp: new Date().toISOString(),\n agentId,\n issueId,\n from: {\n model: context.previousModel,\n runtime: context.previousRuntime,\n sessionId: context.previousSessionId,\n },\n to: {\n model: context.targetModel,\n runtime: context.previousRuntime, // New agent inherits the same harness\n sessionId: undefined, // Will be set after spawn\n },\n trigger,\n reason: context.reason,\n context: {\n costAtHandoff: context.costSoFar,\n handoffCount: context.handoffCount,\n stuckMinutes,\n },\n success,\n errorMessage,\n };\n}\n\n/**\n * Read all handoff events from log\n *\n * @param limit - Maximum number of events to return (most recent first)\n * @returns Array of handoff events\n */\nexport function readHandoffEventsSync(limit?: number): HandoffEvent[] {\n ensureLogDir();\n\n if (!existsSync(HANDOFF_LOG_FILE)) {\n return [];\n }\n\n const content = readFileSync(HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n const events = lines.map(line => JSON.parse(line) as HandoffEvent);\n\n // Return most recent first\n events.reverse();\n\n if (limit) {\n return events.slice(0, limit);\n }\n\n return events;\n}\n\n/**\n * Read handoff events for a specific issue\n *\n * @param issueId - Issue ID\n * @returns Array of handoff events for the issue\n */\nexport function readIssueHandoffEventsSync(issueId: string): HandoffEvent[] {\n const allEvents = readHandoffEventsSync();\n return allEvents.filter(e => e.issueId === issueId);\n}\n\n/**\n * Read handoff events for a specific agent\n *\n * @param agentId - Agent ID\n * @returns Array of handoff events for the agent\n */\nexport function readAgentHandoffEventsSync(agentId: string): HandoffEvent[] {\n const allEvents = readHandoffEventsSync();\n return allEvents.filter(e => e.agentId === agentId);\n}\n\n/**\n * Get handoff statistics\n *\n * Returns both operation success rate (handoff executed) and recovery success rate\n * (agent actually recovered). These are DIFFERENT metrics:\n *\n * - operationSuccessRate: Did the handoff operation complete successfully?\n * - recoverySuccessRate: Did the agent actually start making progress after handoff?\n *\n * @returns Handoff statistics\n */\nexport function getHandoffStats(): {\n totalHandoffs: number;\n todayEscalations: number; // Model escalations (stuck/cost/test) that occurred today\n byTrigger: Record<string, number>;\n byModel: {\n from: Record<string, number>;\n to: Record<string, number>;\n };\n // Legacy field for backwards compatibility\n successRate: number;\n // More detailed metrics\n operationSuccessRate: number; // Handoff operation succeeded\n recoverySuccessRate: number; // Agent actually recovered (verified only)\n pendingVerification: number; // Handoffs that haven't been verified yet\n verifiedCount: number; // Total handoffs that have been verified\n} {\n const events = readHandoffEventsSync();\n const today = new Date().toISOString().split('T')[0];\n\n const stats = {\n totalHandoffs: events.length,\n todayEscalations: 0,\n byTrigger: {} as Record<string, number>,\n byModel: {\n from: {} as Record<string, number>,\n to: {} as Record<string, number>,\n },\n successRate: 0,\n operationSuccessRate: 0,\n recoverySuccessRate: 0,\n pendingVerification: 0,\n verifiedCount: 0,\n };\n\n let operationSuccessCount = 0;\n let recoverySuccessCount = 0;\n\n for (const event of events) {\n // Count today's escalations (all trigger types are escalations)\n if (event.timestamp.startsWith(today)) {\n stats.todayEscalations++;\n }\n\n // Count by trigger\n stats.byTrigger[event.trigger] = (stats.byTrigger[event.trigger] || 0) + 1;\n\n // Count by model\n stats.byModel.from[event.from.model] = (stats.byModel.from[event.from.model] || 0) + 1;\n stats.byModel.to[event.to.model] = (stats.byModel.to[event.to.model] || 0) + 1;\n\n // Count operation successes (handoff executed)\n if (event.success) {\n operationSuccessCount++;\n }\n\n // Count recovery outcomes (agent actually recovered)\n if (event.outcome?.verified) {\n stats.verifiedCount++;\n if (event.outcome.agentRecovered) {\n recoverySuccessCount++;\n }\n } else {\n stats.pendingVerification++;\n }\n }\n\n // Calculate rates\n stats.operationSuccessRate = events.length > 0 ? operationSuccessCount / events.length : 0;\n stats.recoverySuccessRate = stats.verifiedCount > 0 ? recoverySuccessCount / stats.verifiedCount : 0;\n\n // Legacy successRate - use operation success for backwards compatibility\n // but NOTE: This is misleading for \"did the agent recover?\"\n stats.successRate = stats.operationSuccessRate;\n\n return stats;\n}\n\n/**\n * Update the outcome of a handoff event (verify if agent recovered)\n *\n * @param agentId - Agent ID\n * @param timestamp - Original handoff timestamp\n * @param outcome - Recovery outcome\n */\nexport function updateHandoffOutcomeSync(\n agentId: string,\n timestamp: string,\n outcome: {\n agentRecovered: boolean;\n verificationMethod: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n }\n): void {\n ensureLogDir();\n\n if (!existsSync(HANDOFF_LOG_FILE)) {\n return;\n }\n\n const content = readFileSync(HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n // Find and update the matching event\n const updatedLines = lines.map(line => {\n const event = JSON.parse(line) as HandoffEvent;\n if (event.agentId === agentId && event.timestamp === timestamp) {\n event.outcome = {\n verified: true,\n agentRecovered: outcome.agentRecovered,\n verifiedAt: new Date().toISOString(),\n verificationMethod: outcome.verificationMethod,\n notes: outcome.notes,\n };\n return JSON.stringify(event);\n }\n return line;\n });\n\n // Rewrite the file\n writeFileSync(HANDOFF_LOG_FILE, updatedLines.join('\\n') + '\\n', 'utf-8');\n}\n\n/**\n * Get handoffs pending verification\n *\n * @returns Array of handoff events that need outcome verification\n */\nexport function getPendingVerificationHandoffsSync(): HandoffEvent[] {\n const events = readHandoffEventsSync();\n return events.filter(e => e.success && !e.outcome?.verified);\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Async, typed-error variants of the handoff log helpers. The sync variants\n// are preserved for the CLI surfaces (e.g. `pan show`) that already run inside\n// a one-shot Node process where blocking is acceptable; the Effect variants\n// are appropriate for the dashboard server's request handlers.\n\nconst ensureLogDirAsync = (): Effect.Effect<void, FsError> => {\n const logDir = join(OVERDECK_HOME, 'logs');\n return Effect.tryPromise({\n try: () => mkdir(logDir, { recursive: true }),\n catch: (cause) => new FsError({ path: logDir, operation: 'mkdir', cause }),\n });\n};\n\n/** Effect variant of `logHandoffEvent`. */\nexport const logHandoffEvent = (event: HandoffEvent): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n const line = JSON.stringify(event) + '\\n';\n yield* Effect.tryPromise({\n try: () => appendFile(HANDOFF_LOG_FILE, line, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'appendFile', cause }),\n });\n });\n\n/** Effect variant of `readHandoffEvents`. */\nexport const readHandoffEvents = (limit?: number): Effect.Effect<HandoffEvent[], FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n if (!existsSync(HANDOFF_LOG_FILE)) return [];\n\n const content = yield* Effect.tryPromise({\n try: () => readFile(HANDOFF_LOG_FILE, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'readFile', cause }),\n });\n\n const lines = content.trim().split('\\n').filter((line) => line.trim());\n const events = lines.map((line) => JSON.parse(line) as HandoffEvent);\n events.reverse();\n return limit ? events.slice(0, limit) : events;\n });\n\n/** Effect variant of `readIssueHandoffEvents`. */\nexport const readIssueHandoffEvents = (\n issueId: string,\n): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(Effect.map((events) => events.filter((e) => e.issueId === issueId)));\n\n/** Effect variant of `readAgentHandoffEvents`. */\nexport const readAgentHandoffEvents = (\n agentId: string,\n): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(Effect.map((events) => events.filter((e) => e.agentId === agentId)));\n\n/** Effect variant of `updateHandoffOutcome`. */\nexport const updateHandoffOutcome = (\n agentId: string,\n timestamp: string,\n outcome: {\n agentRecovered: boolean;\n verificationMethod: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n },\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n if (!existsSync(HANDOFF_LOG_FILE)) return;\n\n const content = yield* Effect.tryPromise({\n try: () => readFile(HANDOFF_LOG_FILE, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'readFile', cause }),\n });\n\n const lines = content.trim().split('\\n').filter((line) => line.trim());\n\n const updatedLines = lines.map((line) => {\n const event = JSON.parse(line) as HandoffEvent;\n if (event.agentId === agentId && event.timestamp === timestamp) {\n event.outcome = {\n verified: true,\n agentRecovered: outcome.agentRecovered,\n verifiedAt: new Date().toISOString(),\n verificationMethod: outcome.verificationMethod,\n notes: outcome.notes,\n };\n return JSON.stringify(event);\n }\n return line;\n });\n\n yield* Effect.tryPromise({\n try: () => writeFile(HANDOFF_LOG_FILE, updatedLines.join('\\n') + '\\n', 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'writeFile', cause }),\n });\n });\n\n/** Effect variant of `getPendingVerificationHandoffs`. */\nexport const getPendingVerificationHandoffs = (): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(\n Effect.map((events) => events.filter((e) => e.success && !e.outcome?.verified)),\n );\n","/**\n * FPP (Fixed Point Principle) Violation Detection\n *\n * \"Any runnable action is a fixed point and must resolve before the\n * system can rest.\" Borrowed from Gastown; inspired by Doctor Who.\n *\n * Detects when agents have pending work but are idle, and sends\n * escalating nudges to get them back on track.\n */\n\nimport { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';\nimport { join, dirname } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { checkHookSync } from '../hooks.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { getAgentHealth } from './health.js';\nimport { OVERDECK_HOME } from '../paths.js';\n\n/**\n * FPP violation types\n *\n * Currently only 'hook_idle' is implemented. Additional types can be added\n * when detection logic is implemented for PR staleness, review pending, etc.\n */\nexport type FPPViolationType = 'hook_idle';\n\n/**\n * FPP violation record\n */\nexport interface FPPViolation {\n agentId: string;\n type: FPPViolationType;\n detectedAt: string;\n nudgeCount: number;\n lastNudgeAt?: string;\n resolved: boolean;\n}\n\n/**\n * FPP violation configuration\n */\nexport interface FPPViolationConfig {\n hook_idle_minutes: number;\n pr_approved_minutes: number;\n review_pending_minutes: number;\n max_nudges: number;\n}\n\n/**\n * Default FPP violation configuration\n */\nexport const DEFAULT_FPP_CONFIG: FPPViolationConfig = {\n hook_idle_minutes: 5,\n pr_approved_minutes: 10,\n review_pending_minutes: 15,\n max_nudges: 3,\n};\n\n/**\n * Path to violations data file\n */\nconst VIOLATIONS_DATA_FILE = join(OVERDECK_HOME, 'fpp-violations.json');\n\n/**\n * Persisted violations data format\n */\ninterface ViolationsDataPersisted {\n violations: Array<[string, FPPViolation]>;\n}\n\n/**\n * Load violations from file\n */\nfunction loadViolations(): Map<string, FPPViolation> {\n if (!existsSync(VIOLATIONS_DATA_FILE)) {\n return new Map();\n }\n\n try {\n const fileContent = readFileSync(VIOLATIONS_DATA_FILE, 'utf-8');\n const persisted: ViolationsDataPersisted = JSON.parse(fileContent);\n\n return new Map(persisted.violations || []);\n } catch (error) {\n console.error('Failed to load violations data, starting fresh:', error);\n return new Map();\n }\n}\n\n/**\n * Save violations to file (atomic write)\n */\nfunction saveViolations(violations: Map<string, FPPViolation>): void {\n try {\n // Ensure directory exists\n const dir = dirname(VIOLATIONS_DATA_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const persisted: ViolationsDataPersisted = {\n violations: Array.from(violations.entries()),\n };\n\n // Atomic write: write to temp file, then rename\n const tempFile = `${VIOLATIONS_DATA_FILE}.tmp`;\n writeFileSync(tempFile, JSON.stringify(persisted, null, 2));\n writeFileSync(VIOLATIONS_DATA_FILE, readFileSync(tempFile));\n\n // Clean up temp file\n try {\n unlinkSync(tempFile);\n } catch (unlinkError: unknown) {\n // Non-critical: temp file cleanup failure is logged but doesn't block operation\n console.debug('Failed to cleanup temp file:', unlinkError instanceof Error ? unlinkError.message : unlinkError);\n }\n } catch (error) {\n console.error('Failed to save violations data:', error);\n }\n}\n\n/**\n * Store of active violations (persisted to disk)\n */\nconst activeViolations = loadViolations();\n\n/**\n * Get escalating nudge message based on nudge count\n *\n * Currently only supports 'hook_idle' type. Messages escalate from\n * a gentle status check to a direct action request.\n */\nfunction getNudgeMessage(_violation: FPPViolation, nudgeCount: number): string {\n // Currently only 'hook_idle' is implemented, so messages are tailored for that case\n if (nudgeCount === 1) {\n // Nudge 1: Status check\n return \"What's your current status? You have pending work on your hook.\";\n } else if (nudgeCount === 2) {\n // Nudge 2: Gentle reminder\n return \"I notice you've been idle while having pending work. Do you need help with the task on your hook?\";\n } else {\n // Nudge 3: Direct action\n return \"You have pending work on your hook that needs attention. Execute it now or explain why you're blocked.\";\n }\n}\n\n/**\n * Check for FPP violations on an agent\n *\n * @param agentId - Agent to check\n * @param config - FPP violation configuration\n * @returns Violation if detected, null otherwise\n */\nexport function checkAgentForViolations(\n agentId: string,\n config: FPPViolationConfig = DEFAULT_FPP_CONFIG\n): FPPViolation | null {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) return null;\n\n // Get agent health to check activity\n const health = getAgentHealth(agentId, runtime);\n if (!health) return null;\n\n // Only check agents that are idle (stale, warning, stuck)\n if (health.state === 'active') return null;\n\n // Calculate idle time in minutes\n const idleMinutes = health.timeSinceActivity\n ? Math.floor(health.timeSinceActivity / (60 * 1000))\n : 0;\n\n // Check for work on hook\n let hookStatus: ReturnType<typeof checkHookSync>;\n try {\n hookStatus = checkHookSync(agentId);\n } catch (error) {\n console.error(`Failed to check hook for ${agentId}:`, error);\n return null;\n }\n\n if (hookStatus.hasWork && idleMinutes >= config.hook_idle_minutes) {\n // Check if we already have an active violation\n const existingKey = `${agentId}-hook_idle`;\n const existing = activeViolations.get(existingKey);\n\n if (existing && !existing.resolved) {\n return existing;\n }\n\n // Create new violation\n const violation: FPPViolation = {\n agentId,\n type: 'hook_idle',\n detectedAt: new Date().toISOString(),\n nudgeCount: 0,\n resolved: false,\n };\n\n activeViolations.set(existingKey, violation);\n saveViolations(activeViolations);\n return violation;\n }\n\n return null;\n}\n\n/**\n * Send a nudge to an agent about an FPP violation\n *\n * @param violation - The violation to nudge about\n * @returns True if nudge was sent successfully\n */\nexport function sendNudge(violation: FPPViolation): boolean {\n const runtime = getRuntimeForAgent(violation.agentId);\n if (!runtime) return false;\n\n // Increment nudge count\n violation.nudgeCount++;\n violation.lastNudgeAt = new Date().toISOString();\n\n // Get escalating message\n const message = getNudgeMessage(violation, violation.nudgeCount);\n\n try {\n runtime.sendMessage(violation.agentId, message);\n console.log(`🔔 Sent FPP nudge #${violation.nudgeCount} to ${violation.agentId}`);\n saveViolations(activeViolations);\n return true;\n } catch (error) {\n console.error(`Failed to send nudge to ${violation.agentId}:`, error);\n return false;\n }\n}\n\n/**\n * Resolve a violation (agent has addressed the issue)\n *\n * @param agentId - Agent ID\n * @param type - Violation type\n */\nexport function resolveViolationSync(agentId: string, type: FPPViolationType): void {\n const key = `${agentId}-${type}`;\n const violation = activeViolations.get(key);\n\n if (violation) {\n violation.resolved = true;\n console.log(`🔔 FPP violation resolved for ${agentId}: ${type}`);\n saveViolations(activeViolations);\n }\n}\n\n/**\n * Get all active violations\n *\n * @returns Array of active violations\n */\nexport function getActiveViolations(): FPPViolation[] {\n return Array.from(activeViolations.values()).filter(v => !v.resolved);\n}\n\n/**\n * Get violations for a specific agent\n *\n * @param agentId - Agent ID\n * @returns Array of violations for this agent\n */\nexport function getAgentViolations(agentId: string): FPPViolation[] {\n return getActiveViolations().filter(v => v.agentId === agentId);\n}\n\n/**\n * Check if an agent has exceeded max nudges\n *\n * @param violation - The violation to check\n * @param config - FPP violation configuration\n * @returns True if max nudges exceeded\n */\nexport function hasExceededMaxNudges(\n violation: FPPViolation,\n config: FPPViolationConfig = DEFAULT_FPP_CONFIG\n): boolean {\n return violation.nudgeCount >= config.max_nudges;\n}\n\n/**\n * Clear resolved violations older than a certain time\n *\n * @param hoursOld - Clear violations resolved this many hours ago\n */\nexport function clearOldViolationsSync(hoursOld: number = 24): void {\n const cutoff = Date.now() - hoursOld * 60 * 60 * 1000;\n let changed = false;\n\n for (const [key, violation] of activeViolations.entries()) {\n if (violation.resolved && new Date(violation.detectedAt).getTime() < cutoff) {\n activeViolations.delete(key);\n changed = true;\n }\n }\n\n if (changed) {\n saveViolations(activeViolations);\n }\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Async persistence helpers around the in-memory `activeViolations` map. These\n// are additive: the sync variants stay in place because the module-level cache\n// is populated at import time and most callers happen on the orchestrator's\n// patrol thread where sync I/O is acceptable. Route handlers should prefer\n// the Effect variants.\n\nconst saveViolationsAsync = (violations: Map<string, FPPViolation>): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const dir = dirname(VIOLATIONS_DATA_FILE);\n yield* Effect.tryPromise({\n try: () => mkdir(dir, { recursive: true }),\n catch: (cause) => new FsError({ path: dir, operation: 'mkdir', cause }),\n });\n\n const persisted: ViolationsDataPersisted = {\n violations: Array.from(violations.entries()),\n };\n\n const tempFile = `${VIOLATIONS_DATA_FILE}.tmp`;\n yield* Effect.tryPromise({\n try: () => writeFile(tempFile, JSON.stringify(persisted, null, 2), 'utf-8'),\n catch: (cause) => new FsError({ path: tempFile, operation: 'writeFile', cause }),\n });\n yield* Effect.tryPromise({\n try: () => rename(tempFile, VIOLATIONS_DATA_FILE),\n catch: (cause) => new FsError({ path: VIOLATIONS_DATA_FILE, operation: 'rename', cause }),\n });\n\n yield* Effect.tryPromise({\n try: () => unlink(tempFile),\n catch: (cause) => new FsError({ path: tempFile, operation: 'unlink', cause }),\n }).pipe(Effect.catch(() => Effect.succeed(undefined)));\n });\n\n/** Effect variant of `resolveViolation`. */\nexport const resolveViolation = (\n agentId: string,\n type: FPPViolationType,\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const key = `${agentId}-${type}`;\n const violation = activeViolations.get(key);\n if (!violation) return;\n\n violation.resolved = true;\n console.log(`🔔 FPP violation resolved for ${agentId}: ${type}`);\n yield* saveViolationsAsync(activeViolations);\n });\n\n/** Effect variant of `clearOldViolations`. */\nexport const clearOldViolations = (hoursOld: number = 24): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const cutoff = Date.now() - hoursOld * 60 * 60 * 1000;\n let changed = false;\n\n for (const [key, violation] of activeViolations.entries()) {\n if (violation.resolved && new Date(violation.detectedAt).getTime() < cutoff) {\n activeViolations.delete(key);\n changed = true;\n }\n }\n\n if (changed) {\n yield* saveViolationsAsync(activeViolations);\n }\n });\n\n// Silence \"unused\" warnings for fs/promises imports that may be picked up by\n// future expansions of the Effect variants above.\nvoid readFile;\n","/**\n * Cost Monitor\n *\n * Monitors agent costs against configured limits and emits alerts.\n * Does NOT automatically stop agents - just provides visibility and warnings.\n */\n\nimport { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';\nimport { join, dirname } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { loadCloisterConfigSync, type CostLimitsConfig } from './config.js';\nimport {\n getAgentRollup,\n getDailyTrendsSync as getDailyTrends,\n getCostForIssueSync as getCostForIssueFromDb,\n getAgentDailyCostSync,\n} from '../overdeck/cost-sync.js';\n\n/**\n * Cost alert level\n */\nexport type CostAlertLevel = 'warning' | 'limit_reached';\n\n/**\n * Cost alert\n */\nexport interface CostAlert {\n type: 'per_agent' | 'per_issue' | 'daily_total';\n level: CostAlertLevel;\n agentId?: string;\n issueId?: string;\n currentCost: number;\n limit: number;\n percentUsed: number;\n timestamp: string;\n}\n\n/**\n * Cost tracking data (persisted format)\n */\ninterface CostDataPersisted {\n perAgent: Record<string, number>;\n perIssue: Record<string, number>;\n dailyTotal: number;\n lastResetDate: string; // ISO date string (YYYY-MM-DD)\n}\n\n/**\n * Cost tracking data (runtime format)\n */\ninterface CostData {\n perAgent: Map<string, number>;\n perIssue: Map<string, number>;\n dailyTotal: number;\n lastResetDate: string; // ISO date string (YYYY-MM-DD)\n}\n\n/**\n * Path to cost data file\n */\nconst COST_DATA_FILE = join(OVERDECK_HOME, 'cost-data.json');\n\n/**\n * Load cost data from file\n */\nfunction loadCostData(): CostData {\n if (!existsSync(COST_DATA_FILE)) {\n return {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: new Date().toISOString().split('T')[0],\n };\n }\n\n try {\n const fileContent = readFileSync(COST_DATA_FILE, 'utf-8');\n const persisted: CostDataPersisted = JSON.parse(fileContent);\n\n return {\n perAgent: new Map(Object.entries(persisted.perAgent || {})),\n perIssue: new Map(Object.entries(persisted.perIssue || {})),\n dailyTotal: persisted.dailyTotal || 0,\n lastResetDate: persisted.lastResetDate || new Date().toISOString().split('T')[0],\n };\n } catch (error) {\n console.error('Failed to load cost data, starting fresh:', error);\n return {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: new Date().toISOString().split('T')[0],\n };\n }\n}\n\n/**\n * Save cost data to file (atomic write)\n */\nfunction saveCostData(data: CostData): void {\n try {\n // Ensure directory exists\n const dir = dirname(COST_DATA_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const persisted: CostDataPersisted = {\n perAgent: Object.fromEntries(data.perAgent),\n perIssue: Object.fromEntries(data.perIssue),\n dailyTotal: data.dailyTotal,\n lastResetDate: data.lastResetDate,\n };\n\n // Atomic write: write to temp file, then rename\n const tempFile = `${COST_DATA_FILE}.tmp`;\n writeFileSync(tempFile, JSON.stringify(persisted, null, 2));\n writeFileSync(COST_DATA_FILE, readFileSync(tempFile));\n\n // Clean up temp file\n try {\n unlinkSync(tempFile);\n } catch (unlinkError: unknown) {\n // Non-critical: temp file cleanup failure is logged but doesn't block operation\n console.debug('Failed to cleanup temp file:', unlinkError instanceof Error ? unlinkError.message : unlinkError);\n }\n } catch (error) {\n console.error('Failed to save cost data:', error);\n }\n}\n\n// Load cost data on module initialization\nlet costData: CostData = loadCostData();\n\n/**\n * Get today's date as ISO string (YYYY-MM-DD)\n */\nfunction getTodayDate(): string {\n return new Date().toISOString().split('T')[0];\n}\n\n/**\n * Reset daily totals if it's a new day\n */\nfunction checkDailyReset(): void {\n const today = getTodayDate();\n if (costData.lastResetDate !== today) {\n costData.dailyTotal = 0;\n costData.lastResetDate = today;\n console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);\n saveCostData(costData);\n }\n}\n\n/**\n * Record a cost event\n *\n * @param agentId - Agent ID\n * @param cost - Cost in USD\n * @param issueId - Optional issue ID\n */\nexport function recordCostSync(agentId: string, cost: number, issueId?: string): void {\n checkDailyReset();\n\n // Update per-agent cost\n const currentAgentCost = costData.perAgent.get(agentId) || 0;\n costData.perAgent.set(agentId, currentAgentCost + cost);\n\n // Update per-issue cost\n if (issueId) {\n const currentIssueCost = costData.perIssue.get(issueId) || 0;\n costData.perIssue.set(issueId, currentIssueCost + cost);\n }\n\n // Update daily total\n costData.dailyTotal += cost;\n\n // Persist to disk\n saveCostData(costData);\n}\n\n/**\n * Check if any cost limits are being approached or exceeded\n *\n * @param agentId - Agent ID to check\n * @param issueId - Optional issue ID to check\n * @param config - Cost limits configuration\n * @returns Array of alerts (empty if no limits exceeded)\n */\nexport function checkCostLimits(\n agentId: string,\n issueId: string | undefined,\n config: CostLimitsConfig = loadCloisterConfigSync().cost_limits || {\n per_agent_usd: 10.0,\n per_issue_usd: 25.0,\n daily_total_usd: 100.0,\n alert_threshold: 0.8,\n }\n): CostAlert[] {\n const alerts: CostAlert[] = [];\n const now = new Date().toISOString();\n\n // Read agent cost from DB (scoped to today, matching the cap's window)\n // The accumulator should match the cap's window: per-agent daily cap compares daily spend\n if (config.per_agent_usd > 0) {\n const agentCost = getAgentDailyCostSync(agentId);\n const agentPercent = agentCost / config.per_agent_usd;\n\n if (agentPercent >= 1.0) {\n alerts.push({\n type: 'per_agent',\n level: 'limit_reached',\n agentId,\n currentCost: agentCost,\n limit: config.per_agent_usd,\n percentUsed: agentPercent * 100,\n timestamp: now,\n });\n } else if (agentPercent >= config.alert_threshold) {\n alerts.push({\n type: 'per_agent',\n level: 'warning',\n agentId,\n currentCost: agentCost,\n limit: config.per_agent_usd,\n percentUsed: agentPercent * 100,\n timestamp: now,\n });\n }\n }\n\n // Read issue cost from DB (scoped to today, matching the cap's window)\n if (issueId && config.per_issue_usd > 0) {\n const issueDailyTrends = getDailyTrends({ days: 1, issueId });\n const issueCost = issueDailyTrends.reduce((sum, t) => sum + t.totalCost, 0);\n const issuePercent = issueCost / config.per_issue_usd;\n\n if (issuePercent >= 1.0) {\n alerts.push({\n type: 'per_issue',\n level: 'limit_reached',\n issueId,\n currentCost: issueCost,\n limit: config.per_issue_usd,\n percentUsed: issuePercent * 100,\n timestamp: now,\n });\n } else if (issuePercent >= config.alert_threshold) {\n alerts.push({\n type: 'per_issue',\n level: 'warning',\n issueId,\n currentCost: issueCost,\n limit: config.per_issue_usd,\n percentUsed: issuePercent * 100,\n timestamp: now,\n });\n }\n }\n\n // Read today's total from DB (sum of today's events)\n if (config.daily_total_usd > 0) {\n const dailyTotal = getDailyTrends({ days: 1 }).reduce((sum, t) => sum + t.totalCost, 0);\n const dailyPercent = dailyTotal / config.daily_total_usd;\n\n if (dailyPercent >= 1.0) {\n alerts.push({\n type: 'daily_total',\n level: 'limit_reached',\n currentCost: dailyTotal,\n limit: config.daily_total_usd,\n percentUsed: dailyPercent * 100,\n timestamp: now,\n });\n } else if (dailyPercent >= config.alert_threshold) {\n alerts.push({\n type: 'daily_total',\n level: 'warning',\n currentCost: dailyTotal,\n limit: config.daily_total_usd,\n percentUsed: dailyPercent * 100,\n timestamp: now,\n });\n }\n }\n\n return alerts;\n}\n\n/**\n * Get current cost data for an agent\n *\n * @param agentId - Agent ID\n * @returns Current cost\n */\nexport function getAgentCost(agentId: string): number {\n return costData.perAgent.get(agentId) || 0;\n}\n\n/**\n * Get current cost data for an issue\n *\n * @param issueId - Issue ID\n * @returns Current cost\n */\nexport function getIssueCost(issueId: string): number {\n return costData.perIssue.get(issueId) || 0;\n}\n\n/**\n * Get current daily total cost\n *\n * @returns Current daily total\n */\nexport function getDailyTotal(): number {\n checkDailyReset();\n return costData.dailyTotal;\n}\n\n/**\n * Get cost summary\n *\n * @returns Cost summary with top spenders\n */\nexport function getCostSummary(): {\n dailyTotal: number;\n topAgents: Array<{ agentId: string; cost: number }>;\n topIssues: Array<{ issueId: string; cost: number }>;\n} {\n checkDailyReset();\n\n // Sort agents by cost\n const topAgents = Array.from(costData.perAgent.entries())\n .map(([agentId, cost]) => ({ agentId, cost }))\n .sort((a, b) => b.cost - a.cost)\n .slice(0, 10);\n\n // Sort issues by cost\n const topIssues = Array.from(costData.perIssue.entries())\n .map(([issueId, cost]) => ({ issueId, cost }))\n .sort((a, b) => b.cost - a.cost)\n .slice(0, 10);\n\n return {\n dailyTotal: costData.dailyTotal,\n topAgents,\n topIssues,\n };\n}\n\n/**\n * Reset cost tracking (for testing)\n */\nexport function resetCostTrackingSync(): void {\n costData = {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: getTodayDate(),\n };\n saveCostData(costData);\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect variants for cost-monitor I/O. The sync variants remain in\n// place because the module-level `costData` cache is populated synchronously at\n// import time; new callers (route handlers, services in the dashboard server)\n// can use these to avoid blocking the event loop on cost ledger writes.\n\n/** Persist the in-memory cost ledger to disk using fs/promises. */\nconst saveCostDataAsync = (data: CostData): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const dir = dirname(COST_DATA_FILE);\n yield* Effect.tryPromise({\n try: () => mkdir(dir, { recursive: true }),\n catch: (cause) => new FsError({ path: dir, operation: 'mkdir', cause }),\n });\n\n const persisted: CostDataPersisted = {\n perAgent: Object.fromEntries(data.perAgent),\n perIssue: Object.fromEntries(data.perIssue),\n dailyTotal: data.dailyTotal,\n lastResetDate: data.lastResetDate,\n };\n\n const tempFile = `${COST_DATA_FILE}.tmp`;\n yield* Effect.tryPromise({\n try: () => writeFile(tempFile, JSON.stringify(persisted, null, 2), 'utf-8'),\n catch: (cause) => new FsError({ path: tempFile, operation: 'writeFile', cause }),\n });\n\n yield* Effect.tryPromise({\n try: () => rename(tempFile, COST_DATA_FILE),\n catch: (cause) => new FsError({ path: COST_DATA_FILE, operation: 'rename', cause }),\n });\n\n // Best-effort temp cleanup if rename was a no-op on a foreign FS.\n yield* Effect.tryPromise({\n try: () => unlink(tempFile),\n catch: (cause) => new FsError({ path: tempFile, operation: 'unlink', cause }),\n }).pipe(Effect.catch(() => Effect.succeed(undefined)));\n });\n\n/** Effect variant of `recordCost`. */\nexport const recordCost = (\n agentId: string,\n cost: number,\n issueId?: string,\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const today = getTodayDate();\n if (costData.lastResetDate !== today) {\n costData.dailyTotal = 0;\n costData.lastResetDate = today;\n console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);\n }\n\n const currentAgentCost = costData.perAgent.get(agentId) || 0;\n costData.perAgent.set(agentId, currentAgentCost + cost);\n\n if (issueId) {\n const currentIssueCost = costData.perIssue.get(issueId) || 0;\n costData.perIssue.set(issueId, currentIssueCost + cost);\n }\n\n costData.dailyTotal += cost;\n\n yield* saveCostDataAsync(costData);\n });\n\n/** Effect variant of `resetCostTracking`. */\nexport const resetCostTracking = (): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n costData = {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: getTodayDate(),\n };\n yield* saveCostDataAsync(costData);\n });\n\n// Re-export FsError so callers don't need to import it from the shared module.\nexport { FsError } from '../errors.js';\n// Silence \"unused\" warnings for the async fs imports we may use in future\n// extensions of this module.\nvoid readFile;\n","import { Effect } from 'effect';\nimport { getAgentStateSync, type AgentState } from '../agents.js';\nimport { CostDoorLive, CostWriter } from '../overdeck/cost.js';\n\ntype RunningAgent = AgentState & { tmuxActive: boolean };\n\nasync function reconcileSourceForRunningAgents(\n runningAgents: readonly RunningAgent[],\n source: 'ohmypi' | 'codex',\n): Promise<void> {\n if (!runningAgents.some((agent) => getAgentStateSync(agent.id)?.harness === source)) return;\n\n try {\n await Effect.runPromise(\n CostWriter.use((writer) => writer.reconcile({ source })).pipe(\n Effect.provide(CostDoorLive),\n ),\n );\n } catch (error) {\n console.warn(`[cloister] ${source} cost reconcile failed:`, error instanceof Error ? error.message : String(error));\n }\n}\n\nexport async function reconcilePiCostEventsForRunningAgents(runningAgents: readonly RunningAgent[]): Promise<void> {\n await reconcileSourceForRunningAgents(runningAgents, 'ohmypi');\n await reconcileSourceForRunningAgents(runningAgents, 'codex');\n}\n","/**\n * Session Rotation for Specialists\n *\n * Manages context rotation for long-running specialist agents (like merge-agent)\n * when token usage exceeds thresholds. Preserves essential memory while clearing\n * accumulated context.\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { getAgentStateSync } from '../agents.js';\nimport type { SpecialistAgentName } from './specialists.js';\nimport { getTmuxSessionName } from './specialists.js';\nimport { killSession } from '../tmux.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Token threshold for triggering session rotation\n */\nexport const SESSION_ROTATION_THRESHOLD = 100_000;\n\n/**\n * Tiered memory configuration\n */\nexport interface MemoryTiers {\n recent_summary: number; // Most recent N merges with hash + message only\n recent_detailed: number; // Next N merges with more detail\n recent_full: number; // Last N merges with full diffs\n}\n\n/**\n * Default memory tiers for merge-agent\n */\nexport const DEFAULT_MEMORY_TIERS: MemoryTiers = {\n recent_summary: 100, // Last 100 merges: hash + message\n recent_detailed: 50, // Last 50 merges: more details\n recent_full: 20, // Last 20 merges: full diffs\n};\n\n/**\n * Merge record for memory file\n *\n * Note: Only core fields (hash, message, author, date, branch) are stored.\n * Files changed and diffs are computed on-demand when building memory\n * to avoid storing large amounts of redundant data.\n */\ninterface MergeRecord {\n hash: string;\n message: string;\n author?: string;\n date?: string;\n branch?: string;\n}\n\n/**\n * Session rotation result\n */\nexport interface SessionRotationResult {\n success: boolean;\n oldSessionId: string;\n newSessionId?: string;\n memoryFile?: string;\n error?: string;\n}\n\n/**\n * Check if an agent needs session rotation\n *\n * @param agentId - Agent ID\n * @returns True if session should be rotated\n */\nexport function needsSessionRotation(agentId: string): boolean {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) return false;\n\n const tokenUsage = runtime.getTokenUsage(agentId);\n if (!tokenUsage) return false;\n\n const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;\n return totalTokens >= SESSION_ROTATION_THRESHOLD;\n}async function buildMergeAgentMemoryPromise(\n workingDir: string,\n tiers: MemoryTiers = DEFAULT_MEMORY_TIERS\n): Promise<string> {\n const merges: MergeRecord[] = [];\n\n try {\n // Get recent merge commits\n const totalMerges = Math.max(tiers.recent_summary, tiers.recent_detailed, tiers.recent_full);\n const { stdout: gitLog } = await execAsync(\n `git log --merges --format=\"%H|%s|%an|%ad|%D\" -n ${totalMerges}`,\n { cwd: workingDir, encoding: 'utf-8' }\n );\n\n const lines = gitLog.trim().split('\\n').filter(l => l);\n for (const line of lines) {\n const [hash, message, author, date, refs] = line.split('|');\n const branch = refs\n ? refs.split(',').find(r => r.includes('feature/') || r.includes('HEAD'))\n : undefined;\n\n merges.push({ hash, message, author, date, branch });\n }\n } catch (error) {\n console.error('Failed to get merge history:', error);\n return 'No merge history available.\\n';\n }\n\n // Build memory content with tiers\n let memory = '# Merge-Agent Session Memory\\n\\n';\n memory += `This session was rotated due to context limits. Below is a summary of recent merge activity.\\n\\n`;\n\n // Tier 3: Full diffs for most recent merges\n memory += `## Recent Merges (Full Detail)\\n\\n`;\n memory += `Last ${tiers.recent_full} merges with full diffs:\\n\\n`;\n\n for (let i = 0; i < Math.min(tiers.recent_full, merges.length); i++) {\n const merge = merges[i];\n memory += `### ${merge.hash.substring(0, 8)} - ${merge.message}\\n`;\n memory += `- Author: ${merge.author}\\n`;\n memory += `- Date: ${merge.date}\\n`;\n if (merge.branch) memory += `- Branch: ${merge.branch}\\n`;\n\n try {\n // Get files changed\n const { stdout: filesOutput } = await execAsync(`git show --name-only --format= ${merge.hash}`, {\n cwd: workingDir,\n encoding: 'utf-8',\n });\n const files = filesOutput.trim().split('\\n').filter(f => f);\n\n memory += `- Files changed: ${files.length}\\n`;\n\n // Get diff (limited to avoid huge memory files)\n const { stdout: diff } = await execAsync(`git show ${merge.hash} --stat`, {\n cwd: workingDir,\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024,\n });\n\n memory += `\\n\\`\\`\\`diff\\n${diff.substring(0, 5000)}\\n\\`\\`\\`\\n\\n`;\n } catch (error) {\n memory += `- (Error getting details)\\n\\n`;\n }\n }\n\n // Tier 2: Detailed info for next N merges\n if (merges.length > tiers.recent_full) {\n memory += `## Additional Recent Merges (Summary)\\n\\n`;\n memory += `Merges ${tiers.recent_full + 1} to ${Math.min(tiers.recent_detailed, merges.length)}:\\n\\n`;\n\n for (\n let i = tiers.recent_full;\n i < Math.min(tiers.recent_detailed, merges.length);\n i++\n ) {\n const merge = merges[i];\n memory += `- \\`${merge.hash.substring(0, 8)}\\` ${merge.message} (${merge.author}, ${merge.date})\\n`;\n }\n memory += '\\n';\n }\n\n // Tier 1: Hash + message for oldest merges\n if (merges.length > tiers.recent_detailed) {\n memory += `## Older Merges (Hash + Message Only)\\n\\n`;\n memory += `Merges ${tiers.recent_detailed + 1} to ${merges.length}:\\n\\n`;\n\n for (let i = tiers.recent_detailed; i < merges.length; i++) {\n const merge = merges[i];\n memory += `- \\`${merge.hash.substring(0, 8)}\\` ${merge.message}\\n`;\n }\n memory += '\\n';\n }\n\n return memory;\n}async function rotateSpecialistSessionPromise(\n specialistName: SpecialistAgentName,\n workingDir?: string\n): Promise<SessionRotationResult> {\n const agentId = `specialist-${specialistName}`;\n const runtime = getRuntimeForAgent(agentId);\n\n if (!runtime) {\n return {\n success: false,\n oldSessionId: '',\n error: `No runtime found for ${agentId}`,\n };\n }\n\n const agentState = getAgentStateSync(agentId);\n if (!agentState?.sessionId) {\n return {\n success: false,\n oldSessionId: '',\n error: `No session ID found for ${agentId}`,\n };\n }\n\n const oldSessionId = agentState.sessionId;\n\n try {\n // Build memory file (for merge-agent, extract git history)\n let memoryContent = '';\n let memoryFile: string | undefined;\n\n if (specialistName === 'merge-agent' && workingDir) {\n memoryContent = await Effect.runPromise(buildMergeAgentMemory(workingDir));\n memoryFile = join(OVERDECK_HOME, `merge-agent-memory-${Date.now()}.md`);\n writeFileSync(memoryFile, memoryContent);\n console.log(`Built memory file: ${memoryFile}`);\n }\n\n // Kill current session\n const tmuxSession = getTmuxSessionName(specialistName);\n try {\n await Effect.runPromise(killSession(tmuxSession));\n console.log(`Killed session: ${tmuxSession}`);\n } catch (error) {\n // Session might already be dead\n console.log(`Session ${tmuxSession} not found or already killed`);\n }\n\n // Start fresh session with memory\n const prompt = memoryContent\n ? `You are resuming from a rotated session. Here's your memory:\\n\\n${memoryContent}\\n\\nContinue from where you left off.`\n : 'Session rotated. Continue from where you left off.';\n\n const newAgent = await runtime.spawnAgent({\n agentId,\n workspace: agentState.workspace,\n prompt,\n runtime: runtime.name,\n });\n\n console.log(`Started fresh session: ${newAgent.sessionId.substring(0, 8)}`);\n\n return {\n success: true,\n oldSessionId,\n newSessionId: newAgent.sessionId,\n memoryFile,\n };\n } catch (error: unknown) {\n return {\n success: false,\n oldSessionId,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}async function checkAndRotateIfNeededPromise(\n specialistName: SpecialistAgentName,\n workingDir?: string\n): Promise<SessionRotationResult | null> {\n const agentId = `specialist-${specialistName}`;\n\n if (!needsSessionRotation(agentId)) {\n return null;\n }\n\n console.log(`🔔 Session rotation needed for ${specialistName}`);\n return (await Effect.runPromise(rotateSpecialistSession(specialistName, workingDir)));\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n\n/**\n * Effect-typed variant of {@link buildMergeAgentMemory}. The underlying\n * function swallows git errors and returns a fallback string, so this Effect\n * never fails.\n */\nexport function buildMergeAgentMemory(\n workingDir: string,\n tiers: MemoryTiers = DEFAULT_MEMORY_TIERS,\n): Effect.Effect<string> {\n return Effect.promise(() => buildMergeAgentMemoryPromise(workingDir, tiers));\n}\n\n/**\n * Effect-typed variant of {@link rotateSpecialistSession}. Never fails — the\n * Promise version returns `{ success: false, error }` instead of throwing.\n */\nexport function rotateSpecialistSession(\n specialistName: SpecialistAgentName,\n workingDir?: string,\n): Effect.Effect<SessionRotationResult> {\n return Effect.promise(() => rotateSpecialistSessionPromise(specialistName, workingDir));\n}\n\n/**\n * Effect-typed variant of {@link checkAndRotateIfNeeded}. Resolves to `null`\n * when no rotation was needed (mirrors the Promise contract).\n */\nexport function checkAndRotateIfNeeded(\n specialistName: SpecialistAgentName,\n workingDir?: string,\n): Effect.Effect<SessionRotationResult | null> {\n return Effect.promise(() => checkAndRotateIfNeededPromise(specialistName, workingDir));\n}\n","/** Cloister service: core monitoring for all running agents. */\nimport { createHash } from 'crypto';\nimport type { AgentRuntimeSync, HealthState } from '../runtimes/types.js';\nimport { isContextOverflowTail, CONTEXT_OVERFLOW_TAIL_LINES } from '../context-overflow.js';\nimport type { CloisterConfig } from './config.js';\nimport type { AgentHealth, HealthSummary } from './health.js';\nimport type { DomainEvent } from '@overdeck/contracts';\nimport { loadCloisterConfigSync } from './config.js';\nimport {\n getAgentHealth,\n getMultipleAgentHealth,\n generateHealthSummary,\n getAgentsToPoke,\n getAgentsToKill,\n getAgentsNeedingAttention,\n} from './health.js';\nimport { writeHealthEvent, getLatestHealthEvent } from '../overdeck/health-events.js';\n// PAN-378: initializeEnabledSpecialists removed — per-project ephemeral specialists\n// are spawned on-demand, no global initialization needed.\nimport { getGlobalRegistry, getRuntimeForAgent } from '../runtimes/index.js';\nimport { listRunningAgentsSync, getAgentStateSync, getAgentState, getAgentRuntimeStateSync, saveAgentRuntimeState } from '../agents.js';\nimport type { Role } from '../agents.js';\nimport { resolveProjectFromIssueSync } from '../projects.js';\nimport {\n isCloisterSpawnsPausedSync,\n setCloisterSpawnsPausedSync,\n setDeaconGloballyPaused,\n setFlywheelGloballyPaused,\n} from '../overdeck/control-settings.js';\nimport { checkAllTriggers, type TriggerDetection } from './triggers.js';\nimport { performHandoff, type HandoffResult } from './handoff.js';\nimport { logHandoffEventSync, createHandoffEvent } from './handoff-logger.js';\nimport {\n checkAgentForViolations,\n sendNudge,\n resolveViolationSync,\n hasExceededMaxNudges,\n clearOldViolationsSync,\n type FPPViolation,\n} from './fpp-violations.js';\nimport { checkCostLimits, getCostSummary, type CostAlert } from './cost-monitor.js';\nimport { reconcilePiCostEventsForRunningAgents } from './pi-cost-reconciler.js';\nimport { checkAndRotateIfNeeded, type SessionRotationResult } from './session-rotation.js';\nimport {\n startDeacon,\n stopDeacon,\n isDeaconRunning,\n getDeaconStatus,\n assessDeaconPatrolFreshness,\n getLastPatrolResult,\n getDeaconLogs,\n runPatrol,\n type PatrolResult,\n type DeaconLogEntry,\n} from './deacon.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync, renameSync, statSync } from 'fs';\nimport { rm } from 'fs/promises';\nimport { join } from 'path';\nimport { AGENTS_DIR } from '../paths.js';\nimport { loadReviewStatuses, setReviewStatusSync } from '../review-status.js';\nimport { isRoleTerminal, type AdvancingRole } from './reap-terminal-sessions.js';\nimport { sessionExists, killSession } from '../tmux.js';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nconst execAsync = promisify(exec);\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { isIssueClosed } from './issue-closed.js';\nimport { shouldSkipDispatchAsMerged } from './merge-verification.js';\nexport { spawnFlywheel, pauseFlywheel, resumeFlywheel } from './flywheel.js';\n\n// State file for cross-process communication\nconst CLOISTER_STATE_FILE = join(OVERDECK_HOME, 'cloister.state');\nconst LEGACY_SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\n\nasync function cleanupLegacySpecialistsDirectory(): Promise<void> {\n await rm(LEGACY_SPECIALISTS_DIR, { recursive: true, force: true });\n}\n\n/** Return issues orphaned in reviewStatus='reviewing' with no active reviewer. */\nexport function identifyOrphanedReviewingIssues(\n statuses: Record<string, { reviewStatus: string; history?: Array<{ type: string; status: string }> }>,\n activeReviewIssues: Set<string>,\n): string[] {\n const orphaned: string[] = [];\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.reviewStatus !== 'reviewing') continue;\n const hasPassedReview = status.history?.some(\n (h) => h.type === 'review' && h.status === 'passed',\n );\n if (hasPassedReview) continue;\n if (activeReviewIssues.has(issueId.toUpperCase())) continue;\n orphaned.push(issueId);\n }\n return orphaned;\n}\n\nexport function parseSpecialistAgentSession(name: string): {\n projectKey: string;\n specialistType: 'review-agent' | 'test-agent' | 'merge-agent';\n issueId?: string;\n} | null {\n const issueScoped = name.match(/^specialist-(.+)-([A-Z]+-\\d+)-(review-agent|test-agent|merge-agent)$/);\n if (issueScoped) {\n return {\n projectKey: issueScoped[1],\n issueId: issueScoped[2],\n specialistType: issueScoped[3] as 'review-agent' | 'test-agent' | 'merge-agent',\n };\n }\n\n const legacy = name.match(/^specialist-(.+)-(review-agent|test-agent|merge-agent)$/);\n if (legacy) {\n return {\n projectKey: legacy[1],\n specialistType: legacy[2] as 'review-agent' | 'test-agent' | 'merge-agent',\n };\n }\n\n return null;\n}\n\nexport type ReactiveIssueState =\n | 'todo'\n | 'open'\n | 'in_planning'\n | 'in_progress'\n | 'in_review'\n | 'testing'\n | 'shipping'\n | 'closed'\n | 'canceled';\n\nexport interface CloisterDomainEventLike {\n type: string;\n payload?: unknown;\n}\n\ninterface CloisterEventStore {\n append(event: Omit<DomainEvent, 'sequence'>): number;\n subscribe?: (fn: (event: CloisterDomainEventLike) => void) => () => void;\n}\n\nlet cloisterEventStoreProvider: (() => CloisterEventStore) | null = null;\n\nexport function setCloisterEventStoreProvider(provider: (() => CloisterEventStore) | null): void {\n cloisterEventStoreProvider = provider;\n}\n\nconst ROLE_RUN_STATES: Record<ReactiveIssueState, Role | null> = {\n todo: null,\n open: null,\n in_planning: 'plan',\n in_progress: 'work',\n in_review: 'review',\n testing: 'test',\n shipping: null,\n closed: null,\n canceled: null,\n};\n\n/**\n * Map issue lifecycle state to the role that should own that state.\n */\nexport function stateToRole(state: string): Role | null {\n const normalized = state.toLowerCase().replace(/[ -]/g, '_') as ReactiveIssueState;\n return ROLE_RUN_STATES[normalized] ?? null;\n}\n\nfunction normalizeIssueId(issueId: string): string {\n return issueId.trim().toUpperCase();\n}\n\nfunction roleFromAgentId(agentId: string, issueId: string): Role | null {\n const base = `agent-${issueId.toLowerCase()}`;\n if (agentId === base) return 'work';\n const role = agentId.slice(base.length + 1);\n return ['plan', 'review', 'test'].includes(role) ? role as Role : null;\n}\n\n/**\n * Grace window for `starting` states without a tmux session yet. The\n * start-planning route deliberately writes state.json BEFORE the lifecycle\n * transition (PAN-1048) so this guard can see the planner coming — but the\n * tmux session is only created after the transition. Treating any\n * starting-without-session as dead (the original S1 unstick heuristic)\n * re-opened the PAN-1048 race and spawned a duplicate `agent-<issue>-plan`\n * twin on EVERY `pan plan` (PAN-2159, 100% repro). A fresh `starting` state\n * is a startup in progress; only a stale one is a crashed spawn to unstick.\n */\nconst STARTING_WITHOUT_SESSION_GRACE_MS = 120_000;\n\nfunction isFreshStarting(state: { startedAt?: string }): boolean {\n const startedAt = Date.parse(state.startedAt ?? '');\n return Number.isFinite(startedAt) && Date.now() - startedAt < STARTING_WITHOUT_SESSION_GRACE_MS;\n}\n\nasync function activeRoleRunExists(issueId: string, role: Role, workspacePath?: string): Promise<boolean> {\n const issueLower = issueId.toLowerCase();\n\n // C1: For 'plan', also check the legacy planning-pan-X session format\n // alongside the canonical agent-pan-X-plan format. The start-planning route\n // writes to planning-pan-X while spawnRun uses agent-pan-X-plan.\n if (role === 'plan') {\n const legacyId = `planning-${issueLower}`;\n const legacyState = await Effect.runPromise(getAgentState(legacyId));\n if (legacyState?.role === 'plan' && legacyState.status !== 'stopped' && legacyState.status !== 'error') {\n // S1 (age-aware): only a STALE 'starting' state with no live tmux\n // session is a crashed spawn; a fresh one is mid-startup (PAN-2159).\n if (legacyState.status === 'starting' && !(await Effect.runPromise(sessionExists(legacyId)))) {\n return isFreshStarting(legacyState);\n }\n return true;\n }\n }\n\n const candidateId = role === 'work'\n ? `agent-${issueLower}`\n : `agent-${issueLower}-${role}`;\n\n const state = await Effect.runPromise(getAgentState(candidateId));\n if (!state) return false;\n\n const stateRole = state.role ?? roleFromAgentId(candidateId, issueId);\n\n // S1 (age-aware): only a STALE 'starting' state with no live tmux session\n // is a crashed spawn; a fresh one is mid-startup (PAN-2159).\n if (stateRole === role && state.status === 'starting' && !(await Effect.runPromise(sessionExists(candidateId)))) {\n return isFreshStarting(state);\n }\n\n const aliveByStatus = stateRole === role && state.status !== 'stopped' && state.status !== 'error';\n if (!aliveByStatus) return false;\n\n // Zombie detection: an agent that finished its work but never exited keeps\n // status:'running' forever, which would block every future re-dispatch for\n // this role (the ship/test stall bug). When we know the workspace and the\n // run stamped a roleRunHead, compare it against the current workspace HEAD —\n // a HEAD that has advanced past the marker means this session ran against\n // stale code and must not be treated as the active run for the new HEAD.\n if (workspacePath && state.roleRunHead) {\n try {\n const { stdout } = await execAsync('git rev-parse --short=8 HEAD', { cwd: workspacePath });\n const currentHead = stdout.trim();\n if (currentHead && currentHead !== state.roleRunHead) {\n console.log(\n `[cloister] ${issueId}: ${role} session ${candidateId} is stale `\n + `(ran against ${state.roleRunHead}, HEAD is now ${currentHead}) — not active`,\n );\n return false;\n }\n } catch { /* non-fatal — fall through to the status-only result */ }\n }\n\n return true;\n}\n\nfunction buildReactiveRolePrompt(issueId: string, state: string, role: Role): string {\n return `${role.toUpperCase()} TASK for ${issueId}:\n\nThe issue lifecycle transitioned to ${state}. Run the ${role} role for this issue.\n\nRequired steps:\n1. Work only in the workspace configured for ${issueId}.\n2. Read .pan/continue.json, .pan/spec.vbrief.json, project instructions, and issue context.\n3. Follow the boundaries and success criteria in roles/${role}.md exactly.\n4. Report the role-specific terminal status when done.`;\n}\n\n/**\n * Resolve the workspace path for an issue from agent state, then fall back\n * to the canonical `<projectPath>/workspaces/feature-<issueLower>` layout.\n * Mirrors the resolution used by startup recovery (service.ts:583-609) so\n * the reactive scheduler dispatches review/test wrappers with the same\n * workspace contract those wrappers receive on the manual code path.\n */\nasync function resolveWorkspaceForIssue(issueId: string): Promise<string | null> {\n const issueLower = issueId.toLowerCase();\n const agentState = await Effect.runPromise(getAgentState(`agent-${issueLower}`));\n if (agentState?.workspace) return agentState.workspace;\n const resolved = resolveProjectFromIssueSync(issueId);\n if (!resolved) return null;\n return `${resolved.projectPath}/workspaces/feature-${issueLower}`;\n}async function onIssueStateChangePromise(issueId: string, newState: string): Promise<void> {\n const normalizedIssueId = normalizeIssueId(issueId);\n const role = stateToRole(newState);\n if (!role) {\n console.log(`[cloister] ${normalizedIssueId}: no role for issue state '${newState}'`);\n return;\n }\n\n if (await isIssueClosed(normalizedIssueId)) {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — issue is closed`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // PAN-1746: a merged issue is terminal — never re-dispatch an advancing role\n // for work that already landed. Boot reconciliation replays issue-state-change\n // events on restart, and a long-merged issue still carrying its lifecycle\n // state (e.g. `verifying-on-main`) would otherwise re-trigger a ship dispatch\n // for a branch that merged weeks ago. Mirror the isIssueClosed gate above:\n // mergeStatus='merged' is the same terminal signal closed-state is.\n const { getReviewStatusSync } = await import('../review-status.js');\n if (getReviewStatusSync(normalizedIssueId)?.mergeStatus === 'merged') {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — merge already landed (merge_status='merged' is terminal)`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // PAN-2420: GitHub-authoritative guard. Even when merge_status is not yet\n // 'merged' (e.g. a permission failure left it as 'failed'), do not respawn\n // advancing roles against a PR that GitHub already reports merged.\n const mergedGuard = await shouldSkipDispatchAsMerged(normalizedIssueId);\n if (mergedGuard.skip) {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — ${mergedGuard.reason}`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // Resolve the workspace up front so activeRoleRunExists can probe the\n // workspace HEAD for stale-session (zombie) detection.\n const workspace = await resolveWorkspaceForIssue(normalizedIssueId);\n\n if (await activeRoleRunExists(normalizedIssueId, role, workspace ?? undefined)) {\n const message = `${normalizedIssueId}: ${role} role already active; skipping lifecycle spawn`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // activeRoleRunExists returned false. If a tmux session for this role still\n // physically exists, it's a zombie (agent finished work but never exited,\n // and the workspace HEAD has since advanced). Kill it before re-dispatch so\n // the fresh run gets a clean session name instead of colliding with the\n // dead one.\n const issueLower = normalizedIssueId.toLowerCase();\n const roleSessionId = role === 'work' ? `agent-${issueLower}` : `agent-${issueLower}-${role}`;\n if (await Effect.runPromise(sessionExists(roleSessionId))) {\n const message = `${normalizedIssueId}: killing stale ${role} session ${roleSessionId} before re-dispatch`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n try {\n await Effect.runPromise(killSession(roleSessionId));\n } catch (err) {\n console.error(`[cloister] failed to kill stale session ${roleSessionId}:`, err instanceof Error ? err.message : String(err));\n }\n }\n\n try {\n if (role === 'review') {\n if (!workspace) {\n const failure = `${normalizedIssueId}: cannot dispatch review role — no workspace or project resolved`;\n console.error(`[cloister] ${failure}`);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: failure, issueId: normalizedIssueId });\n return;\n }\n const branch = `feature/${normalizedIssueId.toLowerCase()}`;\n const { spawnReviewRoleForIssue } = await import('./review-agent.js');\n const result = await Effect.runPromise(spawnReviewRoleForIssue({ issueId: normalizedIssueId, workspace, branch }));\n const message = `${normalizedIssueId}: review role dispatched from lifecycle state '${newState}' (${result.message})`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: result.success ? 'info' : 'error', message, issueId: normalizedIssueId });\n return;\n }\n\n if (role === 'test') {\n const branch = `feature/${normalizedIssueId.toLowerCase()}`;\n const { dispatchTestAgentAndNotify } = await import('./test-agent-queue.js');\n await Effect.runPromise(dispatchTestAgentAndNotify(normalizedIssueId, workspace ?? undefined, branch));\n const message = `${normalizedIssueId}: test role dispatched from lifecycle state '${newState}'`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n const { spawnRun } = await import('../agents.js');\n const run = await spawnRun(normalizedIssueId, role, {\n prompt: buildReactiveRolePrompt(normalizedIssueId, newState, role),\n });\n const message = `${normalizedIssueId}: ${role} role started from lifecycle state '${newState}' as ${run.id}`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('already running')) {\n const skipMessage = `${normalizedIssueId}: ${role} role already running; skipping lifecycle spawn`;\n console.log(`[cloister] ${skipMessage}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: skipMessage, issueId: normalizedIssueId });\n return;\n }\n console.error(`[cloister] Failed to start ${role} role for ${normalizedIssueId}:`, error);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: `${normalizedIssueId}: failed to start ${role} role: ${message}`, issueId: normalizedIssueId });\n }\n}\n\nfunction payloadRecord(event: CloisterDomainEventLike): Record<string, unknown> {\n return event.payload && typeof event.payload === 'object' ? event.payload as Record<string, unknown> : {};\n}\n\nexport function issueStateChangeFromDomainEvent(event: CloisterDomainEventLike): { issueId: string; state: string } | null {\n const payload = payloadRecord(event);\n const issueId = typeof payload.issueId === 'string' ? payload.issueId : null;\n if (!issueId) return null;\n\n switch (event.type) {\n case 'issue.transitioned':\n return typeof payload.state === 'string' ? { issueId, state: payload.state } : null;\n case 'issue.statusChanged':\n return typeof payload.canonicalStatus === 'string' ? { issueId, state: payload.canonicalStatus } : null;\n case 'issue.closed':\n return { issueId, state: 'closed' };\n case 'agent.completed': {\n // PAN-1048 review feedback 003: agent.completed is emitted by every\n // role's lifecycle (work, review, test, ship). Map it to in_review only\n // when the work role completes — letting other roles land here would\n // ricochet back into review the moment a review or test role finished.\n const role = typeof payload.role === 'string' ? payload.role : undefined;\n if (role === undefined || role === 'work') {\n return { issueId, state: 'in_review' };\n }\n return null;\n }\n case 'work.completed':\n return { issueId, state: 'in_review' };\n case 'review.approved':\n return { issueId, state: 'testing' };\n case 'test.passed':\n return { issueId, state: 'shipping' };\n default:\n return null;\n }\n}\n\nasync function handleCloisterDomainEventPromise(event: CloisterDomainEventLike): Promise<void> {\n // PAN-1908: reactive agent liveness — deacon handles agent.stopped and\n // agent.heartbeat_dead events instead of scanning agent directories.\n if (event.type === 'agent.stopped') {\n const payload = event.payload as { agentId?: string } | undefined;\n const agentId = payload?.agentId;\n if (agentId) {\n const { handleAgentStoppedEvent, handleAgentStoppedForOrphanReviewerSessions } = await import('./deacon.js');\n const { handleAgentLifecycleEventForIdleStack } = await import('./idle-stack-reaper.js');\n handleAgentLifecycleEventForIdleStack(agentId);\n const slotMatch = /^agent-(.+)-slot-\\d+$/.exec(agentId);\n await Promise.all([\n handleAgentStoppedEvent(agentId),\n handleAgentStoppedForOrphanReviewerSessions(agentId),\n slotMatch ? (await import('./deacon-swarm.js')).coordinateSwarmSlots({ issueId: slotMatch[1]!.toUpperCase() }) : Promise.resolve([]),\n ]);\n }\n return;\n }\n if (event.type === 'agent.started') {\n const agentId = (event.payload as { agentId?: string } | undefined)?.agentId;\n if (agentId) (await import('./idle-stack-reaper.js')).handleAgentLifecycleEventForIdleStack(agentId);\n return;\n }\n if (event.type === 'agent.heartbeat_dead') {\n const payload = event.payload as { agentId?: string } | undefined;\n const agentId = payload?.agentId;\n if (agentId) {\n const { handleAgentHeartbeatDeadEvent } = await import('./deacon.js');\n await handleAgentHeartbeatDeadEvent(agentId, 'event');\n }\n return;\n }\n\n // PAN-1908: reactive review-status handlers — deacon handles review lifecycle\n // events instead of scanning directories / the review-status DB.\n if (event.type === 'review.coordinator.died') {\n const payload = event.payload as { issueId?: string; sessionName?: string; reason?: string } | undefined;\n const issueId = payload?.issueId;\n if (issueId) {\n const { handleReviewCoordinatorDied } = await import('./deacon.js');\n await handleReviewCoordinatorDied(issueId, payload?.sessionName ?? '', payload?.reason ?? '');\n }\n return;\n }\n if (event.type === 'work.completed') {\n const payload = event.payload as { issueId?: string } | undefined;\n const issueId = payload?.issueId;\n if (issueId) {\n const { handleWorkCompleted } = await import('./deacon.js');\n await handleWorkCompleted(issueId);\n }\n // Fall through to onIssueStateChange for in_review dispatch.\n }\n\n // PAN-1908: reactive reconcilers — deacon handles issue.statusChanged events\n // for closed issues and proposed specs instead of patrol scans.\n if (event.type === 'issue.statusChanged') {\n const payload = event.payload as { issueId?: string; status?: string; canonicalStatus?: string } | undefined;\n const issueId = payload?.issueId;\n const canonicalStatus = payload?.canonicalStatus?.toLowerCase();\n const status = payload?.status?.toLowerCase();\n if (issueId) {\n if (canonicalStatus === 'closed' || status === 'closed') {\n const { handleIssueStatusChangedClosed } = await import('./closed-issue-reaper.js');\n await handleIssueStatusChangedClosed(issueId);\n return;\n }\n if (canonicalStatus === 'todo' || status === 'planned' || status === 'todo') {\n const { handleOrphanProposedSpec } = await import('./orphan-proposed-reconciler.js');\n await handleOrphanProposedSpec(issueId);\n // Fall through to onIssueStateChange in case it drives role dispatch.\n }\n }\n }\n\n const change = issueStateChangeFromDomainEvent(event);\n if (!change) return;\n await Effect.runPromise(onIssueStateChange(change.issueId, change.state));\n}\n\n/**\n * Write Cloister running state to file for cross-process visibility\n */\nfunction writeStateFile(running: boolean, pid?: number): void {\n try {\n if (running) {\n writeFileSync(CLOISTER_STATE_FILE, JSON.stringify({\n running: true,\n pid: pid || process.pid,\n startedAt: new Date().toISOString(),\n }));\n } else {\n if (existsSync(CLOISTER_STATE_FILE)) {\n unlinkSync(CLOISTER_STATE_FILE);\n }\n }\n } catch (error) {\n // Non-fatal - state file is for convenience\n console.warn('Failed to write Cloister state file:', error);\n }\n}\n\n/**\n * Read Cloister running state from file\n */\nexport function readCloisterStateFile(): { running: boolean; pid?: number; startedAt?: string } {\n try {\n if (existsSync(CLOISTER_STATE_FILE)) {\n const data = JSON.parse(readFileSync(CLOISTER_STATE_FILE, 'utf-8'));\n // Verify the process is still running\n if (data.pid) {\n try {\n process.kill(data.pid, 0); // Signal 0 checks if process exists\n return data;\n } catch {\n // Process doesn't exist - clean up stale state file\n unlinkSync(CLOISTER_STATE_FILE);\n return { running: false };\n }\n }\n return data;\n }\n } catch {\n // State file doesn't exist or is corrupted\n }\n return { running: false };\n}\n\n/**\n * Cloister service status\n */\nexport interface CloisterStatus {\n running: boolean;\n lastCheck: Date | null;\n config: CloisterConfig;\n summary: HealthSummary;\n agentsNeedingAttention: string[];\n patrol: ReturnType<typeof assessDeaconPatrolFreshness> & {\n loopRunning: boolean;\n patrolIntervalMs: number;\n };\n}\n\n/**\n * Agent crash tracker for auto-restart\n */\ninterface AgentCrashTracker {\n agentId: string;\n crashCount: number;\n lastCrash: Date;\n nextRetryAt?: Date;\n gaveUp: boolean;\n}\n\n/**\n * Cloister service event\n */\nexport type CloisterEvent =\n | { type: 'started' }\n | { type: 'stopped' }\n | { type: 'health_check'; agentHealths: AgentHealth[] }\n | { type: 'agent_warning'; agentId: string; health: AgentHealth }\n | { type: 'agent_stuck'; agentId: string; health: AgentHealth }\n | { type: 'poked_agent'; agentId: string }\n | { type: 'killed_agent'; agentId: string }\n | { type: 'agent_crashed'; agentId: string; crashCount: number }\n | { type: 'agent_restarting'; agentId: string; crashCount: number; backoffSeconds: number }\n | { type: 'agent_restart_failed'; agentId: string; crashCount: number; error: string }\n | { type: 'agent_gave_up'; agentId: string; maxRetries: number }\n | { type: 'mass_death_detected'; deathCount: number; windowSeconds: number }\n | { type: 'spawn_paused'; reason: string }\n | { type: 'spawn_resumed' }\n | { type: 'fpp_violation_detected'; agentId: string; violation: FPPViolation }\n | { type: 'fpp_nudge_sent'; agentId: string; nudgeCount: number }\n | { type: 'fpp_max_nudges_exceeded'; agentId: string; violation: FPPViolation }\n | { type: 'cost_alert'; alert: CostAlert }\n | { type: 'session_rotated'; specialistName: string; result: SessionRotationResult }\n | { type: 'handoff_triggered'; agentId: string; trigger: TriggerDetection }\n | { type: 'handoff_completed'; agentId: string; result: HandoffResult }\n | { type: 'emergency_stop'; killedAgents: string[] }\n | { type: 'error'; error: Error };\n\n/**\n * Cloister service event listener\n */\nexport type CloisterEventListener = (event: CloisterEvent) => void;\n\n/**\n * One-shot convoy roles (PAN-1742). The review lenses + synthesis and the test\n * role are spawned fresh via `spawnRun` each cycle; they run once, write their\n * artifact, and exit. They never save a sessionId, so they cannot be resumed.\n */\nconst ONE_SHOT_ROLES: ReadonlySet<string> = new Set(['review', 'test']);\n\n/**\n * Decide whether a vanished tmux session should be treated as a crash worth\n * auto-restarting (PAN-1742). Returns a human-readable reason to SKIP the\n * crash/restart machinery, or `null` when the agent is a genuine restart\n * candidate.\n *\n * Two cases are not crashes:\n * - one-shot convoy roles, whose session-end is a normal completion; and\n * - any agent without a saved session, which `restartAgent` cannot resume\n * (it throws \"No session ID found\"), so counting a crash and scheduling a\n * restart is pure noise.\n *\n * Pure and exported so the rule is unit-tested independent of the service's\n * private health-check plumbing.\n */\nexport function nonRestartableReason(role: string, sessionId: string | undefined): string | null {\n if (ONE_SHOT_ROLES.has(role)) {\n return `(${role}) finished its one-shot run — completion, not a crash`;\n }\n if (!sessionId) {\n return 'ended with no resumable session — not auto-restartable';\n }\n return null;\n}\n\n/**\n * Cloister Service\n *\n * Monitors agent health and performs auto-actions.\n */\nexport class CloisterService {\n private running: boolean = false;\n private starting: boolean = false;\n private checkInterval: NodeJS.Timeout | null = null;\n private lastCheck: Date | null = null;\n private config: CloisterConfig;\n private listeners: CloisterEventListener[] = [];\n private previousStates: Map<string, HealthState> = new Map();\n private crashTrackers: Map<string, AgentCrashTracker> = new Map();\n private previousRunningAgents: Set<string> = new Set();\n private deathTimestamps: Date[] = []; // Rolling window of agent death times\n private spawnsPaused: boolean = false;\n private processedCompletions: Map<string, number> = new Map(); // Track completion marker retry counts (Infinity = done)\n private healthCheckCount: number = 0;\n private lastPokeTimestamps: Map<string, number> = new Map(); // agentId → last poke timestamp (ms)\n // PAN-2452 (idle-alive): progress fingerprint at last poke → consecutive\n // ineffective-poke count. A poke that changes neither the workspace HEAD nor\n // the pane content did nothing; escalating beats poking forever.\n private pokeProgress: Map<string, { fingerprint: string; ineffective: number }> = new Map();\n private domainEventUnsubscribe: (() => void) | null = null;\n private eventStore: CloisterEventStore | null = null;\n\n // ─── Status cache ────────────────────────────────────────────────────────────\n // getStatus() does sync file I/O + tmux calls for every agent. Cache for 3s\n // to eliminate blocking on high-frequency dashboard polls.\n private _statusCache: CloisterStatus | null = null;\n private _statusCacheAt = 0;\n private readonly STATUS_CACHE_TTL_MS = 3_000;\n\n constructor(config?: CloisterConfig) {\n this.config = config || loadCloisterConfigSync();\n }\n\n private getDashboardApiUrl(): string {\n // Cloister always runs in-process with the dashboard, so it must talk to\n // its own loopback — never to a public DASHBOARD_URL like https://overdeck.localhost,\n // which would round-trip through Traefik+TLS and fail validation from inside\n // Node (PAN-845). Use 127.0.0.1 explicitly to avoid the IPv6-first /etc/hosts\n // trap (PAN-841): undici-based fetch connects to [::1] and hangs because the\n // dashboard listens on the IPv4 wildcard.\n return `http://127.0.0.1:${process.env.API_PORT || process.env.PORT || '3011'}`;\n }\n\n /**\n * Start the Cloister service\n */\n async start(): Promise<void> {\n if (this.running || this.starting) {\n console.warn('Cloister is already running');\n return;\n }\n this.starting = true;\n\n console.log('🔔 Starting Cloister agent watchdog...');\n\n try {\n await cleanupLegacySpecialistsDirectory();\n console.log(' ✓ Removed legacy ~/.overdeck/specialists directory');\n } catch (error) {\n console.error(' ✗ Failed to remove legacy specialists directory:', error);\n }\n\n // PAN-493: Reset orphaned verificationStatus === 'running' states.\n // If Cloister dies mid-verification, the status is left stuck at 'running' and the\n // pipeline halts indefinitely. On startup, reset any such states to 'pending' so\n // verification reruns automatically. Verification is idempotent — this is always safe.\n let resetVerificationCount = 0;\n try {\n const statuses = loadReviewStatuses();\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.verificationStatus === 'running') {\n setReviewStatusSync(issueId, { verificationStatus: 'pending' });\n console.log(` ✓ Reset orphaned verification 'running' → 'pending' for ${issueId}`);\n resetVerificationCount++;\n }\n }\n if (resetVerificationCount > 0) {\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Reset ${resetVerificationCount} orphaned verification 'running' → 'pending' on startup` });\n }\n } catch (error) {\n console.error(' ✗ Failed to reset orphaned verification states:', error);\n }\n\n // PAN-511: Clear stale currentIssue from specialist agents that are not actually running.\n // If Cloister dies while a specialist is between tasks or mid-run, the specialist's\n // runtime.json may retain currentIssue and state='active' even though the process is dead.\n // spawnEphemeralSpecialist checks these fields to decide whether\n // to dispatch — a stale 'active' state permanently blocks new dispatches.\n // On startup, clear currentIssue and reset state from any specialist agent that is:\n // (a) idle — safe: idle means no active task, currentIssue is leftover\n // (b) active but tmux session no longer running — state is stale from a crash\n let clearedSpecialistCount = 0;\n try {\n if (existsSync(AGENTS_DIR)) {\n const { isRunning: isSpecialistRunning } = await import('./specialists.js');\n const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const parsed = parseSpecialistAgentSession(entry.name);\n if (!parsed) continue;\n const runtimeState = getAgentRuntimeStateSync(entry.name);\n if (!runtimeState?.currentIssue) continue;\n\n if (runtimeState.state === 'idle') {\n saveAgentRuntimeState(entry.name, { currentIssue: undefined });\n console.log(` ✓ Cleared stale currentIssue '${runtimeState.currentIssue}' from idle ${entry.name}`);\n clearedSpecialistCount++;\n } else if (runtimeState.state === 'active') {\n // Check if the process is actually alive — if not, the state is stale from a crash.\n // For issue-scoped specialists, check the exact tmux session instead of the legacy\n // project/type singleton lookup, which cannot represent PAN-754 session identity.\n const stillRunning = parsed.issueId\n ? await Effect.runPromise(sessionExists(entry.name))\n : await isSpecialistRunning(parsed.specialistType, parsed.projectKey);\n if (!stillRunning) {\n saveAgentRuntimeState(entry.name, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n console.log(` ✓ Cleared stale active state for crashed specialist ${entry.name} (was working on '${runtimeState.currentIssue}')`);\n clearedSpecialistCount++;\n }\n }\n }\n }\n if (clearedSpecialistCount > 0) {\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Cleared ${clearedSpecialistCount} stale specialist state(s) on startup` });\n }\n } catch (error) {\n console.error(' ✗ Failed to clear stale specialist states:', error);\n }\n\n // PAN-511: Startup recovery for orphaned reviewStatus='reviewing' issues.\n // If Cloister crashes after reviewStatus was set to 'reviewing' but before the specialist\n // completes, the issue is stuck. On startup, find such issues and re-dispatch directly.\n try {\n const reviewStatuses = loadReviewStatuses();\n const { resolveProjectFromIssueSync } = await import('../projects.js');\n const { getTmuxSessionName, getAllProjectSpecialistStatuses } = await import('./specialists.js');\n\n // Build set of issue IDs actively being reviewed by a running specialist\n const activeReviewIssues = new Set<string>();\n try {\n const projSpecs = await getAllProjectSpecialistStatuses();\n for (const ps of projSpecs) {\n if (ps.specialistType !== 'review-agent' || !ps.isRunning) continue;\n const rs = getAgentRuntimeStateSync(ps.tmuxSession);\n if (rs?.state === 'active' && rs.currentIssue) {\n activeReviewIssues.add(rs.currentIssue.toUpperCase());\n }\n }\n // Also check global review-agent session\n const globalSession = getTmuxSessionName('review-agent');\n const globalRs = getAgentRuntimeStateSync(globalSession);\n if (globalRs?.state === 'active' && globalRs.currentIssue) {\n activeReviewIssues.add(globalRs.currentIssue.toUpperCase());\n }\n\n // PAN-1048 R5: detect role-primitive review runs (agent-<id>-review).\n // Replaces the legacy getActiveParallelReviewIssues helper that scanned\n // tmux for dispatchParallelReview's coordinator session naming pattern.\n const { listRunningAgents } = await import('../agents.js');\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const role = agent.role ?? (agent.id.endsWith('-review') ? 'review' : null);\n if (role !== 'review') continue;\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (issueId) activeReviewIssues.add(issueId);\n }\n } catch {\n // Non-fatal: if we can't check active sessions, re-dispatch all orphaned\n }\n\n const orphanedReviewing = identifyOrphanedReviewingIssues(reviewStatuses, activeReviewIssues);\n\n if (orphanedReviewing.length > 0) {\n console.log(` ⚠ Found ${orphanedReviewing.length} issue(s) with orphaned reviewStatus='reviewing'`);\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Found ${orphanedReviewing.length} orphaned reviewStatus='reviewing' issue(s) on startup`, details: orphanedReviewing.join(', ') });\n\n for (const issueId of orphanedReviewing) {\n\n const agentId = `agent-${issueId.toLowerCase()}`;\n const agentState = getAgentStateSync(agentId);\n const workspace = agentState?.workspace;\n\n if (!workspace) {\n console.log(` ⚠ ${issueId}: orphaned reviewing but no workspace found — resetting to pending`);\n setReviewStatusSync(issueId, { reviewStatus: 'pending' });\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `${issueId} orphaned reviewing reset to pending — no workspace found`, issueId });\n continue;\n }\n\n const resolved = resolveProjectFromIssueSync(issueId);\n if (!resolved) {\n console.log(` ⚠ ${issueId}: orphaned reviewing but no project configured — resetting to pending`);\n setReviewStatusSync(issueId, { reviewStatus: 'pending' });\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `${issueId} orphaned reviewing reset to pending — no project configured`, issueId });\n continue;\n }\n\n const branch = `feature/${issueId.toLowerCase()}`;\n // PAN-1048 R4: startup recovery now spawns the review role primitive\n // (loads roles/review.md → Agent tool fans out to convoy reviewers)\n // instead of the legacy `pan review run` coordinator.\n const { spawnReviewRoleForIssue } = await import('./review-agent.js');\n const dispatchResult = await Effect.runPromise(spawnReviewRoleForIssue({ issueId, workspace, branch }));\n if (dispatchResult.gated) {\n console.log(` → Deferred recovery review for ${issueId}: ${dispatchResult.message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: `Deferred recovery review for ${issueId}: ${dispatchResult.message}`, issueId });\n continue;\n }\n if (!dispatchResult.success) {\n console.log(` ⚠ Failed to re-dispatch recovery review for ${issueId}: ${dispatchResult.error || dispatchResult.message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Failed to re-dispatch recovery review for ${issueId}: ${dispatchResult.error || dispatchResult.message}`, issueId });\n continue;\n }\n // spawnReviewRoleForIssue sets reviewStatus='reviewing' internally\n console.log(` ✓ Re-dispatched recovery review for ${issueId}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: `Re-dispatched recovery review for ${issueId}`, issueId });\n }\n }\n } catch (error) {\n console.error(' ✗ Failed to recover orphaned reviewing issues:', error);\n }\n\n // PAN-378: Global specialists removed — per-project ephemeral specialists handle all work.\n // No initialization needed; specialists are spawned on-demand via spawnEphemeralSpecialist().\n console.log(' → Specialists: per-project ephemeral mode (no global pool)');\n\n // Start deacon health monitor for specialists\n try {\n console.log(' → Starting deacon health monitor...');\n startDeacon();\n console.log(' ✓ Deacon started');\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: 'Deacon health monitor started' });\n } catch (error) {\n console.error(' ✗ Failed to start deacon:', error);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: `Failed to start deacon: ${error instanceof Error ? error.message : String(error)}` });\n }\n\n this.running = true;\n this.starting = false;\n this._statusCache = null;\n writeStateFile(true);\n this.emit({ type: 'started' });\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: 'Cloister agent watchdog started' });\n\n await this.subscribeToDomainEvents();\n\n // Start monitoring loop\n this.startMonitoringLoop();\n }\n\n private async subscribeToDomainEvents(): Promise<void> {\n if (this.domainEventUnsubscribe) return;\n\n try {\n const injected = cloisterEventStoreProvider?.();\n if (injected) {\n this.eventStore = injected;\n if (injected.subscribe) {\n this.domainEventUnsubscribe = injected.subscribe((event) => {\n void Effect.runPromise(handleCloisterDomainEvent(event)).catch((error) => {\n console.error('[cloister] Reactive lifecycle event handling failed:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Reactive lifecycle event handling failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n });\n });\n }\n console.log(' ✓ Cloister event store provider installed');\n return;\n }\n\n const { initEventStore } = await import('../../dashboard/server/event-store.js');\n const store = await initEventStore();\n this.eventStore = store;\n this.domainEventUnsubscribe = store.subscribe((event) => {\n void Effect.runPromise(handleCloisterDomainEvent(event)).catch((error) => {\n console.error('[cloister] Reactive lifecycle event handling failed:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Reactive lifecycle event handling failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n });\n });\n console.log(' ✓ Cloister reactive lifecycle scheduler subscribed to domain events');\n } catch (error) {\n console.error(' ✗ Failed to subscribe Cloister reactive lifecycle scheduler:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Failed to subscribe reactive lifecycle scheduler: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n\n /**\n * Stop the Cloister service\n *\n * Note: This stops monitoring but does NOT kill agents.\n * Use emergencyStop() to kill all agents.\n */\n stop(): void {\n if (!this.running) {\n console.warn('Cloister is not running');\n return;\n }\n\n console.log('🔔 Stopping Cloister agent watchdog...');\n this.running = false;\n this._statusCache = null;\n writeStateFile(false);\n\n if (this.checkInterval) {\n clearInterval(this.checkInterval);\n this.checkInterval = null;\n }\n\n if (this.domainEventUnsubscribe) {\n this.domainEventUnsubscribe();\n this.domainEventUnsubscribe = null;\n }\n\n // Stop deacon health monitor\n try {\n stopDeacon();\n console.log(' ✓ Deacon stopped');\n } catch (error) {\n console.error('Failed to stop deacon:', error);\n }\n\n this.emit({ type: 'stopped' });\n }\n\n /**\n * Emergency stop - kill ALL agents immediately\n *\n * This is the nuclear option. Use with caution.\n */\n emergencyStop(): string[] {\n console.log('🚨 EMERGENCY STOP - Killing all agents');\n\n // Freeze auto-resume FIRST, before killing — otherwise the deacon patrol or a\n // dashboard restart re-spawns the agents we are about to kill and the money\n // keeps burning. This persists in SQLite, so the freeze survives a restart;\n // the operator clears it explicitly (Deacon resume / flywheel resume) when\n // they are ready to let agents run again.\n try {\n setDeaconGloballyPaused(true);\n setFlywheelGloballyPaused(true);\n console.log(' ✓ Froze Deacon + flywheel auto-resume');\n } catch (error) {\n console.error(' ✗ Failed to set global pause flags:', error);\n }\n\n const runningAgents = listRunningAgentsSync();\n const killedAgents: string[] = [];\n\n for (const agent of runningAgents) {\n if (agent.tmuxActive) {\n try {\n const runtime = getRuntimeForAgent(agent.id);\n if (runtime) {\n runtime.killAgent(agent.id); // killAgent already resets runtime.json to idle\n killedAgents.push(agent.id);\n console.log(` ✓ Killed ${agent.id}`);\n }\n } catch (error) {\n console.error(` ✗ Failed to kill ${agent.id}:`, error);\n }\n }\n }\n\n this.emit({ type: 'emergency_stop', killedAgents });\n\n // Stop monitoring after emergency stop\n this.stop();\n\n return killedAgents;\n }\n\n /**\n * Start the monitoring loop\n */\n private startMonitoringLoop(): void {\n // Run initial check immediately\n this.performHealthCheck();\n\n // Schedule periodic checks\n const intervalMs = this.config.monitoring.check_interval * 1000;\n this.checkInterval = setInterval(() => {\n this.performHealthCheck();\n }, intervalMs);\n }\n\n /**\n * Perform a health check on all running agents\n */\n private async performHealthCheck(): Promise<void> {\n try {\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentIds = runningAgents.map((a) => a.id);\n const currentRunningSet = new Set(agentIds);\n\n // Detect crashed agents (were running before, not running now)\n if (this.previousRunningAgents.size > 0 && this.config.auto_restart?.enabled) {\n for (const previousAgentId of this.previousRunningAgents) {\n if (!currentRunningSet.has(previousAgentId)) {\n // Agent crashed!\n await this.handleAgentCrash(previousAgentId);\n }\n }\n }\n\n // Update the set of running agents for next check\n this.previousRunningAgents = currentRunningSet;\n\n // Completion marker check runs regardless of active agents —\n // completed agents won't have tmux sessions anymore\n this.healthCheckCount++;\n if (this.healthCheckCount % 4 === 0) {\n void this.checkCompletionMarkers();\n }\n\n if (agentIds.length === 0) {\n this.lastCheck = new Date();\n return;\n }\n\n // Get health for all agents\n const agentHealths: AgentHealth[] = [];\n\n for (const agentId of agentIds) {\n const runtime = getRuntimeForAgent(agentId);\n if (runtime) {\n const health = getAgentHealth(agentId, runtime);\n agentHealths.push(health);\n\n // Write health event to database\n this.recordHealthEvent(health);\n }\n }\n\n this.lastCheck = new Date();\n this.emit({ type: 'health_check', agentHealths });\n\n // Check for agents needing attention\n const needsAttention = getAgentsNeedingAttention(agentHealths);\n\n const pokeCooldownMs = this.config.auto_actions.poke_cooldown_ms ?? 30 * 60 * 1000;\n const now = Date.now();\n\n for (const health of needsAttention) {\n // The sequencer is a long-lived singleton that is SUPPOSED to sit idle\n // between ranking passes — \"no progress in a while\" is its normal resting\n // state, not a stall. Never poke or kill it (it was spamming itself with\n // \"are you stuck?\" nudges every cooldown). Health is still recorded above;\n // only the attention/poke/kill action is skipped.\n if (getAgentStateSync(health.agentId)?.role === 'sequencer') continue;\n\n const lastPoke = this.lastPokeTimestamps.get(health.agentId) ?? 0;\n const cooledDown = (now - lastPoke) >= pokeCooldownMs;\n\n if (health.state === 'warning') {\n this.emit({ type: 'agent_warning', agentId: health.agentId, health });\n\n // Auto-poke if configured and cooldown elapsed\n if (this.config.auto_actions.poke_on_warning && cooledDown) {\n this.pokeAgent(health.agentId);\n this.lastPokeTimestamps.set(health.agentId, now);\n }\n } else if (health.state === 'stuck') {\n this.emit({ type: 'agent_stuck', agentId: health.agentId, health });\n\n // Auto-poke stuck agents if configured and cooldown elapsed\n if ((this.config.auto_actions.poke_on_stuck ?? true) && cooledDown) {\n this.pokeAgent(health.agentId);\n this.lastPokeTimestamps.set(health.agentId, now);\n }\n\n // Auto-kill if configured (dangerous!)\n if (this.config.auto_actions.kill_on_stuck) {\n this.killAgent(health.agentId);\n }\n }\n }\n\n // Check for handoff triggers (Phase 4)\n // Note: Intentionally not awaiting - runs in background\n void this.checkHandoffTriggers(agentHealths);\n\n // Check for FPP violations (Phase 6)\n this.checkFPPViolations(agentIds);\n\n await reconcilePiCostEventsForRunningAgents(runningAgents);\n\n // Check cost limits (Phase 6)\n this.checkCostAlerts(agentIds);\n\n // Check for specialist session rotation needs (Phase 6)\n // Only check periodically (every ~10 checks)\n if (Math.random() < 0.1) {\n void this.checkSpecialistRotations();\n }\n\n // Clean up old resolved violations (daily)\n if (Math.random() < 0.01) {\n // ~1% chance each check = roughly once per day\n clearOldViolationsSync(24);\n }\n } catch (error) {\n console.error('Cloister health check failed:', error);\n this.emit({ type: 'error', error: error as Error });\n }\n }\n\n /** Fallback scan for completion markers when `pan done` did not reach the dashboard. */\n private async checkCompletionMarkers(): Promise<void> {\n try {\n if (!existsSync(AGENTS_DIR)) return;\n\n const agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true })\n .filter(d => d.isDirectory() && d.name.startsWith('agent-'));\n\n for (const dir of agentDirs) {\n const completedFile = join(AGENTS_DIR, dir.name, 'completed');\n const processedFile = join(AGENTS_DIR, dir.name, 'completed.processed');\n\n // Skip if no completion marker.\n if (!existsSync(completedFile)) continue;\n\n // A stale `.processed` from a prior round must not block a fresh completion.\n if (existsSync(processedFile)) {\n try {\n const completedMtime = statSync(completedFile).mtimeMs;\n const processedMtime = statSync(processedFile).mtimeMs;\n if (completedMtime > processedMtime) {\n try { unlinkSync(processedFile); } catch {}\n this.processedCompletions.delete(dir.name);\n console.log(`🔔 Cloister: Detected re-completion for ${dir.name} (completed newer than .processed) — clearing stale marker`);\n } else {\n continue;\n }\n } catch {\n continue;\n }\n }\n\n // Skip stale completion markers (older than 24h) — just mark as processed\n try {\n const content = JSON.parse(readFileSync(completedFile, 'utf-8'));\n const ageMs = Date.now() - new Date(content.timestamp).getTime();\n if (ageMs > 24 * 60 * 60 * 1000) {\n console.log(`🔔 Cloister: Skipping stale completion marker for ${dir.name} (${Math.floor(ageMs / 3600000)}h old)`);\n this.processedCompletions.set(dir.name, Infinity);\n try { renameSync(completedFile, processedFile); } catch {}\n continue;\n }\n } catch (parseErr) {\n console.warn(` ⚠ Cloister: Could not parse completion marker for ${dir.name}, skipping`);\n continue;\n }\n\n // Check retry count; reset stale in-memory counters for fresh completions.\n const retryCount = this.processedCompletions.get(dir.name) || 0;\n if (retryCount === Infinity) {\n this.processedCompletions.delete(dir.name);\n } else if (retryCount >= 3) continue;\n\n // Extract issue ID from agent dir name (e.g. \"agent-pan-123\" → \"PAN-123\")\n const issueId = dir.name.replace('agent-', '').toUpperCase();\n\n // Skip if `pan done` already triggered review.\n const { getReviewStatusSync } = await import('../review-status.js');\n const existingReview = getReviewStatusSync(issueId);\n if (existingReview && ['reviewing', 'passed'].includes(existingReview.reviewStatus || '')) {\n console.log(`🔔 Cloister: Completion marker for ${issueId} — review already ${existingReview.reviewStatus}, marking processed`);\n try { renameSync(completedFile, processedFile); } catch {}\n this.processedCompletions.set(dir.name, Infinity);\n continue;\n }\n\n console.log(`🔔 Cloister: Found completion marker for ${issueId}, triggering review...${retryCount > 0 ? ` (retry ${retryCount}/3)` : ''}`);\n\n try {\n // Use fetch() so https dashboard URLs work.\n const result = await (async (): Promise<{ success: boolean; error?: string; alreadyReviewed?: boolean; alreadyMerged?: boolean }> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 5000);\n try {\n const res = await fetch(`${this.getDashboardApiUrl()}/api/review/${issueId}/trigger`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n signal: controller.signal,\n });\n clearTimeout(timer);\n try {\n return (await res.json()) as { success: boolean; error?: string; alreadyReviewed?: boolean; alreadyMerged?: boolean };\n } catch {\n return { success: false, error: `Invalid response (HTTP ${res.status})` };\n }\n } catch (e: any) {\n clearTimeout(timer);\n if (e?.name === 'AbortError') return { success: false, error: 'Timeout (5s)' };\n return { success: false, error: e?.message || String(e) };\n }\n })();\n\n if (result.success) {\n console.log(` ✓ Review triggered for ${issueId}`);\n renameSync(completedFile, processedFile);\n this.processedCompletions.set(dir.name, Infinity);\n } else if (result.alreadyReviewed || result.alreadyMerged) {\n // Terminal state — already handled, mark as processed\n console.log(` ✓ ${issueId} already ${result.alreadyMerged ? 'merged' : 'reviewed'}, marking processed`);\n renameSync(completedFile, processedFile);\n this.processedCompletions.set(dir.name, Infinity);\n } else {\n // Transient failure — increment retry count, will retry on next cycle\n this.processedCompletions.set(dir.name, retryCount + 1);\n console.log(` ⚠ Review trigger failed for ${issueId}: ${result.error || 'unknown'} (will retry, ${2 - retryCount} attempts left)`);\n }\n } catch (err: any) {\n this.processedCompletions.set(dir.name, retryCount + 1);\n console.error(` ✗ Failed to trigger review for ${issueId}: ${err.message} (will retry, ${2 - retryCount} attempts left)`);\n }\n }\n } catch (error) {\n // Non-fatal - just skip this check\n }\n }\n\n /**\n * Poke an agent (send \"are you stuck?\" message).\n *\n * NOTE: runtime.sendMessage() is async — both ClaudeCodeRuntime and PiRuntime\n * are declared `async sendMessage(): Promise<void>`. A `throw` inside an\n * async function before any await still returns a rejected Promise, so the\n * surrounding try/catch CANNOT catch it. Without explicit `.catch()`, the\n * rejection becomes an UnhandledPromiseRejection and crashes the dashboard\n * server. We hit this in production when the deacon health-check polled a\n * dead agent (PAN-1189 wedge sweep #12-13).\n */\n private pokeAgent(agentId: string): void {\n // Fire-and-forget wrapper; the async body measures progress first.\n void this.pokeAgentWithEscalation(agentId).catch((error) => {\n console.error(`Failed to poke ${agentId}:`, error);\n });\n }\n\n /** PAN-2452: fingerprint of observable progress — workspace HEAD + pane tail.\n * Unchanged fingerprint across pokes = the poke did nothing. */\n private async progressFingerprint(agentId: string): Promise<string> {\n const state = getAgentStateSync(agentId);\n let head = '';\n if (state?.workspace) {\n try {\n const { stdout } = await execAsync('git rev-parse HEAD', { cwd: state.workspace, encoding: 'utf-8' });\n head = stdout.trim();\n } catch { /* workspace may be gone; pane still fingerprints */ }\n }\n let pane = '';\n try {\n const { stdout } = await execAsync(\n `tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -${CONTEXT_OVERFLOW_TAIL_LINES}`,\n { encoding: 'utf-8' },\n );\n pane = stdout;\n } catch { /* session may be gone */ }\n const paneHash = createHash('sha1').update(pane).digest('hex').slice(0, 12);\n return `${head}:${paneHash}`;\n }\n\n private async pokeAgentWithEscalation(agentId: string): Promise<void> {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n const fingerprint = await this.progressFingerprint(agentId);\n const prior = this.pokeProgress.get(agentId);\n const ineffective = prior && prior.fingerprint === fingerprint ? prior.ineffective + 1 : 0;\n this.pokeProgress.set(agentId, { fingerprint, ineffective });\n\n // Tier 3 (5th no-progress poke): stop poking — surface to the operator.\n if (ineffective >= 4) {\n const { setAgentPausedSync } = await import('../agents/agent-state.js');\n try {\n setAgentPausedSync(agentId, `needs-you: idle-alive — no observable progress across ${ineffective + 1} pokes (idle-alive escalation)`);\n this.emit({ type: 'agent_stuck', agentId, health: undefined as never });\n console.log(`🛑 ${agentId} paused: idle-alive across ${ineffective + 1} pokes`);\n } catch (pauseErr) {\n console.error(`Failed to pause idle-alive ${agentId}:`, pauseErr);\n }\n return;\n }\n\n // Tier 2 (3rd no-progress poke): escalate — /compact on overflow, else a\n // substantive reconstruct instruction instead of another \"are you stuck?\".\n let pokeMessage =\n 'Hey, I noticed you haven\\'t made progress in a while. Are you stuck? ' +\n 'If you need help or clarification, please ask. Otherwise, please continue with your work.';\n if (ineffective >= 2) {\n let tail = '';\n try {\n const { stdout } = await execAsync(\n `tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -${CONTEXT_OVERFLOW_TAIL_LINES}`,\n { encoding: 'utf-8' },\n );\n tail = stdout;\n } catch { /* fall through to reconstruct nudge */ }\n if (isContextOverflowTail(tail)) {\n pokeMessage = '/compact';\n console.log(`🔔 ${agentId}: overflow tail detected — delivering /compact instead of a poke`);\n } else {\n pokeMessage =\n 'You appear active but have made no observable progress (no new commits, unchanged output) across multiple checks. '\n + 'Do exactly one of: (1) work is done — commit, push, and run pan done; '\n + '(2) blocked — state the blocker in one sentence and commit what you have as WIP; '\n + '(3) lost context — re-read your bead (bd show), your latest .pan/feedback/ file, and git status, then resume. Act now.';\n }\n }\n\n await Promise.resolve(runtime.sendMessage(agentId, pokeMessage)).catch((sendErr) => {\n console.error(`Failed to send poke to ${agentId}:`, sendErr);\n });\n this.emit({ type: 'poked_agent', agentId });\n console.log(`🔔 Poked ${agentId}${ineffective > 0 ? ` (ineffective streak: ${ineffective})` : ''}`);\n }\n\n /**\n * Kill an agent\n *\n * runtime.killAgent() is also async in some runtime implementations — apply\n * the same fire-and-forget guard as pokeAgent so async rejection cannot\n * crash the dashboard from a deacon health-check timer callback.\n */\n private killAgent(agentId: string): void {\n try {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n Promise.resolve(runtime.killAgent(agentId)).catch((killErr) => {\n console.error(`Failed to kill ${agentId}:`, killErr);\n });\n this.emit({ type: 'killed_agent', agentId });\n\n console.log(`🔔 Killed ${agentId}`);\n } catch (error) {\n console.error(`Failed to kill ${agentId}:`, error);\n }\n }\n\n /**\n * Handle agent crash with auto-restart logic\n */\n private async handleAgentCrash(agentId: string): Promise<void> {\n const config = this.config.auto_restart;\n if (!config?.enabled) return;\n\n // Check if agent was intentionally stopped or suspended (not a crash).\n // Both state.json and runtime.json must be checked — stopAgent writes both,\n // but a race between the CLI kill and this health check poll could see one\n // but not the other if only one file is consulted.\n const agentState = getAgentStateSync(agentId);\n if (!agentState || agentState.status === 'stopped') {\n console.log(`🔔 Agent ${agentId} was intentionally stopped, skipping restart`);\n return;\n }\n const runtimeState = getAgentRuntimeStateSync(agentId);\n if (runtimeState?.state === 'suspended') {\n console.log(`🔔 Agent ${agentId} is suspended, skipping restart`);\n return;\n }\n if (runtimeState?.state === 'stopped') {\n console.log(`🔔 Agent ${agentId} runtime is stopped, skipping restart`);\n return;\n }\n\n // PAN-1742: a vanished session is not always a crash. One-shot convoy roles\n // complete-and-exit, and any agent with no saved session is structurally\n // un-restartable. Either way, counting it as a crash pollutes the\n // crash/troubled counters and schedules a doomed restart.\n const skipReason = nonRestartableReason(agentState.role, agentState.sessionId);\n if (skipReason) {\n // PAN-2007: a one-shot review/test session that vanished BEFORE recording a\n // terminal verdict died prematurely — e.g. before it could run\n // `pan specialists done`. Masking that as a normal \"one-shot completion\" and\n // skipping restart strands the issue at reviewStatus=reviewing forever (the\n // exact PAN-1832 symptom). Recover it instead: resume so it can finish and\n // signal. Only a terminal verdict (or a missing session) is a genuine\n // non-restartable completion.\n let recoverUnfinishedOneShot = false;\n if (ONE_SHOT_ROLES.has(agentState.role) && agentState.sessionId && agentState.issueId) {\n try {\n const { getReviewStatusSync } = await import('../review-status.js');\n const status = getReviewStatusSync(agentState.issueId);\n const verdictTerminal = status\n ? isRoleTerminal(agentState.role as AdvancingRole, {\n reviewStatus: status.reviewStatus,\n testStatus: status.testStatus,\n readyForMerge: status.readyForMerge,\n mergeStatus: status.mergeStatus,\n })\n : false;\n recoverUnfinishedOneShot = !verdictTerminal;\n } catch {\n recoverUnfinishedOneShot = false;\n }\n }\n if (!recoverUnfinishedOneShot) {\n console.log(`🔔 Agent ${agentId} ${skipReason}; skipping restart`);\n return;\n }\n console.log(`🔔 Agent ${agentId} (${agentState.role}) vanished before a terminal verdict — recovering instead of treating as a one-shot completion (PAN-2007)`);\n // fall through to the restart/resume path below\n }\n\n // Record death timestamp for mass death detection\n const now = new Date();\n this.deathTimestamps.push(now);\n this.checkForMassDeaths();\n\n // Get or create crash tracker\n let tracker = this.crashTrackers.get(agentId);\n if (!tracker) {\n tracker = {\n agentId,\n crashCount: 0,\n lastCrash: now,\n gaveUp: false,\n };\n this.crashTrackers.set(agentId, tracker);\n }\n\n // Skip if we've already given up on this agent\n if (tracker.gaveUp) return;\n\n // Increment crash count\n tracker.crashCount++;\n tracker.lastCrash = now;\n\n this.emit({ type: 'agent_crashed', agentId, crashCount: tracker.crashCount });\n console.log(`🔔 Agent ${agentId} crashed (crash #${tracker.crashCount})`);\n\n // PAN-1908: emit agent.heartbeat_dead so the event-driven deacon recovery\n // can mark the agent stopped and resume it with the proper gates, rather\n // than Cloister scheduling its own auto-restart in parallel.\n if (this.eventStore) {\n try {\n this.eventStore.append({\n type: 'agent.heartbeat_dead',\n timestamp: new Date().toISOString(),\n payload: { agentId, issueId: agentState.issueId, sessionId: agentState.sessionId },\n });\n } catch (err) {\n console.error(`[cloister] Failed to emit heartbeat_dead for ${agentId}:`, err);\n }\n }\n\n // Check if we've exceeded max retries\n if (tracker.crashCount > config.max_retries) {\n tracker.gaveUp = true;\n this.emit({ type: 'agent_gave_up', agentId, maxRetries: config.max_retries });\n console.error(`🔔 Gave up on restarting ${agentId} after ${config.max_retries} attempts`);\n return;\n }\n\n // Calculate backoff delay for logging/mass-death tracking only — the actual\n // resume is handled by the deacon's agent.heartbeat_dead handler.\n const backoffIndex = Math.min(tracker.crashCount - 1, config.backoff_seconds.length - 1);\n const backoffSeconds = config.backoff_seconds[backoffIndex];\n const nextRetryAt = new Date(Date.now() + backoffSeconds * 1000);\n tracker.nextRetryAt = nextRetryAt;\n\n this.emit({\n type: 'agent_restarting',\n agentId,\n crashCount: tracker.crashCount,\n backoffSeconds,\n });\n\n console.log(\n `🔔 Deacon will recover ${agentId} via heartbeat_dead (attempt ${tracker.crashCount}/${config.max_retries}, backoff ${backoffSeconds}s)`\n );\n }\n\n /**\n * Restart an agent using its saved session\n */\n private async restartAgent(agentId: string): Promise<void> {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n // Get agent state to find session ID and workspace\n const agentState = getAgentStateSync(agentId);\n if (!agentState?.sessionId) {\n throw new Error(`No session ID found for agent ${agentId}`);\n }\n\n if (!agentState.workspace) {\n throw new Error(`No workspace found for agent ${agentId}`);\n }\n\n // Restart with --resume using spawnAgent with sessionId\n console.log(`🔔 Restarting ${agentId} with session ${agentState.sessionId.substring(0, 8)}...`);\n runtime.spawnAgent({\n agentId,\n workspace: agentState.workspace,\n sessionId: agentState.sessionId,\n runtime: runtime.name,\n });\n console.log(`🔔 Successfully restarted ${agentId}`);\n }\n\n /**\n * Check for mass death events\n *\n * Detects when 3+ agents die within 30 seconds and pauses spawns.\n */\n private checkForMassDeaths(): void {\n const MASS_DEATH_THRESHOLD = 3;\n const WINDOW_SECONDS = 30;\n\n const now = Date.now();\n const windowStart = now - WINDOW_SECONDS * 1000;\n\n // Clean up old timestamps outside the window\n this.deathTimestamps = this.deathTimestamps.filter(\n (timestamp) => timestamp.getTime() >= windowStart\n );\n\n // Check if we have mass deaths\n if (this.deathTimestamps.length >= MASS_DEATH_THRESHOLD) {\n // Trigger mass death alert\n this.emit({\n type: 'mass_death_detected',\n deathCount: this.deathTimestamps.length,\n windowSeconds: WINDOW_SECONDS,\n });\n\n // Pause spawns\n if (!this.spawnsPaused) {\n this.pauseSpawns('Mass death detected - system stability concern');\n console.error(\n `🔔 MASS DEATH DETECTED: ${this.deathTimestamps.length} agents died in ${WINDOW_SECONDS}s - spawns paused`\n );\n }\n }\n }\n\n /**\n * Pause new agent spawns\n */\n private pauseSpawns(reason: string): void {\n this.spawnsPaused = true;\n setCloisterSpawnsPausedSync(true);\n this.emit({ type: 'spawn_paused', reason });\n console.log(`🔔 Agent spawns paused: ${reason}`);\n }\n\n /**\n * Resume agent spawns\n *\n * Called manually after user acknowledges mass death alert.\n */\n resumeSpawns(): void {\n this.spawnsPaused = false;\n setCloisterSpawnsPausedSync(false);\n this.deathTimestamps = []; // Clear death window\n this.emit({ type: 'spawn_resumed' });\n console.log(`🔔 Agent spawns resumed`);\n }\n\n /**\n * Check if spawns are currently paused\n */\n isSpawnPaused(): boolean {\n return this.spawnsPaused || isCloisterSpawnsPausedSync();\n }\n\n /**\n * Check for FPP violations and send nudges\n */\n private checkFPPViolations(agentIds: string[]): void {\n for (const agentId of agentIds) {\n const violation = checkAgentForViolations(agentId);\n if (!violation) continue;\n\n // New violation detected\n if (violation.nudgeCount === 0) {\n this.emit({ type: 'fpp_violation_detected', agentId, violation });\n }\n\n // Check if we should send a nudge\n const timeSinceLastNudge = violation.lastNudgeAt\n ? Date.now() - new Date(violation.lastNudgeAt).getTime()\n : Infinity;\n\n // Send nudge every 5 minutes until max nudges\n const NUDGE_INTERVAL_MS = 5 * 60 * 1000;\n if (timeSinceLastNudge >= NUDGE_INTERVAL_MS || violation.nudgeCount === 0) {\n if (hasExceededMaxNudges(violation)) {\n // Max nudges exceeded - alert user\n this.emit({ type: 'fpp_max_nudges_exceeded', agentId, violation });\n console.error(\n `🔔 Agent ${agentId} exceeded max nudges for ${violation.type} - manual intervention required`\n );\n } else {\n // Send nudge\n const sent = sendNudge(violation);\n if (sent) {\n this.emit({ type: 'fpp_nudge_sent', agentId, nudgeCount: violation.nudgeCount });\n }\n }\n }\n }\n }\n\n /**\n * Check for cost limit alerts\n */\n private checkCostAlerts(agentIds: string[]): void {\n const config = this.config.cost_limits;\n if (!config) return;\n\n for (const agentId of agentIds) {\n // Extract issue ID from agent ID (format: agent-issue-123 or issue-123)\n const issueId = agentId.startsWith('agent-')\n ? agentId.replace(/^agent-/, '')\n : agentId;\n\n const alerts = checkCostLimits(agentId, issueId, config);\n for (const alert of alerts) {\n this.emit({ type: 'cost_alert', alert });\n\n // Resolve the entity label: for daily_total, use explicit \"(unattributed)\" bucket\n const entityLabel = alert.agentId || alert.issueId || '(unattributed)';\n\n // Log the alert\n if (alert.level === 'limit_reached') {\n console.error(\n `🔔 COST LIMIT REACHED: ${alert.type} for ${entityLabel} - $${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)}`\n );\n } else {\n console.warn(\n `🔔 Cost warning: ${alert.type} for ${entityLabel} at ${alert.percentUsed.toFixed(0)}% ($${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)})`\n );\n }\n }\n }\n }\n\n /**\n * Get cost summary\n */\n getCostSummary() {\n return getCostSummary();\n }\n\n /**\n * Check if any specialists need session rotation\n */\n private async checkSpecialistRotations(): Promise<void> {\n // Check merge-agent (the main candidate for rotation)\n const mergeAgentResult = await Effect.runPromise(checkAndRotateIfNeeded('merge-agent', process.cwd()));\n if (mergeAgentResult) {\n this.emit({ type: 'session_rotated', specialistName: 'merge-agent', result: mergeAgentResult });\n\n if (mergeAgentResult.success) {\n console.log(\n `🔔 Rotated merge-agent session: ${mergeAgentResult.oldSessionId.substring(0, 8)} → ${mergeAgentResult.newSessionId?.substring(0, 8)}`\n );\n } else {\n console.error(`🔔 Failed to rotate merge-agent: ${mergeAgentResult.error}`);\n }\n }\n\n // Could check other specialists here if needed\n }\n\n /**\n * Record health event to database\n *\n * Only writes events when state changes or on first check.\n */\n private recordHealthEvent(health: AgentHealth): void {\n try {\n const currentState = health.state;\n const previousState = this.previousStates.get(health.agentId);\n\n // Only write event if state changed or this is first check\n if (previousState === undefined || previousState !== currentState) {\n // Determine source from heartbeat\n const source = health.heartbeat?.source\n ? this.mapHeartbeatSource(health.heartbeat.source)\n : 'unknown';\n\n writeHealthEvent({\n agentId: health.agentId,\n timestamp: new Date().toISOString(),\n state: currentState,\n source,\n metadata: health.heartbeat\n ? JSON.stringify({\n confidence: health.heartbeat.confidence,\n lastAction: health.heartbeat.lastAction,\n toolName: health.heartbeat.toolName,\n timeSinceActivity: health.timeSinceActivity,\n })\n : undefined,\n });\n\n // Update tracked state\n this.previousStates.set(health.agentId, currentState);\n }\n } catch (error) {\n console.error(`Failed to record health event for ${health.agentId}:`, error);\n }\n }\n\n /**\n * Check for handoff triggers and execute handoffs (Phase 4)\n *\n * Checks all triggers for each agent and performs handoffs when triggered.\n */\n private async checkHandoffTriggers(agentHealths: AgentHealth[]): Promise<void> {\n for (const health of agentHealths) {\n try {\n // Get agent state\n const agentState = getAgentStateSync(health.agentId);\n if (!agentState) continue;\n\n // Skip if no workspace (can't determine context)\n if (!agentState.workspace) continue;\n\n // Check all triggers\n const triggers = await Effect.runPromise(checkAllTriggers(\n health.agentId,\n agentState.workspace,\n agentState.issueId,\n agentState.model,\n health,\n this.config\n ));\n\n // Execute handoff for first triggered condition\n // (Priority: stuck > planning > test > completion)\n if (triggers.length > 0) {\n const trigger = triggers[0];\n\n // task_complete triggers with a specialist name (e.g. 'test-agent') as suggestedModel\n // are handled by the `pan done` → completion marker → specialist pipeline flow.\n // Do NOT perform a model-swap handoff here — it passes the specialist name as a model ID\n // which is invalid and causes the agent to respawn with an unusable model.\n const specialistNames = ['review-agent', 'test-agent', 'merge-agent', 'inspect-agent', 'uat-agent'];\n if (trigger.type === 'task_complete' && specialistNames.includes(trigger.suggestedModel || '')) {\n console.log(`[cloister] Skipping handoff for ${health.agentId}: task_complete triggers specialist dispatch via completion marker, not model swap`);\n continue;\n }\n\n this.emit({ type: 'handoff_triggered', agentId: health.agentId, trigger });\n\n console.log(`🔔 Handoff triggered for ${health.agentId}: ${trigger.reason}`);\n\n // Perform handoff\n const result = await Effect.runPromise(performHandoff(health.agentId, {\n targetModel: trigger.suggestedModel || 'sonnet',\n reason: trigger.reason,\n }));\n\n this.emit({ type: 'handoff_completed', agentId: health.agentId, result });\n\n // Log handoff event\n if (result.context) {\n const event = createHandoffEvent(\n health.agentId,\n agentState.issueId,\n result.context,\n trigger.type,\n result.success,\n result.error\n );\n logHandoffEventSync(event);\n }\n\n if (result.success) {\n console.log(`✓ Handoff completed: ${health.agentId} → ${result.newAgentId} (${trigger.suggestedModel})`);\n } else {\n console.error(`✗ Handoff failed: ${result.error}`);\n }\n }\n } catch (error) {\n console.error(`Failed to check handoff triggers for ${health.agentId}:`, error);\n }\n }\n }\n\n /**\n * Map heartbeat source to database source string\n */\n private mapHeartbeatSource(source: string): string {\n switch (source) {\n case 'jsonl':\n return 'jsonl_mtime';\n case 'tmux':\n return 'tmux_activity';\n case 'git':\n return 'git_activity';\n case 'active-heartbeat':\n return 'active_heartbeat';\n default:\n return source;\n }\n }\n\n /**\n * Get current status\n *\n * Uses a 3-second TTL cache to avoid blocking the event loop on repeated\n * dashboard polls. The underlying computation does sync file I/O and tmux\n * calls for every agent, which scales poorly with agent count.\n */\n getStatus(): CloisterStatus {\n const now = Date.now();\n if (this._statusCache && now - this._statusCacheAt < this.STATUS_CACHE_TTL_MS) {\n return this._statusCache;\n }\n\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentIds = runningAgents.map((a) => a.id);\n\n const agentHealths: AgentHealth[] = [];\n\n for (const agentId of agentIds) {\n const runtime = getRuntimeForAgent(agentId);\n if (runtime) {\n const health = getAgentHealth(agentId, runtime);\n agentHealths.push(health);\n }\n }\n\n const summary = generateHealthSummary(agentHealths);\n const needsAttention = getAgentsNeedingAttention(agentHealths).map((h) => h.agentId);\n\n const deaconStatus = getDeaconStatus();\n const patrol = assessDeaconPatrolFreshness({\n isRunning: deaconStatus.isRunning,\n lastPatrol: deaconStatus.state.lastPatrol,\n patrolIntervalMs: deaconStatus.config.patrolIntervalMs,\n });\n\n const status: CloisterStatus = {\n running: this.isRunning(),\n lastCheck: this.lastCheck,\n config: this.config,\n summary,\n agentsNeedingAttention: needsAttention,\n patrol: {\n ...patrol,\n loopRunning: deaconStatus.isRunning,\n patrolIntervalMs: deaconStatus.config.patrolIntervalMs,\n },\n };\n\n this._statusCache = status;\n this._statusCacheAt = now;\n return status;\n }\n\n /**\n * Get health for a specific agent\n */\n getAgentHealth(agentId: string): AgentHealth | null {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n return null;\n }\n\n return getAgentHealth(agentId, runtime);\n }\n\n /**\n * Get health for all running agents\n */\n getAllAgentHealth(): AgentHealth[] {\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentHealths: AgentHealth[] = [];\n\n for (const agent of runningAgents) {\n const runtime = getRuntimeForAgent(agent.id);\n if (runtime) {\n const health = getAgentHealth(agent.id, runtime);\n agentHealths.push(health);\n }\n }\n\n return agentHealths;\n }\n\n /**\n * Get deacon (specialist health monitor) status\n */\n getDeaconStatus() {\n return getDeaconStatus();\n }\n\n /**\n * Get the most recent patrol result (actions, cycle, timestamp)\n */\n getLastPatrolResult(): PatrolResult | null {\n return getLastPatrolResult();\n }\n\n /**\n * Get recent deacon log entries\n */\n getDeaconLogs(limit = 100): DeaconLogEntry[] {\n return getDeaconLogs(limit);\n }\n\n /**\n * Run a manual deacon patrol\n */\n async runDeaconPatrol(): Promise<PatrolResult> {\n return runPatrol();\n }\n\n /**\n * Check if deacon is running\n */\n isDeaconRunning(): boolean {\n return isDeaconRunning();\n }\n\n /**\n * Reload configuration\n */\n reloadConfig(): void {\n this.config = loadCloisterConfigSync();\n\n // Restart monitoring loop with new interval if running\n if (this.running && this.checkInterval) {\n clearInterval(this.checkInterval);\n this.startMonitoringLoop();\n }\n }\n\n /**\n * Update configuration\n */\n updateConfig(config: CloisterConfig): void {\n this.config = config;\n\n // Restart monitoring loop with new interval if running\n if (this.running && this.checkInterval) {\n clearInterval(this.checkInterval);\n this.startMonitoringLoop();\n }\n }\n\n /**\n * Register an event listener\n */\n on(listener: CloisterEventListener): void {\n this.listeners.push(listener);\n }\n\n /**\n * Unregister an event listener\n */\n off(listener: CloisterEventListener): void {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n /**\n * Emit an event to all listeners\n */\n private emit(event: CloisterEvent): void {\n for (const listener of this.listeners) {\n try {\n listener(event);\n } catch (error) {\n console.error('Cloister event listener error:', error);\n }\n }\n }\n\n /**\n * Check if service is running\n *\n * Checks both local instance state and cross-process state file.\n * This allows the CLI to detect if Cloister is running in the dashboard process.\n */\n isRunning(): boolean {\n // First check our own instance\n if (this.running) {\n return true;\n }\n // Check if another process has Cloister running\n const stateFile = readCloisterStateFile();\n return stateFile.running;\n }\n}\n\n/**\n * Global Cloister service instance\n */\nlet globalService: CloisterService | null = null;\n\n/**\n * Get the global Cloister service instance\n *\n * Creates a new instance if one doesn't exist.\n */\nexport function getCloisterService(): CloisterService {\n if (!globalService) {\n globalService = new CloisterService();\n }\n return globalService;\n}\n\n/**\n * Set the global Cloister service instance\n *\n * Useful for testing or custom configurations.\n */\nexport function setCloisterService(service: CloisterService): void {\n globalService = service;\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n// service.ts is the top-level Cloister orchestrator (1817 lines, heavy use of\n// closures and direct fs IO). A full Effect rewrite would cascade into half\n// the codebase (review-agent, test-agent-queue, agents.ts) so for the\n// batch-C migration we expose Effect variants only at the two domain-event\n// entry points. The legacy Promise surfaces stay live for existing callers;\n// Effect callers should prefer the *Effect variants. The internal\n// implementations swallow errors (logging via emitActivityEntry instead),\n// so the error channel is `never`.\n\n/**\n * Effect-typed variant of {@link onIssueStateChange}. Never fails — failures\n * surface through `emitActivityEntry` inside the legacy implementation.\n */\nexport function onIssueStateChange(\n issueId: string,\n newState: string,\n): Effect.Effect<void> {\n return Effect.promise(() => onIssueStateChangePromise(issueId, newState));\n}\n\n/**\n * Effect-typed variant of {@link handleCloisterDomainEvent}. Never fails.\n */\nexport function handleCloisterDomainEvent(\n event: CloisterDomainEventLike,\n): Effect.Effect<void> {\n return Effect.promise(() => handleCloisterDomainEventPromise(event));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service-HKDrIy0h.js","names":["existsSync","readdirSync","join","execAsync","promisify","exec","execAsync","promisify","exec","getDailyTrends","execAsync","promisify","exec"],"sources":["../../src/lib/overdeck/cost.ts","../../src/lib/cloister/health.ts","../../src/lib/cloister/handoff-context.ts","../../src/lib/cloister/handoff.ts","../../src/lib/cloister/triggers.ts","../../src/lib/cloister/handoff-logger.ts","../../src/lib/cloister/fpp-violations.ts","../../src/lib/cloister/cost-monitor.ts","../../src/lib/cloister/pi-cost-reconciler.ts","../../src/lib/cloister/session-rotation.ts","../../src/lib/cloister/service.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { Context, Effect, Layer, Schema } from 'effect';\nimport { and, desc, eq, gte, like, sql } from 'drizzle-orm';\nimport { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';\nimport { HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nimport { CostArchive, CostArchiveLive, Db, DbLive, EventBus, EventBusLive } from './infra.js';\nimport { IssueId, type IssueId as IssueIdType } from './issues.js';\nimport {\n getAllBudgetsSync,\n checkBudgetSync,\n createBudgetSync,\n deleteBudgetSync,\n} from '../cost.js';\nimport type { CostBudget } from '../cost.js';\nimport { parseOhmypiSessionCostResultSync } from '../cost-parsers/ohmypi-parser.js';\nimport { parseCodexSessionCostEventsSync, parseCodexSessionSync } from '../cost-parsers/codex-parser.js';\nimport { getOverdeckHome } from '../paths.js';\nimport { deriveTieredAgentCostRole } from '../agents/tier-metrics.js';\n\n// ── Filesystem helpers ────────────────────────────────────────────────────────\n\nfunction walkJsonl(dir: string): string[] {\n if (!existsSync(dir)) return [];\n const result: string[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) result.push(...walkJsonl(full));\n else if (entry.name.endsWith('.jsonl')) result.push(full);\n }\n return result;\n}\n\nfunction readAgentRoleSync(agentDir: string): string {\n try {\n const raw = JSON.parse(readFileSync(join(agentDir, 'state.json'), 'utf8')) as { role?: unknown };\n return typeof raw.role === 'string' && raw.role.trim() ? raw.role : 'work';\n } catch {\n return 'work';\n }\n}\n\n// ── Local Drizzle table definition ───────────────────────────────────────────\n// Mirrors the locked schema (docs/overdeck-remodel/overdeck-schema.ts:173-195).\n// No FK or index definitions here — those live in the compiled schema only.\n\nconst costEventsTable = sqliteTable('cost_events', {\n id: integer('id').primaryKey({ autoIncrement: true }),\n ts: integer('ts', { mode: 'timestamp_ms' }).notNull(),\n issueId: text('issue_id'),\n agentId: text('agent_id'),\n sessionId: text('session_id'),\n sessionType: text('session_type'),\n provider: text('provider'),\n model: text('model'),\n input: integer('input'),\n output: integer('output'),\n cacheRead: integer('cache_read'),\n cacheWrite: integer('cache_write'),\n cost: real('cost'),\n requestId: text('request_id'),\n sourceFile: text('source_file'),\n});\n\n// ── Entities ─────────────────────────────────────────────────────────────────\n\nexport const Tokens = Schema.Struct({\n input: Schema.Number,\n output: Schema.Number,\n cacheRead: Schema.Number,\n cacheWrite: Schema.Number,\n});\nexport type Tokens = typeof Tokens.Type;\n\nexport const CostEvent = Schema.Struct({\n ts: Schema.Date,\n issueId: Schema.NullOr(IssueId),\n agentId: Schema.NullOr(Schema.String),\n sessionId: Schema.NullOr(Schema.String),\n sessionType: Schema.NullOr(Schema.String),\n provider: Schema.NullOr(Schema.String),\n model: Schema.NullOr(Schema.String),\n input: Schema.Number,\n output: Schema.Number,\n cacheRead: Schema.Number,\n cacheWrite: Schema.Number,\n cost: Schema.Number,\n requestId: Schema.NullOr(Schema.String),\n sourceFile: Schema.NullOr(Schema.String),\n warnings: Schema.optional(Schema.Array(Schema.Struct({\n type: Schema.String,\n provider: Schema.NullOr(Schema.String),\n model: Schema.String,\n reason: Schema.String,\n }))),\n});\nexport type CostEvent = typeof CostEvent.Type;\n\nexport interface SkippedCostSession {\n file: string;\n reason: string;\n}\n\nexport interface CostReconcileWarning {\n file: string;\n reason: string;\n provider: string | null;\n model: string;\n}\n\nexport const Rollup = Schema.Struct({\n key: Schema.String,\n role: Schema.optional(Schema.String),\n cost: Schema.Number,\n tokens: Tokens,\n});\nexport type Rollup = typeof Rollup.Type;\n\nexport const Window = Schema.Literals(['day', 'week', 'month']);\nexport type Window = typeof Window.Type;\n\nexport const WindowSummary = Schema.Struct({\n project: Schema.NullOr(Schema.String),\n window: Window,\n totalCost: Schema.Number,\n totalTokens: Schema.Number,\n entryCount: Schema.Number,\n byModel: Schema.Record(Schema.String, Schema.Number),\n});\nexport type WindowSummary = typeof WindowSummary.Type;\n\nexport const IssueCost = Schema.Struct({\n issueId: IssueId,\n totalCost: Schema.Number,\n tokens: Tokens,\n byModel: Schema.Record(Schema.String, Rollup),\n byStage: Schema.Record(Schema.String, Schema.Record(Schema.String, Tokens)),\n});\nexport type IssueCost = typeof IssueCost.Type;\n\nexport const BudgetSpec = Schema.Struct({\n name: Schema.String,\n type: Schema.Literals(['daily', 'monthly', 'project', 'issue', 'feature']),\n limit: Schema.Number,\n currency: Schema.String,\n alertThreshold: Schema.Number,\n});\nexport type BudgetSpec = typeof BudgetSpec.Type;\n\nexport const Budget = Schema.Struct({\n ...BudgetSpec.fields,\n id: Schema.String,\n spent: Schema.Number,\n enabled: Schema.Boolean,\n});\nexport type Budget = typeof Budget.Type;\n\nexport const BudgetStatus = Schema.Struct({\n budget: Budget,\n percentUsed: Schema.Number,\n remaining: Schema.Number,\n alert: Schema.Boolean,\n exceeded: Schema.Boolean,\n});\nexport type BudgetStatus = typeof BudgetStatus.Type;\n\nexport interface CostReconcileSummary {\n imported: number;\n skipped: SkippedCostSession[];\n sessionsScanned: number;\n eventsImported: number;\n duplicatesSkipped: number;\n errors: string[];\n earliestEventTs: string | null;\n latestEventTs: string | null;\n warnings: CostReconcileWarning[];\n}\n\nexport type CostReconcileExtraRoot =\n | { kind: 'codex-global'; root: string }\n | { kind: 'ohmypi-global'; root: string }\n | { kind: 'ohmypi-legacy-agents'; root: string };\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\nexport class CostIngestError extends Schema.TaggedErrorClass<CostIngestError>()(\n 'CostIngestError',\n { reason: Schema.String },\n) {}\n\nexport class BudgetNotFound extends Schema.TaggedErrorClass<BudgetNotFound>()(\n 'BudgetNotFound',\n { id: Schema.String },\n) {}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction mapBudget(b: CostBudget): Budget {\n return {\n id: b.id,\n name: b.name,\n type: b.type,\n limit: b.limit,\n currency: b.currency,\n alertThreshold: b.alertThreshold,\n spent: b.spent,\n enabled: b.enabled,\n };\n}\n\ntype TokenRow = {\n input: number | null;\n output: number | null;\n cacheRead: number | null;\n cacheWrite: number | null;\n};\n\nfunction toTokens(r: TokenRow): Tokens {\n return {\n input: r.input ?? 0,\n output: r.output ?? 0,\n cacheRead: r.cacheRead ?? 0,\n cacheWrite: r.cacheWrite ?? 0,\n };\n}\n\nfunction toRollup(key: string, r: TokenRow & { cost: number | null }, role?: string): Rollup {\n return { key, ...(role ? { role } : {}), cost: r.cost ?? 0, tokens: toTokens(r) };\n}\n\nfunction issueIdFromAgentName(agentName: string): IssueIdType | null {\n const match = agentName.match(/(?:^|-)((?:pan|min|aud|krux|cli)-\\d+)(?:-|$)/i);\n return match ? match[1]!.toUpperCase() as IssueIdType : null;\n}\n\nfunction inferIssueFromPath(path: string | undefined): IssueIdType | null {\n if (!path) return null;\n const match = path.match(/(?:feature[-/])?(pan|min|aud|krux|cli)[-/](\\d+)/i);\n return match ? `${match[1]!.toUpperCase()}-${match[2]}` as IssueIdType : null;\n}\n\nfunction inferIssueFromPiEncodedRootName(name: string): IssueIdType | null {\n return inferIssueFromPath(name.replace(/--+/g, '/'));\n}\n\nfunction inferPiProvider(model: string, provider: string | null): string | null {\n if (provider) return provider;\n const lower = model.toLowerCase();\n if (lower.includes('kimi') || lower.startsWith('minimax')) return 'custom';\n if (lower.includes('claude')) return 'anthropic';\n if (lower.includes('gpt')) return 'openai';\n if (lower.includes('gemini')) return 'google';\n return null;\n}\n\n// ── CostResolver — the read door ──────────────────────────────────────────────\n\nexport class CostResolver extends Context.Service<\n CostResolver,\n {\n // Windowed totals + per-model breakdown, optional project scope\n readonly summary: (window: Window, project?: string) => Effect.Effect<WindowSummary>;\n // All-issue rollup: GROUP BY issue_id\n readonly byIssue: () => Effect.Effect<ReadonlyArray<Rollup>>;\n // One issue's detail: total + per-model + per-stage\n readonly issueDetail: (id: IssueId) => Effect.Effect<IssueCost>;\n // Per-day trend over N days, optional issue filter\n readonly byDay: (days: number, issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per-model rollup, optional issue filter\n readonly byModel: (issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per-agent rollup, optional issue filter\n readonly byAgent: (issue?: IssueId) => Effect.Effect<ReadonlyArray<Rollup>>;\n // Per source_file, last N hours (background-AI costs)\n readonly byBackgroundSource: (hours: number) => Effect.Effect<ReadonlyArray<Rollup>>;\n // GROUP BY issue-id prefix (no project column — extracts from issue_id)\n readonly byProject: () => Effect.Effect<ReadonlyArray<Rollup>>;\n // Recent events, optional since filter\n readonly recent: (limit: number, since?: Date) => Effect.Effect<ReadonlyArray<CostEvent>>;\n // Budget reads (budgets.json — separate from cost_events)\n readonly listBudgets: () => Effect.Effect<ReadonlyArray<Budget>>;\n readonly checkBudget: (id: string) => Effect.Effect<BudgetStatus, BudgetNotFound>;\n }\n>()('overdeck/CostResolver') {}\n\nexport const CostResolverLive = Layer.effect(\n CostResolver,\n Effect.gen(function* () {\n const { q } = yield* Db;\n\n const groupBySelect = {\n cost: sql<number>`SUM(${costEventsTable.cost})`,\n input: sql<number>`SUM(${costEventsTable.input})`,\n output: sql<number>`SUM(${costEventsTable.output})`,\n cacheRead: sql<number>`SUM(${costEventsTable.cacheRead})`,\n cacheWrite: sql<number>`SUM(${costEventsTable.cacheWrite})`,\n } as const;\n\n const summary = (window: Window, project?: string) =>\n Effect.promise(async () => {\n const days = { day: 1, week: 7, month: 30 }[window];\n const cutoff = new Date(Date.now() - days * 86400_000);\n const where = project\n ? and(gte(costEventsTable.ts, cutoff), like(costEventsTable.issueId, `${project}-%`))\n : gte(costEventsTable.ts, cutoff);\n\n const [agg] = await q\n .select({\n totalCost: sql<number>`COALESCE(SUM(${costEventsTable.cost}), 0)`,\n totalTokens: sql<number>`COALESCE(SUM(COALESCE(${costEventsTable.input},0) + COALESCE(${costEventsTable.output},0) + COALESCE(${costEventsTable.cacheRead},0) + COALESCE(${costEventsTable.cacheWrite},0)), 0)`,\n entryCount: sql<number>`COUNT(*)`,\n })\n .from(costEventsTable)\n .where(where);\n\n const modelRows = await q\n .select({ model: costEventsTable.model, cost: sql<number>`COALESCE(SUM(${costEventsTable.cost}), 0)` })\n .from(costEventsTable)\n .where(where)\n .groupBy(costEventsTable.model);\n\n const byModel: Record<string, number> = {};\n for (const r of modelRows) byModel[r.model ?? 'unknown'] = r.cost ?? 0;\n\n return {\n project: project ?? null,\n window,\n totalCost: agg?.totalCost ?? 0,\n totalTokens: agg?.totalTokens ?? 0,\n entryCount: agg?.entryCount ?? 0,\n byModel,\n };\n });\n\n const byIssue = () =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.issueId, ...groupBySelect })\n .from(costEventsTable)\n .groupBy(costEventsTable.issueId);\n return rows.map((r) => toRollup(r.key ?? 'unattributed', r));\n });\n\n const issueDetail = (id: IssueId) =>\n Effect.promise(async () => {\n const modelRows = await q\n .select({ model: costEventsTable.model, ...groupBySelect })\n .from(costEventsTable)\n .where(eq(costEventsTable.issueId, id))\n .groupBy(costEventsTable.model);\n\n const byModel: Record<string, Rollup> = {};\n let totalCost = 0;\n let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;\n for (const r of modelRows) {\n const key = r.model ?? 'unknown';\n const t = toTokens(r);\n byModel[key] = { key, cost: r.cost ?? 0, tokens: t };\n totalCost += r.cost ?? 0;\n totalInput += t.input;\n totalOutput += t.output;\n totalCacheRead += t.cacheRead;\n totalCacheWrite += t.cacheWrite;\n }\n const tokens: Tokens = { input: totalInput, output: totalOutput, cacheRead: totalCacheRead, cacheWrite: totalCacheWrite };\n\n const stageRows = await q\n .select({\n stage: costEventsTable.sessionType,\n model: costEventsTable.model,\n input: sql<number>`SUM(${costEventsTable.input})`,\n output: sql<number>`SUM(${costEventsTable.output})`,\n cacheRead: sql<number>`SUM(${costEventsTable.cacheRead})`,\n cacheWrite: sql<number>`SUM(${costEventsTable.cacheWrite})`,\n })\n .from(costEventsTable)\n .where(eq(costEventsTable.issueId, id))\n .groupBy(costEventsTable.sessionType, costEventsTable.model);\n\n const byStage: Record<string, Record<string, Tokens>> = {};\n for (const r of stageRows) {\n const stage = r.stage ?? 'unknown';\n const model = r.model ?? 'unknown';\n if (!byStage[stage]) byStage[stage] = {};\n byStage[stage][model] = toTokens(r);\n }\n\n return { issueId: id, totalCost, tokens, byModel, byStage };\n });\n\n const byDay = (days: number, issue?: IssueId) =>\n Effect.promise(async () => {\n const cutoff = new Date(Date.now() - days * 86400_000);\n const dayExpr = sql<string>`DATE(${costEventsTable.ts}, 'unixepoch')`;\n const where = issue\n ? and(gte(costEventsTable.ts, cutoff), eq(costEventsTable.issueId, issue))\n : gte(costEventsTable.ts, cutoff);\n const rows = await q\n .select({ key: dayExpr, ...groupBySelect })\n .from(costEventsTable)\n .where(where)\n .groupBy(dayExpr)\n .orderBy(sql`DATE(${costEventsTable.ts}, 'unixepoch') DESC`);\n return rows.map((r) => toRollup(r.key ?? '', r));\n });\n\n const byModel = (issue?: IssueId) =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.model, ...groupBySelect })\n .from(costEventsTable)\n .where(issue ? eq(costEventsTable.issueId, issue) : undefined)\n .groupBy(costEventsTable.model);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const byAgent = (issue?: IssueId) =>\n Effect.promise(async () => {\n const rows = await q\n .select({ key: costEventsTable.agentId, ...groupBySelect })\n .from(costEventsTable)\n .where(issue ? eq(costEventsTable.issueId, issue) : undefined)\n .groupBy(costEventsTable.agentId);\n return rows.map((r) => {\n const key = r.key ?? 'unattributed';\n return toRollup(key, r, deriveTieredAgentCostRole(key, issue));\n });\n });\n\n const byBackgroundSource = (hours: number) =>\n Effect.promise(async () => {\n const cutoff = new Date(Date.now() - hours * 3600_000);\n const rows = await q\n .select({ key: costEventsTable.sourceFile, ...groupBySelect })\n .from(costEventsTable)\n .where(and(gte(costEventsTable.ts, cutoff), sql`${costEventsTable.sourceFile} IS NOT NULL`))\n .groupBy(costEventsTable.sourceFile);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const byProject = () =>\n Effect.promise(async () => {\n const projExpr = sql<string>`UPPER(SUBSTR(${costEventsTable.issueId}, 1, INSTR(${costEventsTable.issueId}, '-') - 1))`;\n const rows = await q\n .select({ key: projExpr, ...groupBySelect })\n .from(costEventsTable)\n .where(sql`${costEventsTable.issueId} IS NOT NULL`)\n .groupBy(projExpr);\n return rows.map((r) => toRollup(r.key ?? 'unknown', r));\n });\n\n const recent = (limit: number, since?: Date) =>\n Effect.promise(async () => {\n const rows = await q\n .select()\n .from(costEventsTable)\n .where(since ? gte(costEventsTable.ts, since) : undefined)\n .orderBy(desc(costEventsTable.ts))\n .limit(limit);\n return rows.map((r) => ({\n ts: r.ts,\n issueId: r.issueId ?? null,\n agentId: r.agentId ?? null,\n sessionId: r.sessionId ?? null,\n sessionType: r.sessionType ?? null,\n provider: r.provider ?? null,\n model: r.model ?? null,\n input: r.input ?? 0,\n output: r.output ?? 0,\n cacheRead: r.cacheRead ?? 0,\n cacheWrite: r.cacheWrite ?? 0,\n cost: r.cost ?? 0,\n requestId: r.requestId ?? null,\n sourceFile: r.sourceFile ?? null,\n })) as ReadonlyArray<CostEvent>;\n });\n\n const listBudgets = () =>\n Effect.sync(() => getAllBudgetsSync().map(mapBudget));\n\n const checkBudget = (id: string) =>\n Effect.gen(function* () {\n const result = yield* Effect.sync(() => checkBudgetSync(id));\n if (!result.budget) return yield* Effect.fail(new BudgetNotFound({ id }));\n return {\n budget: mapBudget(result.budget),\n percentUsed: result.percentUsed,\n remaining: result.remaining,\n alert: result.alert,\n exceeded: result.exceeded,\n };\n });\n\n return CostResolver.of({\n summary, byIssue, issueDetail, byDay, byModel, byAgent,\n byBackgroundSource, byProject, recent, listBudgets, checkBudget,\n });\n }),\n);\n\n// ── CostWriter — the write door ───────────────────────────────────────────────\n\nexport class CostWriter extends Context.Service<\n CostWriter,\n {\n // The ONLY ingest primitive — owns archive fan-out and dedup\n readonly record: (event: CostEvent, opts?: { dryRun?: boolean }) => Effect.Effect<boolean, CostIngestError>;\n // Catch-up sweep (PAN-1935: pi/codex sweep lands here)\n readonly reconcile: (opts?: {\n source?: 'claude' | 'ohmypi' | 'codex' | 'wal';\n dryRun?: boolean;\n extraRoots?: string[];\n extraRootSpecs?: CostReconcileExtraRoot[];\n }) => Effect.Effect<CostReconcileSummary, CostIngestError>;\n // Full rebuild from archive; recomputes cost from tokens\n readonly rebuild: () => Effect.Effect<{ events: number }, CostIngestError>;\n // Budget writes (budgets.json — separate from cost_events)\n readonly createBudget: (spec: BudgetSpec) => Effect.Effect<Budget, CostIngestError>;\n readonly deleteBudget: (id: string) => Effect.Effect<void, BudgetNotFound>;\n }\n>()('overdeck/CostWriter') {}\n\nexport const CostWriterLive = Layer.effect(\n CostWriter,\n Effect.gen(function* () {\n const { q } = yield* Db;\n const archive = yield* CostArchive;\n const bus = yield* EventBus;\n\n // Dedup: prefer the precise requestId when present; fall back to sourceFile\n // only for events with no requestId (codex / background-AI dumps that carry\n // a file path but no request id). Using OR(requestId, sourceFile) is wrong —\n // it collapses every event that shares a sourceFile (e.g. all events\n // reconciled from one pi transcript) down to a single row, silently dropping\n // the rest (PAN-1935).\n const checkDuplicate = (e: CostEvent) =>\n Effect.gen(function* () {\n if (e.requestId == null && e.sourceFile == null) return false;\n const condition =\n e.requestId != null\n ? eq(costEventsTable.requestId, e.requestId)\n : eq(costEventsTable.sourceFile, e.sourceFile as string);\n const existing = yield* Effect.promise(() =>\n q\n .select({ id: costEventsTable.id })\n .from(costEventsTable)\n .where(condition)\n .limit(1),\n );\n return (existing as unknown[]).length > 0;\n });\n\n const record = (event: CostEvent, opts?: { dryRun?: boolean }) =>\n Effect.gen(function* () {\n if (opts?.dryRun) {\n if (yield* checkDuplicate(event)) return false;\n return true;\n }\n\n // 1. DURABLE ARCHIVE FIRST — events.jsonl. Archive deduplicates by\n // requestId/sourceFile so DB-only retries can repair the JSONL log.\n yield* archive.append(event);\n\n if (yield* checkDuplicate(event)) return false;\n\n // 2. Cache insert — UNIQUE(request_id) makes it idempotent on re-import.\n yield* Effect.promise(() =>\n q\n .insert(costEventsTable)\n .values({\n ts: event.ts,\n issueId: event.issueId,\n agentId: event.agentId,\n sessionId: event.sessionId,\n sessionType: event.sessionType,\n provider: event.provider,\n model: event.model,\n input: event.input,\n output: event.output,\n cacheRead: event.cacheRead,\n cacheWrite: event.cacheWrite,\n cost: event.cost,\n requestId: event.requestId,\n sourceFile: event.sourceFile,\n })\n .onConflictDoNothing(),\n );\n\n // 3. Announce — cost.subscribe + /api/costs/stream feed from this.\n yield* bus.emit({ type: 'cost.recorded', payload: { issueId: event.issueId, cost: event.cost } });\n return true;\n });\n\n // Catch-up sweep for pi/codex session files.\n // Walks OVERDECK_HOME/agents/<id>/sessions/**/*.jsonl (pi) or\n // OVERDECK_HOME/agents/<id>/codex-home/sessions/**/*.jsonl (codex),\n // parses each with the existing parsers, and feeds into record() (which deduplicates).\n const reconcile = (opts?: {\n source?: 'claude' | 'ohmypi' | 'codex' | 'wal';\n dryRun?: boolean;\n extraRoots?: string[];\n extraRootSpecs?: CostReconcileExtraRoot[];\n }) =>\n Effect.gen(function* () {\n const source = opts?.source ?? 'claude';\n const empty: CostReconcileSummary = {\n imported: 0,\n skipped: [],\n sessionsScanned: 0,\n eventsImported: 0,\n duplicatesSkipped: 0,\n errors: [],\n earliestEventTs: null,\n latestEventTs: null,\n warnings: [],\n };\n if (source !== 'ohmypi' && source !== 'codex') return empty;\n\n const agentsDir = join(getOverdeckHome(), 'agents');\n const agentNames = yield* Effect.sync(() => {\n if (!existsSync(agentsDir)) return [] as string[];\n return readdirSync(agentsDir, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => e.name);\n });\n\n let imported = 0;\n const skipped: SkippedCostSession[] = [];\n let duplicatesSkipped = 0;\n let sessionsScanned = 0;\n let earliestEventTs: string | null = null;\n let latestEventTs: string | null = null;\n const errors: string[] = [];\n const warnings: CostReconcileWarning[] = [];\n const markSkipped = (file: string, reason: string) => {\n skipped.push({ file, reason });\n console.warn(`[cost-reconcile] skipped ${file}: ${reason}`);\n };\n const markWarning = (warning: CostReconcileWarning) => {\n warnings.push(warning);\n console.warn(\n `[cost-reconcile] warning ${warning.file}: ${warning.reason} provider=${warning.provider ?? 'unknown'} model=${warning.model}`,\n );\n };\n\n const noteImportedTimestamp = (ts: Date) => {\n const iso = ts.toISOString();\n if (earliestEventTs == null || iso < earliestEventTs) earliestEventTs = iso;\n if (latestEventTs == null || iso > latestEventTs) latestEventTs = iso;\n };\n\n type ScanRoot = {\n root: string;\n agentName: string;\n issueId: IssueIdType | null;\n sessionType: 'ohmypi' | 'codex';\n inferIssueFromCwd?: boolean;\n };\n\n const roots: ScanRoot[] = agentNames.map((agentName) => ({\n root: source === 'ohmypi'\n ? join(agentsDir, agentName, 'sessions')\n : join(agentsDir, agentName, 'codex-home', 'sessions'),\n agentName,\n issueId: issueIdFromAgentName(agentName),\n sessionType: source,\n }));\n\n if (source === 'codex') {\n for (const root of opts?.extraRoots ?? []) {\n roots.push({\n root,\n agentName: 'codex-global',\n issueId: null,\n sessionType: 'codex',\n inferIssueFromCwd: true,\n });\n }\n }\n\n for (const extra of opts?.extraRootSpecs ?? []) {\n if (source === 'codex' && extra.kind === 'codex-global') {\n roots.push({\n root: extra.root,\n agentName: 'codex-global',\n issueId: null,\n sessionType: 'codex',\n inferIssueFromCwd: true,\n });\n }\n\n if (source === 'ohmypi' && extra.kind === 'ohmypi-global') {\n const encodedRoots = yield* Effect.sync(() => {\n if (!existsSync(extra.root)) return [] as ScanRoot[];\n return readdirSync(extra.root, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => ({\n root: join(extra.root, e.name),\n agentName: 'pi-global',\n issueId: inferIssueFromPiEncodedRootName(e.name) ?? 'UNKNOWN' as IssueIdType,\n sessionType: 'ohmypi' as const,\n }));\n });\n roots.push(...encodedRoots);\n }\n\n if (source === 'ohmypi' && extra.kind === 'ohmypi-legacy-agents') {\n const legacyRoots = yield* Effect.sync(() => {\n if (!existsSync(extra.root)) return [] as ScanRoot[];\n return readdirSync(extra.root, { withFileTypes: true })\n .filter(e => e.isDirectory())\n .map(e => ({\n root: join(extra.root, e.name, 'sessions'),\n agentName: e.name,\n issueId: issueIdFromAgentName(e.name),\n sessionType: 'ohmypi' as const,\n }));\n });\n roots.push(...legacyRoots);\n }\n }\n\n for (const root of roots) {\n const sessionFiles = yield* Effect.sync(() => walkJsonl(root.root));\n\n for (const sessionFile of sessionFiles) {\n sessionsScanned++;\n if (source === 'ohmypi') {\n const parsed = yield* Effect.sync(() => {\n try {\n return parseOhmypiSessionCostResultSync(sessionFile);\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n errors.push(`${sessionFile}: ${reason}`);\n return { ok: false as const, reason };\n }\n });\n if (!parsed.ok) {\n markSkipped(sessionFile, parsed.reason ?? 'unreadable');\n continue;\n }\n\n const events = parsed.usageEvents ?? [];\n if (events.length === 0) {\n markSkipped(sessionFile, 'no-usage');\n continue;\n }\n const unpricedWarnings = parsed.unpricedModels ?? [];\n if (unpricedWarnings.length) markSkipped(sessionFile, 'unpriced-model');\n\n for (const usage of events) {\n const derivedWarnings = unpricedWarnings\n .filter(warning => warning.model === usage.model && warning.provider === usage.provider)\n .map(warning => ({ type: 'unpriced-model' as const, ...warning }));\n const eventWarnings = usage.warnings?.length ? usage.warnings : derivedWarnings;\n for (const warning of eventWarnings) {\n markWarning({ file: sessionFile, reason: warning.reason, provider: warning.provider, model: warning.model });\n }\n const ts = new Date(usage.timestamp);\n const event: CostEvent = {\n ts,\n issueId: root.issueId,\n agentId: root.agentName,\n sessionId: usage.sessionId,\n sessionType: root.sessionType,\n provider: inferPiProvider(usage.model, usage.provider),\n model: usage.model,\n input: usage.input,\n output: usage.output,\n cacheRead: usage.cacheRead,\n cacheWrite: usage.cacheWrite,\n cost: usage.cost,\n requestId: usage.requestId,\n sourceFile: sessionFile,\n warnings: eventWarnings.length > 0 ? eventWarnings : undefined,\n };\n\n if (yield* record(event, { dryRun: opts?.dryRun })) {\n imported++;\n noteImportedTimestamp(ts);\n } else {\n duplicatesSkipped++;\n }\n }\n continue;\n }\n\n const events = yield* Effect.sync(() => {\n try {\n return parseCodexSessionCostEventsSync(sessionFile);\n } catch (cause) {\n errors.push(`${sessionFile}: ${cause instanceof Error ? cause.message : String(cause)}`);\n return [];\n }\n });\n if (events.length === 0) {\n markSkipped(sessionFile, 'no-usage');\n continue;\n }\n if (events.some((event) => event.model === 'unknown')) {\n markSkipped(sessionFile, 'unknown-model');\n }\n const codexSession = root.inferIssueFromCwd\n ? yield* Effect.sync(() => parseCodexSessionSync(sessionFile))\n : null;\n const issueId = root.inferIssueFromCwd\n ? (inferIssueFromPath(codexSession?.cwd) ?? 'UNKNOWN' as IssueIdType)\n : root.issueId;\n for (const usage of events) {\n const ts = new Date(usage.timestamp);\n const event: CostEvent = {\n ts,\n issueId,\n agentId: root.agentName,\n sessionId: usage.sessionId,\n sessionType: root.sessionType,\n provider: usage.provider,\n model: usage.model,\n input: usage.input,\n output: usage.output,\n cacheRead: usage.cacheRead,\n cacheWrite: usage.cacheWrite,\n cost: usage.cost,\n requestId: usage.requestId,\n sourceFile: sessionFile,\n };\n\n if (yield* record(event, { dryRun: opts?.dryRun })) {\n imported++;\n noteImportedTimestamp(ts);\n } else {\n duplicatesSkipped++;\n }\n }\n }\n }\n\n return {\n imported,\n skipped,\n sessionsScanned,\n eventsImported: imported,\n duplicatesSkipped,\n errors,\n earliestEventTs,\n latestEventTs,\n warnings,\n };\n });\n\n // Stub — full rebuild (recomputes cost from tokens) deferred to\n // workspace-3zhmy.\n const rebuild = () =>\n Effect.gen(function* () {\n return { events: 0 };\n });\n\n const createBudget = (spec: BudgetSpec) =>\n Effect.gen(function* () {\n const created = yield* Effect.sync(() =>\n createBudgetSync({ ...spec, enabled: true }),\n );\n return mapBudget(created);\n });\n\n const deleteBudget = (id: string) =>\n Effect.gen(function* () {\n const deleted = yield* Effect.sync(() => deleteBudgetSync(id));\n if (!deleted) return yield* Effect.fail(new BudgetNotFound({ id }));\n });\n\n return CostWriter.of({ record, reconcile, rebuild, createBudget, deleteBudget });\n }),\n);\n\nexport const CostDoorLive = CostWriterLive.pipe(\n Layer.provide(Layer.mergeAll(DbLive, EventBusLive.pipe(Layer.provide(DbLive)), CostArchiveLive)),\n);\n\n// ── CostApi — the controller (HttpApiGroup, no handler wiring) ────────────────\n// Handler wiring (CostApiLive) is deferred to workspace-m9n4j (server bootstrap).\n\nexport const CostApi = HttpApiGroup.make('costs')\n // ── reads ──\n .add(\n HttpApiEndpoint.get('summary', '/costs/summary', {\n query: {\n window: Schema.optional(Window),\n project: Schema.optional(Schema.String),\n },\n success: WindowSummary,\n }),\n )\n .add(HttpApiEndpoint.get('byIssue', '/costs/by-issue', { success: Schema.Array(Rollup) }))\n .add(\n HttpApiEndpoint.get('issueDetail', '/costs/issue/:id', {\n params: { id: IssueId },\n success: IssueCost,\n }),\n )\n .add(\n HttpApiEndpoint.get('byDay', '/costs/trends', {\n query: {\n days: Schema.optional(Schema.NumberFromString),\n issueId: Schema.optional(IssueId),\n },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byModel', '/costs/by-model', {\n query: { issueId: Schema.optional(IssueId) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byAgent', '/costs/by-agent', {\n query: { issueId: Schema.optional(IssueId) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(\n HttpApiEndpoint.get('byBackgroundSource', '/costs/background', {\n query: { hours: Schema.optional(Schema.NumberFromString) },\n success: Schema.Array(Rollup),\n }),\n )\n .add(HttpApiEndpoint.get('byProject', '/costs/by-project', { success: Schema.Array(Rollup) }))\n .add(\n HttpApiEndpoint.get('recent', '/costs/stream', {\n query: {\n limit: Schema.optional(Schema.NumberFromString),\n since: Schema.optional(Schema.String),\n },\n success: Schema.Array(CostEvent),\n }),\n )\n .add(HttpApiEndpoint.get('listBudgets', '/costs/budget', { success: Schema.Array(Budget) }))\n .add(\n HttpApiEndpoint.get('checkBudget', '/costs/budget/:id', {\n params: { id: Schema.String },\n success: BudgetStatus,\n error: BudgetNotFound,\n }),\n )\n // ── writes ──\n .add(\n HttpApiEndpoint.post('reconcile', '/costs/reconcile', {\n payload: Schema.Struct({\n source: Schema.optional(Schema.Literals(['claude', 'ohmypi', 'codex', 'wal'])),\n dryRun: Schema.optional(Schema.Boolean),\n extraRoots: Schema.optional(Schema.Array(Schema.String)),\n }),\n success: Schema.Struct({\n imported: Schema.Number,\n skipped: Schema.Array(Schema.Struct({ file: Schema.String, reason: Schema.String })),\n sessionsScanned: Schema.Number,\n eventsImported: Schema.Number,\n duplicatesSkipped: Schema.Number,\n errors: Schema.Array(Schema.String),\n earliestEventTs: Schema.NullOr(Schema.String),\n latestEventTs: Schema.NullOr(Schema.String),\n warnings: Schema.Array(Schema.Struct({\n file: Schema.String,\n reason: Schema.String,\n provider: Schema.NullOr(Schema.String),\n model: Schema.String,\n })),\n }),\n error: CostIngestError,\n }),\n )\n .add(\n HttpApiEndpoint.post('rebuild', '/costs/rebuild', {\n success: Schema.Struct({ events: Schema.Number }),\n error: CostIngestError,\n }),\n )\n .add(\n HttpApiEndpoint.post('createBudget', '/costs/budget', {\n payload: BudgetSpec,\n success: Budget,\n error: CostIngestError,\n }),\n )\n // HttpApiEndpoint exports 'del' as 'delete' (reserved word alias)\n .add(\n HttpApiEndpoint['delete']('deleteBudget', '/costs/budget/:id', {\n params: { id: Schema.String },\n error: BudgetNotFound,\n }),\n );\n","/**\n * Cloister Health Evaluator\n *\n * Evaluates agent health based on heartbeats and activity timestamps.\n */\n\nimport type { HealthState, Heartbeat, AgentRuntimeSync } from '../runtimes/types.js';\nimport { getAgentRuntimeStateSync } from '../agents.js';\nimport type { HealthThresholds } from './config.js';\nimport { getHealthThresholdsMs } from './config.js';\n\n/**\n * Agent health status\n */\nexport interface AgentHealth {\n agentId: string;\n state: HealthState;\n lastActivity: Date | null;\n timeSinceActivity: number | null; // milliseconds\n heartbeat: Heartbeat | null;\n isRunning: boolean;\n contextSaturatedAt?: string;\n}\n\n/**\n * Health summary for all agents\n */\nexport interface HealthSummary {\n active: number;\n stale: number;\n warning: number;\n stuck: number;\n wedged: number;\n total: number;\n}\n\n/**\n * Evaluate health state based on time since last activity\n *\n * @param timeSinceActivityMs - Milliseconds since last activity\n * @param thresholds - Health thresholds in milliseconds\n * @returns Health state\n */\nexport function evaluateHealthState(\n timeSinceActivityMs: number,\n thresholds: { stale: number; warning: number; stuck: number }\n): HealthState {\n if (timeSinceActivityMs < thresholds.stale) {\n return 'active';\n } else if (timeSinceActivityMs < thresholds.warning) {\n return 'stale';\n } else if (timeSinceActivityMs < thresholds.stuck) {\n return 'warning';\n } else {\n return 'stuck';\n }\n}\n\n/**\n * Get health status for a single agent\n *\n * @param agentId - Agent identifier\n * @param runtime - Runtime to query for heartbeat\n * @param thresholds - Health thresholds (optional, uses config if not provided)\n * @returns Agent health status\n */\nexport function getAgentHealth(\n agentId: string,\n runtime: AgentRuntimeSync,\n thresholds?: { stale: number; warning: number; stuck: number }\n): AgentHealth {\n const thresholdsMs = thresholds || getHealthThresholdsMs();\n\n // Check if agent is running\n const isRunning = runtime.isRunning(agentId);\n\n if (!isRunning) {\n return {\n agentId,\n state: 'stuck',\n lastActivity: null,\n timeSinceActivity: null,\n heartbeat: null,\n isRunning: false,\n };\n }\n\n // Get heartbeat\n const heartbeat = runtime.getHeartbeat(agentId);\n const contextSaturatedAt = getAgentRuntimeStateSync(agentId)?.contextSaturatedAt;\n\n if (!heartbeat) {\n // No heartbeat available - agent might be starting up\n return {\n agentId,\n state: contextSaturatedAt ? 'wedged' : 'active', // Assume active if no heartbeat yet\n lastActivity: null,\n timeSinceActivity: null,\n heartbeat: null,\n isRunning: true,\n contextSaturatedAt,\n };\n }\n\n if (contextSaturatedAt) {\n const timeSinceActivity = new Date().getTime() - heartbeat.timestamp.getTime();\n return {\n agentId,\n state: 'wedged',\n lastActivity: heartbeat.timestamp,\n timeSinceActivity,\n heartbeat,\n isRunning: true,\n contextSaturatedAt,\n };\n }\n\n // Calculate time since last activity\n const now = new Date();\n const timeSinceActivity = now.getTime() - heartbeat.timestamp.getTime();\n\n // Evaluate health state\n const state = evaluateHealthState(timeSinceActivity, thresholdsMs);\n\n return {\n agentId,\n state,\n lastActivity: heartbeat.timestamp,\n timeSinceActivity,\n heartbeat,\n isRunning: true,\n };\n}\n\n/**\n * Get health status for multiple agents\n *\n * @param agentIds - Array of agent identifiers\n * @param runtime - Runtime to query for heartbeats\n * @param thresholds - Health thresholds (optional, uses config if not provided)\n * @returns Array of agent health statuses\n */\nexport function getMultipleAgentHealth(\n agentIds: string[],\n runtime: AgentRuntimeSync,\n thresholds?: { stale: number; warning: number; stuck: number }\n): AgentHealth[] {\n return agentIds.map((agentId) => getAgentHealth(agentId, runtime, thresholds));\n}\n\n/**\n * Generate health summary from agent health statuses\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Health summary with counts by state\n */\nexport function generateHealthSummary(agentHealths: AgentHealth[]): HealthSummary {\n const summary: HealthSummary = {\n active: 0,\n stale: 0,\n warning: 0,\n stuck: 0,\n wedged: 0,\n total: agentHealths.length,\n };\n\n for (const health of agentHealths) {\n summary[health.state]++;\n }\n\n return summary;\n}\n\n/**\n * Check if an agent needs attention (warning or stuck)\n *\n * @param health - Agent health status\n * @returns True if agent needs attention\n */\nexport function needsAttention(health: AgentHealth): boolean {\n return health.state === 'warning' || health.state === 'stuck' || health.state === 'wedged';\n}\n\n/**\n * Check if an agent should be poked (warning state)\n *\n * @param health - Agent health status\n * @returns True if agent should be poked\n */\nexport function shouldPoke(health: AgentHealth): boolean {\n return health.state === 'warning';\n}\n\n/**\n * Check if an agent should be killed (stuck state)\n *\n * @param health - Agent health status\n * @returns True if agent should be killed\n */\nexport function shouldKill(health: AgentHealth): boolean {\n return health.state === 'stuck';\n}\n\n/**\n * Get agents that need attention\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents needing attention\n */\nexport function getAgentsNeedingAttention(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(needsAttention);\n}\n\n/**\n * Get agents that should be poked\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents to poke\n */\nexport function getAgentsToPoke(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(shouldPoke);\n}\n\n/**\n * Get agents that should be killed\n *\n * @param agentHealths - Array of agent health statuses\n * @returns Array of agents to kill\n */\nexport function getAgentsToKill(agentHealths: AgentHealth[]): AgentHealth[] {\n return agentHealths.filter(shouldKill);\n}\n\n/**\n * Format time duration in human-readable format\n *\n * @param ms - Milliseconds\n * @returns Human-readable duration (e.g., \"5m\", \"2h\", \"1d\")\n */\nexport function formatDuration(ms: number | null): string {\n if (ms === null) {\n return 'unknown';\n }\n\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d`;\n } else if (hours > 0) {\n return `${hours}h`;\n } else if (minutes > 0) {\n return `${minutes}m`;\n } else {\n return `${seconds}s`;\n }\n}\n\n/**\n * Get health emoji for a health state\n *\n * @param state - Health state\n * @returns Emoji representing the state\n */\nexport function getHealthEmoji(state: HealthState): string {\n switch (state) {\n case 'active':\n return '🟢';\n case 'stale':\n return '🟡';\n case 'warning':\n return '🟠';\n case 'stuck':\n return '🔴';\n case 'wedged':\n return '🧱';\n }\n}\n\n/**\n * Get health label for a health state\n *\n * @param state - Health state\n * @returns Human-readable label\n */\nexport function getHealthLabel(state: HealthState): string {\n switch (state) {\n case 'active':\n return 'Active';\n case 'stale':\n return 'Stale';\n case 'warning':\n return 'Warning';\n case 'stuck':\n return 'Stuck';\n case 'wedged':\n return 'Wedged';\n }\n}\n","/**\n * Handoff Context Module\n *\n * Captures and serializes agent context for handoffs between models.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Data, Effect } from 'effect';\nimport type { TokenUsage, RuntimeName } from '../runtimes/types.js';\nimport type { ComplexityLevel } from './complexity.js';\nimport type { AgentState } from '../agents.js';\nimport { renderPrompt } from './prompts.js';\nimport type { ContinueState } from '../vbrief/continue-state.js';\nimport { getProjectConfigFromWorkspacePath, readRecordContinueViewSync, resolveProjectForIssue } from '../pan-dir/record.js';\nimport { withBdMutex } from '../bd-mutex.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Beads task snapshot for handoff\n */\nexport interface BeadsTask {\n id: string;\n title: string;\n description?: string;\n status: 'open' | 'in_progress' | 'closed';\n priority: number;\n labels?: string[];\n complexity?: ComplexityLevel;\n}\n\n/**\n * Handoff context - captures full state for agent transition\n */\nexport interface HandoffContext {\n // Agent identity\n issueId: string;\n agentId: string;\n workspace: string;\n\n // Source info\n previousModel: string;\n previousRuntime: RuntimeName;\n previousSessionId?: string;\n\n // Files\n /** Parsed scope continue file. */\n continueState?: ContinueState;\n claudeMd?: string; // CLAUDE.md content\n\n // Git state\n gitBranch?: string;\n uncommittedFiles?: string[];\n lastCommit?: string;\n\n // Beads state\n activeBeadsTasks?: BeadsTask[];\n remainingTasks?: BeadsTask[];\n completedTasks?: BeadsTask[];\n\n // AI summaries\n whatWasDone?: string;\n whatRemains?: string;\n blockers?: string[];\n decisions?: string[];\n\n // Metrics\n tokenUsage?: TokenUsage;\n costSoFar?: number;\n handoffCount?: number;\n\n // New agent target\n targetModel: string;\n reason: string;\n}async function captureHandoffContextPromise(\n agentState: AgentState,\n targetModel: string,\n reason: string\n): Promise<HandoffContext> {\n const context: HandoffContext = {\n issueId: agentState.issueId,\n agentId: agentState.id,\n workspace: agentState.workspace,\n previousModel: agentState.model,\n previousRuntime: agentState.harness ?? 'claude-code',\n previousSessionId: agentState.sessionId,\n targetModel,\n reason,\n handoffCount: 0,\n costSoFar: agentState.costSoFar || 0,\n };\n\n // Capture files (continue file, CLAUDE.md)\n await captureFiles(context, agentState.workspace, agentState.issueId);\n\n // Capture git state\n await captureGitState(context, agentState.workspace);\n\n // Capture beads tasks\n await captureBeadsTasks(context, agentState.issueId);\n\n return context;\n}\n\n/**\n * Capture workspace files (continue file, CLAUDE.md)\n */\nasync function captureFiles(\n context: HandoffContext,\n workspace: string,\n issueId: string,\n): Promise<void> {\n try {\n // Read continue context from the per-issue record (PAN-1919).\n try {\n const project = resolveProjectForIssue(issueId) ?? getProjectConfigFromWorkspacePath(workspace);\n const recordView = readRecordContinueViewSync(project, issueId);\n if (recordView) {\n context.continueState = recordView as unknown as ContinueState;\n }\n } catch { /* ignore */ }\n\n // Read CLAUDE.md if it exists\n const claudeMd = join(workspace, 'CLAUDE.md');\n if (existsSync(claudeMd)) {\n context.claudeMd = readFileSync(claudeMd, 'utf-8');\n }\n } catch (error) {\n console.error('Error capturing files:', error);\n }\n}\n\n/**\n * Capture git state\n */\nasync function captureGitState(context: HandoffContext, workspace: string): Promise<void> {\n try {\n // Get current branch\n const { stdout: branch } = await execAsync('git branch --show-current', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.gitBranch = branch.trim();\n\n // Get uncommitted files\n const { stdout: status } = await execAsync('git status --porcelain', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.uncommittedFiles = status\n .split('\\n')\n .filter(line => line.trim())\n .map(line => line.substring(3)); // Remove status prefix\n\n // Get last commit\n const { stdout: lastCommit } = await execAsync('git log -1 --oneline', {\n cwd: workspace,\n encoding: 'utf-8',\n });\n context.lastCommit = lastCommit.trim();\n } catch (error) {\n console.error('Error capturing git state:', error);\n }\n}\n\n/**\n * Capture beads tasks state\n */\nasync function captureBeadsTasks(context: HandoffContext, issueId: string): Promise<void> {\n try {\n // List all tasks with this issue's label\n const label = issueId.toLowerCase();\n const { stdout: output } = await execAsync(`bd list --json -l ${label}`, {\n encoding: 'utf-8',\n });\n\n const tasks: BeadsTask[] = JSON.parse(output);\n\n // Categorize tasks\n context.activeBeadsTasks = tasks.filter(t => t.status === 'in_progress');\n context.remainingTasks = tasks.filter(t => t.status === 'open');\n context.completedTasks = tasks.filter(t => t.status === 'closed');\n } catch (error) {\n console.error('Error capturing beads tasks:', error);\n context.activeBeadsTasks = [];\n context.remainingTasks = [];\n context.completedTasks = [];\n }\n}\n\n/**\n * Serialize handoff context to markdown for agent prompt\n *\n * @param context - Handoff context\n * @returns Markdown representation\n */\nexport function serializeHandoffContext(context: HandoffContext): string {\n const lines: string[] = [];\n\n lines.push('# Handoff Context');\n lines.push('');\n lines.push(`**Reason:** ${context.reason}`);\n lines.push(`**From:** ${context.previousModel}`);\n lines.push(`**To:** ${context.targetModel}`);\n lines.push(`**Handoff Count:** ${context.handoffCount}`);\n if (context.costSoFar) {\n lines.push(`**Cost So Far:** $${context.costSoFar.toFixed(4)}`);\n }\n lines.push('');\n\n // Git state\n if (context.gitBranch) {\n lines.push('## Git State');\n lines.push('');\n lines.push(`**Branch:** ${context.gitBranch}`);\n if (context.lastCommit) {\n lines.push(`**Last Commit:** ${context.lastCommit}`);\n }\n if (context.uncommittedFiles && context.uncommittedFiles.length > 0) {\n lines.push(`**Uncommitted Files:** ${context.uncommittedFiles.length}`);\n lines.push('```');\n context.uncommittedFiles.forEach(file => lines.push(file));\n lines.push('```');\n }\n lines.push('');\n }\n\n // Beads tasks\n if (context.completedTasks && context.completedTasks.length > 0) {\n lines.push('## Completed Tasks');\n lines.push('');\n context.completedTasks.forEach(task => {\n lines.push(`- [x] ${task.title} (${task.id})`);\n });\n lines.push('');\n }\n\n if (context.activeBeadsTasks && context.activeBeadsTasks.length > 0) {\n lines.push('## Active Tasks');\n lines.push('');\n context.activeBeadsTasks.forEach(task => {\n lines.push(`- [ ] ${task.title} (${task.id}) - IN PROGRESS`);\n });\n lines.push('');\n }\n\n if (context.remainingTasks && context.remainingTasks.length > 0) {\n lines.push('## Remaining Tasks');\n lines.push('');\n context.remainingTasks.forEach(task => {\n lines.push(`- [ ] ${task.title} (${task.id})`);\n });\n lines.push('');\n }\n\n // Continue file content (structured planning state)\n if (context.continueState) {\n lines.push('## Current State (continue.vbrief.json)');\n lines.push('');\n lines.push('```json');\n lines.push(JSON.stringify(context.continueState, null, 2));\n lines.push('```');\n lines.push('');\n }\n\n // AI summaries (if available)\n if (context.whatWasDone) {\n lines.push('## What Was Done');\n lines.push('');\n lines.push(context.whatWasDone);\n lines.push('');\n }\n\n if (context.whatRemains) {\n lines.push('## What Remains');\n lines.push('');\n lines.push(context.whatRemains);\n lines.push('');\n }\n\n if (context.blockers && context.blockers.length > 0) {\n lines.push('## Blockers');\n lines.push('');\n context.blockers.forEach(blocker => lines.push(`- ${blocker}`));\n lines.push('');\n }\n\n if (context.decisions && context.decisions.length > 0) {\n lines.push('## Decisions Made');\n lines.push('');\n context.decisions.forEach(decision => lines.push(`- ${decision}`));\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Build handoff prompt for new agent\n *\n * @param context - Handoff context\n * @param additionalInstructions - Optional additional instructions\n * @returns Prompt for new agent\n */\nexport function buildHandoffPrompt(\n context: HandoffContext,\n additionalInstructions?: string\n): string {\n return Effect.runSync(renderPrompt({\n name: 'handoff-to-work',\n vars: {\n ISSUE_ID: context.issueId,\n PREVIOUS_MODEL: context.previousModel,\n REASON: context.reason,\n HANDOFF_CONTEXT: serializeHandoffContext(context),\n ADDITIONAL_INSTRUCTIONS_BLOCK: additionalInstructions || '',\n },\n }));\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect-channel variants. The async variants above stay so existing\n// callers keep working; Effect-based callers can compose without runPromise.\n\n/** Tagged error for handoff-context Effect variants. */\nexport class HandoffContextError extends Data.TaggedError('HandoffContextError')<{\n readonly issueId: string;\n readonly stage: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/** Effect variant of `captureHandoffContext`. */\nexport const captureHandoffContext = (\n agentState: AgentState,\n targetModel: string,\n reason: string,\n): Effect.Effect<HandoffContext, HandoffContextError> =>\n Effect.tryPromise({\n try: () => captureHandoffContextPromise(agentState, targetModel, reason),\n catch: (cause) =>\n new HandoffContextError({\n issueId: agentState.issueId,\n stage: 'captureHandoffContext',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","/**\n * Handoff Manager\n *\n * Orchestrates model handoffs for running agents using two methods:\n * 1. Kill & Spawn: For general agents (clean handoff with context preservation)\n * 2. Legacy specialist wake has been removed; all handoffs use role-based respawn.\n */\n\nimport { existsSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { Data, Effect } from 'effect';\nimport type { AgentState } from '../agents.js';\nimport { getAgentStateSync, saveAgentStateSync, stopAgentSync, spawnAgent, spawnRun, getAgentDir } from '../agents.js';\nimport type { HandoffContext } from './handoff-context.js';\nimport { captureHandoffContext, buildHandoffPrompt } from './handoff-context.js';\nimport { sessionExistsSync } from '../tmux.js';\nimport { requireModelOverrideSync } from '../model-validation.js';\n\n/**\n * Handoff method type\n */\nexport type HandoffMethod = 'kill-spawn' | 'specialist-wake';\n\n/**\n * Handoff result\n */\nexport interface HandoffResult {\n success: boolean;\n method: HandoffMethod;\n newAgentId?: string;\n newSessionId?: string;\n context?: HandoffContext;\n error?: string;\n}\n\n/**\n * Handoff options\n */\nexport interface HandoffOptions {\n targetModel: string;\n reason: string;\n method?: HandoffMethod; // Auto-detect if not specified\n waitForIdle?: boolean; // Wait for agent to be idle before killing (default: true)\n idleTimeoutMs?: number; // How long to wait for idle (default: 30000)\n additionalInstructions?: string; // Extra instructions for new agent\n}async function performHandoffPromise(\n agentId: string,\n options: HandoffOptions\n): Promise<HandoffResult> {\n // Get current agent state\n const state = getAgentStateSync(agentId);\n if (!state) {\n return {\n success: false,\n method: 'kill-spawn',\n error: `Agent ${agentId} not found`,\n };\n }\n\n let targetModel: string;\n try {\n targetModel = requireModelOverrideSync(options.targetModel);\n } catch (error) {\n return {\n success: false,\n method: 'kill-spawn',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n\n // Legacy specialist wake has been deleted; normalize all handoffs to role respawn.\n const method = options.method === 'specialist-wake' ? 'kill-spawn' : (options.method || detectHandoffMethod(agentId));\n\n return await performKillAndSpawn(state, { ...options, targetModel, method });\n}\n\n/**\n * Detect appropriate handoff method for an agent\n *\n * @param agentId - Agent ID\n * @returns Handoff method\n */\nfunction detectHandoffMethod(_agentId: string): HandoffMethod {\n return 'kill-spawn';\n}\n\n/**\n * Kill & Spawn handoff method\n *\n * Process:\n * 1. Signal agent to save state (update continue file)\n * 2. Wait for idle (check for activity)\n * 3. Capture handoff context\n * 4. Kill current agent\n * 5. Build handoff prompt\n * 6. Spawn new agent with target model\n *\n * @param state - Current agent state\n * @param options - Handoff options\n * @returns Handoff result\n */\nasync function performKillAndSpawn(\n state: AgentState,\n options: HandoffOptions\n): Promise<HandoffResult> {\n try {\n // Step 1: Signal agent to save state\n // TODO: Send message to agent asking to update the continue file\n // For now, we'll capture what's there\n\n // Step 2: Wait for idle if requested\n if (options.waitForIdle !== false) {\n const timeout = options.idleTimeoutMs || 30000;\n const idle = await waitForIdle(state.id, timeout);\n if (!idle) {\n console.warn(`Agent ${state.id} did not become idle within ${timeout}ms`);\n }\n }\n\n // Step 3: Capture handoff context\n const context = await Effect.runPromise(captureHandoffContext(state, options.targetModel, options.reason));\n\n // Step 4: Kill current agent\n stopAgentSync(state.id);\n\n // Step 5: Build handoff prompt\n const prompt = buildHandoffPrompt(context, options.additionalInstructions);\n\n // Save handoff prompt for debugging\n const handoffDir = join(getAgentDir(state.id), 'handoffs');\n mkdirSync(handoffDir, { recursive: true });\n const handoffFile = join(handoffDir, `handoff-${Date.now()}.md`);\n writeFileSync(handoffFile, prompt);\n\n // Step 6: Spawn new agent with target model\n // Use same agent ID to preserve identity\n const newState = await spawnAgent({\n issueId: state.issueId,\n workspace: state.workspace,\n harness: state.harness,\n model: options.targetModel,\n role: 'work',\n prompt,\n allowHost: state.hostOverride === true,\n });\n\n // Preserve accumulated cost without reintroducing legacy phase/complexity routing fields.\n newState.costSoFar = state.costSoFar || 0;\n saveAgentStateSync(newState);\n\n return {\n success: true,\n method: 'kill-spawn',\n newAgentId: newState.id,\n context,\n };\n } catch (error) {\n return {\n success: false,\n method: 'kill-spawn',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n/**\n * Wait for agent to become idle\n *\n * @param agentId - Agent ID\n * @param timeoutMs - Timeout in milliseconds\n * @returns True if agent became idle, false if timeout\n */\nasync function waitForIdle(agentId: string, timeoutMs: number): Promise<boolean> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n // Check if agent session still exists\n if (!sessionExistsSync(agentId)) {\n return true; // Agent is gone, consider it idle\n }\n\n // Check for recent activity\n // TODO: Implement proper activity detection\n // For now, just wait a bit\n await sleep(1000);\n }\n\n return false; // Timeout\n}\n\n/**\n * Sleep helper\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Check if handoff is recommended for an agent\n *\n * This is a placeholder for future trigger logic.\n * Triggers will be implemented in Phase C.\n *\n * @param agentId - Agent ID\n * @returns True if handoff is recommended\n */\nexport function shouldHandoff(agentId: string): boolean {\n // TODO: Implement trigger logic in Phase C\n return false;\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// The handoff orchestration touches multiple subsystems (model validation,\n// agent state, tmux session detection, prompt persistence) — failures from any\n// of them surface here. The Effect variant collapses these into a single typed\n// error channel so callers can `Effect.catchTag` the relevant case.\n\n/** Tagged error for `performHandoffProgram` failures. */\nexport class HandoffError extends Data.TaggedError('HandoffError')<{\n readonly agentId: string;\n readonly stage: string;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/**\n * Effect variant of `performHandoff`. Wraps the Promise-based implementation\n * and maps any thrown error into a typed `HandoffError`. The success result is\n * the same `HandoffResult` shape as the sync variant — including `success:\n * false` results when the agent isn't found or the model override is invalid.\n */\nexport const performHandoff = (\n agentId: string,\n options: HandoffOptions,\n): Effect.Effect<HandoffResult, HandoffError> =>\n Effect.tryPromise({\n try: () => performHandoffPromise(agentId, options),\n catch: (cause) =>\n new HandoffError({\n agentId,\n stage: 'performHandoff',\n message: cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n });\n","/**\n * Handoff Triggers\n *\n * Detects conditions that should trigger model handoffs:\n * 1. Stuck escalation - Agent inactive for too long\n * 2. Planning complete - Planning phase finished, ready for implementation\n * 3. Test failure - Tests failing, need more powerful model\n * 4. Task completion - Implementation done, ready for specialist testing\n */\n\nimport { existsSync, statSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport type { AgentHealth } from './health.js';\nimport type { CloisterConfig } from './config.js';\nimport { loadCloisterConfigSync } from './config.js';\nimport { withBdMutex } from '../bd-mutex.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Cache for checkTaskCompletion keyed by `${workspace}::${issueId}`.\n * Invalidated by mtime of `.beads/issues.jsonl` — same trick as\n * computePlanningState (PAN-1024 hotfix). The handoff-suggestion endpoint\n * is polled every 30s per agent panel; without this cache, every poll\n * fires two `bd list --json` invocations which each take ~1.87s wall-clock\n * and 1+ MB of disk I/O, causing sustained disk thrashing across N agents.\n *\n * Single-flight via a Promise map prevents stampedes when the page first\n * loads multiple agent panels concurrently.\n */\ninterface TaskCompletionCacheEntry {\n mtimeMs: number;\n result: TriggerDetection;\n}\nconst taskCompletionCache = new Map<string, TaskCompletionCacheEntry>();\nconst taskCompletionInflight = new Map<string, Promise<TriggerDetection>>();\n\n/**\n * Trigger type\n */\nexport type TriggerType =\n | 'stuck_escalation'\n | 'test_failure'\n | 'task_complete'\n | 'manual';\n\n/**\n * Trigger detection result\n */\nexport interface TriggerDetection {\n triggered: boolean;\n type: TriggerType;\n reason: string;\n suggestedModel?: string;\n confidence: 'high' | 'medium' | 'low';\n}\n\n/**\n * Check if agent should be handed off due to stuck escalation\n *\n * Model-specific thresholds:\n * - Haiku: Stuck after 10 minutes → escalate to Sonnet\n * - Sonnet: Stuck after 20 minutes → escalate to Opus\n * - Opus: Stuck after 30 minutes → alert user (no auto-escalation)\n *\n * @param health - Agent health state\n * @param currentModel - Current model\n * @param config - Cloister configuration\n * @returns Trigger detection result\n */\nexport function checkStuckEscalation(\n health: AgentHealth,\n currentModel: string,\n config?: CloisterConfig\n): TriggerDetection {\n const conf = config || loadCloisterConfigSync();\n\n // Get stuck escalation config\n const stuckConfig = conf.handoffs?.auto_triggers?.stuck_escalation;\n if (!stuckConfig?.enabled) {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'Stuck escalation disabled in config',\n confidence: 'high',\n };\n }\n\n // Check if agent is stuck based on health state\n if (health.state !== 'stuck') {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: `Agent is ${health.state}, not stuck`,\n confidence: 'high',\n };\n }\n\n // Get minutes since last activity\n if (!health.lastActivity) {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'No last activity timestamp available',\n confidence: 'low',\n };\n }\n const minutesSinceActivity = (Date.now() - health.lastActivity.getTime()) / (1000 * 60);\n\n // Check model-specific thresholds\n if (currentModel === 'haiku' && minutesSinceActivity >= stuckConfig.haiku_to_sonnet_minutes) {\n return {\n triggered: true,\n type: 'stuck_escalation',\n reason: `Haiku agent stuck for ${Math.round(minutesSinceActivity)} minutes`,\n suggestedModel: 'sonnet',\n confidence: 'high',\n };\n }\n\n if (currentModel === 'sonnet' && minutesSinceActivity >= stuckConfig.sonnet_to_opus_minutes) {\n return {\n triggered: true,\n type: 'stuck_escalation',\n reason: `Sonnet agent stuck for ${Math.round(minutesSinceActivity)} minutes`,\n suggestedModel: 'opus',\n confidence: 'high',\n };\n }\n\n if (currentModel === 'opus') {\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: 'Opus agent stuck - no higher model available, manual intervention needed',\n confidence: 'high',\n };\n }\n\n return {\n triggered: false,\n type: 'stuck_escalation',\n reason: `Agent stuck but threshold not reached (${Math.round(minutesSinceActivity)} minutes)`,\n confidence: 'medium',\n };\n}\n\n/**\n * Check if test failures should trigger escalation\n *\n * Aggressive escalation: Any test failure from Haiku escalates to Sonnet\n * Reasoning: Haiku is for simple tasks - if tests fail, the task isn't simple\n *\n * @param workspace - Workspace path\n * @param currentModel - Current model\n * @param config - Cloister configuration\n * @returns Trigger detection result\n */\nexport function checkTestFailure(\n workspace: string,\n currentModel: string,\n config?: CloisterConfig\n): TriggerDetection {\n const conf = config || loadCloisterConfigSync();\n\n // Get test failure config\n const testConfig = conf.handoffs?.auto_triggers?.test_failure;\n if (!testConfig?.enabled) {\n return {\n triggered: false,\n type: 'test_failure',\n reason: 'Test failure escalation disabled in config',\n confidence: 'high',\n };\n }\n\n // Only escalate from Haiku or configured from_model\n if (currentModel !== testConfig.from_model) {\n return {\n triggered: false,\n type: 'test_failure',\n reason: `Test failure escalation only applies to ${testConfig.from_model} model`,\n confidence: 'high',\n };\n }\n\n // Check for test failures\n // Look for common test result files/patterns\n const testFailure = detectTestFailure(workspace);\n\n if (testFailure.failed) {\n return {\n triggered: true,\n type: 'test_failure',\n reason: `Test failures detected: ${testFailure.reason}`,\n suggestedModel: testConfig.to_model,\n confidence: testFailure.confidence,\n };\n }\n\n return {\n triggered: false,\n type: 'test_failure',\n reason: 'No test failures detected',\n confidence: 'medium',\n };\n}\n\n/**\n * Detect test failures in workspace\n *\n * Checks for:\n * - npm test output\n * - Jest results\n * - pytest results\n * - cargo test results\n *\n * @param workspace - Workspace path\n * @returns Test failure detection\n */\nfunction detectTestFailure(workspace: string): {\n failed: boolean;\n reason: string;\n confidence: 'high' | 'medium' | 'low';\n} {\n // TODO: Implement proper test result detection\n // For now, this is a placeholder that checks for common test result files\n\n // Check for .test-results or similar\n const commonTestPaths = [\n '.test-results',\n 'test-results',\n 'junit.xml',\n 'coverage',\n '.nyc_output',\n ];\n\n // This is a simplified check - in reality we'd parse test output\n return {\n failed: false,\n reason: 'Test result detection not yet implemented',\n confidence: 'low',\n };\n}async function checkTaskCompletionPromise(\n issueId: string,\n config?: CloisterConfig,\n workspace?: string,\n): Promise<TriggerDetection> {\n const conf = config || loadCloisterConfigSync();\n\n // Get task completion config\n const completionConfig = conf.handoffs?.auto_triggers?.implementation_complete;\n if (!completionConfig?.enabled) {\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'Task completion detection disabled in config',\n confidence: 'high',\n };\n }\n\n // mtime-based cache\n const cacheKey = `${workspace ?? ''}::${issueId.toLowerCase()}`;\n let mtimeMs = 0;\n if (workspace) {\n const beadsFile = join(workspace, '.beads', 'issues.jsonl');\n try {\n mtimeMs = statSync(beadsFile).mtimeMs;\n } catch {\n // No beads file — fall through to compute (cheap when JSONL absent).\n }\n if (mtimeMs > 0) {\n const cached = taskCompletionCache.get(cacheKey);\n if (cached && cached.mtimeMs === mtimeMs) {\n return cached.result;\n }\n // Single-flight: if another request is computing this same key, await\n // its result instead of firing another pair of `bd list` invocations.\n const inflight = taskCompletionInflight.get(cacheKey);\n if (inflight) return inflight;\n }\n }\n\n const computePromise = (async (): Promise<TriggerDetection> => {\n // PAN-1812: the workspace beads DB must be queried from the agent's own\n // workspace. Without cwd the query can run against the wrong directory and\n // falsely report no open beads.\n if (!workspace) {\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'No workspace available to check task completion',\n confidence: 'high',\n };\n }\n\n const bdOptions = { encoding: 'utf-8' as const, cwd: workspace };\n const issueLower = issueId.toLowerCase();\n\n try {\n const { stdout: output } = await Effect.runPromise(withBdMutex(() => Effect.tryPromise({\n try: () => execAsync(`bd list --json --title-contains ${issueLower} --status closed`, bdOptions),\n catch: (cause) => cause,\n })));\n const tasks = JSON.parse(output);\n const implementTask = tasks.find((t: any) =>\n t.title.toLowerCase().includes('implement') ||\n t.labels?.includes('implementation')\n );\n\n if (implementTask) {\n const { stdout: openOutput } = await execAsync(`bd list --json --title-contains ${issueLower} --status open`, bdOptions);\n const openTasks = JSON.parse(openOutput);\n\n if (openTasks.length === 0) {\n return {\n triggered: true,\n type: 'task_complete',\n reason: 'Implementation task closed, no remaining tasks',\n suggestedModel: completionConfig.to_specialist,\n confidence: 'high',\n };\n }\n return {\n triggered: false,\n type: 'task_complete',\n reason: `Implementation task closed but ${openTasks.length} tasks remain`,\n confidence: 'medium',\n };\n }\n } catch {\n // Beads not available or error querying — fall through to default.\n }\n\n return {\n triggered: false,\n type: 'task_complete',\n reason: 'No implementation completion signals detected',\n confidence: 'high',\n };\n })();\n\n if (workspace && mtimeMs > 0) {\n taskCompletionInflight.set(cacheKey, computePromise);\n try {\n const result = await computePromise;\n taskCompletionCache.set(cacheKey, { mtimeMs, result });\n return result;\n } finally {\n taskCompletionInflight.delete(cacheKey);\n }\n }\n return computePromise;\n}async function checkAllTriggersPromise(\n agentId: string,\n workspace: string,\n issueId: string,\n currentModel: string,\n health: AgentHealth,\n config?: CloisterConfig\n): Promise<TriggerDetection[]> {\n const triggers: TriggerDetection[] = [];\n\n // Check each trigger type\n const stuckCheck = checkStuckEscalation(health, currentModel, config);\n if (stuckCheck.triggered) triggers.push(stuckCheck);\n\n const testCheck = checkTestFailure(workspace, currentModel, config);\n if (testCheck.triggered) triggers.push(testCheck);\n\n const completionCheck = await Effect.runPromise(checkTaskCompletion(issueId, config, workspace));\n if (completionCheck.triggered) triggers.push(completionCheck);\n\n return triggers;\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n\n/**\n * Effect-typed variant of {@link checkTaskCompletion}. Wraps the Promise-based\n * implementation; never fails (the underlying function swallows errors and\n * returns a \"not triggered\" detection on failure).\n */\nexport function checkTaskCompletion(\n issueId: string,\n config?: CloisterConfig,\n workspace?: string,\n): Effect.Effect<TriggerDetection> {\n return Effect.promise(() => checkTaskCompletionPromise(issueId, config, workspace));\n}\n\n/**\n * Effect-typed variant of {@link checkAllTriggers}. Never fails — uses\n * `Effect.promise` because the underlying async path absorbs bd / fs errors.\n */\nexport function checkAllTriggers(\n agentId: string,\n workspace: string,\n issueId: string,\n currentModel: string,\n health: AgentHealth,\n config?: CloisterConfig,\n): Effect.Effect<TriggerDetection[]> {\n return Effect.promise(() => checkAllTriggersPromise(agentId, workspace, issueId, currentModel, health, config));\n}\n","/**\n * Handoff Event Logger\n *\n * Logs handoff events to JSONL file for tracking and analysis.\n */\n\nimport { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs';\nimport { appendFile, mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport type { HandoffContext } from './handoff-context.js';\nimport type { TriggerType } from './triggers.js';\n\n/**\n * Handoff event structure\n */\nexport interface HandoffEvent {\n timestamp: string;\n agentId: string;\n issueId: string;\n\n // Model transition\n from: {\n model: string;\n runtime: string;\n sessionId?: string;\n };\n to: {\n model: string;\n runtime: string;\n sessionId?: string;\n };\n\n // Trigger information\n trigger: TriggerType | 'manual';\n reason: string;\n\n // Context\n context: {\n beadsTaskCompleted?: string;\n stuckMinutes?: number;\n costAtHandoff?: number;\n handoffCount?: number;\n };\n\n // Result - handoff OPERATION success (did we spawn new agent?)\n success: boolean;\n errorMessage?: string;\n\n // Recovery outcome - did the agent ACTUALLY recover?\n // This is verified after the handoff, not at handoff time\n outcome?: {\n verified: boolean; // Has recovery been checked?\n agentRecovered: boolean; // Did the agent start making progress?\n verifiedAt?: string; // When was this verified?\n verificationMethod?: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n };\n}\n\n/**\n * Handoff log file path\n */\nconst HANDOFF_LOG_FILE = join(OVERDECK_HOME, 'logs', 'handoffs.jsonl');\n\n/**\n * Ensure log directory exists\n */\nfunction ensureLogDir(): void {\n const logDir = join(OVERDECK_HOME, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n}\n\n/**\n * Log a handoff event\n *\n * @param event - Handoff event to log\n */\nexport function logHandoffEventSync(event: HandoffEvent): void {\n ensureLogDir();\n\n const line = JSON.stringify(event) + '\\n';\n appendFileSync(HANDOFF_LOG_FILE, line, 'utf-8');\n}\n\n/**\n * Create a handoff event from handoff result\n *\n * @param agentId - Agent ID\n * @param issueId - Issue ID\n * @param context - Handoff context\n * @param trigger - Trigger type\n * @param success - Whether handoff succeeded\n * @param errorMessage - Error message if failed\n * @returns Handoff event\n */\nexport function createHandoffEvent(\n agentId: string,\n issueId: string,\n context: HandoffContext,\n trigger: TriggerType | 'manual',\n success: boolean,\n errorMessage?: string\n): HandoffEvent {\n // Calculate stuck minutes if applicable\n let stuckMinutes: number | undefined;\n if (trigger === 'stuck_escalation') {\n // This would be calculated from health state\n // For now, we'll omit it unless available in context\n }\n\n return {\n timestamp: new Date().toISOString(),\n agentId,\n issueId,\n from: {\n model: context.previousModel,\n runtime: context.previousRuntime,\n sessionId: context.previousSessionId,\n },\n to: {\n model: context.targetModel,\n runtime: context.previousRuntime, // New agent inherits the same harness\n sessionId: undefined, // Will be set after spawn\n },\n trigger,\n reason: context.reason,\n context: {\n costAtHandoff: context.costSoFar,\n handoffCount: context.handoffCount,\n stuckMinutes,\n },\n success,\n errorMessage,\n };\n}\n\n/**\n * Read all handoff events from log\n *\n * @param limit - Maximum number of events to return (most recent first)\n * @returns Array of handoff events\n */\nexport function readHandoffEventsSync(limit?: number): HandoffEvent[] {\n ensureLogDir();\n\n if (!existsSync(HANDOFF_LOG_FILE)) {\n return [];\n }\n\n const content = readFileSync(HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n const events = lines.map(line => JSON.parse(line) as HandoffEvent);\n\n // Return most recent first\n events.reverse();\n\n if (limit) {\n return events.slice(0, limit);\n }\n\n return events;\n}\n\n/**\n * Read handoff events for a specific issue\n *\n * @param issueId - Issue ID\n * @returns Array of handoff events for the issue\n */\nexport function readIssueHandoffEventsSync(issueId: string): HandoffEvent[] {\n const allEvents = readHandoffEventsSync();\n return allEvents.filter(e => e.issueId === issueId);\n}\n\n/**\n * Read handoff events for a specific agent\n *\n * @param agentId - Agent ID\n * @returns Array of handoff events for the agent\n */\nexport function readAgentHandoffEventsSync(agentId: string): HandoffEvent[] {\n const allEvents = readHandoffEventsSync();\n return allEvents.filter(e => e.agentId === agentId);\n}\n\n/**\n * Get handoff statistics\n *\n * Returns both operation success rate (handoff executed) and recovery success rate\n * (agent actually recovered). These are DIFFERENT metrics:\n *\n * - operationSuccessRate: Did the handoff operation complete successfully?\n * - recoverySuccessRate: Did the agent actually start making progress after handoff?\n *\n * @returns Handoff statistics\n */\nexport function getHandoffStats(): {\n totalHandoffs: number;\n todayEscalations: number; // Model escalations (stuck/cost/test) that occurred today\n byTrigger: Record<string, number>;\n byModel: {\n from: Record<string, number>;\n to: Record<string, number>;\n };\n // Legacy field for backwards compatibility\n successRate: number;\n // More detailed metrics\n operationSuccessRate: number; // Handoff operation succeeded\n recoverySuccessRate: number; // Agent actually recovered (verified only)\n pendingVerification: number; // Handoffs that haven't been verified yet\n verifiedCount: number; // Total handoffs that have been verified\n} {\n const events = readHandoffEventsSync();\n const today = new Date().toISOString().split('T')[0];\n\n const stats = {\n totalHandoffs: events.length,\n todayEscalations: 0,\n byTrigger: {} as Record<string, number>,\n byModel: {\n from: {} as Record<string, number>,\n to: {} as Record<string, number>,\n },\n successRate: 0,\n operationSuccessRate: 0,\n recoverySuccessRate: 0,\n pendingVerification: 0,\n verifiedCount: 0,\n };\n\n let operationSuccessCount = 0;\n let recoverySuccessCount = 0;\n\n for (const event of events) {\n // Count today's escalations (all trigger types are escalations)\n if (event.timestamp.startsWith(today)) {\n stats.todayEscalations++;\n }\n\n // Count by trigger\n stats.byTrigger[event.trigger] = (stats.byTrigger[event.trigger] || 0) + 1;\n\n // Count by model\n stats.byModel.from[event.from.model] = (stats.byModel.from[event.from.model] || 0) + 1;\n stats.byModel.to[event.to.model] = (stats.byModel.to[event.to.model] || 0) + 1;\n\n // Count operation successes (handoff executed)\n if (event.success) {\n operationSuccessCount++;\n }\n\n // Count recovery outcomes (agent actually recovered)\n if (event.outcome?.verified) {\n stats.verifiedCount++;\n if (event.outcome.agentRecovered) {\n recoverySuccessCount++;\n }\n } else {\n stats.pendingVerification++;\n }\n }\n\n // Calculate rates\n stats.operationSuccessRate = events.length > 0 ? operationSuccessCount / events.length : 0;\n stats.recoverySuccessRate = stats.verifiedCount > 0 ? recoverySuccessCount / stats.verifiedCount : 0;\n\n // Legacy successRate - use operation success for backwards compatibility\n // but NOTE: This is misleading for \"did the agent recover?\"\n stats.successRate = stats.operationSuccessRate;\n\n return stats;\n}\n\n/**\n * Update the outcome of a handoff event (verify if agent recovered)\n *\n * @param agentId - Agent ID\n * @param timestamp - Original handoff timestamp\n * @param outcome - Recovery outcome\n */\nexport function updateHandoffOutcomeSync(\n agentId: string,\n timestamp: string,\n outcome: {\n agentRecovered: boolean;\n verificationMethod: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n }\n): void {\n ensureLogDir();\n\n if (!existsSync(HANDOFF_LOG_FILE)) {\n return;\n }\n\n const content = readFileSync(HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n // Find and update the matching event\n const updatedLines = lines.map(line => {\n const event = JSON.parse(line) as HandoffEvent;\n if (event.agentId === agentId && event.timestamp === timestamp) {\n event.outcome = {\n verified: true,\n agentRecovered: outcome.agentRecovered,\n verifiedAt: new Date().toISOString(),\n verificationMethod: outcome.verificationMethod,\n notes: outcome.notes,\n };\n return JSON.stringify(event);\n }\n return line;\n });\n\n // Rewrite the file\n writeFileSync(HANDOFF_LOG_FILE, updatedLines.join('\\n') + '\\n', 'utf-8');\n}\n\n/**\n * Get handoffs pending verification\n *\n * @returns Array of handoff events that need outcome verification\n */\nexport function getPendingVerificationHandoffsSync(): HandoffEvent[] {\n const events = readHandoffEventsSync();\n return events.filter(e => e.success && !e.outcome?.verified);\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Async, typed-error variants of the handoff log helpers. The sync variants\n// are preserved for the CLI surfaces (e.g. `pan show`) that already run inside\n// a one-shot Node process where blocking is acceptable; the Effect variants\n// are appropriate for the dashboard server's request handlers.\n\nconst ensureLogDirAsync = (): Effect.Effect<void, FsError> => {\n const logDir = join(OVERDECK_HOME, 'logs');\n return Effect.tryPromise({\n try: () => mkdir(logDir, { recursive: true }),\n catch: (cause) => new FsError({ path: logDir, operation: 'mkdir', cause }),\n });\n};\n\n/** Effect variant of `logHandoffEvent`. */\nexport const logHandoffEvent = (event: HandoffEvent): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n const line = JSON.stringify(event) + '\\n';\n yield* Effect.tryPromise({\n try: () => appendFile(HANDOFF_LOG_FILE, line, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'appendFile', cause }),\n });\n });\n\n/** Effect variant of `readHandoffEvents`. */\nexport const readHandoffEvents = (limit?: number): Effect.Effect<HandoffEvent[], FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n if (!existsSync(HANDOFF_LOG_FILE)) return [];\n\n const content = yield* Effect.tryPromise({\n try: () => readFile(HANDOFF_LOG_FILE, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'readFile', cause }),\n });\n\n const lines = content.trim().split('\\n').filter((line) => line.trim());\n const events = lines.map((line) => JSON.parse(line) as HandoffEvent);\n events.reverse();\n return limit ? events.slice(0, limit) : events;\n });\n\n/** Effect variant of `readIssueHandoffEvents`. */\nexport const readIssueHandoffEvents = (\n issueId: string,\n): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(Effect.map((events) => events.filter((e) => e.issueId === issueId)));\n\n/** Effect variant of `readAgentHandoffEvents`. */\nexport const readAgentHandoffEvents = (\n agentId: string,\n): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(Effect.map((events) => events.filter((e) => e.agentId === agentId)));\n\n/** Effect variant of `updateHandoffOutcome`. */\nexport const updateHandoffOutcome = (\n agentId: string,\n timestamp: string,\n outcome: {\n agentRecovered: boolean;\n verificationMethod: 'heartbeat' | 'manual' | 'task_complete';\n notes?: string;\n },\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n yield* ensureLogDirAsync();\n if (!existsSync(HANDOFF_LOG_FILE)) return;\n\n const content = yield* Effect.tryPromise({\n try: () => readFile(HANDOFF_LOG_FILE, 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'readFile', cause }),\n });\n\n const lines = content.trim().split('\\n').filter((line) => line.trim());\n\n const updatedLines = lines.map((line) => {\n const event = JSON.parse(line) as HandoffEvent;\n if (event.agentId === agentId && event.timestamp === timestamp) {\n event.outcome = {\n verified: true,\n agentRecovered: outcome.agentRecovered,\n verifiedAt: new Date().toISOString(),\n verificationMethod: outcome.verificationMethod,\n notes: outcome.notes,\n };\n return JSON.stringify(event);\n }\n return line;\n });\n\n yield* Effect.tryPromise({\n try: () => writeFile(HANDOFF_LOG_FILE, updatedLines.join('\\n') + '\\n', 'utf-8'),\n catch: (cause) => new FsError({ path: HANDOFF_LOG_FILE, operation: 'writeFile', cause }),\n });\n });\n\n/** Effect variant of `getPendingVerificationHandoffs`. */\nexport const getPendingVerificationHandoffs = (): Effect.Effect<HandoffEvent[], FsError> =>\n readHandoffEvents().pipe(\n Effect.map((events) => events.filter((e) => e.success && !e.outcome?.verified)),\n );\n","/**\n * FPP (Fixed Point Principle) Violation Detection\n *\n * \"Any runnable action is a fixed point and must resolve before the\n * system can rest.\" Borrowed from Gastown; inspired by Doctor Who.\n *\n * Detects when agents have pending work but are idle, and sends\n * escalating nudges to get them back on track.\n */\n\nimport { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';\nimport { join, dirname } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { checkHookSync } from '../hooks.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { getAgentHealth } from './health.js';\nimport { OVERDECK_HOME } from '../paths.js';\n\n/**\n * FPP violation types\n *\n * Currently only 'hook_idle' is implemented. Additional types can be added\n * when detection logic is implemented for PR staleness, review pending, etc.\n */\nexport type FPPViolationType = 'hook_idle';\n\n/**\n * FPP violation record\n */\nexport interface FPPViolation {\n agentId: string;\n type: FPPViolationType;\n detectedAt: string;\n nudgeCount: number;\n lastNudgeAt?: string;\n resolved: boolean;\n}\n\n/**\n * FPP violation configuration\n */\nexport interface FPPViolationConfig {\n hook_idle_minutes: number;\n pr_approved_minutes: number;\n review_pending_minutes: number;\n max_nudges: number;\n}\n\n/**\n * Default FPP violation configuration\n */\nexport const DEFAULT_FPP_CONFIG: FPPViolationConfig = {\n hook_idle_minutes: 5,\n pr_approved_minutes: 10,\n review_pending_minutes: 15,\n max_nudges: 3,\n};\n\n/**\n * Path to violations data file\n */\nconst VIOLATIONS_DATA_FILE = join(OVERDECK_HOME, 'fpp-violations.json');\n\n/**\n * Persisted violations data format\n */\ninterface ViolationsDataPersisted {\n violations: Array<[string, FPPViolation]>;\n}\n\n/**\n * Load violations from file\n */\nfunction loadViolations(): Map<string, FPPViolation> {\n if (!existsSync(VIOLATIONS_DATA_FILE)) {\n return new Map();\n }\n\n try {\n const fileContent = readFileSync(VIOLATIONS_DATA_FILE, 'utf-8');\n const persisted: ViolationsDataPersisted = JSON.parse(fileContent);\n\n return new Map(persisted.violations || []);\n } catch (error) {\n console.error('Failed to load violations data, starting fresh:', error);\n return new Map();\n }\n}\n\n/**\n * Save violations to file (atomic write)\n */\nfunction saveViolations(violations: Map<string, FPPViolation>): void {\n try {\n // Ensure directory exists\n const dir = dirname(VIOLATIONS_DATA_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const persisted: ViolationsDataPersisted = {\n violations: Array.from(violations.entries()),\n };\n\n // Atomic write: write to temp file, then rename\n const tempFile = `${VIOLATIONS_DATA_FILE}.tmp`;\n writeFileSync(tempFile, JSON.stringify(persisted, null, 2));\n writeFileSync(VIOLATIONS_DATA_FILE, readFileSync(tempFile));\n\n // Clean up temp file\n try {\n unlinkSync(tempFile);\n } catch (unlinkError: unknown) {\n // Non-critical: temp file cleanup failure is logged but doesn't block operation\n console.debug('Failed to cleanup temp file:', unlinkError instanceof Error ? unlinkError.message : unlinkError);\n }\n } catch (error) {\n console.error('Failed to save violations data:', error);\n }\n}\n\n/**\n * Store of active violations (persisted to disk)\n */\nconst activeViolations = loadViolations();\n\n/**\n * Get escalating nudge message based on nudge count\n *\n * Currently only supports 'hook_idle' type. Messages escalate from\n * a gentle status check to a direct action request.\n */\nfunction getNudgeMessage(_violation: FPPViolation, nudgeCount: number): string {\n // Currently only 'hook_idle' is implemented, so messages are tailored for that case\n if (nudgeCount === 1) {\n // Nudge 1: Status check\n return \"What's your current status? You have pending work on your hook.\";\n } else if (nudgeCount === 2) {\n // Nudge 2: Gentle reminder\n return \"I notice you've been idle while having pending work. Do you need help with the task on your hook?\";\n } else {\n // Nudge 3: Direct action\n return \"You have pending work on your hook that needs attention. Execute it now or explain why you're blocked.\";\n }\n}\n\n/**\n * Check for FPP violations on an agent\n *\n * @param agentId - Agent to check\n * @param config - FPP violation configuration\n * @returns Violation if detected, null otherwise\n */\nexport function checkAgentForViolations(\n agentId: string,\n config: FPPViolationConfig = DEFAULT_FPP_CONFIG\n): FPPViolation | null {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) return null;\n\n // Get agent health to check activity\n const health = getAgentHealth(agentId, runtime);\n if (!health) return null;\n\n // Only check agents that are idle (stale, warning, stuck)\n if (health.state === 'active') return null;\n\n // Calculate idle time in minutes\n const idleMinutes = health.timeSinceActivity\n ? Math.floor(health.timeSinceActivity / (60 * 1000))\n : 0;\n\n // Check for work on hook\n let hookStatus: ReturnType<typeof checkHookSync>;\n try {\n hookStatus = checkHookSync(agentId);\n } catch (error) {\n console.error(`Failed to check hook for ${agentId}:`, error);\n return null;\n }\n\n if (hookStatus.hasWork && idleMinutes >= config.hook_idle_minutes) {\n // Check if we already have an active violation\n const existingKey = `${agentId}-hook_idle`;\n const existing = activeViolations.get(existingKey);\n\n if (existing && !existing.resolved) {\n return existing;\n }\n\n // Create new violation\n const violation: FPPViolation = {\n agentId,\n type: 'hook_idle',\n detectedAt: new Date().toISOString(),\n nudgeCount: 0,\n resolved: false,\n };\n\n activeViolations.set(existingKey, violation);\n saveViolations(activeViolations);\n return violation;\n }\n\n return null;\n}\n\n/**\n * Send a nudge to an agent about an FPP violation\n *\n * @param violation - The violation to nudge about\n * @returns True if nudge was sent successfully\n */\nexport function sendNudge(violation: FPPViolation): boolean {\n const runtime = getRuntimeForAgent(violation.agentId);\n if (!runtime) return false;\n\n // Increment nudge count\n violation.nudgeCount++;\n violation.lastNudgeAt = new Date().toISOString();\n\n // Get escalating message\n const message = getNudgeMessage(violation, violation.nudgeCount);\n\n try {\n runtime.sendMessage(violation.agentId, message);\n console.log(`🔔 Sent FPP nudge #${violation.nudgeCount} to ${violation.agentId}`);\n saveViolations(activeViolations);\n return true;\n } catch (error) {\n console.error(`Failed to send nudge to ${violation.agentId}:`, error);\n return false;\n }\n}\n\n/**\n * Resolve a violation (agent has addressed the issue)\n *\n * @param agentId - Agent ID\n * @param type - Violation type\n */\nexport function resolveViolationSync(agentId: string, type: FPPViolationType): void {\n const key = `${agentId}-${type}`;\n const violation = activeViolations.get(key);\n\n if (violation) {\n violation.resolved = true;\n console.log(`🔔 FPP violation resolved for ${agentId}: ${type}`);\n saveViolations(activeViolations);\n }\n}\n\n/**\n * Get all active violations\n *\n * @returns Array of active violations\n */\nexport function getActiveViolations(): FPPViolation[] {\n return Array.from(activeViolations.values()).filter(v => !v.resolved);\n}\n\n/**\n * Get violations for a specific agent\n *\n * @param agentId - Agent ID\n * @returns Array of violations for this agent\n */\nexport function getAgentViolations(agentId: string): FPPViolation[] {\n return getActiveViolations().filter(v => v.agentId === agentId);\n}\n\n/**\n * Check if an agent has exceeded max nudges\n *\n * @param violation - The violation to check\n * @param config - FPP violation configuration\n * @returns True if max nudges exceeded\n */\nexport function hasExceededMaxNudges(\n violation: FPPViolation,\n config: FPPViolationConfig = DEFAULT_FPP_CONFIG\n): boolean {\n return violation.nudgeCount >= config.max_nudges;\n}\n\n/**\n * Clear resolved violations older than a certain time\n *\n * @param hoursOld - Clear violations resolved this many hours ago\n */\nexport function clearOldViolationsSync(hoursOld: number = 24): void {\n const cutoff = Date.now() - hoursOld * 60 * 60 * 1000;\n let changed = false;\n\n for (const [key, violation] of activeViolations.entries()) {\n if (violation.resolved && new Date(violation.detectedAt).getTime() < cutoff) {\n activeViolations.delete(key);\n changed = true;\n }\n }\n\n if (changed) {\n saveViolations(activeViolations);\n }\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Async persistence helpers around the in-memory `activeViolations` map. These\n// are additive: the sync variants stay in place because the module-level cache\n// is populated at import time and most callers happen on the orchestrator's\n// patrol thread where sync I/O is acceptable. Route handlers should prefer\n// the Effect variants.\n\nconst saveViolationsAsync = (violations: Map<string, FPPViolation>): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const dir = dirname(VIOLATIONS_DATA_FILE);\n yield* Effect.tryPromise({\n try: () => mkdir(dir, { recursive: true }),\n catch: (cause) => new FsError({ path: dir, operation: 'mkdir', cause }),\n });\n\n const persisted: ViolationsDataPersisted = {\n violations: Array.from(violations.entries()),\n };\n\n const tempFile = `${VIOLATIONS_DATA_FILE}.tmp`;\n yield* Effect.tryPromise({\n try: () => writeFile(tempFile, JSON.stringify(persisted, null, 2), 'utf-8'),\n catch: (cause) => new FsError({ path: tempFile, operation: 'writeFile', cause }),\n });\n yield* Effect.tryPromise({\n try: () => rename(tempFile, VIOLATIONS_DATA_FILE),\n catch: (cause) => new FsError({ path: VIOLATIONS_DATA_FILE, operation: 'rename', cause }),\n });\n\n yield* Effect.tryPromise({\n try: () => unlink(tempFile),\n catch: (cause) => new FsError({ path: tempFile, operation: 'unlink', cause }),\n }).pipe(Effect.catch(() => Effect.succeed(undefined)));\n });\n\n/** Effect variant of `resolveViolation`. */\nexport const resolveViolation = (\n agentId: string,\n type: FPPViolationType,\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const key = `${agentId}-${type}`;\n const violation = activeViolations.get(key);\n if (!violation) return;\n\n violation.resolved = true;\n console.log(`🔔 FPP violation resolved for ${agentId}: ${type}`);\n yield* saveViolationsAsync(activeViolations);\n });\n\n/** Effect variant of `clearOldViolations`. */\nexport const clearOldViolations = (hoursOld: number = 24): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const cutoff = Date.now() - hoursOld * 60 * 60 * 1000;\n let changed = false;\n\n for (const [key, violation] of activeViolations.entries()) {\n if (violation.resolved && new Date(violation.detectedAt).getTime() < cutoff) {\n activeViolations.delete(key);\n changed = true;\n }\n }\n\n if (changed) {\n yield* saveViolationsAsync(activeViolations);\n }\n });\n\n// Silence \"unused\" warnings for fs/promises imports that may be picked up by\n// future expansions of the Effect variants above.\nvoid readFile;\n","/**\n * Cost Monitor\n *\n * Monitors agent costs against configured limits and emits alerts.\n * Does NOT automatically stop agents - just provides visibility and warnings.\n */\n\nimport { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';\nimport { join, dirname } from 'path';\nimport { Effect } from 'effect';\nimport { FsError } from '../errors.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { loadCloisterConfigSync, type CostLimitsConfig } from './config.js';\nimport {\n getAgentRollup,\n getDailyTrendsSync as getDailyTrends,\n getCostForIssueSync as getCostForIssueFromDb,\n getAgentDailyCostSync,\n} from '../overdeck/cost-sync.js';\n\n/**\n * Cost alert level\n */\nexport type CostAlertLevel = 'warning' | 'limit_reached';\n\n/**\n * Cost alert\n */\nexport interface CostAlert {\n type: 'per_agent' | 'per_issue' | 'daily_total';\n level: CostAlertLevel;\n agentId?: string;\n issueId?: string;\n currentCost: number;\n limit: number;\n percentUsed: number;\n timestamp: string;\n}\n\n/**\n * Cost tracking data (persisted format)\n */\ninterface CostDataPersisted {\n perAgent: Record<string, number>;\n perIssue: Record<string, number>;\n dailyTotal: number;\n lastResetDate: string; // ISO date string (YYYY-MM-DD)\n}\n\n/**\n * Cost tracking data (runtime format)\n */\ninterface CostData {\n perAgent: Map<string, number>;\n perIssue: Map<string, number>;\n dailyTotal: number;\n lastResetDate: string; // ISO date string (YYYY-MM-DD)\n}\n\n/**\n * Path to cost data file\n */\nconst COST_DATA_FILE = join(OVERDECK_HOME, 'cost-data.json');\n\n/**\n * Load cost data from file\n */\nfunction loadCostData(): CostData {\n if (!existsSync(COST_DATA_FILE)) {\n return {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: new Date().toISOString().split('T')[0],\n };\n }\n\n try {\n const fileContent = readFileSync(COST_DATA_FILE, 'utf-8');\n const persisted: CostDataPersisted = JSON.parse(fileContent);\n\n return {\n perAgent: new Map(Object.entries(persisted.perAgent || {})),\n perIssue: new Map(Object.entries(persisted.perIssue || {})),\n dailyTotal: persisted.dailyTotal || 0,\n lastResetDate: persisted.lastResetDate || new Date().toISOString().split('T')[0],\n };\n } catch (error) {\n console.error('Failed to load cost data, starting fresh:', error);\n return {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: new Date().toISOString().split('T')[0],\n };\n }\n}\n\n/**\n * Save cost data to file (atomic write)\n */\nfunction saveCostData(data: CostData): void {\n try {\n // Ensure directory exists\n const dir = dirname(COST_DATA_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const persisted: CostDataPersisted = {\n perAgent: Object.fromEntries(data.perAgent),\n perIssue: Object.fromEntries(data.perIssue),\n dailyTotal: data.dailyTotal,\n lastResetDate: data.lastResetDate,\n };\n\n // Atomic write: write to temp file, then rename\n const tempFile = `${COST_DATA_FILE}.tmp`;\n writeFileSync(tempFile, JSON.stringify(persisted, null, 2));\n writeFileSync(COST_DATA_FILE, readFileSync(tempFile));\n\n // Clean up temp file\n try {\n unlinkSync(tempFile);\n } catch (unlinkError: unknown) {\n // Non-critical: temp file cleanup failure is logged but doesn't block operation\n console.debug('Failed to cleanup temp file:', unlinkError instanceof Error ? unlinkError.message : unlinkError);\n }\n } catch (error) {\n console.error('Failed to save cost data:', error);\n }\n}\n\n// Load cost data on module initialization\nlet costData: CostData = loadCostData();\n\n/**\n * Get today's date as ISO string (YYYY-MM-DD)\n */\nfunction getTodayDate(): string {\n return new Date().toISOString().split('T')[0];\n}\n\n/**\n * Reset daily totals if it's a new day\n */\nfunction checkDailyReset(): void {\n const today = getTodayDate();\n if (costData.lastResetDate !== today) {\n costData.dailyTotal = 0;\n costData.lastResetDate = today;\n console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);\n saveCostData(costData);\n }\n}\n\n/**\n * Record a cost event\n *\n * @param agentId - Agent ID\n * @param cost - Cost in USD\n * @param issueId - Optional issue ID\n */\nexport function recordCostSync(agentId: string, cost: number, issueId?: string): void {\n checkDailyReset();\n\n // Update per-agent cost\n const currentAgentCost = costData.perAgent.get(agentId) || 0;\n costData.perAgent.set(agentId, currentAgentCost + cost);\n\n // Update per-issue cost\n if (issueId) {\n const currentIssueCost = costData.perIssue.get(issueId) || 0;\n costData.perIssue.set(issueId, currentIssueCost + cost);\n }\n\n // Update daily total\n costData.dailyTotal += cost;\n\n // Persist to disk\n saveCostData(costData);\n}\n\n/**\n * Check if any cost limits are being approached or exceeded\n *\n * @param agentId - Agent ID to check\n * @param issueId - Optional issue ID to check\n * @param config - Cost limits configuration\n * @returns Array of alerts (empty if no limits exceeded)\n */\nexport function checkCostLimits(\n agentId: string,\n issueId: string | undefined,\n config: CostLimitsConfig = loadCloisterConfigSync().cost_limits || {\n per_agent_usd: 10.0,\n per_issue_usd: 25.0,\n daily_total_usd: 100.0,\n alert_threshold: 0.8,\n }\n): CostAlert[] {\n const alerts: CostAlert[] = [];\n const now = new Date().toISOString();\n\n // Read agent cost from DB (scoped to today, matching the cap's window)\n // The accumulator should match the cap's window: per-agent daily cap compares daily spend\n if (config.per_agent_usd > 0) {\n const agentCost = getAgentDailyCostSync(agentId);\n const agentPercent = agentCost / config.per_agent_usd;\n\n if (agentPercent >= 1.0) {\n alerts.push({\n type: 'per_agent',\n level: 'limit_reached',\n agentId,\n currentCost: agentCost,\n limit: config.per_agent_usd,\n percentUsed: agentPercent * 100,\n timestamp: now,\n });\n } else if (agentPercent >= config.alert_threshold) {\n alerts.push({\n type: 'per_agent',\n level: 'warning',\n agentId,\n currentCost: agentCost,\n limit: config.per_agent_usd,\n percentUsed: agentPercent * 100,\n timestamp: now,\n });\n }\n }\n\n // Read issue cost from DB (scoped to today, matching the cap's window)\n if (issueId && config.per_issue_usd > 0) {\n const issueDailyTrends = getDailyTrends({ days: 1, issueId });\n const issueCost = issueDailyTrends.reduce((sum, t) => sum + t.totalCost, 0);\n const issuePercent = issueCost / config.per_issue_usd;\n\n if (issuePercent >= 1.0) {\n alerts.push({\n type: 'per_issue',\n level: 'limit_reached',\n issueId,\n currentCost: issueCost,\n limit: config.per_issue_usd,\n percentUsed: issuePercent * 100,\n timestamp: now,\n });\n } else if (issuePercent >= config.alert_threshold) {\n alerts.push({\n type: 'per_issue',\n level: 'warning',\n issueId,\n currentCost: issueCost,\n limit: config.per_issue_usd,\n percentUsed: issuePercent * 100,\n timestamp: now,\n });\n }\n }\n\n // Read today's total from DB (sum of today's events)\n if (config.daily_total_usd > 0) {\n const dailyTotal = getDailyTrends({ days: 1 }).reduce((sum, t) => sum + t.totalCost, 0);\n const dailyPercent = dailyTotal / config.daily_total_usd;\n\n if (dailyPercent >= 1.0) {\n alerts.push({\n type: 'daily_total',\n level: 'limit_reached',\n currentCost: dailyTotal,\n limit: config.daily_total_usd,\n percentUsed: dailyPercent * 100,\n timestamp: now,\n });\n } else if (dailyPercent >= config.alert_threshold) {\n alerts.push({\n type: 'daily_total',\n level: 'warning',\n currentCost: dailyTotal,\n limit: config.daily_total_usd,\n percentUsed: dailyPercent * 100,\n timestamp: now,\n });\n }\n }\n\n return alerts;\n}\n\n/**\n * Get current cost data for an agent\n *\n * @param agentId - Agent ID\n * @returns Current cost\n */\nexport function getAgentCost(agentId: string): number {\n return costData.perAgent.get(agentId) || 0;\n}\n\n/**\n * Get current cost data for an issue\n *\n * @param issueId - Issue ID\n * @returns Current cost\n */\nexport function getIssueCost(issueId: string): number {\n return costData.perIssue.get(issueId) || 0;\n}\n\n/**\n * Get current daily total cost\n *\n * @returns Current daily total\n */\nexport function getDailyTotal(): number {\n checkDailyReset();\n return costData.dailyTotal;\n}\n\n/**\n * Get cost summary\n *\n * @returns Cost summary with top spenders\n */\nexport function getCostSummary(): {\n dailyTotal: number;\n topAgents: Array<{ agentId: string; cost: number }>;\n topIssues: Array<{ issueId: string; cost: number }>;\n} {\n checkDailyReset();\n\n // Sort agents by cost\n const topAgents = Array.from(costData.perAgent.entries())\n .map(([agentId, cost]) => ({ agentId, cost }))\n .sort((a, b) => b.cost - a.cost)\n .slice(0, 10);\n\n // Sort issues by cost\n const topIssues = Array.from(costData.perIssue.entries())\n .map(([issueId, cost]) => ({ issueId, cost }))\n .sort((a, b) => b.cost - a.cost)\n .slice(0, 10);\n\n return {\n dailyTotal: costData.dailyTotal,\n topAgents,\n topIssues,\n };\n}\n\n/**\n * Reset cost tracking (for testing)\n */\nexport function resetCostTrackingSync(): void {\n costData = {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: getTodayDate(),\n };\n saveCostData(costData);\n}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n//\n// Additive Effect variants for cost-monitor I/O. The sync variants remain in\n// place because the module-level `costData` cache is populated synchronously at\n// import time; new callers (route handlers, services in the dashboard server)\n// can use these to avoid blocking the event loop on cost ledger writes.\n\n/** Persist the in-memory cost ledger to disk using fs/promises. */\nconst saveCostDataAsync = (data: CostData): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const dir = dirname(COST_DATA_FILE);\n yield* Effect.tryPromise({\n try: () => mkdir(dir, { recursive: true }),\n catch: (cause) => new FsError({ path: dir, operation: 'mkdir', cause }),\n });\n\n const persisted: CostDataPersisted = {\n perAgent: Object.fromEntries(data.perAgent),\n perIssue: Object.fromEntries(data.perIssue),\n dailyTotal: data.dailyTotal,\n lastResetDate: data.lastResetDate,\n };\n\n const tempFile = `${COST_DATA_FILE}.tmp`;\n yield* Effect.tryPromise({\n try: () => writeFile(tempFile, JSON.stringify(persisted, null, 2), 'utf-8'),\n catch: (cause) => new FsError({ path: tempFile, operation: 'writeFile', cause }),\n });\n\n yield* Effect.tryPromise({\n try: () => rename(tempFile, COST_DATA_FILE),\n catch: (cause) => new FsError({ path: COST_DATA_FILE, operation: 'rename', cause }),\n });\n\n // Best-effort temp cleanup if rename was a no-op on a foreign FS.\n yield* Effect.tryPromise({\n try: () => unlink(tempFile),\n catch: (cause) => new FsError({ path: tempFile, operation: 'unlink', cause }),\n }).pipe(Effect.catch(() => Effect.succeed(undefined)));\n });\n\n/** Effect variant of `recordCost`. */\nexport const recordCost = (\n agentId: string,\n cost: number,\n issueId?: string,\n): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n const today = getTodayDate();\n if (costData.lastResetDate !== today) {\n costData.dailyTotal = 0;\n costData.lastResetDate = today;\n console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);\n }\n\n const currentAgentCost = costData.perAgent.get(agentId) || 0;\n costData.perAgent.set(agentId, currentAgentCost + cost);\n\n if (issueId) {\n const currentIssueCost = costData.perIssue.get(issueId) || 0;\n costData.perIssue.set(issueId, currentIssueCost + cost);\n }\n\n costData.dailyTotal += cost;\n\n yield* saveCostDataAsync(costData);\n });\n\n/** Effect variant of `resetCostTracking`. */\nexport const resetCostTracking = (): Effect.Effect<void, FsError> =>\n Effect.gen(function* () {\n costData = {\n perAgent: new Map(),\n perIssue: new Map(),\n dailyTotal: 0,\n lastResetDate: getTodayDate(),\n };\n yield* saveCostDataAsync(costData);\n });\n\n// Re-export FsError so callers don't need to import it from the shared module.\nexport { FsError } from '../errors.js';\n// Silence \"unused\" warnings for the async fs imports we may use in future\n// extensions of this module.\nvoid readFile;\n","import { Effect } from 'effect';\nimport { getAgentStateSync, type AgentState } from '../agents.js';\nimport { CostDoorLive, CostWriter } from '../overdeck/cost.js';\n\ntype RunningAgent = AgentState & { tmuxActive: boolean };\n\nasync function reconcileSourceForRunningAgents(\n runningAgents: readonly RunningAgent[],\n source: 'ohmypi' | 'codex',\n): Promise<void> {\n if (!runningAgents.some((agent) => getAgentStateSync(agent.id)?.harness === source)) return;\n\n try {\n await Effect.runPromise(\n CostWriter.use((writer) => writer.reconcile({ source })).pipe(\n Effect.provide(CostDoorLive),\n ),\n );\n } catch (error) {\n console.warn(`[cloister] ${source} cost reconcile failed:`, error instanceof Error ? error.message : String(error));\n }\n}\n\nexport async function reconcilePiCostEventsForRunningAgents(runningAgents: readonly RunningAgent[]): Promise<void> {\n await reconcileSourceForRunningAgents(runningAgents, 'ohmypi');\n await reconcileSourceForRunningAgents(runningAgents, 'codex');\n}\n","/**\n * Session Rotation for Specialists\n *\n * Manages context rotation for long-running specialist agents (like merge-agent)\n * when token usage exceeds thresholds. Preserves essential memory while clearing\n * accumulated context.\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { getRuntimeForAgent } from '../runtimes/index.js';\nimport { getAgentStateSync } from '../agents.js';\nimport type { SpecialistAgentName } from './specialists.js';\nimport { getTmuxSessionName } from './specialists.js';\nimport { killSession } from '../tmux.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Token threshold for triggering session rotation\n */\nexport const SESSION_ROTATION_THRESHOLD = 100_000;\n\n/**\n * Tiered memory configuration\n */\nexport interface MemoryTiers {\n recent_summary: number; // Most recent N merges with hash + message only\n recent_detailed: number; // Next N merges with more detail\n recent_full: number; // Last N merges with full diffs\n}\n\n/**\n * Default memory tiers for merge-agent\n */\nexport const DEFAULT_MEMORY_TIERS: MemoryTiers = {\n recent_summary: 100, // Last 100 merges: hash + message\n recent_detailed: 50, // Last 50 merges: more details\n recent_full: 20, // Last 20 merges: full diffs\n};\n\n/**\n * Merge record for memory file\n *\n * Note: Only core fields (hash, message, author, date, branch) are stored.\n * Files changed and diffs are computed on-demand when building memory\n * to avoid storing large amounts of redundant data.\n */\ninterface MergeRecord {\n hash: string;\n message: string;\n author?: string;\n date?: string;\n branch?: string;\n}\n\n/**\n * Session rotation result\n */\nexport interface SessionRotationResult {\n success: boolean;\n oldSessionId: string;\n newSessionId?: string;\n memoryFile?: string;\n error?: string;\n}\n\n/**\n * Check if an agent needs session rotation\n *\n * @param agentId - Agent ID\n * @returns True if session should be rotated\n */\nexport function needsSessionRotation(agentId: string): boolean {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) return false;\n\n const tokenUsage = runtime.getTokenUsage(agentId);\n if (!tokenUsage) return false;\n\n const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;\n return totalTokens >= SESSION_ROTATION_THRESHOLD;\n}async function buildMergeAgentMemoryPromise(\n workingDir: string,\n tiers: MemoryTiers = DEFAULT_MEMORY_TIERS\n): Promise<string> {\n const merges: MergeRecord[] = [];\n\n try {\n // Get recent merge commits\n const totalMerges = Math.max(tiers.recent_summary, tiers.recent_detailed, tiers.recent_full);\n const { stdout: gitLog } = await execAsync(\n `git log --merges --format=\"%H|%s|%an|%ad|%D\" -n ${totalMerges}`,\n { cwd: workingDir, encoding: 'utf-8' }\n );\n\n const lines = gitLog.trim().split('\\n').filter(l => l);\n for (const line of lines) {\n const [hash, message, author, date, refs] = line.split('|');\n const branch = refs\n ? refs.split(',').find(r => r.includes('feature/') || r.includes('HEAD'))\n : undefined;\n\n merges.push({ hash, message, author, date, branch });\n }\n } catch (error) {\n console.error('Failed to get merge history:', error);\n return 'No merge history available.\\n';\n }\n\n // Build memory content with tiers\n let memory = '# Merge-Agent Session Memory\\n\\n';\n memory += `This session was rotated due to context limits. Below is a summary of recent merge activity.\\n\\n`;\n\n // Tier 3: Full diffs for most recent merges\n memory += `## Recent Merges (Full Detail)\\n\\n`;\n memory += `Last ${tiers.recent_full} merges with full diffs:\\n\\n`;\n\n for (let i = 0; i < Math.min(tiers.recent_full, merges.length); i++) {\n const merge = merges[i];\n memory += `### ${merge.hash.substring(0, 8)} - ${merge.message}\\n`;\n memory += `- Author: ${merge.author}\\n`;\n memory += `- Date: ${merge.date}\\n`;\n if (merge.branch) memory += `- Branch: ${merge.branch}\\n`;\n\n try {\n // Get files changed\n const { stdout: filesOutput } = await execAsync(`git show --name-only --format= ${merge.hash}`, {\n cwd: workingDir,\n encoding: 'utf-8',\n });\n const files = filesOutput.trim().split('\\n').filter(f => f);\n\n memory += `- Files changed: ${files.length}\\n`;\n\n // Get diff (limited to avoid huge memory files)\n const { stdout: diff } = await execAsync(`git show ${merge.hash} --stat`, {\n cwd: workingDir,\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024,\n });\n\n memory += `\\n\\`\\`\\`diff\\n${diff.substring(0, 5000)}\\n\\`\\`\\`\\n\\n`;\n } catch (error) {\n memory += `- (Error getting details)\\n\\n`;\n }\n }\n\n // Tier 2: Detailed info for next N merges\n if (merges.length > tiers.recent_full) {\n memory += `## Additional Recent Merges (Summary)\\n\\n`;\n memory += `Merges ${tiers.recent_full + 1} to ${Math.min(tiers.recent_detailed, merges.length)}:\\n\\n`;\n\n for (\n let i = tiers.recent_full;\n i < Math.min(tiers.recent_detailed, merges.length);\n i++\n ) {\n const merge = merges[i];\n memory += `- \\`${merge.hash.substring(0, 8)}\\` ${merge.message} (${merge.author}, ${merge.date})\\n`;\n }\n memory += '\\n';\n }\n\n // Tier 1: Hash + message for oldest merges\n if (merges.length > tiers.recent_detailed) {\n memory += `## Older Merges (Hash + Message Only)\\n\\n`;\n memory += `Merges ${tiers.recent_detailed + 1} to ${merges.length}:\\n\\n`;\n\n for (let i = tiers.recent_detailed; i < merges.length; i++) {\n const merge = merges[i];\n memory += `- \\`${merge.hash.substring(0, 8)}\\` ${merge.message}\\n`;\n }\n memory += '\\n';\n }\n\n return memory;\n}async function rotateSpecialistSessionPromise(\n specialistName: SpecialistAgentName,\n workingDir?: string\n): Promise<SessionRotationResult> {\n const agentId = `specialist-${specialistName}`;\n const runtime = getRuntimeForAgent(agentId);\n\n if (!runtime) {\n return {\n success: false,\n oldSessionId: '',\n error: `No runtime found for ${agentId}`,\n };\n }\n\n const agentState = getAgentStateSync(agentId);\n if (!agentState?.sessionId) {\n return {\n success: false,\n oldSessionId: '',\n error: `No session ID found for ${agentId}`,\n };\n }\n\n const oldSessionId = agentState.sessionId;\n\n try {\n // Build memory file (for merge-agent, extract git history)\n let memoryContent = '';\n let memoryFile: string | undefined;\n\n if (specialistName === 'merge-agent' && workingDir) {\n memoryContent = await Effect.runPromise(buildMergeAgentMemory(workingDir));\n memoryFile = join(OVERDECK_HOME, `merge-agent-memory-${Date.now()}.md`);\n writeFileSync(memoryFile, memoryContent);\n console.log(`Built memory file: ${memoryFile}`);\n }\n\n // Kill current session\n const tmuxSession = getTmuxSessionName(specialistName);\n try {\n await Effect.runPromise(killSession(tmuxSession));\n console.log(`Killed session: ${tmuxSession}`);\n } catch (error) {\n // Session might already be dead\n console.log(`Session ${tmuxSession} not found or already killed`);\n }\n\n // Start fresh session with memory\n const prompt = memoryContent\n ? `You are resuming from a rotated session. Here's your memory:\\n\\n${memoryContent}\\n\\nContinue from where you left off.`\n : 'Session rotated. Continue from where you left off.';\n\n const newAgent = await runtime.spawnAgent({\n agentId,\n workspace: agentState.workspace,\n prompt,\n runtime: runtime.name,\n });\n\n console.log(`Started fresh session: ${newAgent.sessionId.substring(0, 8)}`);\n\n return {\n success: true,\n oldSessionId,\n newSessionId: newAgent.sessionId,\n memoryFile,\n };\n } catch (error: unknown) {\n return {\n success: false,\n oldSessionId,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}async function checkAndRotateIfNeededPromise(\n specialistName: SpecialistAgentName,\n workingDir?: string\n): Promise<SessionRotationResult | null> {\n const agentId = `specialist-${specialistName}`;\n\n if (!needsSessionRotation(agentId)) {\n return null;\n }\n\n console.log(`🔔 Session rotation needed for ${specialistName}`);\n return (await Effect.runPromise(rotateSpecialistSession(specialistName, workingDir)));\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n\n/**\n * Effect-typed variant of {@link buildMergeAgentMemory}. The underlying\n * function swallows git errors and returns a fallback string, so this Effect\n * never fails.\n */\nexport function buildMergeAgentMemory(\n workingDir: string,\n tiers: MemoryTiers = DEFAULT_MEMORY_TIERS,\n): Effect.Effect<string> {\n return Effect.promise(() => buildMergeAgentMemoryPromise(workingDir, tiers));\n}\n\n/**\n * Effect-typed variant of {@link rotateSpecialistSession}. Never fails — the\n * Promise version returns `{ success: false, error }` instead of throwing.\n */\nexport function rotateSpecialistSession(\n specialistName: SpecialistAgentName,\n workingDir?: string,\n): Effect.Effect<SessionRotationResult> {\n return Effect.promise(() => rotateSpecialistSessionPromise(specialistName, workingDir));\n}\n\n/**\n * Effect-typed variant of {@link checkAndRotateIfNeeded}. Resolves to `null`\n * when no rotation was needed (mirrors the Promise contract).\n */\nexport function checkAndRotateIfNeeded(\n specialistName: SpecialistAgentName,\n workingDir?: string,\n): Effect.Effect<SessionRotationResult | null> {\n return Effect.promise(() => checkAndRotateIfNeededPromise(specialistName, workingDir));\n}\n","/** Cloister service: core monitoring for all running agents. */\nimport { createHash } from 'crypto';\nimport type { AgentRuntimeSync, HealthState } from '../runtimes/types.js';\nimport { isContextOverflowTail, CONTEXT_OVERFLOW_TAIL_LINES } from '../context-overflow.js';\nimport type { CloisterConfig } from './config.js';\nimport type { AgentHealth, HealthSummary } from './health.js';\nimport type { DomainEvent } from '@overdeck/contracts';\nimport { loadCloisterConfigSync } from './config.js';\nimport {\n getAgentHealth,\n getMultipleAgentHealth,\n generateHealthSummary,\n getAgentsToPoke,\n getAgentsToKill,\n getAgentsNeedingAttention,\n} from './health.js';\nimport { writeHealthEvent, getLatestHealthEvent } from '../overdeck/health-events.js';\n// PAN-378: initializeEnabledSpecialists removed — per-project ephemeral specialists\n// are spawned on-demand, no global initialization needed.\nimport { getGlobalRegistry, getRuntimeForAgent } from '../runtimes/index.js';\nimport { listRunningAgentsSync, getAgentStateSync, getAgentState, getAgentRuntimeStateSync, saveAgentRuntimeState } from '../agents.js';\nimport type { Role } from '../agents.js';\nimport { resolveProjectFromIssueSync } from '../projects.js';\nimport {\n isCloisterSpawnsPausedSync,\n setCloisterSpawnsPausedSync,\n setDeaconGloballyPaused,\n setFlywheelGloballyPaused,\n} from '../overdeck/control-settings.js';\nimport { checkAllTriggers, type TriggerDetection } from './triggers.js';\nimport { performHandoff, type HandoffResult } from './handoff.js';\nimport { logHandoffEventSync, createHandoffEvent } from './handoff-logger.js';\nimport {\n checkAgentForViolations,\n sendNudge,\n resolveViolationSync,\n hasExceededMaxNudges,\n clearOldViolationsSync,\n type FPPViolation,\n} from './fpp-violations.js';\nimport { checkCostLimits, getCostSummary, type CostAlert } from './cost-monitor.js';\nimport { reconcilePiCostEventsForRunningAgents } from './pi-cost-reconciler.js';\nimport { checkAndRotateIfNeeded, type SessionRotationResult } from './session-rotation.js';\nimport {\n startDeacon,\n stopDeacon,\n isDeaconRunning,\n getDeaconStatus,\n assessDeaconPatrolFreshness,\n getLastPatrolResult,\n getDeaconLogs,\n runPatrol,\n type PatrolResult,\n type DeaconLogEntry,\n} from './deacon.js';\nimport { OVERDECK_HOME } from '../paths.js';\nimport { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync, renameSync, statSync } from 'fs';\nimport { rm } from 'fs/promises';\nimport { join } from 'path';\nimport { AGENTS_DIR } from '../paths.js';\nimport { loadReviewStatuses, setReviewStatusSync } from '../review-status.js';\nimport { isRoleTerminal, type AdvancingRole } from './reap-terminal-sessions.js';\nimport { sessionExists, killSession } from '../tmux.js';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { Effect } from 'effect';\n\nconst execAsync = promisify(exec);\nimport { emitActivityEntrySync } from '../activity-logger.js';\nimport { isIssueClosed } from './issue-closed.js';\nimport { shouldSkipDispatchAsMerged } from './merge-verification.js';\nexport { spawnFlywheel, pauseFlywheel, resumeFlywheel } from './flywheel.js';\n\n// State file for cross-process communication\nconst CLOISTER_STATE_FILE = join(OVERDECK_HOME, 'cloister.state');\nconst LEGACY_SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\n\nasync function cleanupLegacySpecialistsDirectory(): Promise<void> {\n await rm(LEGACY_SPECIALISTS_DIR, { recursive: true, force: true });\n}\n\n/** Return issues orphaned in reviewStatus='reviewing' with no active reviewer. */\nexport function identifyOrphanedReviewingIssues(\n statuses: Record<string, { reviewStatus: string; history?: Array<{ type: string; status: string }> }>,\n activeReviewIssues: Set<string>,\n): string[] {\n const orphaned: string[] = [];\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.reviewStatus !== 'reviewing') continue;\n const hasPassedReview = status.history?.some(\n (h) => h.type === 'review' && h.status === 'passed',\n );\n if (hasPassedReview) continue;\n if (activeReviewIssues.has(issueId.toUpperCase())) continue;\n orphaned.push(issueId);\n }\n return orphaned;\n}\n\nexport function parseSpecialistAgentSession(name: string): {\n projectKey: string;\n specialistType: 'review-agent' | 'test-agent' | 'merge-agent';\n issueId?: string;\n} | null {\n const issueScoped = name.match(/^specialist-(.+)-([A-Z]+-\\d+)-(review-agent|test-agent|merge-agent)$/);\n if (issueScoped) {\n return {\n projectKey: issueScoped[1],\n issueId: issueScoped[2],\n specialistType: issueScoped[3] as 'review-agent' | 'test-agent' | 'merge-agent',\n };\n }\n\n const legacy = name.match(/^specialist-(.+)-(review-agent|test-agent|merge-agent)$/);\n if (legacy) {\n return {\n projectKey: legacy[1],\n specialistType: legacy[2] as 'review-agent' | 'test-agent' | 'merge-agent',\n };\n }\n\n return null;\n}\n\nexport type ReactiveIssueState =\n | 'todo'\n | 'open'\n | 'in_planning'\n | 'in_progress'\n | 'in_review'\n | 'testing'\n | 'shipping'\n | 'closed'\n | 'canceled';\n\nexport interface CloisterDomainEventLike {\n type: string;\n payload?: unknown;\n}\n\ninterface CloisterEventStore {\n append(event: Omit<DomainEvent, 'sequence'>): number;\n subscribe?: (fn: (event: CloisterDomainEventLike) => void) => () => void;\n}\n\nlet cloisterEventStoreProvider: (() => CloisterEventStore) | null = null;\n\nexport function setCloisterEventStoreProvider(provider: (() => CloisterEventStore) | null): void {\n cloisterEventStoreProvider = provider;\n}\n\nconst ROLE_RUN_STATES: Record<ReactiveIssueState, Role | null> = {\n todo: null,\n open: null,\n in_planning: 'plan',\n in_progress: 'work',\n in_review: 'review',\n testing: 'test',\n shipping: null,\n closed: null,\n canceled: null,\n};\n\n/**\n * Map issue lifecycle state to the role that should own that state.\n */\nexport function stateToRole(state: string): Role | null {\n const normalized = state.toLowerCase().replace(/[ -]/g, '_') as ReactiveIssueState;\n return ROLE_RUN_STATES[normalized] ?? null;\n}\n\nfunction normalizeIssueId(issueId: string): string {\n return issueId.trim().toUpperCase();\n}\n\nfunction roleFromAgentId(agentId: string, issueId: string): Role | null {\n const base = `agent-${issueId.toLowerCase()}`;\n if (agentId === base) return 'work';\n const role = agentId.slice(base.length + 1);\n return ['plan', 'review', 'test'].includes(role) ? role as Role : null;\n}\n\n/**\n * Grace window for `starting` states without a tmux session yet. The\n * start-planning route deliberately writes state.json BEFORE the lifecycle\n * transition (PAN-1048) so this guard can see the planner coming — but the\n * tmux session is only created after the transition. Treating any\n * starting-without-session as dead (the original S1 unstick heuristic)\n * re-opened the PAN-1048 race and spawned a duplicate `agent-<issue>-plan`\n * twin on EVERY `pan plan` (PAN-2159, 100% repro). A fresh `starting` state\n * is a startup in progress; only a stale one is a crashed spawn to unstick.\n */\nconst STARTING_WITHOUT_SESSION_GRACE_MS = 120_000;\n\nfunction isFreshStarting(state: { startedAt?: string }): boolean {\n const startedAt = Date.parse(state.startedAt ?? '');\n return Number.isFinite(startedAt) && Date.now() - startedAt < STARTING_WITHOUT_SESSION_GRACE_MS;\n}\n\nasync function activeRoleRunExists(issueId: string, role: Role, workspacePath?: string): Promise<boolean> {\n const issueLower = issueId.toLowerCase();\n\n // C1: For 'plan', also check the legacy planning-pan-X session format\n // alongside the canonical agent-pan-X-plan format. The start-planning route\n // writes to planning-pan-X while spawnRun uses agent-pan-X-plan.\n if (role === 'plan') {\n const legacyId = `planning-${issueLower}`;\n const legacyState = await Effect.runPromise(getAgentState(legacyId));\n if (legacyState?.role === 'plan' && legacyState.status !== 'stopped' && legacyState.status !== 'error') {\n // S1 (age-aware): only a STALE 'starting' state with no live tmux\n // session is a crashed spawn; a fresh one is mid-startup (PAN-2159).\n if (legacyState.status === 'starting' && !(await Effect.runPromise(sessionExists(legacyId)))) {\n return isFreshStarting(legacyState);\n }\n return true;\n }\n }\n\n const candidateId = role === 'work'\n ? `agent-${issueLower}`\n : `agent-${issueLower}-${role}`;\n\n const state = await Effect.runPromise(getAgentState(candidateId));\n if (!state) return false;\n\n const stateRole = state.role ?? roleFromAgentId(candidateId, issueId);\n\n // S1 (age-aware): only a STALE 'starting' state with no live tmux session\n // is a crashed spawn; a fresh one is mid-startup (PAN-2159).\n if (stateRole === role && state.status === 'starting' && !(await Effect.runPromise(sessionExists(candidateId)))) {\n return isFreshStarting(state);\n }\n\n const aliveByStatus = stateRole === role && state.status !== 'stopped' && state.status !== 'error';\n if (!aliveByStatus) return false;\n\n // Zombie detection: an agent that finished its work but never exited keeps\n // status:'running' forever, which would block every future re-dispatch for\n // this role (the ship/test stall bug). When we know the workspace and the\n // run stamped a roleRunHead, compare it against the current workspace HEAD —\n // a HEAD that has advanced past the marker means this session ran against\n // stale code and must not be treated as the active run for the new HEAD.\n if (workspacePath && state.roleRunHead) {\n try {\n const { stdout } = await execAsync('git rev-parse --short=8 HEAD', { cwd: workspacePath });\n const currentHead = stdout.trim();\n if (currentHead && currentHead !== state.roleRunHead) {\n console.log(\n `[cloister] ${issueId}: ${role} session ${candidateId} is stale `\n + `(ran against ${state.roleRunHead}, HEAD is now ${currentHead}) — not active`,\n );\n return false;\n }\n } catch { /* non-fatal — fall through to the status-only result */ }\n }\n\n return true;\n}\n\nfunction buildReactiveRolePrompt(issueId: string, state: string, role: Role): string {\n return `${role.toUpperCase()} TASK for ${issueId}:\n\nThe issue lifecycle transitioned to ${state}. Run the ${role} role for this issue.\n\nRequired steps:\n1. Work only in the workspace configured for ${issueId}.\n2. Read .pan/continue.json, .pan/spec.vbrief.json, project instructions, and issue context.\n3. Follow the boundaries and success criteria in roles/${role}.md exactly.\n4. Report the role-specific terminal status when done.`;\n}\n\n/**\n * Resolve the workspace path for an issue from agent state, then fall back\n * to the canonical `<projectPath>/workspaces/feature-<issueLower>` layout.\n * Mirrors the resolution used by startup recovery (service.ts:583-609) so\n * the reactive scheduler dispatches review/test wrappers with the same\n * workspace contract those wrappers receive on the manual code path.\n */\nasync function resolveWorkspaceForIssue(issueId: string): Promise<string | null> {\n const issueLower = issueId.toLowerCase();\n const agentState = await Effect.runPromise(getAgentState(`agent-${issueLower}`));\n if (agentState?.workspace) return agentState.workspace;\n const resolved = resolveProjectFromIssueSync(issueId);\n if (!resolved) return null;\n return `${resolved.projectPath}/workspaces/feature-${issueLower}`;\n}async function onIssueStateChangePromise(issueId: string, newState: string): Promise<void> {\n const normalizedIssueId = normalizeIssueId(issueId);\n const role = stateToRole(newState);\n if (!role) {\n console.log(`[cloister] ${normalizedIssueId}: no role for issue state '${newState}'`);\n return;\n }\n\n if (await isIssueClosed(normalizedIssueId)) {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — issue is closed`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // PAN-1746: a merged issue is terminal — never re-dispatch an advancing role\n // for work that already landed. Boot reconciliation replays issue-state-change\n // events on restart, and a long-merged issue still carrying its lifecycle\n // state (e.g. `verifying-on-main`) would otherwise re-trigger a ship dispatch\n // for a branch that merged weeks ago. Mirror the isIssueClosed gate above:\n // mergeStatus='merged' is the same terminal signal closed-state is.\n const { getReviewStatusSync } = await import('../review-status.js');\n if (getReviewStatusSync(normalizedIssueId)?.mergeStatus === 'merged') {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — merge already landed (merge_status='merged' is terminal)`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // PAN-2420: GitHub-authoritative guard. Even when merge_status is not yet\n // 'merged' (e.g. a permission failure left it as 'failed'), do not respawn\n // advancing roles against a PR that GitHub already reports merged.\n const mergedGuard = await shouldSkipDispatchAsMerged(normalizedIssueId);\n if (mergedGuard.skip) {\n const message = `${normalizedIssueId}: skipping ${role} dispatch — ${mergedGuard.reason}`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // Resolve the workspace up front so activeRoleRunExists can probe the\n // workspace HEAD for stale-session (zombie) detection.\n const workspace = await resolveWorkspaceForIssue(normalizedIssueId);\n\n if (await activeRoleRunExists(normalizedIssueId, role, workspace ?? undefined)) {\n const message = `${normalizedIssueId}: ${role} role already active; skipping lifecycle spawn`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n // activeRoleRunExists returned false. If a tmux session for this role still\n // physically exists, it's a zombie (agent finished work but never exited,\n // and the workspace HEAD has since advanced). Kill it before re-dispatch so\n // the fresh run gets a clean session name instead of colliding with the\n // dead one.\n const issueLower = normalizedIssueId.toLowerCase();\n const roleSessionId = role === 'work' ? `agent-${issueLower}` : `agent-${issueLower}-${role}`;\n if (await Effect.runPromise(sessionExists(roleSessionId))) {\n const message = `${normalizedIssueId}: killing stale ${role} session ${roleSessionId} before re-dispatch`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n try {\n await Effect.runPromise(killSession(roleSessionId));\n } catch (err) {\n console.error(`[cloister] failed to kill stale session ${roleSessionId}:`, err instanceof Error ? err.message : String(err));\n }\n }\n\n try {\n if (role === 'review') {\n if (!workspace) {\n const failure = `${normalizedIssueId}: cannot dispatch review role — no workspace or project resolved`;\n console.error(`[cloister] ${failure}`);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: failure, issueId: normalizedIssueId });\n return;\n }\n const branch = `feature/${normalizedIssueId.toLowerCase()}`;\n const { spawnReviewRoleForIssue } = await import('./review-agent.js');\n const result = await Effect.runPromise(spawnReviewRoleForIssue({ issueId: normalizedIssueId, workspace, branch }));\n const message = `${normalizedIssueId}: review role dispatched from lifecycle state '${newState}' (${result.message})`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: result.success ? 'info' : 'error', message, issueId: normalizedIssueId });\n return;\n }\n\n if (role === 'test') {\n const branch = `feature/${normalizedIssueId.toLowerCase()}`;\n const { dispatchTestAgentAndNotify } = await import('./test-agent-queue.js');\n await Effect.runPromise(dispatchTestAgentAndNotify(normalizedIssueId, workspace ?? undefined, branch));\n const message = `${normalizedIssueId}: test role dispatched from lifecycle state '${newState}'`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n return;\n }\n\n const { spawnRun } = await import('../agents.js');\n const run = await spawnRun(normalizedIssueId, role, {\n prompt: buildReactiveRolePrompt(normalizedIssueId, newState, role),\n });\n const message = `${normalizedIssueId}: ${role} role started from lifecycle state '${newState}' as ${run.id}`;\n console.log(`[cloister] ${message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message, issueId: normalizedIssueId });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('already running')) {\n const skipMessage = `${normalizedIssueId}: ${role} role already running; skipping lifecycle spawn`;\n console.log(`[cloister] ${skipMessage}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: skipMessage, issueId: normalizedIssueId });\n return;\n }\n console.error(`[cloister] Failed to start ${role} role for ${normalizedIssueId}:`, error);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: `${normalizedIssueId}: failed to start ${role} role: ${message}`, issueId: normalizedIssueId });\n }\n}\n\nfunction payloadRecord(event: CloisterDomainEventLike): Record<string, unknown> {\n return event.payload && typeof event.payload === 'object' ? event.payload as Record<string, unknown> : {};\n}\n\nexport function issueStateChangeFromDomainEvent(event: CloisterDomainEventLike): { issueId: string; state: string } | null {\n const payload = payloadRecord(event);\n const issueId = typeof payload.issueId === 'string' ? payload.issueId : null;\n if (!issueId) return null;\n\n switch (event.type) {\n case 'issue.transitioned':\n return typeof payload.state === 'string' ? { issueId, state: payload.state } : null;\n case 'issue.statusChanged':\n return typeof payload.canonicalStatus === 'string' ? { issueId, state: payload.canonicalStatus } : null;\n case 'issue.closed':\n return { issueId, state: 'closed' };\n case 'agent.completed': {\n // PAN-1048 review feedback 003: agent.completed is emitted by every\n // role's lifecycle (work, review, test, ship). Map it to in_review only\n // when the work role completes — letting other roles land here would\n // ricochet back into review the moment a review or test role finished.\n const role = typeof payload.role === 'string' ? payload.role : undefined;\n if (role === undefined || role === 'work') {\n return { issueId, state: 'in_review' };\n }\n return null;\n }\n case 'work.completed':\n return { issueId, state: 'in_review' };\n case 'review.approved':\n return { issueId, state: 'testing' };\n case 'test.passed':\n return { issueId, state: 'shipping' };\n default:\n return null;\n }\n}\n\nasync function handleCloisterDomainEventPromise(event: CloisterDomainEventLike): Promise<void> {\n // PAN-1908: reactive agent liveness — deacon handles agent.stopped and\n // agent.heartbeat_dead events instead of scanning agent directories.\n if (event.type === 'agent.stopped') {\n const payload = event.payload as { agentId?: string } | undefined;\n const agentId = payload?.agentId;\n if (agentId) {\n const { handleAgentStoppedEvent, handleAgentStoppedForOrphanReviewerSessions } = await import('./deacon.js');\n const { handleAgentLifecycleEventForIdleStack } = await import('./idle-stack-reaper.js');\n handleAgentLifecycleEventForIdleStack(agentId);\n const slotMatch = /^agent-(.+)-slot-\\d+$/.exec(agentId);\n await Promise.all([\n handleAgentStoppedEvent(agentId),\n handleAgentStoppedForOrphanReviewerSessions(agentId),\n slotMatch ? (await import('./deacon-swarm.js')).coordinateSwarmSlots({ issueId: slotMatch[1]!.toUpperCase() }) : Promise.resolve([]),\n ]);\n }\n return;\n }\n if (event.type === 'agent.started') {\n const agentId = (event.payload as { agentId?: string } | undefined)?.agentId;\n if (agentId) (await import('./idle-stack-reaper.js')).handleAgentLifecycleEventForIdleStack(agentId);\n return;\n }\n if (event.type === 'agent.heartbeat_dead') {\n const payload = event.payload as { agentId?: string } | undefined;\n const agentId = payload?.agentId;\n if (agentId) {\n const { handleAgentHeartbeatDeadEvent } = await import('./deacon.js');\n await handleAgentHeartbeatDeadEvent(agentId, 'event');\n }\n return;\n }\n\n // PAN-1908: reactive review-status handlers — deacon handles review lifecycle\n // events instead of scanning directories / the review-status DB.\n if (event.type === 'review.coordinator.died') {\n const payload = event.payload as { issueId?: string; sessionName?: string; reason?: string } | undefined;\n const issueId = payload?.issueId;\n if (issueId) {\n const { handleReviewCoordinatorDied } = await import('./deacon.js');\n await handleReviewCoordinatorDied(issueId, payload?.sessionName ?? '', payload?.reason ?? '');\n }\n return;\n }\n if (event.type === 'work.completed') {\n const payload = event.payload as { issueId?: string } | undefined;\n const issueId = payload?.issueId;\n if (issueId) {\n const { handleWorkCompleted } = await import('./deacon.js');\n await handleWorkCompleted(issueId);\n }\n // Fall through to onIssueStateChange for in_review dispatch.\n }\n\n // PAN-1908: reactive reconcilers — deacon handles issue.statusChanged events\n // for closed issues and proposed specs instead of patrol scans.\n if (event.type === 'issue.statusChanged') {\n const payload = event.payload as { issueId?: string; status?: string; canonicalStatus?: string } | undefined;\n const issueId = payload?.issueId;\n const canonicalStatus = payload?.canonicalStatus?.toLowerCase();\n const status = payload?.status?.toLowerCase();\n if (issueId) {\n if (canonicalStatus === 'closed' || status === 'closed') {\n const { handleIssueStatusChangedClosed } = await import('./closed-issue-reaper.js');\n await handleIssueStatusChangedClosed(issueId);\n return;\n }\n if (canonicalStatus === 'todo' || status === 'planned' || status === 'todo') {\n const { handleOrphanProposedSpec } = await import('./orphan-proposed-reconciler.js');\n await handleOrphanProposedSpec(issueId);\n // Fall through to onIssueStateChange in case it drives role dispatch.\n }\n }\n }\n\n const change = issueStateChangeFromDomainEvent(event);\n if (!change) return;\n await Effect.runPromise(onIssueStateChange(change.issueId, change.state));\n}\n\n/**\n * Write Cloister running state to file for cross-process visibility\n */\nfunction writeStateFile(running: boolean, pid?: number): void {\n try {\n if (running) {\n writeFileSync(CLOISTER_STATE_FILE, JSON.stringify({\n running: true,\n pid: pid || process.pid,\n startedAt: new Date().toISOString(),\n }));\n } else {\n if (existsSync(CLOISTER_STATE_FILE)) {\n unlinkSync(CLOISTER_STATE_FILE);\n }\n }\n } catch (error) {\n // Non-fatal - state file is for convenience\n console.warn('Failed to write Cloister state file:', error);\n }\n}\n\n/**\n * Read Cloister running state from file\n */\nexport function readCloisterStateFile(): { running: boolean; pid?: number; startedAt?: string } {\n try {\n if (existsSync(CLOISTER_STATE_FILE)) {\n const data = JSON.parse(readFileSync(CLOISTER_STATE_FILE, 'utf-8'));\n // Verify the process is still running\n if (data.pid) {\n try {\n process.kill(data.pid, 0); // Signal 0 checks if process exists\n return data;\n } catch {\n // Process doesn't exist - clean up stale state file\n unlinkSync(CLOISTER_STATE_FILE);\n return { running: false };\n }\n }\n return data;\n }\n } catch {\n // State file doesn't exist or is corrupted\n }\n return { running: false };\n}\n\n/**\n * Cloister service status\n */\nexport interface CloisterStatus {\n running: boolean;\n lastCheck: Date | null;\n config: CloisterConfig;\n summary: HealthSummary;\n agentsNeedingAttention: string[];\n patrol: ReturnType<typeof assessDeaconPatrolFreshness> & {\n loopRunning: boolean;\n patrolIntervalMs: number;\n };\n}\n\n/**\n * Agent crash tracker for auto-restart\n */\ninterface AgentCrashTracker {\n agentId: string;\n crashCount: number;\n lastCrash: Date;\n nextRetryAt?: Date;\n gaveUp: boolean;\n}\n\n/**\n * Cloister service event\n */\nexport type CloisterEvent =\n | { type: 'started' }\n | { type: 'stopped' }\n | { type: 'health_check'; agentHealths: AgentHealth[] }\n | { type: 'agent_warning'; agentId: string; health: AgentHealth }\n | { type: 'agent_stuck'; agentId: string; health: AgentHealth }\n | { type: 'poked_agent'; agentId: string }\n | { type: 'killed_agent'; agentId: string }\n | { type: 'agent_crashed'; agentId: string; crashCount: number }\n | { type: 'agent_restarting'; agentId: string; crashCount: number; backoffSeconds: number }\n | { type: 'agent_restart_failed'; agentId: string; crashCount: number; error: string }\n | { type: 'agent_gave_up'; agentId: string; maxRetries: number }\n | { type: 'mass_death_detected'; deathCount: number; windowSeconds: number }\n | { type: 'spawn_paused'; reason: string }\n | { type: 'spawn_resumed' }\n | { type: 'fpp_violation_detected'; agentId: string; violation: FPPViolation }\n | { type: 'fpp_nudge_sent'; agentId: string; nudgeCount: number }\n | { type: 'fpp_max_nudges_exceeded'; agentId: string; violation: FPPViolation }\n | { type: 'cost_alert'; alert: CostAlert }\n | { type: 'session_rotated'; specialistName: string; result: SessionRotationResult }\n | { type: 'handoff_triggered'; agentId: string; trigger: TriggerDetection }\n | { type: 'handoff_completed'; agentId: string; result: HandoffResult }\n | { type: 'emergency_stop'; killedAgents: string[] }\n | { type: 'error'; error: Error };\n\n/**\n * Cloister service event listener\n */\nexport type CloisterEventListener = (event: CloisterEvent) => void;\n\n/**\n * One-shot convoy roles (PAN-1742). The review lenses + synthesis and the test\n * role are spawned fresh via `spawnRun` each cycle; they run once, write their\n * artifact, and exit. They never save a sessionId, so they cannot be resumed.\n */\nconst ONE_SHOT_ROLES: ReadonlySet<string> = new Set(['review', 'test']);\n\n/**\n * Decide whether a vanished tmux session should be treated as a crash worth\n * auto-restarting (PAN-1742). Returns a human-readable reason to SKIP the\n * crash/restart machinery, or `null` when the agent is a genuine restart\n * candidate.\n *\n * Two cases are not crashes:\n * - one-shot convoy roles, whose session-end is a normal completion; and\n * - any agent without a saved session, which `restartAgent` cannot resume\n * (it throws \"No session ID found\"), so counting a crash and scheduling a\n * restart is pure noise.\n *\n * Pure and exported so the rule is unit-tested independent of the service's\n * private health-check plumbing.\n */\nexport function nonRestartableReason(role: string, sessionId: string | undefined): string | null {\n if (ONE_SHOT_ROLES.has(role)) {\n return `(${role}) finished its one-shot run — completion, not a crash`;\n }\n if (!sessionId) {\n return 'ended with no resumable session — not auto-restartable';\n }\n return null;\n}\n\n/**\n * Cloister Service\n *\n * Monitors agent health and performs auto-actions.\n */\nexport class CloisterService {\n private running: boolean = false;\n private starting: boolean = false;\n private checkInterval: NodeJS.Timeout | null = null;\n private lastCheck: Date | null = null;\n private config: CloisterConfig;\n private listeners: CloisterEventListener[] = [];\n private previousStates: Map<string, HealthState> = new Map();\n private crashTrackers: Map<string, AgentCrashTracker> = new Map();\n private previousRunningAgents: Set<string> = new Set();\n private deathTimestamps: Date[] = []; // Rolling window of agent death times\n private spawnsPaused: boolean = false;\n private processedCompletions: Map<string, number> = new Map(); // Track completion marker retry counts (Infinity = done)\n private healthCheckCount: number = 0;\n private lastPokeTimestamps: Map<string, number> = new Map(); // agentId → last poke timestamp (ms)\n // PAN-2452 (idle-alive): progress fingerprint at last poke → consecutive\n // ineffective-poke count. A poke that changes neither the workspace HEAD nor\n // the pane content did nothing; escalating beats poking forever.\n private pokeProgress: Map<string, { fingerprint: string; ineffective: number }> = new Map();\n private domainEventUnsubscribe: (() => void) | null = null;\n private eventStore: CloisterEventStore | null = null;\n\n // ─── Status cache ────────────────────────────────────────────────────────────\n // getStatus() does sync file I/O + tmux calls for every agent. Cache for 3s\n // to eliminate blocking on high-frequency dashboard polls.\n private _statusCache: CloisterStatus | null = null;\n private _statusCacheAt = 0;\n private readonly STATUS_CACHE_TTL_MS = 3_000;\n\n constructor(config?: CloisterConfig) {\n this.config = config || loadCloisterConfigSync();\n }\n\n private getDashboardApiUrl(): string {\n // Cloister always runs in-process with the dashboard, so it must talk to\n // its own loopback — never to a public DASHBOARD_URL like https://overdeck.localhost,\n // which would round-trip through Traefik+TLS and fail validation from inside\n // Node (PAN-845). Use 127.0.0.1 explicitly to avoid the IPv6-first /etc/hosts\n // trap (PAN-841): undici-based fetch connects to [::1] and hangs because the\n // dashboard listens on the IPv4 wildcard.\n return `http://127.0.0.1:${process.env.API_PORT || process.env.PORT || '3011'}`;\n }\n\n /**\n * Start the Cloister service\n */\n async start(): Promise<void> {\n if (this.running || this.starting) {\n console.warn('Cloister is already running');\n return;\n }\n this.starting = true;\n\n console.log('🔔 Starting Cloister agent watchdog...');\n\n try {\n await cleanupLegacySpecialistsDirectory();\n console.log(' ✓ Removed legacy ~/.overdeck/specialists directory');\n } catch (error) {\n console.error(' ✗ Failed to remove legacy specialists directory:', error);\n }\n\n // PAN-493: Reset orphaned verificationStatus === 'running' states.\n // If Cloister dies mid-verification, the status is left stuck at 'running' and the\n // pipeline halts indefinitely. On startup, reset any such states to 'pending' so\n // verification reruns automatically. Verification is idempotent — this is always safe.\n let resetVerificationCount = 0;\n try {\n const statuses = loadReviewStatuses();\n for (const [issueId, status] of Object.entries(statuses)) {\n if (status.verificationStatus === 'running') {\n setReviewStatusSync(issueId, { verificationStatus: 'pending' });\n console.log(` ✓ Reset orphaned verification 'running' → 'pending' for ${issueId}`);\n resetVerificationCount++;\n }\n }\n if (resetVerificationCount > 0) {\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Reset ${resetVerificationCount} orphaned verification 'running' → 'pending' on startup` });\n }\n } catch (error) {\n console.error(' ✗ Failed to reset orphaned verification states:', error);\n }\n\n // PAN-511: Clear stale currentIssue from specialist agents that are not actually running.\n // If Cloister dies while a specialist is between tasks or mid-run, the specialist's\n // runtime.json may retain currentIssue and state='active' even though the process is dead.\n // spawnEphemeralSpecialist checks these fields to decide whether\n // to dispatch — a stale 'active' state permanently blocks new dispatches.\n // On startup, clear currentIssue and reset state from any specialist agent that is:\n // (a) idle — safe: idle means no active task, currentIssue is leftover\n // (b) active but tmux session no longer running — state is stale from a crash\n let clearedSpecialistCount = 0;\n try {\n if (existsSync(AGENTS_DIR)) {\n const { isRunning: isSpecialistRunning } = await import('./specialists.js');\n const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const parsed = parseSpecialistAgentSession(entry.name);\n if (!parsed) continue;\n const runtimeState = getAgentRuntimeStateSync(entry.name);\n if (!runtimeState?.currentIssue) continue;\n\n if (runtimeState.state === 'idle') {\n saveAgentRuntimeState(entry.name, { currentIssue: undefined });\n console.log(` ✓ Cleared stale currentIssue '${runtimeState.currentIssue}' from idle ${entry.name}`);\n clearedSpecialistCount++;\n } else if (runtimeState.state === 'active') {\n // Check if the process is actually alive — if not, the state is stale from a crash.\n // For issue-scoped specialists, check the exact tmux session instead of the legacy\n // project/type singleton lookup, which cannot represent PAN-754 session identity.\n const stillRunning = parsed.issueId\n ? await Effect.runPromise(sessionExists(entry.name))\n : await isSpecialistRunning(parsed.specialistType, parsed.projectKey);\n if (!stillRunning) {\n saveAgentRuntimeState(entry.name, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n console.log(` ✓ Cleared stale active state for crashed specialist ${entry.name} (was working on '${runtimeState.currentIssue}')`);\n clearedSpecialistCount++;\n }\n }\n }\n }\n if (clearedSpecialistCount > 0) {\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Cleared ${clearedSpecialistCount} stale specialist state(s) on startup` });\n }\n } catch (error) {\n console.error(' ✗ Failed to clear stale specialist states:', error);\n }\n\n // PAN-511: Startup recovery for orphaned reviewStatus='reviewing' issues.\n // If Cloister crashes after reviewStatus was set to 'reviewing' but before the specialist\n // completes, the issue is stuck. On startup, find such issues and re-dispatch directly.\n try {\n const reviewStatuses = loadReviewStatuses();\n const { resolveProjectFromIssueSync } = await import('../projects.js');\n const { getTmuxSessionName, getAllProjectSpecialistStatuses } = await import('./specialists.js');\n\n // Build set of issue IDs actively being reviewed by a running specialist\n const activeReviewIssues = new Set<string>();\n try {\n const projSpecs = await getAllProjectSpecialistStatuses();\n for (const ps of projSpecs) {\n if (ps.specialistType !== 'review-agent' || !ps.isRunning) continue;\n const rs = getAgentRuntimeStateSync(ps.tmuxSession);\n if (rs?.state === 'active' && rs.currentIssue) {\n activeReviewIssues.add(rs.currentIssue.toUpperCase());\n }\n }\n // Also check global review-agent session\n const globalSession = getTmuxSessionName('review-agent');\n const globalRs = getAgentRuntimeStateSync(globalSession);\n if (globalRs?.state === 'active' && globalRs.currentIssue) {\n activeReviewIssues.add(globalRs.currentIssue.toUpperCase());\n }\n\n // PAN-1048 R5: detect role-primitive review runs (agent-<id>-review).\n // Replaces the legacy getActiveParallelReviewIssues helper that scanned\n // tmux for dispatchParallelReview's coordinator session naming pattern.\n const { listRunningAgents } = await import('../agents.js');\n const agents = await Effect.runPromise(listRunningAgents());\n for (const agent of agents) {\n if (agent.status === 'stopped' || agent.status === 'error') continue;\n const role = agent.role ?? (agent.id.endsWith('-review') ? 'review' : null);\n if (role !== 'review') continue;\n const issueId = (agent.issueId ?? '').trim().toUpperCase();\n if (issueId) activeReviewIssues.add(issueId);\n }\n } catch {\n // Non-fatal: if we can't check active sessions, re-dispatch all orphaned\n }\n\n const orphanedReviewing = identifyOrphanedReviewingIssues(reviewStatuses, activeReviewIssues);\n\n if (orphanedReviewing.length > 0) {\n console.log(` ⚠ Found ${orphanedReviewing.length} issue(s) with orphaned reviewStatus='reviewing'`);\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Found ${orphanedReviewing.length} orphaned reviewStatus='reviewing' issue(s) on startup`, details: orphanedReviewing.join(', ') });\n\n for (const issueId of orphanedReviewing) {\n\n const agentId = `agent-${issueId.toLowerCase()}`;\n const agentState = getAgentStateSync(agentId);\n const workspace = agentState?.workspace;\n\n if (!workspace) {\n console.log(` ⚠ ${issueId}: orphaned reviewing but no workspace found — resetting to pending`);\n setReviewStatusSync(issueId, { reviewStatus: 'pending' });\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `${issueId} orphaned reviewing reset to pending — no workspace found`, issueId });\n continue;\n }\n\n const resolved = resolveProjectFromIssueSync(issueId);\n if (!resolved) {\n console.log(` ⚠ ${issueId}: orphaned reviewing but no project configured — resetting to pending`);\n setReviewStatusSync(issueId, { reviewStatus: 'pending' });\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `${issueId} orphaned reviewing reset to pending — no project configured`, issueId });\n continue;\n }\n\n const branch = `feature/${issueId.toLowerCase()}`;\n // PAN-1048 R4: startup recovery now spawns the review role primitive\n // (loads roles/review.md → Agent tool fans out to convoy reviewers)\n // instead of the legacy `pan review run` coordinator.\n const { spawnReviewRoleForIssue } = await import('./review-agent.js');\n const dispatchResult = await Effect.runPromise(spawnReviewRoleForIssue({ issueId, workspace, branch }));\n if (dispatchResult.gated) {\n console.log(` → Deferred recovery review for ${issueId}: ${dispatchResult.message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: `Deferred recovery review for ${issueId}: ${dispatchResult.message}`, issueId });\n continue;\n }\n if (!dispatchResult.success) {\n console.log(` ⚠ Failed to re-dispatch recovery review for ${issueId}: ${dispatchResult.error || dispatchResult.message}`);\n emitActivityEntrySync({ source: 'cloister', level: 'warn', message: `Failed to re-dispatch recovery review for ${issueId}: ${dispatchResult.error || dispatchResult.message}`, issueId });\n continue;\n }\n // spawnReviewRoleForIssue sets reviewStatus='reviewing' internally\n console.log(` ✓ Re-dispatched recovery review for ${issueId}`);\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: `Re-dispatched recovery review for ${issueId}`, issueId });\n }\n }\n } catch (error) {\n console.error(' ✗ Failed to recover orphaned reviewing issues:', error);\n }\n\n // PAN-378: Global specialists removed — per-project ephemeral specialists handle all work.\n // No initialization needed; specialists are spawned on-demand via spawnEphemeralSpecialist().\n console.log(' → Specialists: per-project ephemeral mode (no global pool)');\n\n // Start deacon health monitor for specialists\n try {\n console.log(' → Starting deacon health monitor...');\n startDeacon();\n console.log(' ✓ Deacon started');\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: 'Deacon health monitor started' });\n } catch (error) {\n console.error(' ✗ Failed to start deacon:', error);\n emitActivityEntrySync({ source: 'cloister', level: 'error', message: `Failed to start deacon: ${error instanceof Error ? error.message : String(error)}` });\n }\n\n this.running = true;\n this.starting = false;\n this._statusCache = null;\n writeStateFile(true);\n this.emit({ type: 'started' });\n emitActivityEntrySync({ source: 'cloister', level: 'info', message: 'Cloister agent watchdog started' });\n\n await this.subscribeToDomainEvents();\n\n // Start monitoring loop\n this.startMonitoringLoop();\n }\n\n private async subscribeToDomainEvents(): Promise<void> {\n if (this.domainEventUnsubscribe) return;\n\n try {\n const injected = cloisterEventStoreProvider?.();\n if (injected) {\n this.eventStore = injected;\n if (injected.subscribe) {\n this.domainEventUnsubscribe = injected.subscribe((event) => {\n void Effect.runPromise(handleCloisterDomainEvent(event)).catch((error) => {\n console.error('[cloister] Reactive lifecycle event handling failed:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Reactive lifecycle event handling failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n });\n });\n }\n console.log(' ✓ Cloister event store provider installed');\n return;\n }\n\n const { initEventStore } = await import('../../dashboard/server/event-store.js');\n const store = await initEventStore();\n this.eventStore = store;\n this.domainEventUnsubscribe = store.subscribe((event) => {\n void Effect.runPromise(handleCloisterDomainEvent(event)).catch((error) => {\n console.error('[cloister] Reactive lifecycle event handling failed:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Reactive lifecycle event handling failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n });\n });\n console.log(' ✓ Cloister reactive lifecycle scheduler subscribed to domain events');\n } catch (error) {\n console.error(' ✗ Failed to subscribe Cloister reactive lifecycle scheduler:', error);\n emitActivityEntrySync({\n source: 'cloister',\n level: 'error',\n message: `Failed to subscribe reactive lifecycle scheduler: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n\n /**\n * Stop the Cloister service\n *\n * Note: This stops monitoring but does NOT kill agents.\n * Use emergencyStop() to kill all agents.\n */\n stop(): void {\n if (!this.running) {\n console.warn('Cloister is not running');\n return;\n }\n\n console.log('🔔 Stopping Cloister agent watchdog...');\n this.running = false;\n this._statusCache = null;\n writeStateFile(false);\n\n if (this.checkInterval) {\n clearInterval(this.checkInterval);\n this.checkInterval = null;\n }\n\n if (this.domainEventUnsubscribe) {\n this.domainEventUnsubscribe();\n this.domainEventUnsubscribe = null;\n }\n\n // Stop deacon health monitor\n try {\n stopDeacon();\n console.log(' ✓ Deacon stopped');\n } catch (error) {\n console.error('Failed to stop deacon:', error);\n }\n\n this.emit({ type: 'stopped' });\n }\n\n /**\n * Emergency stop - kill ALL agents immediately\n *\n * This is the nuclear option. Use with caution.\n */\n emergencyStop(): string[] {\n console.log('🚨 EMERGENCY STOP - Killing all agents');\n\n // Freeze auto-resume FIRST, before killing — otherwise the deacon patrol or a\n // dashboard restart re-spawns the agents we are about to kill and the money\n // keeps burning. This persists in SQLite, so the freeze survives a restart;\n // the operator clears it explicitly (Deacon resume / flywheel resume) when\n // they are ready to let agents run again.\n try {\n setDeaconGloballyPaused(true);\n setFlywheelGloballyPaused(true);\n console.log(' ✓ Froze Deacon + flywheel auto-resume');\n } catch (error) {\n console.error(' ✗ Failed to set global pause flags:', error);\n }\n\n const runningAgents = listRunningAgentsSync();\n const killedAgents: string[] = [];\n\n for (const agent of runningAgents) {\n if (agent.tmuxActive) {\n try {\n const runtime = getRuntimeForAgent(agent.id);\n if (runtime) {\n runtime.killAgent(agent.id); // killAgent already resets runtime.json to idle\n killedAgents.push(agent.id);\n console.log(` ✓ Killed ${agent.id}`);\n }\n } catch (error) {\n console.error(` ✗ Failed to kill ${agent.id}:`, error);\n }\n }\n }\n\n this.emit({ type: 'emergency_stop', killedAgents });\n\n // Stop monitoring after emergency stop\n this.stop();\n\n return killedAgents;\n }\n\n /**\n * Start the monitoring loop\n */\n private startMonitoringLoop(): void {\n // Run initial check immediately\n this.performHealthCheck();\n\n // Schedule periodic checks\n const intervalMs = this.config.monitoring.check_interval * 1000;\n this.checkInterval = setInterval(() => {\n this.performHealthCheck();\n }, intervalMs);\n }\n\n /**\n * Perform a health check on all running agents\n */\n private async performHealthCheck(): Promise<void> {\n try {\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentIds = runningAgents.map((a) => a.id);\n const currentRunningSet = new Set(agentIds);\n\n // Detect crashed agents (were running before, not running now)\n if (this.previousRunningAgents.size > 0 && this.config.auto_restart?.enabled) {\n for (const previousAgentId of this.previousRunningAgents) {\n if (!currentRunningSet.has(previousAgentId)) {\n // Agent crashed!\n await this.handleAgentCrash(previousAgentId);\n }\n }\n }\n\n // Update the set of running agents for next check\n this.previousRunningAgents = currentRunningSet;\n\n // Completion marker check runs regardless of active agents —\n // completed agents won't have tmux sessions anymore\n this.healthCheckCount++;\n if (this.healthCheckCount % 4 === 0) {\n void this.checkCompletionMarkers();\n }\n\n if (agentIds.length === 0) {\n this.lastCheck = new Date();\n return;\n }\n\n // Get health for all agents\n const agentHealths: AgentHealth[] = [];\n\n for (const agentId of agentIds) {\n const runtime = getRuntimeForAgent(agentId);\n if (runtime) {\n const health = getAgentHealth(agentId, runtime);\n agentHealths.push(health);\n\n // Write health event to database\n this.recordHealthEvent(health);\n }\n }\n\n this.lastCheck = new Date();\n this.emit({ type: 'health_check', agentHealths });\n\n // Check for agents needing attention\n const needsAttention = getAgentsNeedingAttention(agentHealths);\n\n const pokeCooldownMs = this.config.auto_actions.poke_cooldown_ms ?? 30 * 60 * 1000;\n const now = Date.now();\n\n for (const health of needsAttention) {\n // The sequencer is a long-lived singleton that is SUPPOSED to sit idle\n // between ranking passes — \"no progress in a while\" is its normal resting\n // state, not a stall. Never poke or kill it (it was spamming itself with\n // \"are you stuck?\" nudges every cooldown). Health is still recorded above;\n // only the attention/poke/kill action is skipped.\n if (getAgentStateSync(health.agentId)?.role === 'sequencer') continue;\n\n const lastPoke = this.lastPokeTimestamps.get(health.agentId) ?? 0;\n const cooledDown = (now - lastPoke) >= pokeCooldownMs;\n\n if (health.state === 'warning') {\n this.emit({ type: 'agent_warning', agentId: health.agentId, health });\n\n // Auto-poke if configured and cooldown elapsed\n if (this.config.auto_actions.poke_on_warning && cooledDown) {\n this.pokeAgent(health.agentId);\n this.lastPokeTimestamps.set(health.agentId, now);\n }\n } else if (health.state === 'stuck') {\n this.emit({ type: 'agent_stuck', agentId: health.agentId, health });\n\n // Auto-poke stuck agents if configured and cooldown elapsed\n if ((this.config.auto_actions.poke_on_stuck ?? true) && cooledDown) {\n this.pokeAgent(health.agentId);\n this.lastPokeTimestamps.set(health.agentId, now);\n }\n\n // Auto-kill if configured (dangerous!)\n if (this.config.auto_actions.kill_on_stuck) {\n this.killAgent(health.agentId);\n }\n }\n }\n\n // Check for handoff triggers (Phase 4)\n // Note: Intentionally not awaiting - runs in background\n void this.checkHandoffTriggers(agentHealths);\n\n // Check for FPP violations (Phase 6)\n this.checkFPPViolations(agentIds);\n\n await reconcilePiCostEventsForRunningAgents(runningAgents);\n\n // Check cost limits (Phase 6)\n this.checkCostAlerts(agentIds);\n\n // Check for specialist session rotation needs (Phase 6)\n // Only check periodically (every ~10 checks)\n if (Math.random() < 0.1) {\n void this.checkSpecialistRotations();\n }\n\n // Clean up old resolved violations (daily)\n if (Math.random() < 0.01) {\n // ~1% chance each check = roughly once per day\n clearOldViolationsSync(24);\n }\n } catch (error) {\n console.error('Cloister health check failed:', error);\n this.emit({ type: 'error', error: error as Error });\n }\n }\n\n /** Fallback scan for completion markers when `pan done` did not reach the dashboard. */\n private async checkCompletionMarkers(): Promise<void> {\n try {\n if (!existsSync(AGENTS_DIR)) return;\n\n const agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true })\n .filter(d => d.isDirectory() && d.name.startsWith('agent-'));\n\n for (const dir of agentDirs) {\n const completedFile = join(AGENTS_DIR, dir.name, 'completed');\n const processedFile = join(AGENTS_DIR, dir.name, 'completed.processed');\n\n // Skip if no completion marker.\n if (!existsSync(completedFile)) continue;\n\n // A stale `.processed` from a prior round must not block a fresh completion.\n if (existsSync(processedFile)) {\n try {\n const completedMtime = statSync(completedFile).mtimeMs;\n const processedMtime = statSync(processedFile).mtimeMs;\n if (completedMtime > processedMtime) {\n try { unlinkSync(processedFile); } catch {}\n this.processedCompletions.delete(dir.name);\n console.log(`🔔 Cloister: Detected re-completion for ${dir.name} (completed newer than .processed) — clearing stale marker`);\n } else {\n continue;\n }\n } catch {\n continue;\n }\n }\n\n // Skip stale completion markers (older than 24h) — just mark as processed\n try {\n const content = JSON.parse(readFileSync(completedFile, 'utf-8'));\n const ageMs = Date.now() - new Date(content.timestamp).getTime();\n if (ageMs > 24 * 60 * 60 * 1000) {\n console.log(`🔔 Cloister: Skipping stale completion marker for ${dir.name} (${Math.floor(ageMs / 3600000)}h old)`);\n this.processedCompletions.set(dir.name, Infinity);\n try { renameSync(completedFile, processedFile); } catch {}\n continue;\n }\n } catch (parseErr) {\n console.warn(` ⚠ Cloister: Could not parse completion marker for ${dir.name}, skipping`);\n continue;\n }\n\n // Check retry count; reset stale in-memory counters for fresh completions.\n const retryCount = this.processedCompletions.get(dir.name) || 0;\n if (retryCount === Infinity) {\n this.processedCompletions.delete(dir.name);\n } else if (retryCount >= 3) continue;\n\n // Extract issue ID from agent dir name (e.g. \"agent-pan-123\" → \"PAN-123\")\n const issueId = dir.name.replace('agent-', '').toUpperCase();\n\n // Skip if `pan done` already triggered review.\n const { getReviewStatusSync } = await import('../review-status.js');\n const existingReview = getReviewStatusSync(issueId);\n if (existingReview && ['reviewing', 'passed'].includes(existingReview.reviewStatus || '')) {\n console.log(`🔔 Cloister: Completion marker for ${issueId} — review already ${existingReview.reviewStatus}, marking processed`);\n try { renameSync(completedFile, processedFile); } catch {}\n this.processedCompletions.set(dir.name, Infinity);\n continue;\n }\n\n console.log(`🔔 Cloister: Found completion marker for ${issueId}, triggering review...${retryCount > 0 ? ` (retry ${retryCount}/3)` : ''}`);\n\n try {\n // Use fetch() so https dashboard URLs work.\n const result = await (async (): Promise<{ success: boolean; error?: string; alreadyReviewed?: boolean; alreadyMerged?: boolean }> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 5000);\n try {\n const res = await fetch(`${this.getDashboardApiUrl()}/api/review/${issueId}/trigger`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n signal: controller.signal,\n });\n clearTimeout(timer);\n try {\n return (await res.json()) as { success: boolean; error?: string; alreadyReviewed?: boolean; alreadyMerged?: boolean };\n } catch {\n return { success: false, error: `Invalid response (HTTP ${res.status})` };\n }\n } catch (e: any) {\n clearTimeout(timer);\n if (e?.name === 'AbortError') return { success: false, error: 'Timeout (5s)' };\n return { success: false, error: e?.message || String(e) };\n }\n })();\n\n if (result.success) {\n console.log(` ✓ Review triggered for ${issueId}`);\n renameSync(completedFile, processedFile);\n this.processedCompletions.set(dir.name, Infinity);\n } else if (result.alreadyReviewed || result.alreadyMerged) {\n // Terminal state — already handled, mark as processed\n console.log(` ✓ ${issueId} already ${result.alreadyMerged ? 'merged' : 'reviewed'}, marking processed`);\n renameSync(completedFile, processedFile);\n this.processedCompletions.set(dir.name, Infinity);\n } else {\n // Transient failure — increment retry count, will retry on next cycle\n this.processedCompletions.set(dir.name, retryCount + 1);\n console.log(` ⚠ Review trigger failed for ${issueId}: ${result.error || 'unknown'} (will retry, ${2 - retryCount} attempts left)`);\n }\n } catch (err: any) {\n this.processedCompletions.set(dir.name, retryCount + 1);\n console.error(` ✗ Failed to trigger review for ${issueId}: ${err.message} (will retry, ${2 - retryCount} attempts left)`);\n }\n }\n } catch (error) {\n // Non-fatal - just skip this check\n }\n }\n\n /**\n * Poke an agent (send \"are you stuck?\" message).\n *\n * NOTE: runtime.sendMessage() is async — both ClaudeCodeRuntime and PiRuntime\n * are declared `async sendMessage(): Promise<void>`. A `throw` inside an\n * async function before any await still returns a rejected Promise, so the\n * surrounding try/catch CANNOT catch it. Without explicit `.catch()`, the\n * rejection becomes an UnhandledPromiseRejection and crashes the dashboard\n * server. We hit this in production when the deacon health-check polled a\n * dead agent (PAN-1189 wedge sweep #12-13).\n */\n private pokeAgent(agentId: string): void {\n // Fire-and-forget wrapper; the async body measures progress first.\n void this.pokeAgentWithEscalation(agentId).catch((error) => {\n console.error(`Failed to poke ${agentId}:`, error);\n });\n }\n\n /** PAN-2452: fingerprint of observable progress — workspace HEAD + pane tail.\n * Unchanged fingerprint across pokes = the poke did nothing. */\n private async progressFingerprint(agentId: string): Promise<string> {\n const state = getAgentStateSync(agentId);\n let head = '';\n if (state?.workspace) {\n try {\n const { stdout } = await execAsync('git rev-parse HEAD', { cwd: state.workspace, encoding: 'utf-8' });\n head = stdout.trim();\n } catch { /* workspace may be gone; pane still fingerprints */ }\n }\n let pane = '';\n try {\n const { stdout } = await execAsync(\n `tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -${CONTEXT_OVERFLOW_TAIL_LINES}`,\n { encoding: 'utf-8' },\n );\n pane = stdout;\n } catch { /* session may be gone */ }\n const paneHash = createHash('sha1').update(pane).digest('hex').slice(0, 12);\n return `${head}:${paneHash}`;\n }\n\n private async pokeAgentWithEscalation(agentId: string): Promise<void> {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n const fingerprint = await this.progressFingerprint(agentId);\n const prior = this.pokeProgress.get(agentId);\n const ineffective = prior && prior.fingerprint === fingerprint ? prior.ineffective + 1 : 0;\n this.pokeProgress.set(agentId, { fingerprint, ineffective });\n\n // Tier 3 (5th no-progress poke): stop poking — surface to the operator.\n if (ineffective >= 4) {\n const { setAgentPausedSync } = await import('../agents/agent-state.js');\n try {\n setAgentPausedSync(agentId, `needs-you: idle-alive — no observable progress across ${ineffective + 1} pokes (idle-alive escalation)`);\n this.emit({ type: 'agent_stuck', agentId, health: undefined as never });\n console.log(`🛑 ${agentId} paused: idle-alive across ${ineffective + 1} pokes`);\n } catch (pauseErr) {\n console.error(`Failed to pause idle-alive ${agentId}:`, pauseErr);\n }\n return;\n }\n\n // Tier 2 (3rd no-progress poke): escalate — /compact on overflow, else a\n // substantive reconstruct instruction instead of another \"are you stuck?\".\n let pokeMessage =\n 'Hey, I noticed you haven\\'t made progress in a while. Are you stuck? ' +\n 'If you need help or clarification, please ask. Otherwise, please continue with your work.';\n if (ineffective >= 2) {\n let tail = '';\n try {\n const { stdout } = await execAsync(\n `tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -${CONTEXT_OVERFLOW_TAIL_LINES}`,\n { encoding: 'utf-8' },\n );\n tail = stdout;\n } catch { /* fall through to reconstruct nudge */ }\n if (isContextOverflowTail(tail)) {\n pokeMessage = '/compact';\n console.log(`🔔 ${agentId}: overflow tail detected — delivering /compact instead of a poke`);\n } else {\n pokeMessage =\n 'You appear active but have made no observable progress (no new commits, unchanged output) across multiple checks. '\n + 'Do exactly one of: (1) work is done — commit, push, and run pan done; '\n + '(2) blocked — state the blocker in one sentence and commit what you have as WIP; '\n + '(3) lost context — re-read your bead (bd show), your latest .pan/feedback/ file, and git status, then resume. Act now.';\n }\n }\n\n await Promise.resolve(runtime.sendMessage(agentId, pokeMessage)).catch((sendErr) => {\n console.error(`Failed to send poke to ${agentId}:`, sendErr);\n });\n this.emit({ type: 'poked_agent', agentId });\n console.log(`🔔 Poked ${agentId}${ineffective > 0 ? ` (ineffective streak: ${ineffective})` : ''}`);\n }\n\n /**\n * Kill an agent\n *\n * runtime.killAgent() is also async in some runtime implementations — apply\n * the same fire-and-forget guard as pokeAgent so async rejection cannot\n * crash the dashboard from a deacon health-check timer callback.\n */\n private killAgent(agentId: string): void {\n try {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n Promise.resolve(runtime.killAgent(agentId)).catch((killErr) => {\n console.error(`Failed to kill ${agentId}:`, killErr);\n });\n this.emit({ type: 'killed_agent', agentId });\n\n console.log(`🔔 Killed ${agentId}`);\n } catch (error) {\n console.error(`Failed to kill ${agentId}:`, error);\n }\n }\n\n /**\n * Handle agent crash with auto-restart logic\n */\n private async handleAgentCrash(agentId: string): Promise<void> {\n const config = this.config.auto_restart;\n if (!config?.enabled) return;\n\n // Check if agent was intentionally stopped or suspended (not a crash).\n // Both state.json and runtime.json must be checked — stopAgent writes both,\n // but a race between the CLI kill and this health check poll could see one\n // but not the other if only one file is consulted.\n const agentState = getAgentStateSync(agentId);\n if (!agentState || agentState.status === 'stopped') {\n console.log(`🔔 Agent ${agentId} was intentionally stopped, skipping restart`);\n return;\n }\n const runtimeState = getAgentRuntimeStateSync(agentId);\n if (runtimeState?.state === 'suspended') {\n console.log(`🔔 Agent ${agentId} is suspended, skipping restart`);\n return;\n }\n if (runtimeState?.state === 'stopped') {\n console.log(`🔔 Agent ${agentId} runtime is stopped, skipping restart`);\n return;\n }\n\n // PAN-1742: a vanished session is not always a crash. One-shot convoy roles\n // complete-and-exit, and any agent with no saved session is structurally\n // un-restartable. Either way, counting it as a crash pollutes the\n // crash/troubled counters and schedules a doomed restart.\n const skipReason = nonRestartableReason(agentState.role, agentState.sessionId);\n if (skipReason) {\n // PAN-2007: a one-shot review/test session that vanished BEFORE recording a\n // terminal verdict died prematurely — e.g. before it could run\n // `pan specialists done`. Masking that as a normal \"one-shot completion\" and\n // skipping restart strands the issue at reviewStatus=reviewing forever (the\n // exact PAN-1832 symptom). Recover it instead: resume so it can finish and\n // signal. Only a terminal verdict (or a missing session) is a genuine\n // non-restartable completion.\n let recoverUnfinishedOneShot = false;\n if (ONE_SHOT_ROLES.has(agentState.role) && agentState.sessionId && agentState.issueId) {\n try {\n const { getReviewStatusSync } = await import('../review-status.js');\n const status = getReviewStatusSync(agentState.issueId);\n const verdictTerminal = status\n ? isRoleTerminal(agentState.role as AdvancingRole, {\n reviewStatus: status.reviewStatus,\n testStatus: status.testStatus,\n readyForMerge: status.readyForMerge,\n mergeStatus: status.mergeStatus,\n })\n : false;\n recoverUnfinishedOneShot = !verdictTerminal;\n } catch {\n recoverUnfinishedOneShot = false;\n }\n }\n if (!recoverUnfinishedOneShot) {\n console.log(`🔔 Agent ${agentId} ${skipReason}; skipping restart`);\n return;\n }\n console.log(`🔔 Agent ${agentId} (${agentState.role}) vanished before a terminal verdict — recovering instead of treating as a one-shot completion (PAN-2007)`);\n // fall through to the restart/resume path below\n }\n\n // Record death timestamp for mass death detection\n const now = new Date();\n this.deathTimestamps.push(now);\n this.checkForMassDeaths();\n\n // Get or create crash tracker\n let tracker = this.crashTrackers.get(agentId);\n if (!tracker) {\n tracker = {\n agentId,\n crashCount: 0,\n lastCrash: now,\n gaveUp: false,\n };\n this.crashTrackers.set(agentId, tracker);\n }\n\n // Skip if we've already given up on this agent\n if (tracker.gaveUp) return;\n\n // Increment crash count\n tracker.crashCount++;\n tracker.lastCrash = now;\n\n this.emit({ type: 'agent_crashed', agentId, crashCount: tracker.crashCount });\n console.log(`🔔 Agent ${agentId} crashed (crash #${tracker.crashCount})`);\n\n // PAN-1908: emit agent.heartbeat_dead so the event-driven deacon recovery\n // can mark the agent stopped and resume it with the proper gates, rather\n // than Cloister scheduling its own auto-restart in parallel.\n if (this.eventStore) {\n try {\n this.eventStore.append({\n type: 'agent.heartbeat_dead',\n timestamp: new Date().toISOString(),\n payload: { agentId, issueId: agentState.issueId, sessionId: agentState.sessionId },\n });\n } catch (err) {\n console.error(`[cloister] Failed to emit heartbeat_dead for ${agentId}:`, err);\n }\n }\n\n // Check if we've exceeded max retries\n if (tracker.crashCount > config.max_retries) {\n tracker.gaveUp = true;\n this.emit({ type: 'agent_gave_up', agentId, maxRetries: config.max_retries });\n console.error(`🔔 Gave up on restarting ${agentId} after ${config.max_retries} attempts`);\n return;\n }\n\n // Calculate backoff delay for logging/mass-death tracking only — the actual\n // resume is handled by the deacon's agent.heartbeat_dead handler.\n const backoffIndex = Math.min(tracker.crashCount - 1, config.backoff_seconds.length - 1);\n const backoffSeconds = config.backoff_seconds[backoffIndex];\n const nextRetryAt = new Date(Date.now() + backoffSeconds * 1000);\n tracker.nextRetryAt = nextRetryAt;\n\n this.emit({\n type: 'agent_restarting',\n agentId,\n crashCount: tracker.crashCount,\n backoffSeconds,\n });\n\n console.log(\n `🔔 Deacon will recover ${agentId} via heartbeat_dead (attempt ${tracker.crashCount}/${config.max_retries}, backoff ${backoffSeconds}s)`\n );\n }\n\n /**\n * Restart an agent using its saved session\n */\n private async restartAgent(agentId: string): Promise<void> {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n throw new Error(`No runtime found for agent ${agentId}`);\n }\n\n // Get agent state to find session ID and workspace\n const agentState = getAgentStateSync(agentId);\n if (!agentState?.sessionId) {\n throw new Error(`No session ID found for agent ${agentId}`);\n }\n\n if (!agentState.workspace) {\n throw new Error(`No workspace found for agent ${agentId}`);\n }\n\n // Restart with --resume using spawnAgent with sessionId\n console.log(`🔔 Restarting ${agentId} with session ${agentState.sessionId.substring(0, 8)}...`);\n runtime.spawnAgent({\n agentId,\n workspace: agentState.workspace,\n sessionId: agentState.sessionId,\n runtime: runtime.name,\n });\n console.log(`🔔 Successfully restarted ${agentId}`);\n }\n\n /**\n * Check for mass death events\n *\n * Detects when 3+ agents die within 30 seconds and pauses spawns.\n */\n private checkForMassDeaths(): void {\n const MASS_DEATH_THRESHOLD = 3;\n const WINDOW_SECONDS = 30;\n\n const now = Date.now();\n const windowStart = now - WINDOW_SECONDS * 1000;\n\n // Clean up old timestamps outside the window\n this.deathTimestamps = this.deathTimestamps.filter(\n (timestamp) => timestamp.getTime() >= windowStart\n );\n\n // Check if we have mass deaths\n if (this.deathTimestamps.length >= MASS_DEATH_THRESHOLD) {\n // Trigger mass death alert\n this.emit({\n type: 'mass_death_detected',\n deathCount: this.deathTimestamps.length,\n windowSeconds: WINDOW_SECONDS,\n });\n\n // Pause spawns\n if (!this.spawnsPaused) {\n this.pauseSpawns('Mass death detected - system stability concern');\n console.error(\n `🔔 MASS DEATH DETECTED: ${this.deathTimestamps.length} agents died in ${WINDOW_SECONDS}s - spawns paused`\n );\n }\n }\n }\n\n /**\n * Pause new agent spawns\n */\n private pauseSpawns(reason: string): void {\n this.spawnsPaused = true;\n setCloisterSpawnsPausedSync(true);\n this.emit({ type: 'spawn_paused', reason });\n console.log(`🔔 Agent spawns paused: ${reason}`);\n }\n\n /**\n * Resume agent spawns\n *\n * Called manually after user acknowledges mass death alert.\n */\n resumeSpawns(): void {\n this.spawnsPaused = false;\n setCloisterSpawnsPausedSync(false);\n this.deathTimestamps = []; // Clear death window\n this.emit({ type: 'spawn_resumed' });\n console.log(`🔔 Agent spawns resumed`);\n }\n\n /**\n * Check if spawns are currently paused\n */\n isSpawnPaused(): boolean {\n return this.spawnsPaused || isCloisterSpawnsPausedSync();\n }\n\n /**\n * Check for FPP violations and send nudges\n */\n private checkFPPViolations(agentIds: string[]): void {\n for (const agentId of agentIds) {\n const violation = checkAgentForViolations(agentId);\n if (!violation) continue;\n\n // New violation detected\n if (violation.nudgeCount === 0) {\n this.emit({ type: 'fpp_violation_detected', agentId, violation });\n }\n\n // Check if we should send a nudge\n const timeSinceLastNudge = violation.lastNudgeAt\n ? Date.now() - new Date(violation.lastNudgeAt).getTime()\n : Infinity;\n\n // Send nudge every 5 minutes until max nudges\n const NUDGE_INTERVAL_MS = 5 * 60 * 1000;\n if (timeSinceLastNudge >= NUDGE_INTERVAL_MS || violation.nudgeCount === 0) {\n if (hasExceededMaxNudges(violation)) {\n // Max nudges exceeded - alert user\n this.emit({ type: 'fpp_max_nudges_exceeded', agentId, violation });\n console.error(\n `🔔 Agent ${agentId} exceeded max nudges for ${violation.type} - manual intervention required`\n );\n } else {\n // Send nudge\n const sent = sendNudge(violation);\n if (sent) {\n this.emit({ type: 'fpp_nudge_sent', agentId, nudgeCount: violation.nudgeCount });\n }\n }\n }\n }\n }\n\n /**\n * Check for cost limit alerts\n */\n private checkCostAlerts(agentIds: string[]): void {\n const config = this.config.cost_limits;\n if (!config) return;\n\n for (const agentId of agentIds) {\n // Extract issue ID from agent ID (format: agent-issue-123 or issue-123)\n const issueId = agentId.startsWith('agent-')\n ? agentId.replace(/^agent-/, '')\n : agentId;\n\n const alerts = checkCostLimits(agentId, issueId, config);\n for (const alert of alerts) {\n this.emit({ type: 'cost_alert', alert });\n\n // Resolve the entity label: for daily_total, use explicit \"(unattributed)\" bucket\n const entityLabel = alert.agentId || alert.issueId || '(unattributed)';\n\n // Log the alert\n if (alert.level === 'limit_reached') {\n console.error(\n `🔔 COST LIMIT REACHED: ${alert.type} for ${entityLabel} - $${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)}`\n );\n } else {\n console.warn(\n `🔔 Cost warning: ${alert.type} for ${entityLabel} at ${alert.percentUsed.toFixed(0)}% ($${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)})`\n );\n }\n }\n }\n }\n\n /**\n * Get cost summary\n */\n getCostSummary() {\n return getCostSummary();\n }\n\n /**\n * Check if any specialists need session rotation\n */\n private async checkSpecialistRotations(): Promise<void> {\n // Check merge-agent (the main candidate for rotation)\n const mergeAgentResult = await Effect.runPromise(checkAndRotateIfNeeded('merge-agent', process.cwd()));\n if (mergeAgentResult) {\n this.emit({ type: 'session_rotated', specialistName: 'merge-agent', result: mergeAgentResult });\n\n if (mergeAgentResult.success) {\n console.log(\n `🔔 Rotated merge-agent session: ${mergeAgentResult.oldSessionId.substring(0, 8)} → ${mergeAgentResult.newSessionId?.substring(0, 8)}`\n );\n } else {\n console.error(`🔔 Failed to rotate merge-agent: ${mergeAgentResult.error}`);\n }\n }\n\n // Could check other specialists here if needed\n }\n\n /**\n * Record health event to database\n *\n * Only writes events when state changes or on first check.\n */\n private recordHealthEvent(health: AgentHealth): void {\n try {\n const currentState = health.state;\n const previousState = this.previousStates.get(health.agentId);\n\n // Only write event if state changed or this is first check\n if (previousState === undefined || previousState !== currentState) {\n // Determine source from heartbeat\n const source = health.heartbeat?.source\n ? this.mapHeartbeatSource(health.heartbeat.source)\n : 'unknown';\n\n writeHealthEvent({\n agentId: health.agentId,\n timestamp: new Date().toISOString(),\n state: currentState,\n source,\n metadata: health.heartbeat\n ? JSON.stringify({\n confidence: health.heartbeat.confidence,\n lastAction: health.heartbeat.lastAction,\n toolName: health.heartbeat.toolName,\n timeSinceActivity: health.timeSinceActivity,\n })\n : undefined,\n });\n\n // Update tracked state\n this.previousStates.set(health.agentId, currentState);\n }\n } catch (error) {\n console.error(`Failed to record health event for ${health.agentId}:`, error);\n }\n }\n\n /**\n * Check for handoff triggers and execute handoffs (Phase 4)\n *\n * Checks all triggers for each agent and performs handoffs when triggered.\n */\n private async checkHandoffTriggers(agentHealths: AgentHealth[]): Promise<void> {\n for (const health of agentHealths) {\n try {\n // Get agent state\n const agentState = getAgentStateSync(health.agentId);\n if (!agentState) continue;\n\n // Skip if no workspace (can't determine context)\n if (!agentState.workspace) continue;\n\n // Check all triggers\n const triggers = await Effect.runPromise(checkAllTriggers(\n health.agentId,\n agentState.workspace,\n agentState.issueId,\n agentState.model,\n health,\n this.config\n ));\n\n // Execute handoff for first triggered condition\n // (Priority: stuck > planning > test > completion)\n if (triggers.length > 0) {\n const trigger = triggers[0];\n\n // task_complete triggers with a specialist name (e.g. 'test-agent') as suggestedModel\n // are handled by the `pan done` → completion marker → specialist pipeline flow.\n // Do NOT perform a model-swap handoff here — it passes the specialist name as a model ID\n // which is invalid and causes the agent to respawn with an unusable model.\n const specialistNames = ['review-agent', 'test-agent', 'merge-agent', 'inspect-agent', 'uat-agent'];\n if (trigger.type === 'task_complete' && specialistNames.includes(trigger.suggestedModel || '')) {\n console.log(`[cloister] Skipping handoff for ${health.agentId}: task_complete triggers specialist dispatch via completion marker, not model swap`);\n continue;\n }\n\n this.emit({ type: 'handoff_triggered', agentId: health.agentId, trigger });\n\n console.log(`🔔 Handoff triggered for ${health.agentId}: ${trigger.reason}`);\n\n // Perform handoff\n const result = await Effect.runPromise(performHandoff(health.agentId, {\n targetModel: trigger.suggestedModel || 'sonnet',\n reason: trigger.reason,\n }));\n\n this.emit({ type: 'handoff_completed', agentId: health.agentId, result });\n\n // Log handoff event\n if (result.context) {\n const event = createHandoffEvent(\n health.agentId,\n agentState.issueId,\n result.context,\n trigger.type,\n result.success,\n result.error\n );\n logHandoffEventSync(event);\n }\n\n if (result.success) {\n console.log(`✓ Handoff completed: ${health.agentId} → ${result.newAgentId} (${trigger.suggestedModel})`);\n } else {\n console.error(`✗ Handoff failed: ${result.error}`);\n }\n }\n } catch (error) {\n console.error(`Failed to check handoff triggers for ${health.agentId}:`, error);\n }\n }\n }\n\n /**\n * Map heartbeat source to database source string\n */\n private mapHeartbeatSource(source: string): string {\n switch (source) {\n case 'jsonl':\n return 'jsonl_mtime';\n case 'tmux':\n return 'tmux_activity';\n case 'git':\n return 'git_activity';\n case 'active-heartbeat':\n return 'active_heartbeat';\n default:\n return source;\n }\n }\n\n /**\n * Get current status\n *\n * Uses a 3-second TTL cache to avoid blocking the event loop on repeated\n * dashboard polls. The underlying computation does sync file I/O and tmux\n * calls for every agent, which scales poorly with agent count.\n */\n getStatus(): CloisterStatus {\n const now = Date.now();\n if (this._statusCache && now - this._statusCacheAt < this.STATUS_CACHE_TTL_MS) {\n return this._statusCache;\n }\n\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentIds = runningAgents.map((a) => a.id);\n\n const agentHealths: AgentHealth[] = [];\n\n for (const agentId of agentIds) {\n const runtime = getRuntimeForAgent(agentId);\n if (runtime) {\n const health = getAgentHealth(agentId, runtime);\n agentHealths.push(health);\n }\n }\n\n const summary = generateHealthSummary(agentHealths);\n const needsAttention = getAgentsNeedingAttention(agentHealths).map((h) => h.agentId);\n\n const deaconStatus = getDeaconStatus();\n const patrol = assessDeaconPatrolFreshness({\n isRunning: deaconStatus.isRunning,\n lastPatrol: deaconStatus.state.lastPatrol,\n patrolIntervalMs: deaconStatus.config.patrolIntervalMs,\n });\n\n const status: CloisterStatus = {\n running: this.isRunning(),\n lastCheck: this.lastCheck,\n config: this.config,\n summary,\n agentsNeedingAttention: needsAttention,\n patrol: {\n ...patrol,\n loopRunning: deaconStatus.isRunning,\n patrolIntervalMs: deaconStatus.config.patrolIntervalMs,\n },\n };\n\n this._statusCache = status;\n this._statusCacheAt = now;\n return status;\n }\n\n /**\n * Get health for a specific agent\n */\n getAgentHealth(agentId: string): AgentHealth | null {\n const runtime = getRuntimeForAgent(agentId);\n if (!runtime) {\n return null;\n }\n\n return getAgentHealth(agentId, runtime);\n }\n\n /**\n * Get health for all running agents\n */\n getAllAgentHealth(): AgentHealth[] {\n const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);\n const agentHealths: AgentHealth[] = [];\n\n for (const agent of runningAgents) {\n const runtime = getRuntimeForAgent(agent.id);\n if (runtime) {\n const health = getAgentHealth(agent.id, runtime);\n agentHealths.push(health);\n }\n }\n\n return agentHealths;\n }\n\n /**\n * Get deacon (specialist health monitor) status\n */\n getDeaconStatus() {\n return getDeaconStatus();\n }\n\n /**\n * Get the most recent patrol result (actions, cycle, timestamp)\n */\n getLastPatrolResult(): PatrolResult | null {\n return getLastPatrolResult();\n }\n\n /**\n * Get recent deacon log entries\n */\n getDeaconLogs(limit = 100): DeaconLogEntry[] {\n return getDeaconLogs(limit);\n }\n\n /**\n * Run a manual deacon patrol\n */\n async runDeaconPatrol(): Promise<PatrolResult> {\n return runPatrol();\n }\n\n /**\n * Check if deacon is running\n */\n isDeaconRunning(): boolean {\n return isDeaconRunning();\n }\n\n /**\n * Reload configuration\n */\n reloadConfig(): void {\n this.config = loadCloisterConfigSync();\n\n // Restart monitoring loop with new interval if running\n if (this.running && this.checkInterval) {\n clearInterval(this.checkInterval);\n this.startMonitoringLoop();\n }\n }\n\n /**\n * Update configuration\n */\n updateConfig(config: CloisterConfig): void {\n this.config = config;\n\n // Restart monitoring loop with new interval if running\n if (this.running && this.checkInterval) {\n clearInterval(this.checkInterval);\n this.startMonitoringLoop();\n }\n }\n\n /**\n * Register an event listener\n */\n on(listener: CloisterEventListener): void {\n this.listeners.push(listener);\n }\n\n /**\n * Unregister an event listener\n */\n off(listener: CloisterEventListener): void {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n /**\n * Emit an event to all listeners\n */\n private emit(event: CloisterEvent): void {\n for (const listener of this.listeners) {\n try {\n listener(event);\n } catch (error) {\n console.error('Cloister event listener error:', error);\n }\n }\n }\n\n /**\n * Check if service is running\n *\n * Checks both local instance state and cross-process state file.\n * This allows the CLI to detect if Cloister is running in the dashboard process.\n */\n isRunning(): boolean {\n // First check our own instance\n if (this.running) {\n return true;\n }\n // Check if another process has Cloister running\n const stateFile = readCloisterStateFile();\n return stateFile.running;\n }\n}\n\n/**\n * Global Cloister service instance\n */\nlet globalService: CloisterService | null = null;\n\n/**\n * Get the global Cloister service instance\n *\n * Creates a new instance if one doesn't exist.\n */\nexport function getCloisterService(): CloisterService {\n if (!globalService) {\n globalService = new CloisterService();\n }\n return globalService;\n}\n\n/**\n * Set the global Cloister service instance\n *\n * Useful for testing or custom configurations.\n */\nexport function setCloisterService(service: CloisterService): void {\n globalService = service;\n}\n\n// ─── PAN-1249: additive Effect variants ───────────────────────────────────────\n// service.ts is the top-level Cloister orchestrator (1817 lines, heavy use of\n// closures and direct fs IO). A full Effect rewrite would cascade into half\n// the codebase (review-agent, test-agent-queue, agents.ts) so for the\n// batch-C migration we expose Effect variants only at the two domain-event\n// entry points. The legacy Promise surfaces stay live for existing callers;\n// Effect callers should prefer the *Effect variants. The internal\n// implementations swallow errors (logging via emitActivityEntry instead),\n// so the error channel is `never`.\n\n/**\n * Effect-typed variant of {@link onIssueStateChange}. Never fails — failures\n * surface through `emitActivityEntry` inside the legacy implementation.\n */\nexport function onIssueStateChange(\n issueId: string,\n newState: string,\n): Effect.Effect<void> {\n return Effect.promise(() => onIssueStateChangePromise(issueId, newState));\n}\n\n/**\n * Effect-typed variant of {@link handleCloisterDomainEvent}. Never fails.\n */\nexport function handleCloisterDomainEvent(\n event: CloisterDomainEventLike,\n): Effect.Effect<void> {\n return Effect.promise(() => handleCloisterDomainEventPromise(event));\n}\n"],"mappings":"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"}
|