@overdeck/desktop 0.45.13 → 0.45.16
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/package.json +1 -1
- package/server/{acceptance-criteria-CfujPRrz.js → acceptance-criteria-BZ_nQS1f.js} +2 -2
- package/server/{advancing-selfheal-Bn5QiLtn.js → advancing-selfheal-DJYAvGv-.js} +6 -5
- package/server/{agent-gc-Cl9Nm0Zu.js → agent-gc-D0wDIeV1.js} +3 -3
- package/server/{agent-state-C6Rm6FPR.js → agent-state-lGvgPP4m.js} +1 -1
- package/server/{agent-state-service-U8feBf8t.js → agent-state-service-CZrayD_G.js} +1 -1
- package/server/{agent-state-service-JhFzYCP_.js → agent-state-service-DFslS5go.js} +9 -52
- package/server/{agents-DqQa4Dq_.js → agents-B9GwfCyp.js} +155 -153
- package/server/{agents-CxbuBjID.js → agents-CEcaxSkR.js} +1 -1
- package/server/{archive-planning-CPitEZo4.js → archive-planning-CbghE9-o.js} +5 -5
- package/server/{archive-planning-DmpT1_14.js → archive-planning-CoyIbgGK.js} +1 -1
- package/server/{artifact-index-jobs-sMnNtPhy.js → artifact-index-jobs-CcSrUoSE.js} +2 -2
- package/server/auto-commit-IW-eik_b.js +3 -0
- package/server/{backlog-auto-trigger-Cf9C8fwD.js → backlog-auto-trigger-BfgDsII0.js} +3 -3
- package/server/{bd-process-lock-D1aJxeYv.js → bd-process-lock-XAXAet9O.js} +32 -11
- package/server/{beads-CivpXztb.js → beads-B1u04n3o.js} +1 -1
- package/server/{beads-DRoj6-e1.js → beads-BeSMEjGT.js} +96 -196
- package/server/{beads-query-tOsAYP5h.js → beads-query-Dl4FhT8J.js} +24 -99
- package/server/beads-restore-BUItPOjI.js +23 -0
- package/server/behavior-BTpYbAv4.js +10 -0
- package/server/{briefing-freshness-BL3Q6p7R.js → briefing-freshness-DESTCWo-.js} +4 -4
- package/server/{checkpoint-manager-DCxjQvP_.js → checkpoint-manager-BwFdegJF.js} +2 -2
- package/server/{checkpoint-manager-j5URlFY7.js → checkpoint-manager-DWSXooZV.js} +1 -1
- package/server/checkpoint-worker.js +1 -1
- package/server/{checkpoints-BoX1H2Hf.js → checkpoints-BtUonbs3.js} +2 -2
- package/server/{checkpoints-Bu_M_Q9W.js → checkpoints-fjadfOKF.js} +1 -1
- package/server/{claude-hooks-provision-D-jGXmu1.js → claude-hooks-provision-DAtxutOs.js} +132 -114
- package/server/claude-hooks-provision-DiK62u0k.js +3 -0
- package/server/{claude-permissions-DxIjkOVl.js → claude-permissions-CThLgf8d.js} +2 -2
- package/server/{claude-settings-overlay-BPDS6GfE.js → claude-settings-overlay-BF9MPIwx.js} +2 -2
- package/server/{claude-settings-overlay-5b9X2S0C.js → claude-settings-overlay-Dx_Uqcmv.js} +1 -1
- package/server/{cli-s7bPWy-E.js → cli-ByKIIMfT.js} +9 -9
- package/server/cli-D9cUTbBr.js +3 -0
- package/server/{cliproxy-BituqWtR.js → cliproxy-B4c1BUvs.js} +2 -2
- package/server/{cliproxy-D3sHB0sI.js → cliproxy-C_GuT5DU.js} +1 -1
- package/server/{close-issue-CrBE7voz.js → close-issue-BOuLKr6e.js} +5 -5
- package/server/{closed-issue-reaper-BRQBc7Xa.js → closed-issue-reaper-CksfuiFD.js} +1 -1
- package/server/{closed-issue-reaper-DX9mYUji.js → closed-issue-reaper-DCAYzNXW.js} +102 -30
- package/server/{codex-Bi1ITSOo.js → codex-BYPz6ZdF.js} +1 -1
- package/server/{codex-CO0HrK3j.js → codex-BbqIRZjd.js} +6 -5
- package/server/{behavior-D3d2mVyD.js → codex-parser-mFYZh8vR.js} +3 -9
- package/server/{compact-beads-DUkCUFxA.js → compact-beads-CyJtoXJi.js} +15 -8
- package/server/{compact-beads-CrA8-k10.js → compact-beads-N00E4EVa.js} +1 -1
- package/server/{config-DsCxzjXK.js → config-DVLdmnMJ.js} +2 -2
- package/server/{config-B4_d7z7z.js → config-Dj6N8EP9.js} +1 -1
- package/server/{config-yaml-C_djSurJ.js → config-yaml-cNOZcEww.js} +118 -23
- package/server/{conflict-gate-D47Kl78u.js → conflict-gate-BMKm_5DV.js} +3 -3
- package/server/{conflict-gate-CpOn9KC-.js → conflict-gate-CuXUv-bP.js} +1 -1
- package/server/{context-layers-CHl3Nroz.js → context-layers-CeAwSYNt.js} +2 -2
- package/server/{context-layers-BNhUBTkf.js → context-layers-c0JqYHcS.js} +3 -3
- package/server/{control-settings-KyYNXZLv.js → control-settings-D4EFsRBd.js} +4 -4
- package/server/{conversation-compaction-D3aS5PnS.js → conversation-compaction-BCggyih4.js} +8 -8
- package/server/{conversation-compaction-D4Wed1BJ.js → conversation-compaction-kHVfVRg3.js} +1 -1
- package/server/{conversation-service-CFZIjs-M.js → conversation-service-B8LFQQm1.js} +6 -5
- package/server/{conversation-service-DcDB5usR.js → conversation-service-BTzObqGi.js} +1 -1
- package/server/{conversations-BSOU5qnu.js → conversations-B-lcHv-5.js} +4 -4
- package/server/{conversations-BbDPrz89.js → conversations-BM7zlXjr.js} +1 -1
- package/server/{cost-BE_ulOrw.js → cost-BcpRQ-7o.js} +3 -3
- package/server/{cost-CaiG63FG.js → cost-D9kPIY_5.js} +2 -2
- package/server/{cost-sync-D6AX8jv-.js → cost-sync-DF6odxK9.js} +3 -3
- package/server/{dag-BQFtXOuJ.js → dag-DKI3sOmX.js} +3 -3
- package/server/{dangerous-git-ops-04Ba-Qu3.js → dangerous-git-ops-Bn18PEcN.js} +1 -1
- package/server/dashboard-db-worker.js +6 -6
- package/server/{deacon-Bz-6Wjmn.js → deacon-DONuopff.js} +3 -3
- package/server/{deacon-2EvA9Ci3.js → deacon-DdARMjZq.js} +188 -1601
- package/server/{deacon-swarm-DYYOzHRH.js → deacon-swarm-BNGkmYR_.js} +12 -105
- package/server/deacon-swarm-CvdFekIO.js +2 -0
- package/server/deacon.js +7 -7
- package/server/{devcontainer-renderer-Dx0C7VWJ.js → devcontainer-renderer-C-34Sxyz.js} +3 -3
- package/server/{dist-ya66D9ze.js → dist-BMXvKiZP.js} +1 -1
- package/server/dist-BNwrgsUO.js +3 -0
- package/server/{dist-src-Byxt1ifL.js → dist-src-BgGUblXg.js} +1 -1
- package/server/{docker-D2TbXDO6.js → docker-apZ9pBYw.js} +67 -28
- package/server/{ensure-devcontainer-BTiMfFp5.js → ensure-devcontainer-Bqi1EF1P.js} +4 -4
- package/server/{ensure-devcontainer-BIShzs3M.js → ensure-devcontainer-Dq3KoOIl.js} +1 -1
- package/server/{esm-shims-DiV9jHa_.js → esm-shims-BEjvINX3.js} +1 -1
- package/server/{event-store-C02Gprug.js → event-store-CqDgfbAs.js} +1 -1
- package/server/{event-store-C71x6h0i.js → event-store-D89fMfXS.js} +3 -3
- package/server/{events-BFDuPV6n.js → events-S_C0pvl6.js} +3 -3
- package/server/{events-D2gP2-Yv.js → events-rIGncmP1.js} +1 -1
- package/server/export-BJHSVwKP.js +87 -0
- package/server/{factory-ByfbrCab.js → factory-DV18m0rc.js} +5 -5
- package/server/{feature-registry-population-CSH66UO8.js → feature-registry-population-CqZSnqQi.js} +4 -4
- package/server/{feature-registry-storage-DW7dUefc.js → feature-registry-storage-BeXmNBwK.js} +2 -2
- package/server/{features-x_aLR7U9.js → features-u5XeMaOK.js} +2 -2
- package/server/{feedback-target-BFo9fScH.js → feedback-target-CwyWf0Dg.js} +6 -6
- package/server/{feedback-target-C4VMPTZu.js → feedback-target-DDG1PWl3.js} +1 -1
- package/server/{feedback-writer-DCyd75yJ.js → feedback-writer-BsJiw5aT.js} +1 -1
- package/server/{feedback-writer-B9ZqnnX3.js → feedback-writer-D20HSXre.js} +4 -4
- package/server/{flywheel-DwQCoZQG.js → flywheel-9Y1X7IM-.js} +13 -13
- package/server/{flywheel-D9NKvWqb.js → flywheel-Dczmgchd.js} +23 -15
- package/server/{flywheel-DB48X7Nn.js → flywheel-RCnHAgc4.js} +1 -1
- package/server/{flywheel-merge-order-DrjmFXg4.js → flywheel-merge-order-CtCYsFJP.js} +7 -7
- package/server/{flywheel-merge-order-f1bi5XIf.js → flywheel-merge-order-DEtEd3aK.js} +1 -1
- package/server/forge-8y6aEPTB.js +2 -0
- package/server/{forge-C-SoKy8U.js → forge-DFDThFil.js} +2 -2
- package/server/{from-fu7ZumnY.js → from-NSJypkI8.js} +1 -1
- package/server/{fts-db-CL-YzSRG.js → fts-db-C5A_TTOM.js} +2 -2
- package/server/{fts-operations-Bylu5NjM.js → fts-operations-DvCSimfU.js} +2 -2
- package/server/{git-utils-Ym380hJZ.js → git-utils-C0neHvQx.js} +1 -1
- package/server/{git-utils-C8koBs8j.js → git-utils-Dr0L7mFk.js} +2 -2
- package/server/{github-app-DIr70YLg.js → github-app-Bwt9lK4A.js} +1 -1
- package/server/{github-app-DhycHVLF.js → github-app-u9-SX7Gc.js} +2 -2
- package/server/{github-client-7gkcXcBS.js → github-client-Btyy3uan.js} +3 -3
- package/server/{github-client-Ji9lUTNF.js → github-client-Do3k-N1W.js} +1 -1
- package/server/{github-pr-lookup-rt7T9FwA.js → github-pr-lookup-DOF1U2WW.js} +2 -2
- package/server/{harness-resolve-CvGXUnAF.js → harness-resolve-C6g2R0Cd.js} +3 -3
- package/server/{health-events-D4E1DLN-.js → health-events-B4u-2a9m.js} +2 -2
- package/server/{health-events-DDfkoO5a.js → health-events-Bos3sk5g.js} +1 -1
- package/server/{hooks-D_6pAbuJ.js → hooks-BCoOjL_R.js} +2 -2
- package/server/{hooks-DvdnCVv6.js → hooks-uEr8WTx_.js} +1 -1
- package/server/{hume-DkVOs1Qi.js → hume-BqxYKbUf.js} +2 -2
- package/server/hume-CerGi2RH.js +3 -0
- package/server/{idle-stack-reaper-cDRXDpgE.js → idle-stack-reaper-DbZ8ukMd.js} +2 -2
- package/server/{idle-stack-reaper-DNbV9-fj.js → idle-stack-reaper-H0W6XtHh.js} +1 -1
- package/server/{infra-Clew3DpF.js → infra-Ba9mS6e6.js} +3 -3
- package/server/{injection-QCIHOpk6.js → injection-C1DKXeIZ.js} +1 -1
- package/server/{injection-ql0Nddcn.js → injection-yrMBxzVq.js} +7 -7
- package/server/{inspect-agent-DY18KEx4.js → inspect-agent-B-mnb4uh.js} +9 -9
- package/server/inspect-agent-CWkeAB7J.js +2 -0
- package/server/{internal-token-C90jBp-y.js → internal-token-BPYEaXFh.js} +1 -1
- package/server/{internal-token-BeXjPLKy.js → internal-token-BtqSbKTx.js} +2 -2
- package/server/{io-CbAFXQge.js → io-VEGSAqsd.js} +15 -22
- package/server/{issue-closed-BYC2TCk-.js → issue-closed-BxcePXL-.js} +4 -4
- package/server/{issue-lifecycle-BoJa6qIt.js → issue-lifecycle--8MRxrG1.js} +22 -22
- package/server/{issue-lifecycle-CLHbj8RS.js → issue-lifecycle-CdMUauyb.js} +1 -1
- package/server/{issue-service-singleton-BS9NENuR.js → issue-service-singleton-CkTUI-eu.js} +10 -10
- package/server/{issue-service-singleton-3g_CCY-1.js → issue-service-singleton-MZsDhAob.js} +1 -1
- package/server/{issues-BPcqPqfV.js → issues-CBAsuoAO.js} +2 -2
- package/server/jsonl-parser-BUW5_sSK.js +245 -0
- package/server/{label-cleanup-CwiCSZrb.js → label-cleanup-htK6fBRU.js} +3 -3
- package/server/{layers-OHHLphl0.js → layers-jjL16At6.js} +2 -2
- package/server/{lifecycle-B8IdvUbt.js → lifecycle-Ck2UR27D.js} +7 -7
- package/server/{lifecycle-io-BMCH1Bxg.js → lifecycle-io-DXv8Utct.js} +1 -1
- package/server/{lifecycle-io-C73m5e6R.js → lifecycle-io-IehcRt2v.js} +36 -47
- package/server/{linear-client-BMxgycoy.js → linear-client-By7hjwuN.js} +1 -1
- package/server/{linear-client-CgQlM4aT.js → linear-client-D9cSQfvD.js} +3 -3
- package/server/memory-fts-worker.js +1 -1
- package/server/{merge-BRkVaIx-.js → merge-BLcEZNXz.js} +1 -1
- package/server/{merge-B7Oi-g6k.js → merge-BN8FGJ2q.js} +3 -3
- package/server/{merge-agent-DNFFsiEm.js → merge-agent-B3u2szOL.js} +29 -29
- package/server/{merge-agent-Bwm34MZ5.js → merge-agent-ejC7Wos8.js} +1 -1
- package/server/{merge-batch-BpYXUaqJ.js → merge-batch-DyqlgYGp.js} +1 -1
- package/server/{merge-blockers-DIOZmqdK.js → merge-blockers-cq-aS2KG.js} +2 -2
- package/server/merge-ops-CFkQtXEj.js +2 -0
- package/server/{merge-rebase-C8WH39MB.js → merge-rebase-Dl7yIxtV.js} +2 -2
- package/server/{merge-set-CsEE2CyQ.js → merge-set-CZ9Nbay0.js} +3 -3
- package/server/{merge-set-DOsBTBQc.js → merge-set-Dy0D0r3i.js} +1 -1
- package/server/{merge-sync-CcLM2vwW.js → merge-sync-CG9kDCdT.js} +1 -1
- package/server/{merge-sync-C2B322Ot.js → merge-sync-D7jWLgCC.js} +2 -2
- package/server/{merge-train-BIm7-7cy.js → merge-train-CH33XJOH.js} +3 -3
- package/server/{merge-train-deps-DG1oQ2DT.js → merge-train-deps-JoVdGnOg.js} +4 -4
- package/server/{merge-verification-BAbjxEd5.js → merge-verification-D4wTChdX.js} +4 -4
- package/server/{multipart-parser-jKtKAXtl.js → multipart-parser-Csn8U-K9.js} +2 -2
- package/server/{no-resume-mode-yrPEQ_pC.js → no-resume-mode-Gh5R_YLF.js} +1 -1
- package/server/node_modules/.package-lock.json +1 -1
- package/server/{orphan-proposed-reconciler-BqVGaOIj.js → orphan-proposed-reconciler-BIcNu4tl.js} +1 -1
- package/server/{orphan-proposed-reconciler-zHrwJ3xr.js → orphan-proposed-reconciler-DI_IS1p_.js} +15 -39
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
- package/server/{pan-dir-BlNrLzW7.js → pan-dir-CQIfKG8c.js} +4 -4
- package/server/{paths-CiVeo4N_.js → paths-Blj6A9AJ.js} +2 -2
- package/server/{pickup-KLRUhThD.js → pickup-BtvRBbvI.js} +1 -1
- package/server/{pipeline-notifier-nkWCe042.js → pipeline-notifier-BH4PAi-w.js} +1 -1
- package/server/{pipeline-notifier-DBF6dHxH.js → pipeline-notifier-D_WYtEwl.js} +2 -2
- package/server/project-pipelines-jMTitpeO.js +162 -0
- package/server/projects-CrBE6IhN.js +3 -0
- package/server/{prompts-CXbQvevY.js → prompts-B3njaImU.js} +2 -2
- package/server/{providers-CUdAb5Qu.js → providers-BbEyFhPX.js} +7 -7
- package/server/providers-DnPxkHoG.js +3 -0
- package/server/public/assets/chunk-BO2N2NFS-3qh9TUJi.js +103 -0
- package/server/public/assets/{diagram-OG6HWLK6-NACO1xW5.js → diagram-OG6HWLK6-D8UVTPj7.js} +1 -1
- package/server/public/assets/{dist-DQD-ZjQY.js → dist-BvLLn1YC.js} +3 -3
- package/server/public/assets/{dist-D1FjqGgm.js → dist-CyubL7Wj.js} +1 -1
- package/server/public/assets/{ganttDiagram-6RSMTGT7-CEMAKgmE.js → ganttDiagram-6RSMTGT7-BI4E1LbQ.js} +1 -1
- package/server/public/assets/{highlighted-body-OFNGDK62-D5lC7FW8.js → highlighted-body-OFNGDK62-D_bapWkF.js} +1 -1
- package/server/public/assets/index-BAZxZ-8I.js +796 -0
- package/server/public/assets/index-FBG3JBfq.css +1 -0
- package/server/public/assets/{linear-DDHAMRPB.js → linear-CEdobObr.js} +1 -1
- package/server/public/assets/mermaid-GHXKKRXX-BYP8MI37.js +1 -0
- package/server/public/assets/{pieDiagram-4H26LBE5-DwBdeKBb.js → pieDiagram-4H26LBE5-DT79L01f.js} +1 -1
- package/server/public/assets/{prod-DAZt0c4G.js → prod-CR_K2DFV.js} +4 -4
- package/server/public/assets/{quadrantDiagram-W4KKPZXB-CNDLp-_m.js → quadrantDiagram-W4KKPZXB-BEkKXXrL.js} +1 -1
- package/server/public/assets/{sankeyDiagram-5OEKKPKP-oIUL_uO1.js → sankeyDiagram-5OEKKPKP-BxSCqn-j.js} +1 -1
- package/server/public/assets/{xychartDiagram-2RQKCTM6-B-IM6Cck.js → xychartDiagram-2RQKCTM6-DicwJGG_.js} +1 -1
- package/server/public/index.html +2 -2
- package/server/{pull-requests-DZ-V7_LB.js → pull-requests-43LPDfM8.js} +1 -1
- package/server/{pull-requests-kTRl7cWC.js → pull-requests-DyUkQl4W.js} +3 -3
- package/server/{rally-xCTZ-UcI.js → rally-BtWjybOa.js} +3 -3
- package/server/{rally-api-jOk4vit_.js → rally-api-B-YA_oXx.js} +1 -1
- package/server/{rally-api-DpZo3WwF.js → rally-api-B-osULNL.js} +2 -2
- package/server/{rally-client-BQjm3aaq.js → rally-client-CzFymP7P.js} +1 -1
- package/server/{rally-client-CoWZeVEj.js → rally-client-sErNQBO0.js} +5 -5
- package/server/rally-oggyp4cs.js +3 -0
- package/server/{reap-terminal-sessions-CAmURC2h.js → reap-terminal-sessions-2s-fvilz.js} +1 -1
- package/server/{reap-terminal-sessions-BwJb2DLJ.js → reap-terminal-sessions-DnDQ7pf9.js} +1 -1
- package/server/{rebuild-stack-BZ7j27xY.js → rebuild-stack-CoU-HqZj.js} +6 -6
- package/server/{rebuild-stack-r-cxcr26.js → rebuild-stack-nGD_nPZo.js} +1 -1
- package/server/{reconstruct-cache-13EkMg_i.js → reconstruct-cache-Wj8GrYB0.js} +6 -6
- package/server/record-lEYK__LD.js +3 -0
- package/server/recovery-trip-BT7nb9OV.js +98 -0
- package/server/{release-engine-YLTnEO_K.js → release-engine-CBLQXDlH.js} +5 -5
- package/server/{release-set-DwvT_Opu.js → release-set-BNiqi7hy.js} +3 -3
- package/server/{remote-B062S1Kc.js → remote-DW_TWQnr.js} +3 -3
- package/server/{remote-9u9WL0NJ.js → remote-DcO9An_f.js} +3 -3
- package/server/{remote-agents-BBTg-vLT.js → remote-agents-C4cyxHQS.js} +6 -6
- package/server/{remote-agents-B4eWgwNd.js → remote-agents-Cv-U-_V8.js} +1 -1
- package/server/{remote-completion-B0rmBnbL.js → remote-completion-PhXGGFQy.js} +13 -13
- package/server/resolver-C7dLn8OK.js +158 -0
- package/server/resume-BYZBIab-.js +3 -0
- package/server/{review-agent-tfR5RDzG.js → review-agent-Dn-yn7Zj.js} +23 -23
- package/server/{review-agent-Bf9XjLnl.js → review-agent-u5q3ip7H.js} +1 -1
- package/server/{review-artifacts-D0_IE6DQ.js → review-artifacts-D4T04aYI.js} +6 -6
- package/server/{review-context-CXO5ml5D.js → review-context-Rc0VDLG_.js} +8 -8
- package/server/{review-monitor-DfFUSx0J.js → review-monitor-BC--miCO.js} +2 -2
- package/server/{review-status-B0Axo-Ma.js → review-status-CG0iDuWa.js} +1 -1
- package/server/{review-status-BcfwcAcO.js → review-status-Dc1l4dVz.js} +18 -18
- package/server/{review-status-enrichment-Cp5kFTGl.js → review-status-enrichment-C1a1OG_1.js} +3 -3
- package/server/{review-status-sync-CEFty1pW.js → review-status-sync-CAr-hfnJ.js} +2 -2
- package/server/{review-status-sync-Bc0c_u12.js → review-status-sync-CfSixhhX.js} +1 -1
- package/server/{review-verdict-feedback-BOKRLLDf.js → review-verdict-feedback-CVDbIDBe.js} +7 -7
- package/server/{review-verdict-feedback-BnGtT0vN.js → review-verdict-feedback-D2Q960wX.js} +1 -1
- package/server/{rollup-BVqFuiS9.js → rollup-CakjztPl.js} +1 -1
- package/server/{rollup-C22QDVxh.js → rollup-DctoIu-A.js} +8 -8
- package/server/{scanner-BLCnuXqY.js → scanner-BTM1jc_u.js} +7 -7
- package/server/{sequence-io-BozlUb6f.js → sequence-io-pnoIDGv4.js} +4 -4
- package/server/{sequencer-agent-BEDEHfd2.js → sequencer-agent-9Soi-WL-.js} +10 -6
- package/server/server.js +1239 -1836
- package/server/{service-C8jKYZ0w.js → service-DfTVoqJg.js} +1668 -1408
- package/server/{session-fork-BDGksK4e.js → session-fork-C4jAFHj8.js} +2 -2
- package/server/session-history-DanhhiCw.js +168 -0
- package/server/{settings-CSLhh6qR.js → settings-Cuc12oP_.js} +2 -2
- package/server/settings-Cy7SCFO9.js +2 -0
- package/server/{settings-DrRQCren.js → settings-DfT3f86D.js} +3 -3
- package/server/{shadow-engineering-BMcBN-Of.js → shadow-engineering-bKuKTCn-.js} +3 -3
- package/server/{shadow-state-CHqWHD3T.js → shadow-state-CVHpRWlb.js} +1 -1
- package/server/{shadow-state-B4ayC7nx.js → shadow-state-DJz6_pb_.js} +2 -2
- package/server/{ship-log-CDVmOlZP.js → ship-log-CwqPFfqx.js} +1 -1
- package/server/{smart-compaction-V9DBrGcr.js → smart-compaction-B4KlpZGN.js} +1 -1
- package/server/{smart-compaction-C-F5sP9P.js → smart-compaction-CiWfGfRZ.js} +7 -7
- package/server/{spawn-planning-session-dSpOU0lP.js → spawn-planning-session-CU-DwyDl.js} +15 -15
- package/server/{spawn-planning-session-Bz4_JES2.js → spawn-planning-session-D5N3RuLG.js} +1 -1
- package/server/{specialist-completion-Coz3eTSr.js → specialist-completion-CJcv-NXH.js} +1 -1
- package/server/{specialist-context-fmb03vXQ.js → specialist-context-B2HlbNxS.js} +5 -5
- package/server/{specialist-handoff-logger-B9HK_lRu.js → specialist-handoff-logger-CQyF1ygu.js} +2 -2
- package/server/{specialist-logs-B24R2B9Y.js → specialist-logs-CdcUpfjj.js} +3 -3
- package/server/{specialists-CL7E8yUg.js → specialists-CFgw_0Q6.js} +1657 -113
- package/server/{specialists-BGNZ0_ZW.js → specialists-CwzFdapR.js} +20 -256
- package/server/{specialists-B6f5-5nG.js → specialists-D7AcFi1u.js} +1 -1
- package/server/{specialists-Chvvwf5z.js → specialists-k22_Btaq.js} +1 -1
- package/server/specs-RzcDgozr.js +3 -0
- package/server/{src-DGAEtABV.js → src-B4TTzNSr.js} +84 -2
- package/server/{src-BVxZuHMg.js → src-BvpMg4EB.js} +3 -3
- package/server/{src-D-VXCC9C.js → src-D4YUZrwR.js} +6 -6
- package/server/{stack-health-C-le5erA.js → stack-health-CSgcYcr8.js} +2 -2
- package/server/{stack-health-BKbTTOT2.js → stack-health-DVN7zRnL.js} +1 -1
- package/server/{state-home-CoyWsECg.js → state-home-CEvD_v5F.js} +6 -6
- package/server/{state-mapping-BvAgFK8n.js → state-mapping-C13UaCL7.js} +1 -1
- package/server/stuck-remediation-CxsyxwFO.js +1513 -0
- package/server/stuck-remediation-nTIvhGOR.js +2 -0
- package/server/{summary-fork-BF_0vxrq.js → summary-fork-CnRMPCYd.js} +2 -2
- package/server/{summary-fork-BlE30Q-I.js → summary-fork-gKSoFAnK.js} +8 -8
- package/server/{supervisor-BSeML_p0.js → supervisor-CZMs8s0M.js} +2 -2
- package/server/supervisor-CfX192ma.js +2 -0
- package/server/{swarm-readiness-CQ4sjrXc.js → swarm-readiness-h6-pwY8w.js} +3 -3
- package/server/{system-prerequisites-DnmLYOnU.js → system-prerequisites-DZZTi2G6.js} +1 -1
- package/server/{teardown-workspace-CpcjDrZ4.js → teardown-workspace-B4D-oCVg.js} +37 -68
- package/server/{test-agent-queue-Bq298zZl.js → test-agent-queue-CnuYku4B.js} +6 -6
- package/server/{test-verdict-CCDd25uN.js → test-verdict-CRsPpByy.js} +1 -1
- package/server/{test-verdict-Bm4uv_AA.js → test-verdict-CXMLATGc.js} +1 -1
- package/server/{tldr-daemon-Rn1Zue0P.js → tldr-daemon-CgZQ3jmZ.js} +1 -1
- package/server/{tldr-daemon-CaSUXd9r.js → tldr-daemon-CuA8hz21.js} +2 -2
- package/server/{tmux-CbcrkUx5.js → tmux-By8E_Yv_.js} +1 -1
- package/server/{tmux-BVc-NeKM.js → tmux-C3g9VFcg.js} +2 -2
- package/server/{token-D2OC_9ms.js → token-g_sJ19g3.js} +2 -2
- package/server/token-util-C9u4An3K.js +3 -0
- package/server/{token-util-CROoF5ic.js → token-util-DiwnnbJo.js} +1 -1
- package/server/{tracker-config-EsBoOWEy.js → tracker-config-BzfVj41Z.js} +1 -1
- package/server/{tracker-config-C1Y_2CIo.js → tracker-config-dQxC15vH.js} +2 -2
- package/server/{tracker-utils-lph9C3Pw.js → tracker-utils--z8FLSWn.js} +2 -2
- package/server/{tracker-utils-DSb9cfrF.js → tracker-utils-DkKyRT0H.js} +1 -1
- package/server/{traefik-connect-5EP1WS_Z.js → traefik-connect-CZYB3dlm.js} +1 -1
- package/server/{tunnel-BFBCeo97.js → tunnel-BpWZs0Jy.js} +3 -3
- package/server/{tunnel-DDe-ATFF.js → tunnel-CN2px87h.js} +1 -1
- package/server/{typed-errors-DSPn_xWP.js → typed-errors-CrN1AByg.js} +1 -1
- package/server/{typed-errors-BEJhbjJg.js → typed-errors-sLpGfegz.js} +1 -1
- package/server/{types-CE1m_daD.js → types-BLS_Zbmd.js} +1 -1
- package/server/{types-B5u3NxMl.js → types-tlXf80x3.js} +1 -1
- package/server/{uat-train-DtKHHy1U.js → uat-train-BRo6DM7p.js} +24 -24
- package/server/{uat-train-8T24rPzc.js → uat-train-utGVmqdm.js} +1 -1
- package/server/{validation-lyudzPvD.js → validation-CsPVsoA0.js} +4 -4
- package/server/{validation-COOXwNXU.js → validation-Dq_wwcw3.js} +1 -1
- package/server/{verification-runner-DO0nYTJx.js → verification-runner-CMT5WRMS.js} +12 -12
- package/server/{verification-runner-Dd1FX8Qk.js → verification-runner-D0Uh-9jc.js} +1 -1
- package/server/{vtt-parser-C57tEF7-.js → vtt-parser-C_XPlifj.js} +1 -1
- package/server/{work-agent-prompt-B2F3ZwKm.js → work-agent-prompt-ZTAb2Hyf.js} +12 -12
- package/server/workflows-BLwxUMP7.js +2 -0
- package/server/{workflows-Clz_AFwR.js → workflows-CXRwhw_o.js} +14 -14
- package/server/{workspace-D0rCFemz.js → workspace-9UEsKuiP.js} +1 -1
- package/server/{workspace-CY_2-Ddv.js → workspace-CNfuj1-5.js} +3 -3
- package/server/{workspace-config-DA8U97k9.js → workspace-config-ChWOECy0.js} +1 -1
- package/server/{workspace-manager-BIfpzqdU.js → workspace-manager-BO-6k3sP.js} +1 -1
- package/server/{workspace-manager-D6NgGkiF.js → workspace-manager-D8AByFIT.js} +13 -13
- package/server/{workspace-metadata-C8yn8kAH.js → workspace-metadata-CvC268MN.js} +2 -2
- package/server/{workspace-metadata-DfgR40YU.js → workspace-metadata-DZiJtnuF.js} +1 -1
- package/server/{workspaces-DOeHyKmA.js → workspaces-D-uIoqbg.js} +104 -122
- package/server/writer-BUweuRPx.js +3 -0
- package/server/writer-CrP2xuBn.js +251 -0
- package/sync-sources/hooks/ask-user-question-hook +7 -1
- package/sync-sources/hooks/record-cost-event.js +4 -3
- package/sync-sources/hooks/record-cost-event.js.map +1 -1
- package/server/agent-runtime-mirror-DrWxopNA.js +0 -37
- package/server/auto-commit-wZ9X7Isd.js +0 -3
- package/server/bd-mutex-BItGvLQ8.js +0 -35
- package/server/beads-restore-CMbDSB5R.js +0 -60
- package/server/cli-CqgiSAU8.js +0 -3
- package/server/deacon-swarm-B8pgK2a7.js +0 -2
- package/server/dist-BYSj075e.js +0 -3
- package/server/forge-CUCYK7h_.js +0 -2
- package/server/hume-x9voULey.js +0 -3
- package/server/inspect-agent-D8UBDgfR.js +0 -2
- package/server/merge-ops-B7m-hBEZ.js +0 -2
- package/server/projects-BqRMlDOv.js +0 -3
- package/server/providers-gZ0cUgQM.js +0 -3
- package/server/public/assets/chunk-BO2N2NFS-C-6y8VFI.js +0 -103
- package/server/public/assets/index-BDqWyuZQ.js +0 -796
- package/server/public/assets/index-CZiADHcL.css +0 -1
- package/server/public/assets/mermaid-GHXKKRXX-DRM2ANcR.js +0 -1
- package/server/rally-CRteqXe0.js +0 -3
- package/server/record-CJsjvRO4.js +0 -3
- package/server/resume-R7xkSmLW.js +0 -3
- package/server/settings-CSaXtkoA.js +0 -2
- package/server/specs-oaaYaA5c.js +0 -3
- package/server/supervisor-03Tj_Wra.js +0 -2
- package/server/token-util-xNaV-00f.js +0 -3
- package/server/workflows-B4fIN8VP.js +0 -2
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fn as init_projects, Ft as readRecordContinueViewSync, It as resolveProjectForIssue, Jr as init_paths, Ot as getProjectConfigFromWorkspacePath, Sr as AGENTS_DIR, jr as OVERDECK_HOME, kt as init_record, qn as resolveProjectFromIssueSync, qr as getOverdeckHome, xr as init_errors } from "./config-yaml-cNOZcEww.js";
|
|
2
2
|
import { a as init_activity_logger, n as emitActivityEntrySync } from "./activity-logger-pcbf-e30.js";
|
|
3
|
-
import { D as sql, E as like, S as gte, _ as integer, a as EventBus, b as and, f as init_infra, g as real, h as text, i as DbLive, m as sqliteTable, n as CostArchiveLive, o as EventBusLive, p as init_sqlite_core, r as Db, t as CostArchive, v as init_drizzle_orm, x as eq, y as desc } from "./infra-
|
|
4
|
-
import { L as sessionExists, R as sessionExistsSync, w as killSession, x as init_tmux } from "./tmux-
|
|
5
|
-
import { c as init_review_status, u as loadReviewStatuses, y as setReviewStatusSync } from "./review-status-
|
|
6
|
-
import { n as init_issues, t as IssueId } from "./issues-
|
|
7
|
-
import { a as getAllBudgetsSync, i as deleteBudgetSync, n as checkBudgetSync, r as createBudgetSync, s as init_cost } from "./cost-
|
|
8
|
-
import { An as getAgentDir, D as init_context_overflow, Jn as saveAgentStateSync, Mn as getAgentState, Pn as getAgentStateSync, Q as listRunningAgentsSync,
|
|
9
|
-
import {
|
|
10
|
-
import { _ as init_tier_metrics, c as getDailyTrendsSync, d as init_cost_sync, h as deriveTieredAgentCostRole, t as getAgentDailyCostSync } from "./cost-sync-
|
|
11
|
-
import { c as requireModelOverrideSync, o as init_model_validation } from "./state-home-
|
|
12
|
-
import { _ as setDeaconGloballyPaused, c as isCloisterSpawnsPausedSync, g as setCloisterSpawnsPausedSync, s as init_control_settings, x as setFlywheelGloballyPaused } from "./control-settings-
|
|
13
|
-
import { n as
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import "./
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { n as init_issue_closed, r as isIssueClosed } from "./issue-closed-
|
|
22
|
-
import { n as shouldSkipDispatchAsMerged, t as init_merge_verification } from "./merge-verification-
|
|
3
|
+
import { D as sql, E as like, S as gte, _ as integer, a as EventBus, b as and, f as init_infra, g as real, h as text, i as DbLive, m as sqliteTable, n as CostArchiveLive, o as EventBusLive, p as init_sqlite_core, r as Db, t as CostArchive, v as init_drizzle_orm, x as eq, y as desc } from "./infra-Ba9mS6e6.js";
|
|
4
|
+
import { L as sessionExists, R as sessionExistsSync, d as exactPaneTarget, w as killSession, x as init_tmux } from "./tmux-C3g9VFcg.js";
|
|
5
|
+
import { c as init_review_status, u as loadReviewStatuses, y as setReviewStatusSync } from "./review-status-Dc1l4dVz.js";
|
|
6
|
+
import { n as init_issues, t as IssueId } from "./issues-CBAsuoAO.js";
|
|
7
|
+
import { a as getAllBudgetsSync, i as deleteBudgetSync, n as checkBudgetSync, r as createBudgetSync, s as init_cost } from "./cost-D9kPIY_5.js";
|
|
8
|
+
import { An as getAgentDir, D as init_context_overflow, Jn as saveAgentStateSync, Mn as getAgentState, Pn as getAgentStateSync, Q as listRunningAgentsSync, U as spawnAgent, _t as init_config, gt as getHealthThresholdsMs, hr as saveAgentRuntimeState, k as isContextOverflowTail, pr as getAgentRuntimeStateSync, q as stopAgentSync, t as init_agents, vr as init_ohmypi_parser, yr as parseOhmypiSessionCostResultSync, yt as loadCloisterConfigSync } from "./agents-B9GwfCyp.js";
|
|
9
|
+
import { n as parseCodexSessionCostEventsSync, r as parseCodexSessionSync, t as init_codex_parser } from "./codex-parser-mFYZh8vR.js";
|
|
10
|
+
import { _ as init_tier_metrics, c as getDailyTrendsSync, d as init_cost_sync, h as deriveTieredAgentCostRole, t as getAgentDailyCostSync } from "./cost-sync-DF6odxK9.js";
|
|
11
|
+
import { c as requireModelOverrideSync, o as init_model_validation } from "./state-home-CEvD_v5F.js";
|
|
12
|
+
import { _ as setDeaconGloballyPaused, c as isCloisterSpawnsPausedSync, g as setCloisterSpawnsPausedSync, s as init_control_settings, x as setFlywheelGloballyPaused } from "./control-settings-D4EFsRBd.js";
|
|
13
|
+
import { n as init_resolver, t as createBeadsResolver } from "./resolver-C7dLn8OK.js";
|
|
14
|
+
import { n as checkHookSync, p as init_hooks } from "./hooks-BCoOjL_R.js";
|
|
15
|
+
import { r as isRoleTerminal, t as init_reap_terminal_sessions } from "./reap-terminal-sessions-2s-fvilz.js";
|
|
16
|
+
import { o as getRuntimeForAgent } from "./stuck-remediation-CxsyxwFO.js";
|
|
17
|
+
import "./flywheel-Dczmgchd.js";
|
|
18
|
+
import { C as getLastPatrolResult, D as isDeaconRunning, N as runPatrol, R as startDeacon, S as getDeaconStatus, n as assessDeaconPatrolFreshness, x as getDeaconLogs, z as stopDeacon } from "./deacon-DdARMjZq.js";
|
|
19
|
+
import { n as renderPrompt, t as init_prompts } from "./prompts-B3njaImU.js";
|
|
20
|
+
import { O as init_specialists, T as getTmuxSessionName } from "./specialists-CwzFdapR.js";
|
|
21
|
+
import { n as init_issue_closed, r as isIssueClosed } from "./issue-closed-BxcePXL-.js";
|
|
22
|
+
import { n as shouldSkipDispatchAsMerged, t as init_merge_verification } from "./merge-verification-D4wTChdX.js";
|
|
23
|
+
import { r as writeHealthEvent } from "./health-events-B4u-2a9m.js";
|
|
23
24
|
import { join } from "node:path";
|
|
24
25
|
import { Context, Data, Effect, Layer, Schema } from "effect";
|
|
25
26
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
@@ -27,7 +28,7 @@ import { dirname as dirname$1, join as join$1 } from "path";
|
|
|
27
28
|
import { existsSync as existsSync$1, readdirSync as readdirSync$1 } from "node:fs";
|
|
28
29
|
import { createHash } from "crypto";
|
|
29
30
|
import { rm } from "fs/promises";
|
|
30
|
-
import { exec } from "node:child_process";
|
|
31
|
+
import { exec, execFile } from "node:child_process";
|
|
31
32
|
import { promisify } from "node:util";
|
|
32
33
|
import { exec as exec$1 } from "child_process";
|
|
33
34
|
import { promisify as promisify$1 } from "util";
|
|
@@ -726,7 +727,10 @@ HttpApiGroup.make("costs").add(HttpApiEndpoint.get("summary", "/costs/summary",
|
|
|
726
727
|
}));
|
|
727
728
|
//#endregion
|
|
728
729
|
//#region ../../lib/cloister/health.ts
|
|
729
|
-
|
|
730
|
+
init_cost_sync();
|
|
731
|
+
init_review_status();
|
|
732
|
+
init_control_settings();
|
|
733
|
+
init_hooks();
|
|
730
734
|
init_agents();
|
|
731
735
|
init_config();
|
|
732
736
|
/**
|
|
@@ -831,9 +835,6 @@ function getAgentsNeedingAttention(agentHealths) {
|
|
|
831
835
|
}
|
|
832
836
|
//#endregion
|
|
833
837
|
//#region ../../lib/cloister/handoff-context.ts
|
|
834
|
-
init_review_status();
|
|
835
|
-
init_control_settings();
|
|
836
|
-
init_hooks();
|
|
837
838
|
/**
|
|
838
839
|
* Handoff Context Module
|
|
839
840
|
*
|
|
@@ -1168,310 +1169,595 @@ const performHandoff = (agentId, options) => Effect.tryPromise({
|
|
|
1168
1169
|
})
|
|
1169
1170
|
});
|
|
1170
1171
|
//#endregion
|
|
1171
|
-
//#region ../../lib/cloister/
|
|
1172
|
+
//#region ../../lib/cloister/cost-monitor.ts
|
|
1172
1173
|
/**
|
|
1173
|
-
*
|
|
1174
|
+
* Cost Monitor
|
|
1174
1175
|
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* 2. Planning complete - Planning phase finished, ready for implementation
|
|
1178
|
-
* 3. Test failure - Tests failing, need more powerful model
|
|
1179
|
-
* 4. Task completion - Implementation done, ready for specialist testing
|
|
1176
|
+
* Monitors agent costs against configured limits and emits alerts.
|
|
1177
|
+
* Does NOT automatically stop agents - just provides visibility and warnings.
|
|
1180
1178
|
*/
|
|
1179
|
+
init_paths();
|
|
1181
1180
|
init_config();
|
|
1182
|
-
const execAsync$2 = promisify$1(exec$1);
|
|
1183
|
-
const taskCompletionCache = /* @__PURE__ */ new Map();
|
|
1184
|
-
const taskCompletionInflight = /* @__PURE__ */ new Map();
|
|
1185
1181
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* Model-specific thresholds:
|
|
1189
|
-
* - Haiku: Stuck after 10 minutes → escalate to Sonnet
|
|
1190
|
-
* - Sonnet: Stuck after 20 minutes → escalate to Opus
|
|
1191
|
-
* - Opus: Stuck after 30 minutes → alert user (no auto-escalation)
|
|
1192
|
-
*
|
|
1193
|
-
* @param health - Agent health state
|
|
1194
|
-
* @param currentModel - Current model
|
|
1195
|
-
* @param config - Cloister configuration
|
|
1196
|
-
* @returns Trigger detection result
|
|
1182
|
+
* Path to cost data file
|
|
1197
1183
|
*/
|
|
1198
|
-
|
|
1199
|
-
const stuckConfig = (config || loadCloisterConfigSync()).handoffs?.auto_triggers?.stuck_escalation;
|
|
1200
|
-
if (!stuckConfig?.enabled) return {
|
|
1201
|
-
triggered: false,
|
|
1202
|
-
type: "stuck_escalation",
|
|
1203
|
-
reason: "Stuck escalation disabled in config",
|
|
1204
|
-
confidence: "high"
|
|
1205
|
-
};
|
|
1206
|
-
if (health.state !== "stuck") return {
|
|
1207
|
-
triggered: false,
|
|
1208
|
-
type: "stuck_escalation",
|
|
1209
|
-
reason: `Agent is ${health.state}, not stuck`,
|
|
1210
|
-
confidence: "high"
|
|
1211
|
-
};
|
|
1212
|
-
if (!health.lastActivity) return {
|
|
1213
|
-
triggered: false,
|
|
1214
|
-
type: "stuck_escalation",
|
|
1215
|
-
reason: "No last activity timestamp available",
|
|
1216
|
-
confidence: "low"
|
|
1217
|
-
};
|
|
1218
|
-
const minutesSinceActivity = (Date.now() - health.lastActivity.getTime()) / (1e3 * 60);
|
|
1219
|
-
if (currentModel === "haiku" && minutesSinceActivity >= stuckConfig.haiku_to_sonnet_minutes) return {
|
|
1220
|
-
triggered: true,
|
|
1221
|
-
type: "stuck_escalation",
|
|
1222
|
-
reason: `Haiku agent stuck for ${Math.round(minutesSinceActivity)} minutes`,
|
|
1223
|
-
suggestedModel: "sonnet",
|
|
1224
|
-
confidence: "high"
|
|
1225
|
-
};
|
|
1226
|
-
if (currentModel === "sonnet" && minutesSinceActivity >= stuckConfig.sonnet_to_opus_minutes) return {
|
|
1227
|
-
triggered: true,
|
|
1228
|
-
type: "stuck_escalation",
|
|
1229
|
-
reason: `Sonnet agent stuck for ${Math.round(minutesSinceActivity)} minutes`,
|
|
1230
|
-
suggestedModel: "opus",
|
|
1231
|
-
confidence: "high"
|
|
1232
|
-
};
|
|
1233
|
-
if (currentModel === "opus") return {
|
|
1234
|
-
triggered: false,
|
|
1235
|
-
type: "stuck_escalation",
|
|
1236
|
-
reason: "Opus agent stuck - no higher model available, manual intervention needed",
|
|
1237
|
-
confidence: "high"
|
|
1238
|
-
};
|
|
1239
|
-
return {
|
|
1240
|
-
triggered: false,
|
|
1241
|
-
type: "stuck_escalation",
|
|
1242
|
-
reason: `Agent stuck but threshold not reached (${Math.round(minutesSinceActivity)} minutes)`,
|
|
1243
|
-
confidence: "medium"
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1184
|
+
const COST_DATA_FILE = join$1(OVERDECK_HOME, "cost-data.json");
|
|
1246
1185
|
/**
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
* Aggressive escalation: Any test failure from Haiku escalates to Sonnet
|
|
1250
|
-
* Reasoning: Haiku is for simple tasks - if tests fail, the task isn't simple
|
|
1251
|
-
*
|
|
1252
|
-
* @param workspace - Workspace path
|
|
1253
|
-
* @param currentModel - Current model
|
|
1254
|
-
* @param config - Cloister configuration
|
|
1255
|
-
* @returns Trigger detection result
|
|
1186
|
+
* Load cost data from file
|
|
1256
1187
|
*/
|
|
1257
|
-
function
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
confidence: "high"
|
|
1264
|
-
};
|
|
1265
|
-
if (currentModel !== testConfig.from_model) return {
|
|
1266
|
-
triggered: false,
|
|
1267
|
-
type: "test_failure",
|
|
1268
|
-
reason: `Test failure escalation only applies to ${testConfig.from_model} model`,
|
|
1269
|
-
confidence: "high"
|
|
1270
|
-
};
|
|
1271
|
-
const testFailure = detectTestFailure(workspace);
|
|
1272
|
-
if (testFailure.failed) return {
|
|
1273
|
-
triggered: true,
|
|
1274
|
-
type: "test_failure",
|
|
1275
|
-
reason: `Test failures detected: ${testFailure.reason}`,
|
|
1276
|
-
suggestedModel: testConfig.to_model,
|
|
1277
|
-
confidence: testFailure.confidence
|
|
1278
|
-
};
|
|
1279
|
-
return {
|
|
1280
|
-
triggered: false,
|
|
1281
|
-
type: "test_failure",
|
|
1282
|
-
reason: "No test failures detected",
|
|
1283
|
-
confidence: "medium"
|
|
1188
|
+
function loadCostData() {
|
|
1189
|
+
if (!existsSync(COST_DATA_FILE)) return {
|
|
1190
|
+
perAgent: /* @__PURE__ */ new Map(),
|
|
1191
|
+
perIssue: /* @__PURE__ */ new Map(),
|
|
1192
|
+
dailyTotal: 0,
|
|
1193
|
+
lastResetDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1284
1194
|
};
|
|
1195
|
+
try {
|
|
1196
|
+
const fileContent = readFileSync(COST_DATA_FILE, "utf-8");
|
|
1197
|
+
const persisted = JSON.parse(fileContent);
|
|
1198
|
+
return {
|
|
1199
|
+
perAgent: new Map(Object.entries(persisted.perAgent || {})),
|
|
1200
|
+
perIssue: new Map(Object.entries(persisted.perIssue || {})),
|
|
1201
|
+
dailyTotal: persisted.dailyTotal || 0,
|
|
1202
|
+
lastResetDate: persisted.lastResetDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1203
|
+
};
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error("Failed to load cost data, starting fresh:", error);
|
|
1206
|
+
return {
|
|
1207
|
+
perAgent: /* @__PURE__ */ new Map(),
|
|
1208
|
+
perIssue: /* @__PURE__ */ new Map(),
|
|
1209
|
+
dailyTotal: 0,
|
|
1210
|
+
lastResetDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1285
1213
|
}
|
|
1286
1214
|
/**
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
* Checks for:
|
|
1290
|
-
* - npm test output
|
|
1291
|
-
* - Jest results
|
|
1292
|
-
* - pytest results
|
|
1293
|
-
* - cargo test results
|
|
1294
|
-
*
|
|
1295
|
-
* @param workspace - Workspace path
|
|
1296
|
-
* @returns Test failure detection
|
|
1215
|
+
* Save cost data to file (atomic write)
|
|
1297
1216
|
*/
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
if (!completionConfig?.enabled) return {
|
|
1308
|
-
triggered: false,
|
|
1309
|
-
type: "task_complete",
|
|
1310
|
-
reason: "Task completion detection disabled in config",
|
|
1311
|
-
confidence: "high"
|
|
1312
|
-
};
|
|
1313
|
-
const cacheKey = `${workspace ?? ""}::${issueId.toLowerCase()}`;
|
|
1314
|
-
let mtimeMs = 0;
|
|
1315
|
-
if (workspace) {
|
|
1316
|
-
const beadsFile = join$1(workspace, ".beads", "issues.jsonl");
|
|
1317
|
-
try {
|
|
1318
|
-
mtimeMs = statSync(beadsFile).mtimeMs;
|
|
1319
|
-
} catch {}
|
|
1320
|
-
if (mtimeMs > 0) {
|
|
1321
|
-
const cached = taskCompletionCache.get(cacheKey);
|
|
1322
|
-
if (cached && cached.mtimeMs === mtimeMs) return cached.result;
|
|
1323
|
-
const inflight = taskCompletionInflight.get(cacheKey);
|
|
1324
|
-
if (inflight) return inflight;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
const computePromise = (async () => {
|
|
1328
|
-
if (!workspace) return {
|
|
1329
|
-
triggered: false,
|
|
1330
|
-
type: "task_complete",
|
|
1331
|
-
reason: "No workspace available to check task completion",
|
|
1332
|
-
confidence: "high"
|
|
1333
|
-
};
|
|
1334
|
-
const bdOptions = {
|
|
1335
|
-
encoding: "utf-8",
|
|
1336
|
-
cwd: workspace
|
|
1337
|
-
};
|
|
1338
|
-
const issueLower = issueId.toLowerCase();
|
|
1339
|
-
try {
|
|
1340
|
-
const { stdout: output } = await Effect.runPromise(withBdMutex(() => Effect.tryPromise({
|
|
1341
|
-
try: () => execAsync$2(`bd list --json --title-contains ${issueLower} --status closed`, bdOptions),
|
|
1342
|
-
catch: (cause) => cause
|
|
1343
|
-
})));
|
|
1344
|
-
if (JSON.parse(output).find((t) => t.title.toLowerCase().includes("implement") || t.labels?.includes("implementation"))) {
|
|
1345
|
-
const { stdout: openOutput } = await execAsync$2(`bd list --json --title-contains ${issueLower} --status open`, bdOptions);
|
|
1346
|
-
const openTasks = JSON.parse(openOutput);
|
|
1347
|
-
if (openTasks.length === 0) return {
|
|
1348
|
-
triggered: true,
|
|
1349
|
-
type: "task_complete",
|
|
1350
|
-
reason: "Implementation task closed, no remaining tasks",
|
|
1351
|
-
suggestedModel: completionConfig.to_specialist,
|
|
1352
|
-
confidence: "high"
|
|
1353
|
-
};
|
|
1354
|
-
return {
|
|
1355
|
-
triggered: false,
|
|
1356
|
-
type: "task_complete",
|
|
1357
|
-
reason: `Implementation task closed but ${openTasks.length} tasks remain`,
|
|
1358
|
-
confidence: "medium"
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
} catch {}
|
|
1362
|
-
return {
|
|
1363
|
-
triggered: false,
|
|
1364
|
-
type: "task_complete",
|
|
1365
|
-
reason: "No implementation completion signals detected",
|
|
1366
|
-
confidence: "high"
|
|
1217
|
+
function saveCostData(data) {
|
|
1218
|
+
try {
|
|
1219
|
+
const dir = dirname$1(COST_DATA_FILE);
|
|
1220
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1221
|
+
const persisted = {
|
|
1222
|
+
perAgent: Object.fromEntries(data.perAgent),
|
|
1223
|
+
perIssue: Object.fromEntries(data.perIssue),
|
|
1224
|
+
dailyTotal: data.dailyTotal,
|
|
1225
|
+
lastResetDate: data.lastResetDate
|
|
1367
1226
|
};
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1227
|
+
const tempFile = `${COST_DATA_FILE}.tmp`;
|
|
1228
|
+
writeFileSync(tempFile, JSON.stringify(persisted, null, 2));
|
|
1229
|
+
writeFileSync(COST_DATA_FILE, readFileSync(tempFile));
|
|
1371
1230
|
try {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
result
|
|
1376
|
-
});
|
|
1377
|
-
return result;
|
|
1378
|
-
} finally {
|
|
1379
|
-
taskCompletionInflight.delete(cacheKey);
|
|
1231
|
+
unlinkSync(tempFile);
|
|
1232
|
+
} catch (unlinkError) {
|
|
1233
|
+
console.debug("Failed to cleanup temp file:", unlinkError instanceof Error ? unlinkError.message : unlinkError);
|
|
1380
1234
|
}
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
console.error("Failed to save cost data:", error);
|
|
1381
1237
|
}
|
|
1382
|
-
return computePromise;
|
|
1383
|
-
}
|
|
1384
|
-
async function checkAllTriggersPromise(agentId, workspace, issueId, currentModel, health, config) {
|
|
1385
|
-
const triggers = [];
|
|
1386
|
-
const stuckCheck = checkStuckEscalation(health, currentModel, config);
|
|
1387
|
-
if (stuckCheck.triggered) triggers.push(stuckCheck);
|
|
1388
|
-
const testCheck = checkTestFailure(workspace, currentModel, config);
|
|
1389
|
-
if (testCheck.triggered) triggers.push(testCheck);
|
|
1390
|
-
const completionCheck = await Effect.runPromise(checkTaskCompletion(issueId, config, workspace));
|
|
1391
|
-
if (completionCheck.triggered) triggers.push(completionCheck);
|
|
1392
|
-
return triggers;
|
|
1393
1238
|
}
|
|
1239
|
+
let costData = loadCostData();
|
|
1394
1240
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
* implementation; never fails (the underlying function swallows errors and
|
|
1397
|
-
* returns a "not triggered" detection on failure).
|
|
1241
|
+
* Get today's date as ISO string (YYYY-MM-DD)
|
|
1398
1242
|
*/
|
|
1399
|
-
function
|
|
1400
|
-
return
|
|
1243
|
+
function getTodayDate() {
|
|
1244
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1401
1245
|
}
|
|
1402
1246
|
/**
|
|
1403
|
-
*
|
|
1404
|
-
* `Effect.promise` because the underlying async path absorbs bd / fs errors.
|
|
1247
|
+
* Reset daily totals if it's a new day
|
|
1405
1248
|
*/
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1249
|
+
function checkDailyReset() {
|
|
1250
|
+
const today = getTodayDate();
|
|
1251
|
+
if (costData.lastResetDate !== today) {
|
|
1252
|
+
costData.dailyTotal = 0;
|
|
1253
|
+
costData.lastResetDate = today;
|
|
1254
|
+
console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);
|
|
1255
|
+
saveCostData(costData);
|
|
1256
|
+
}
|
|
1408
1257
|
}
|
|
1409
|
-
//#endregion
|
|
1410
|
-
//#region ../../lib/cloister/handoff-logger.ts
|
|
1411
1258
|
/**
|
|
1412
|
-
*
|
|
1259
|
+
* Check if any cost limits are being approached or exceeded
|
|
1413
1260
|
*
|
|
1414
|
-
*
|
|
1415
|
-
|
|
1416
|
-
|
|
1261
|
+
* @param agentId - Agent ID to check
|
|
1262
|
+
* @param issueId - Optional issue ID to check
|
|
1263
|
+
* @param config - Cost limits configuration
|
|
1264
|
+
* @returns Array of alerts (empty if no limits exceeded)
|
|
1265
|
+
*/
|
|
1266
|
+
function checkCostLimits(agentId, issueId, config = loadCloisterConfigSync().cost_limits || {
|
|
1267
|
+
per_agent_usd: 10,
|
|
1268
|
+
per_issue_usd: 25,
|
|
1269
|
+
daily_total_usd: 100,
|
|
1270
|
+
alert_threshold: .8
|
|
1271
|
+
}) {
|
|
1272
|
+
const alerts = [];
|
|
1273
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1274
|
+
if (config.per_agent_usd > 0) {
|
|
1275
|
+
const agentCost = getAgentDailyCostSync(agentId);
|
|
1276
|
+
const agentPercent = agentCost / config.per_agent_usd;
|
|
1277
|
+
if (agentPercent >= 1) alerts.push({
|
|
1278
|
+
type: "per_agent",
|
|
1279
|
+
level: "limit_reached",
|
|
1280
|
+
agentId,
|
|
1281
|
+
currentCost: agentCost,
|
|
1282
|
+
limit: config.per_agent_usd,
|
|
1283
|
+
percentUsed: agentPercent * 100,
|
|
1284
|
+
timestamp: now
|
|
1285
|
+
});
|
|
1286
|
+
else if (agentPercent >= config.alert_threshold) alerts.push({
|
|
1287
|
+
type: "per_agent",
|
|
1288
|
+
level: "warning",
|
|
1289
|
+
agentId,
|
|
1290
|
+
currentCost: agentCost,
|
|
1291
|
+
limit: config.per_agent_usd,
|
|
1292
|
+
percentUsed: agentPercent * 100,
|
|
1293
|
+
timestamp: now
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (issueId && config.per_issue_usd > 0) {
|
|
1297
|
+
const issueCost = getDailyTrendsSync({
|
|
1298
|
+
days: 1,
|
|
1299
|
+
issueId
|
|
1300
|
+
}).reduce((sum, t) => sum + t.totalCost, 0);
|
|
1301
|
+
const issuePercent = issueCost / config.per_issue_usd;
|
|
1302
|
+
if (issuePercent >= 1) alerts.push({
|
|
1303
|
+
type: "per_issue",
|
|
1304
|
+
level: "limit_reached",
|
|
1305
|
+
issueId,
|
|
1306
|
+
currentCost: issueCost,
|
|
1307
|
+
limit: config.per_issue_usd,
|
|
1308
|
+
percentUsed: issuePercent * 100,
|
|
1309
|
+
timestamp: now
|
|
1310
|
+
});
|
|
1311
|
+
else if (issuePercent >= config.alert_threshold) alerts.push({
|
|
1312
|
+
type: "per_issue",
|
|
1313
|
+
level: "warning",
|
|
1314
|
+
issueId,
|
|
1315
|
+
currentCost: issueCost,
|
|
1316
|
+
limit: config.per_issue_usd,
|
|
1317
|
+
percentUsed: issuePercent * 100,
|
|
1318
|
+
timestamp: now
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (config.daily_total_usd > 0) {
|
|
1322
|
+
const dailyTotal = getDailyTrendsSync({ days: 1 }).reduce((sum, t) => sum + t.totalCost, 0);
|
|
1323
|
+
const dailyPercent = dailyTotal / config.daily_total_usd;
|
|
1324
|
+
if (dailyPercent >= 1) alerts.push({
|
|
1325
|
+
type: "daily_total",
|
|
1326
|
+
level: "limit_reached",
|
|
1327
|
+
currentCost: dailyTotal,
|
|
1328
|
+
limit: config.daily_total_usd,
|
|
1329
|
+
percentUsed: dailyPercent * 100,
|
|
1330
|
+
timestamp: now
|
|
1331
|
+
});
|
|
1332
|
+
else if (dailyPercent >= config.alert_threshold) alerts.push({
|
|
1333
|
+
type: "daily_total",
|
|
1334
|
+
level: "warning",
|
|
1335
|
+
currentCost: dailyTotal,
|
|
1336
|
+
limit: config.daily_total_usd,
|
|
1337
|
+
percentUsed: dailyPercent * 100,
|
|
1338
|
+
timestamp: now
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return alerts;
|
|
1342
|
+
}
|
|
1417
1343
|
/**
|
|
1418
|
-
*
|
|
1344
|
+
* Get cost summary
|
|
1345
|
+
*
|
|
1346
|
+
* @returns Cost summary with top spenders
|
|
1419
1347
|
*/
|
|
1420
|
-
|
|
1348
|
+
function getCostSummary() {
|
|
1349
|
+
checkDailyReset();
|
|
1350
|
+
const topAgents = Array.from(costData.perAgent.entries()).map(([agentId, cost]) => ({
|
|
1351
|
+
agentId,
|
|
1352
|
+
cost
|
|
1353
|
+
})).sort((a, b) => b.cost - a.cost).slice(0, 10);
|
|
1354
|
+
const topIssues = Array.from(costData.perIssue.entries()).map(([issueId, cost]) => ({
|
|
1355
|
+
issueId,
|
|
1356
|
+
cost
|
|
1357
|
+
})).sort((a, b) => b.cost - a.cost).slice(0, 10);
|
|
1358
|
+
return {
|
|
1359
|
+
dailyTotal: costData.dailyTotal,
|
|
1360
|
+
topAgents,
|
|
1361
|
+
topIssues
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region ../../lib/cloister/service-reactive.ts
|
|
1366
|
+
/** Cloister reactive lifecycle scheduler. */
|
|
1367
|
+
init_agents();
|
|
1368
|
+
init_tmux();
|
|
1369
|
+
init_issue_closed();
|
|
1370
|
+
init_merge_verification();
|
|
1371
|
+
const execAsync$2 = promisify(exec);
|
|
1372
|
+
/** Return issues orphaned in reviewStatus='reviewing' with no active reviewer. */
|
|
1373
|
+
function identifyOrphanedReviewingIssues(statuses, activeReviewIssues) {
|
|
1374
|
+
const orphaned = [];
|
|
1375
|
+
for (const [issueId, status] of Object.entries(statuses)) {
|
|
1376
|
+
if (status.reviewStatus !== "reviewing") continue;
|
|
1377
|
+
if (status.history?.some((h) => h.type === "review" && h.status === "passed")) continue;
|
|
1378
|
+
if (activeReviewIssues.has(issueId.toUpperCase())) continue;
|
|
1379
|
+
orphaned.push(issueId);
|
|
1380
|
+
}
|
|
1381
|
+
return orphaned;
|
|
1382
|
+
}
|
|
1383
|
+
function parseSpecialistAgentSession(name) {
|
|
1384
|
+
const issueScoped = name.match(/^specialist-(.+)-([A-Z]+-\d+)-(review-agent|test-agent|merge-agent)$/);
|
|
1385
|
+
if (issueScoped) return {
|
|
1386
|
+
projectKey: issueScoped[1],
|
|
1387
|
+
issueId: issueScoped[2],
|
|
1388
|
+
specialistType: issueScoped[3]
|
|
1389
|
+
};
|
|
1390
|
+
const legacy = name.match(/^specialist-(.+)-(review-agent|test-agent|merge-agent)$/);
|
|
1391
|
+
if (legacy) return {
|
|
1392
|
+
projectKey: legacy[1],
|
|
1393
|
+
specialistType: legacy[2]
|
|
1394
|
+
};
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
const ROLE_RUN_STATES = {
|
|
1398
|
+
todo: null,
|
|
1399
|
+
open: null,
|
|
1400
|
+
in_planning: "plan",
|
|
1401
|
+
in_progress: "work",
|
|
1402
|
+
in_review: "review",
|
|
1403
|
+
testing: "test",
|
|
1404
|
+
shipping: null,
|
|
1405
|
+
closed: null,
|
|
1406
|
+
canceled: null
|
|
1407
|
+
};
|
|
1421
1408
|
/**
|
|
1422
|
-
*
|
|
1409
|
+
* Map issue lifecycle state to the role that should own that state.
|
|
1423
1410
|
*/
|
|
1424
|
-
function
|
|
1425
|
-
|
|
1426
|
-
|
|
1411
|
+
function stateToRole(state) {
|
|
1412
|
+
return ROLE_RUN_STATES[state.toLowerCase().replace(/[ -]/g, "_")] ?? null;
|
|
1413
|
+
}
|
|
1414
|
+
function normalizeIssueId(issueId) {
|
|
1415
|
+
return issueId.trim().toUpperCase();
|
|
1416
|
+
}
|
|
1417
|
+
function roleFromAgentId(agentId, issueId) {
|
|
1418
|
+
const base = `agent-${issueId.toLowerCase()}`;
|
|
1419
|
+
if (agentId === base) return "work";
|
|
1420
|
+
const role = agentId.slice(base.length + 1);
|
|
1421
|
+
return [
|
|
1422
|
+
"plan",
|
|
1423
|
+
"review",
|
|
1424
|
+
"test"
|
|
1425
|
+
].includes(role) ? role : null;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Grace window for `starting` states without a tmux session yet. The
|
|
1429
|
+
* start-planning route deliberately writes state.json BEFORE the lifecycle
|
|
1430
|
+
* transition (PAN-1048) so this guard can see the planner coming — but the
|
|
1431
|
+
* tmux session is only created after the transition. Treating any
|
|
1432
|
+
* starting-without-session as dead (the original S1 unstick heuristic)
|
|
1433
|
+
* re-opened the PAN-1048 race and spawned a duplicate `agent-<issue>-plan`
|
|
1434
|
+
* twin on EVERY `pan plan` (PAN-2159, 100% repro). A fresh `starting` state
|
|
1435
|
+
* is a startup in progress; only a stale one is a crashed spawn to unstick.
|
|
1436
|
+
*/
|
|
1437
|
+
const STARTING_WITHOUT_SESSION_GRACE_MS = 12e4;
|
|
1438
|
+
function isFreshStarting(state) {
|
|
1439
|
+
const startedAt = Date.parse(state.startedAt ?? "");
|
|
1440
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt < STARTING_WITHOUT_SESSION_GRACE_MS;
|
|
1441
|
+
}
|
|
1442
|
+
async function activeRoleRunExists(issueId, role, workspacePath) {
|
|
1443
|
+
const issueLower = issueId.toLowerCase();
|
|
1444
|
+
if (role === "plan") {
|
|
1445
|
+
const legacyId = `planning-${issueLower}`;
|
|
1446
|
+
const legacyState = await Effect.runPromise(getAgentState(legacyId));
|
|
1447
|
+
if (legacyState?.role === "plan" && legacyState.status !== "stopped" && legacyState.status !== "error") {
|
|
1448
|
+
if (legacyState.status === "starting" && !await Effect.runPromise(sessionExists(legacyId))) return isFreshStarting(legacyState);
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const candidateId = role === "work" ? `agent-${issueLower}` : `agent-${issueLower}-${role}`;
|
|
1453
|
+
const state = await Effect.runPromise(getAgentState(candidateId));
|
|
1454
|
+
if (!state) return false;
|
|
1455
|
+
const stateRole = state.role ?? roleFromAgentId(candidateId, issueId);
|
|
1456
|
+
if (stateRole === role && state.status === "starting" && !await Effect.runPromise(sessionExists(candidateId))) return isFreshStarting(state);
|
|
1457
|
+
if (!(stateRole === role && state.status !== "stopped" && state.status !== "error")) return false;
|
|
1458
|
+
if (workspacePath && state.roleRunHead) try {
|
|
1459
|
+
const { stdout } = await execAsync$2("git rev-parse --short=8 HEAD", { cwd: workspacePath });
|
|
1460
|
+
const currentHead = stdout.trim();
|
|
1461
|
+
if (currentHead && currentHead !== state.roleRunHead) {
|
|
1462
|
+
console.log(`[cloister] ${issueId}: ${role} session ${candidateId} is stale (ran against ${state.roleRunHead}, HEAD is now ${currentHead}) — not active`);
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
} catch {}
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
function buildReactiveRolePrompt(issueId, state, role) {
|
|
1469
|
+
return `${role.toUpperCase()} TASK for ${issueId}:
|
|
1470
|
+
|
|
1471
|
+
The issue lifecycle transitioned to ${state}. Run the ${role} role for this issue.
|
|
1472
|
+
|
|
1473
|
+
Required steps:
|
|
1474
|
+
1. Work only in the workspace configured for ${issueId}.
|
|
1475
|
+
2. Read .pan/continue.json, .pan/spec.vbrief.json, project instructions, and issue context.
|
|
1476
|
+
3. Follow the boundaries and success criteria in roles/${role}.md exactly.
|
|
1477
|
+
4. Report the role-specific terminal status when done.`;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Resolve the workspace path for an issue from agent state, then fall back
|
|
1481
|
+
* to the canonical `<projectPath>/workspaces/feature-<issueLower>` layout.
|
|
1482
|
+
* Mirrors the resolution used by startup recovery (service.ts:583-609) so
|
|
1483
|
+
* the reactive scheduler dispatches review/test wrappers with the same
|
|
1484
|
+
* workspace contract those wrappers receive on the manual code path.
|
|
1485
|
+
*/
|
|
1486
|
+
async function resolveWorkspaceForIssue(issueId) {
|
|
1487
|
+
const issueLower = issueId.toLowerCase();
|
|
1488
|
+
const agentState = await Effect.runPromise(getAgentState(`agent-${issueLower}`));
|
|
1489
|
+
if (agentState?.workspace) return agentState.workspace;
|
|
1490
|
+
const resolved = resolveProjectFromIssueSync(issueId);
|
|
1491
|
+
if (!resolved) return null;
|
|
1492
|
+
return `${resolved.projectPath}/workspaces/feature-${issueLower}`;
|
|
1493
|
+
}
|
|
1494
|
+
async function onIssueStateChangePromise(issueId, newState) {
|
|
1495
|
+
const normalizedIssueId = normalizeIssueId(issueId);
|
|
1496
|
+
const role = stateToRole(newState);
|
|
1497
|
+
if (!role) {
|
|
1498
|
+
console.log(`[cloister] ${normalizedIssueId}: no role for issue state '${newState}'`);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (await isIssueClosed(normalizedIssueId)) {
|
|
1502
|
+
const message = `${normalizedIssueId}: skipping ${role} dispatch — issue is closed`;
|
|
1503
|
+
console.log(`[cloister] ${message}`);
|
|
1504
|
+
emitActivityEntrySync({
|
|
1505
|
+
source: "cloister",
|
|
1506
|
+
level: "info",
|
|
1507
|
+
message,
|
|
1508
|
+
issueId: normalizedIssueId
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const { getReviewStatusSync } = await import("./review-status-CG0iDuWa.js");
|
|
1513
|
+
if (getReviewStatusSync(normalizedIssueId)?.mergeStatus === "merged") {
|
|
1514
|
+
const message = `${normalizedIssueId}: skipping ${role} dispatch — merge already landed (merge_status='merged' is terminal)`;
|
|
1515
|
+
console.log(`[cloister] ${message}`);
|
|
1516
|
+
emitActivityEntrySync({
|
|
1517
|
+
source: "cloister",
|
|
1518
|
+
level: "info",
|
|
1519
|
+
message,
|
|
1520
|
+
issueId: normalizedIssueId
|
|
1521
|
+
});
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const mergedGuard = await shouldSkipDispatchAsMerged(normalizedIssueId);
|
|
1525
|
+
if (mergedGuard.skip) {
|
|
1526
|
+
const message = `${normalizedIssueId}: skipping ${role} dispatch — ${mergedGuard.reason}`;
|
|
1527
|
+
console.log(`[cloister] ${message}`);
|
|
1528
|
+
emitActivityEntrySync({
|
|
1529
|
+
source: "cloister",
|
|
1530
|
+
level: "info",
|
|
1531
|
+
message,
|
|
1532
|
+
issueId: normalizedIssueId
|
|
1533
|
+
});
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const workspace = await resolveWorkspaceForIssue(normalizedIssueId);
|
|
1537
|
+
if (await activeRoleRunExists(normalizedIssueId, role, workspace ?? void 0)) {
|
|
1538
|
+
const message = `${normalizedIssueId}: ${role} role already active; skipping lifecycle spawn`;
|
|
1539
|
+
console.log(`[cloister] ${message}`);
|
|
1540
|
+
emitActivityEntrySync({
|
|
1541
|
+
source: "cloister",
|
|
1542
|
+
level: "info",
|
|
1543
|
+
message,
|
|
1544
|
+
issueId: normalizedIssueId
|
|
1545
|
+
});
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const issueLower = normalizedIssueId.toLowerCase();
|
|
1549
|
+
const roleSessionId = role === "work" ? `agent-${issueLower}` : `agent-${issueLower}-${role}`;
|
|
1550
|
+
if (await Effect.runPromise(sessionExists(roleSessionId))) {
|
|
1551
|
+
const message = `${normalizedIssueId}: killing stale ${role} session ${roleSessionId} before re-dispatch`;
|
|
1552
|
+
console.log(`[cloister] ${message}`);
|
|
1553
|
+
emitActivityEntrySync({
|
|
1554
|
+
source: "cloister",
|
|
1555
|
+
level: "info",
|
|
1556
|
+
message,
|
|
1557
|
+
issueId: normalizedIssueId
|
|
1558
|
+
});
|
|
1559
|
+
try {
|
|
1560
|
+
await Effect.runPromise(killSession(roleSessionId));
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
console.error(`[cloister] failed to kill stale session ${roleSessionId}:`, err instanceof Error ? err.message : String(err));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
if (role === "review") {
|
|
1567
|
+
if (!workspace) {
|
|
1568
|
+
const failure = `${normalizedIssueId}: cannot dispatch review role — no workspace or project resolved`;
|
|
1569
|
+
console.error(`[cloister] ${failure}`);
|
|
1570
|
+
emitActivityEntrySync({
|
|
1571
|
+
source: "cloister",
|
|
1572
|
+
level: "error",
|
|
1573
|
+
message: failure,
|
|
1574
|
+
issueId: normalizedIssueId
|
|
1575
|
+
});
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const branch = `feature/${normalizedIssueId.toLowerCase()}`;
|
|
1579
|
+
const { spawnReviewRoleForIssue } = await import("./review-agent-u5q3ip7H.js");
|
|
1580
|
+
const result = await Effect.runPromise(spawnReviewRoleForIssue({
|
|
1581
|
+
issueId: normalizedIssueId,
|
|
1582
|
+
workspace,
|
|
1583
|
+
branch
|
|
1584
|
+
}));
|
|
1585
|
+
const message = `${normalizedIssueId}: review role dispatched from lifecycle state '${newState}' (${result.message})`;
|
|
1586
|
+
console.log(`[cloister] ${message}`);
|
|
1587
|
+
emitActivityEntrySync({
|
|
1588
|
+
source: "cloister",
|
|
1589
|
+
level: result.success ? "info" : "error",
|
|
1590
|
+
message,
|
|
1591
|
+
issueId: normalizedIssueId
|
|
1592
|
+
});
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (role === "test") {
|
|
1596
|
+
const branch = `feature/${normalizedIssueId.toLowerCase()}`;
|
|
1597
|
+
const { dispatchTestAgentAndNotify } = await import("./test-agent-queue-CnuYku4B.js");
|
|
1598
|
+
await Effect.runPromise(dispatchTestAgentAndNotify(normalizedIssueId, workspace ?? void 0, branch));
|
|
1599
|
+
const message = `${normalizedIssueId}: test role dispatched from lifecycle state '${newState}'`;
|
|
1600
|
+
console.log(`[cloister] ${message}`);
|
|
1601
|
+
emitActivityEntrySync({
|
|
1602
|
+
source: "cloister",
|
|
1603
|
+
level: "info",
|
|
1604
|
+
message,
|
|
1605
|
+
issueId: normalizedIssueId
|
|
1606
|
+
});
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const { spawnRun } = await import("./agents-CEcaxSkR.js");
|
|
1610
|
+
const message = `${normalizedIssueId}: ${role} role started from lifecycle state '${newState}' as ${(await spawnRun(normalizedIssueId, role, { prompt: buildReactiveRolePrompt(normalizedIssueId, newState, role) })).id}`;
|
|
1611
|
+
console.log(`[cloister] ${message}`);
|
|
1612
|
+
emitActivityEntrySync({
|
|
1613
|
+
source: "cloister",
|
|
1614
|
+
level: "info",
|
|
1615
|
+
message,
|
|
1616
|
+
issueId: normalizedIssueId
|
|
1617
|
+
});
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1620
|
+
if (message.includes("already running")) {
|
|
1621
|
+
const skipMessage = `${normalizedIssueId}: ${role} role already running; skipping lifecycle spawn`;
|
|
1622
|
+
console.log(`[cloister] ${skipMessage}`);
|
|
1623
|
+
emitActivityEntrySync({
|
|
1624
|
+
source: "cloister",
|
|
1625
|
+
level: "info",
|
|
1626
|
+
message: skipMessage,
|
|
1627
|
+
issueId: normalizedIssueId
|
|
1628
|
+
});
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
console.error(`[cloister] Failed to start ${role} role for ${normalizedIssueId}:`, error);
|
|
1632
|
+
emitActivityEntrySync({
|
|
1633
|
+
source: "cloister",
|
|
1634
|
+
level: "error",
|
|
1635
|
+
message: `${normalizedIssueId}: failed to start ${role} role: ${message}`,
|
|
1636
|
+
issueId: normalizedIssueId
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function payloadRecord(event) {
|
|
1641
|
+
return event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
1642
|
+
}
|
|
1643
|
+
function issueStateChangeFromDomainEvent(event) {
|
|
1644
|
+
const payload = payloadRecord(event);
|
|
1645
|
+
const issueId = typeof payload.issueId === "string" ? payload.issueId : null;
|
|
1646
|
+
if (!issueId) return null;
|
|
1647
|
+
switch (event.type) {
|
|
1648
|
+
case "issue.transitioned": return typeof payload.state === "string" ? {
|
|
1649
|
+
issueId,
|
|
1650
|
+
state: payload.state
|
|
1651
|
+
} : null;
|
|
1652
|
+
case "issue.statusChanged": return typeof payload.canonicalStatus === "string" ? {
|
|
1653
|
+
issueId,
|
|
1654
|
+
state: payload.canonicalStatus
|
|
1655
|
+
} : null;
|
|
1656
|
+
case "issue.closed": return {
|
|
1657
|
+
issueId,
|
|
1658
|
+
state: "closed"
|
|
1659
|
+
};
|
|
1660
|
+
case "agent.completed": {
|
|
1661
|
+
const role = typeof payload.role === "string" ? payload.role : void 0;
|
|
1662
|
+
if (role === void 0 || role === "work") return {
|
|
1663
|
+
issueId,
|
|
1664
|
+
state: "in_review"
|
|
1665
|
+
};
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
case "work.completed": return {
|
|
1669
|
+
issueId,
|
|
1670
|
+
state: "in_review"
|
|
1671
|
+
};
|
|
1672
|
+
case "review.approved": return {
|
|
1673
|
+
issueId,
|
|
1674
|
+
state: "testing"
|
|
1675
|
+
};
|
|
1676
|
+
case "test.passed": return {
|
|
1677
|
+
issueId,
|
|
1678
|
+
state: "shipping"
|
|
1679
|
+
};
|
|
1680
|
+
default: return null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async function handleCloisterDomainEventPromise(event) {
|
|
1684
|
+
if (event.type === "agent.stopped") {
|
|
1685
|
+
const agentId = event.payload?.agentId;
|
|
1686
|
+
if (agentId) {
|
|
1687
|
+
const { handleAgentStoppedEvent, handleAgentStoppedForOrphanReviewerSessions } = await import("./deacon-DONuopff.js");
|
|
1688
|
+
const { handleAgentLifecycleEventForIdleStack } = await import("./idle-stack-reaper-H0W6XtHh.js");
|
|
1689
|
+
handleAgentLifecycleEventForIdleStack(agentId);
|
|
1690
|
+
const slotMatch = /^agent-(.+)-slot-\d+$/.exec(agentId);
|
|
1691
|
+
await Promise.all([
|
|
1692
|
+
handleAgentStoppedEvent(agentId),
|
|
1693
|
+
handleAgentStoppedForOrphanReviewerSessions(agentId),
|
|
1694
|
+
slotMatch ? (await import("./deacon-swarm-CvdFekIO.js")).coordinateSwarmSlots({ issueId: slotMatch[1].toUpperCase() }) : Promise.resolve([])
|
|
1695
|
+
]);
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (event.type === "agent.started") {
|
|
1700
|
+
const agentId = event.payload?.agentId;
|
|
1701
|
+
if (agentId) (await import("./idle-stack-reaper-H0W6XtHh.js")).handleAgentLifecycleEventForIdleStack(agentId);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (event.type === "agent.heartbeat_dead") {
|
|
1705
|
+
const agentId = event.payload?.agentId;
|
|
1706
|
+
if (agentId) {
|
|
1707
|
+
const { handleAgentHeartbeatDeadEvent } = await import("./deacon-DONuopff.js");
|
|
1708
|
+
await handleAgentHeartbeatDeadEvent(agentId, "event");
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (event.type === "review.coordinator.died") {
|
|
1713
|
+
const payload = event.payload;
|
|
1714
|
+
const issueId = payload?.issueId;
|
|
1715
|
+
if (issueId) {
|
|
1716
|
+
const { handleReviewCoordinatorDied } = await import("./deacon-DONuopff.js");
|
|
1717
|
+
await handleReviewCoordinatorDied(issueId, payload?.sessionName ?? "", payload?.reason ?? "");
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (event.type === "work.completed") {
|
|
1722
|
+
const issueId = event.payload?.issueId;
|
|
1723
|
+
if (issueId) {
|
|
1724
|
+
const { handleWorkCompleted } = await import("./deacon-DONuopff.js");
|
|
1725
|
+
await handleWorkCompleted(issueId);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (event.type === "issue.statusChanged") {
|
|
1729
|
+
const payload = event.payload;
|
|
1730
|
+
const issueId = payload?.issueId;
|
|
1731
|
+
const canonicalStatus = payload?.canonicalStatus?.toLowerCase();
|
|
1732
|
+
const status = payload?.status?.toLowerCase();
|
|
1733
|
+
if (issueId) {
|
|
1734
|
+
if (canonicalStatus === "closed" || status === "closed") {
|
|
1735
|
+
const { handleIssueStatusChangedClosed } = await import("./closed-issue-reaper-CksfuiFD.js");
|
|
1736
|
+
await handleIssueStatusChangedClosed(issueId);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (canonicalStatus === "todo" || status === "planned" || status === "todo") {
|
|
1740
|
+
const { handleOrphanProposedSpec } = await import("./orphan-proposed-reconciler-BIcNu4tl.js");
|
|
1741
|
+
await handleOrphanProposedSpec(issueId);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const change = issueStateChangeFromDomainEvent(event);
|
|
1746
|
+
if (!change) return;
|
|
1747
|
+
await Effect.runPromise(onIssueStateChange(change.issueId, change.state));
|
|
1427
1748
|
}
|
|
1428
1749
|
/**
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
* @param event - Handoff event to log
|
|
1750
|
+
* Effect-typed variant of {@link onIssueStateChange}. Never fails — failures
|
|
1751
|
+
* surface through `emitActivityEntry` inside the legacy implementation.
|
|
1432
1752
|
*/
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
appendFileSync(HANDOFF_LOG_FILE, JSON.stringify(event) + "\n", "utf-8");
|
|
1753
|
+
function onIssueStateChange(issueId, newState) {
|
|
1754
|
+
return Effect.promise(() => onIssueStateChangePromise(issueId, newState));
|
|
1436
1755
|
}
|
|
1437
1756
|
/**
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
* @param agentId - Agent ID
|
|
1441
|
-
* @param issueId - Issue ID
|
|
1442
|
-
* @param context - Handoff context
|
|
1443
|
-
* @param trigger - Trigger type
|
|
1444
|
-
* @param success - Whether handoff succeeded
|
|
1445
|
-
* @param errorMessage - Error message if failed
|
|
1446
|
-
* @returns Handoff event
|
|
1757
|
+
* Effect-typed variant of {@link handleCloisterDomainEvent}. Never fails.
|
|
1447
1758
|
*/
|
|
1448
|
-
function
|
|
1449
|
-
|
|
1450
|
-
if (trigger === "stuck_escalation") {}
|
|
1451
|
-
return {
|
|
1452
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1453
|
-
agentId,
|
|
1454
|
-
issueId,
|
|
1455
|
-
from: {
|
|
1456
|
-
model: context.previousModel,
|
|
1457
|
-
runtime: context.previousRuntime,
|
|
1458
|
-
sessionId: context.previousSessionId
|
|
1459
|
-
},
|
|
1460
|
-
to: {
|
|
1461
|
-
model: context.targetModel,
|
|
1462
|
-
runtime: context.previousRuntime,
|
|
1463
|
-
sessionId: void 0
|
|
1464
|
-
},
|
|
1465
|
-
trigger,
|
|
1466
|
-
reason: context.reason,
|
|
1467
|
-
context: {
|
|
1468
|
-
costAtHandoff: context.costSoFar,
|
|
1469
|
-
handoffCount: context.handoffCount,
|
|
1470
|
-
stuckMinutes
|
|
1471
|
-
},
|
|
1472
|
-
success,
|
|
1473
|
-
errorMessage
|
|
1474
|
-
};
|
|
1759
|
+
function handleCloisterDomainEvent(event) {
|
|
1760
|
+
return Effect.promise(() => handleCloisterDomainEventPromise(event));
|
|
1475
1761
|
}
|
|
1476
1762
|
//#endregion
|
|
1477
1763
|
//#region ../../lib/cloister/fpp-violations.ts
|
|
@@ -1633,198 +1919,71 @@ function clearOldViolationsSync(hoursOld = 24) {
|
|
|
1633
1919
|
if (changed) saveViolations(activeViolations);
|
|
1634
1920
|
}
|
|
1635
1921
|
//#endregion
|
|
1636
|
-
//#region ../../lib/cloister/
|
|
1922
|
+
//#region ../../lib/cloister/handoff-logger.ts
|
|
1637
1923
|
/**
|
|
1638
|
-
*
|
|
1924
|
+
* Handoff Event Logger
|
|
1639
1925
|
*
|
|
1640
|
-
*
|
|
1641
|
-
* Does NOT automatically stop agents - just provides visibility and warnings.
|
|
1926
|
+
* Logs handoff events to JSONL file for tracking and analysis.
|
|
1642
1927
|
*/
|
|
1643
1928
|
init_errors();
|
|
1644
|
-
init_paths();
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (!existsSync(COST_DATA_FILE)) return {
|
|
1656
|
-
perAgent: /* @__PURE__ */ new Map(),
|
|
1657
|
-
perIssue: /* @__PURE__ */ new Map(),
|
|
1658
|
-
dailyTotal: 0,
|
|
1659
|
-
lastResetDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1660
|
-
};
|
|
1661
|
-
try {
|
|
1662
|
-
const fileContent = readFileSync(COST_DATA_FILE, "utf-8");
|
|
1663
|
-
const persisted = JSON.parse(fileContent);
|
|
1664
|
-
return {
|
|
1665
|
-
perAgent: new Map(Object.entries(persisted.perAgent || {})),
|
|
1666
|
-
perIssue: new Map(Object.entries(persisted.perIssue || {})),
|
|
1667
|
-
dailyTotal: persisted.dailyTotal || 0,
|
|
1668
|
-
lastResetDate: persisted.lastResetDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1669
|
-
};
|
|
1670
|
-
} catch (error) {
|
|
1671
|
-
console.error("Failed to load cost data, starting fresh:", error);
|
|
1672
|
-
return {
|
|
1673
|
-
perAgent: /* @__PURE__ */ new Map(),
|
|
1674
|
-
perIssue: /* @__PURE__ */ new Map(),
|
|
1675
|
-
dailyTotal: 0,
|
|
1676
|
-
lastResetDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
/**
|
|
1681
|
-
* Save cost data to file (atomic write)
|
|
1682
|
-
*/
|
|
1683
|
-
function saveCostData(data) {
|
|
1684
|
-
try {
|
|
1685
|
-
const dir = dirname$1(COST_DATA_FILE);
|
|
1686
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1687
|
-
const persisted = {
|
|
1688
|
-
perAgent: Object.fromEntries(data.perAgent),
|
|
1689
|
-
perIssue: Object.fromEntries(data.perIssue),
|
|
1690
|
-
dailyTotal: data.dailyTotal,
|
|
1691
|
-
lastResetDate: data.lastResetDate
|
|
1692
|
-
};
|
|
1693
|
-
const tempFile = `${COST_DATA_FILE}.tmp`;
|
|
1694
|
-
writeFileSync(tempFile, JSON.stringify(persisted, null, 2));
|
|
1695
|
-
writeFileSync(COST_DATA_FILE, readFileSync(tempFile));
|
|
1696
|
-
try {
|
|
1697
|
-
unlinkSync(tempFile);
|
|
1698
|
-
} catch (unlinkError) {
|
|
1699
|
-
console.debug("Failed to cleanup temp file:", unlinkError instanceof Error ? unlinkError.message : unlinkError);
|
|
1700
|
-
}
|
|
1701
|
-
} catch (error) {
|
|
1702
|
-
console.error("Failed to save cost data:", error);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
let costData = loadCostData();
|
|
1706
|
-
/**
|
|
1707
|
-
* Get today's date as ISO string (YYYY-MM-DD)
|
|
1708
|
-
*/
|
|
1709
|
-
function getTodayDate() {
|
|
1710
|
-
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Reset daily totals if it's a new day
|
|
1714
|
-
*/
|
|
1715
|
-
function checkDailyReset() {
|
|
1716
|
-
const today = getTodayDate();
|
|
1717
|
-
if (costData.lastResetDate !== today) {
|
|
1718
|
-
costData.dailyTotal = 0;
|
|
1719
|
-
costData.lastResetDate = today;
|
|
1720
|
-
console.log(`🔔 Cost monitor: Daily totals reset for ${today}`);
|
|
1721
|
-
saveCostData(costData);
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
/**
|
|
1725
|
-
* Check if any cost limits are being approached or exceeded
|
|
1726
|
-
*
|
|
1727
|
-
* @param agentId - Agent ID to check
|
|
1728
|
-
* @param issueId - Optional issue ID to check
|
|
1729
|
-
* @param config - Cost limits configuration
|
|
1730
|
-
* @returns Array of alerts (empty if no limits exceeded)
|
|
1731
|
-
*/
|
|
1732
|
-
function checkCostLimits(agentId, issueId, config = loadCloisterConfigSync().cost_limits || {
|
|
1733
|
-
per_agent_usd: 10,
|
|
1734
|
-
per_issue_usd: 25,
|
|
1735
|
-
daily_total_usd: 100,
|
|
1736
|
-
alert_threshold: .8
|
|
1737
|
-
}) {
|
|
1738
|
-
const alerts = [];
|
|
1739
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1740
|
-
if (config.per_agent_usd > 0) {
|
|
1741
|
-
const agentCost = getAgentDailyCostSync(agentId);
|
|
1742
|
-
const agentPercent = agentCost / config.per_agent_usd;
|
|
1743
|
-
if (agentPercent >= 1) alerts.push({
|
|
1744
|
-
type: "per_agent",
|
|
1745
|
-
level: "limit_reached",
|
|
1746
|
-
agentId,
|
|
1747
|
-
currentCost: agentCost,
|
|
1748
|
-
limit: config.per_agent_usd,
|
|
1749
|
-
percentUsed: agentPercent * 100,
|
|
1750
|
-
timestamp: now
|
|
1751
|
-
});
|
|
1752
|
-
else if (agentPercent >= config.alert_threshold) alerts.push({
|
|
1753
|
-
type: "per_agent",
|
|
1754
|
-
level: "warning",
|
|
1755
|
-
agentId,
|
|
1756
|
-
currentCost: agentCost,
|
|
1757
|
-
limit: config.per_agent_usd,
|
|
1758
|
-
percentUsed: agentPercent * 100,
|
|
1759
|
-
timestamp: now
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
if (issueId && config.per_issue_usd > 0) {
|
|
1763
|
-
const issueCost = getDailyTrendsSync({
|
|
1764
|
-
days: 1,
|
|
1765
|
-
issueId
|
|
1766
|
-
}).reduce((sum, t) => sum + t.totalCost, 0);
|
|
1767
|
-
const issuePercent = issueCost / config.per_issue_usd;
|
|
1768
|
-
if (issuePercent >= 1) alerts.push({
|
|
1769
|
-
type: "per_issue",
|
|
1770
|
-
level: "limit_reached",
|
|
1771
|
-
issueId,
|
|
1772
|
-
currentCost: issueCost,
|
|
1773
|
-
limit: config.per_issue_usd,
|
|
1774
|
-
percentUsed: issuePercent * 100,
|
|
1775
|
-
timestamp: now
|
|
1776
|
-
});
|
|
1777
|
-
else if (issuePercent >= config.alert_threshold) alerts.push({
|
|
1778
|
-
type: "per_issue",
|
|
1779
|
-
level: "warning",
|
|
1780
|
-
issueId,
|
|
1781
|
-
currentCost: issueCost,
|
|
1782
|
-
limit: config.per_issue_usd,
|
|
1783
|
-
percentUsed: issuePercent * 100,
|
|
1784
|
-
timestamp: now
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
if (config.daily_total_usd > 0) {
|
|
1788
|
-
const dailyTotal = getDailyTrendsSync({ days: 1 }).reduce((sum, t) => sum + t.totalCost, 0);
|
|
1789
|
-
const dailyPercent = dailyTotal / config.daily_total_usd;
|
|
1790
|
-
if (dailyPercent >= 1) alerts.push({
|
|
1791
|
-
type: "daily_total",
|
|
1792
|
-
level: "limit_reached",
|
|
1793
|
-
currentCost: dailyTotal,
|
|
1794
|
-
limit: config.daily_total_usd,
|
|
1795
|
-
percentUsed: dailyPercent * 100,
|
|
1796
|
-
timestamp: now
|
|
1797
|
-
});
|
|
1798
|
-
else if (dailyPercent >= config.alert_threshold) alerts.push({
|
|
1799
|
-
type: "daily_total",
|
|
1800
|
-
level: "warning",
|
|
1801
|
-
currentCost: dailyTotal,
|
|
1802
|
-
limit: config.daily_total_usd,
|
|
1803
|
-
percentUsed: dailyPercent * 100,
|
|
1804
|
-
timestamp: now
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
return alerts;
|
|
1929
|
+
init_paths();
|
|
1930
|
+
/**
|
|
1931
|
+
* Handoff log file path
|
|
1932
|
+
*/
|
|
1933
|
+
const HANDOFF_LOG_FILE = join$1(OVERDECK_HOME, "logs", "handoffs.jsonl");
|
|
1934
|
+
/**
|
|
1935
|
+
* Ensure log directory exists
|
|
1936
|
+
*/
|
|
1937
|
+
function ensureLogDir() {
|
|
1938
|
+
const logDir = join$1(OVERDECK_HOME, "logs");
|
|
1939
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
1808
1940
|
}
|
|
1809
1941
|
/**
|
|
1810
|
-
*
|
|
1942
|
+
* Log a handoff event
|
|
1811
1943
|
*
|
|
1812
|
-
* @
|
|
1944
|
+
* @param event - Handoff event to log
|
|
1813
1945
|
*/
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1816
|
-
|
|
1946
|
+
function logHandoffEventSync(event) {
|
|
1947
|
+
ensureLogDir();
|
|
1948
|
+
appendFileSync(HANDOFF_LOG_FILE, JSON.stringify(event) + "\n", "utf-8");
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Create a handoff event from handoff result
|
|
1952
|
+
*
|
|
1953
|
+
* @param agentId - Agent ID
|
|
1954
|
+
* @param issueId - Issue ID
|
|
1955
|
+
* @param context - Handoff context
|
|
1956
|
+
* @param trigger - Trigger type
|
|
1957
|
+
* @param success - Whether handoff succeeded
|
|
1958
|
+
* @param errorMessage - Error message if failed
|
|
1959
|
+
* @returns Handoff event
|
|
1960
|
+
*/
|
|
1961
|
+
function createHandoffEvent(agentId, issueId, context, trigger, success, errorMessage) {
|
|
1962
|
+
let stuckMinutes;
|
|
1963
|
+
if (trigger === "stuck_escalation") {}
|
|
1964
|
+
return {
|
|
1965
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1817
1966
|
agentId,
|
|
1818
|
-
cost
|
|
1819
|
-
})).sort((a, b) => b.cost - a.cost).slice(0, 10);
|
|
1820
|
-
const topIssues = Array.from(costData.perIssue.entries()).map(([issueId, cost]) => ({
|
|
1821
1967
|
issueId,
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1968
|
+
from: {
|
|
1969
|
+
model: context.previousModel,
|
|
1970
|
+
runtime: context.previousRuntime,
|
|
1971
|
+
sessionId: context.previousSessionId
|
|
1972
|
+
},
|
|
1973
|
+
to: {
|
|
1974
|
+
model: context.targetModel,
|
|
1975
|
+
runtime: context.previousRuntime,
|
|
1976
|
+
sessionId: void 0
|
|
1977
|
+
},
|
|
1978
|
+
trigger,
|
|
1979
|
+
reason: context.reason,
|
|
1980
|
+
context: {
|
|
1981
|
+
costAtHandoff: context.costSoFar,
|
|
1982
|
+
handoffCount: context.handoffCount,
|
|
1983
|
+
stuckMinutes
|
|
1984
|
+
},
|
|
1985
|
+
success,
|
|
1986
|
+
errorMessage
|
|
1828
1987
|
};
|
|
1829
1988
|
}
|
|
1830
1989
|
//#endregion
|
|
@@ -2032,404 +2191,944 @@ function checkAndRotateIfNeeded(specialistName, workingDir) {
|
|
|
2032
2191
|
return Effect.promise(() => checkAndRotateIfNeededPromise(specialistName, workingDir));
|
|
2033
2192
|
}
|
|
2034
2193
|
//#endregion
|
|
2035
|
-
//#region ../../lib/cloister/
|
|
2036
|
-
/** Cloister service: core monitoring for all running agents. */
|
|
2194
|
+
//#region ../../lib/cloister/triggers.ts
|
|
2037
2195
|
init_config();
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2196
|
+
init_resolver();
|
|
2197
|
+
/** Single-flight prevents concurrent dashboard polls from duplicating the canonical read. */
|
|
2198
|
+
const taskCompletionInflight = /* @__PURE__ */ new Map();
|
|
2199
|
+
/**
|
|
2200
|
+
* Check if agent should be handed off due to stuck escalation
|
|
2201
|
+
*
|
|
2202
|
+
* Model-specific thresholds:
|
|
2203
|
+
* - Haiku: Stuck after 10 minutes → escalate to Sonnet
|
|
2204
|
+
* - Sonnet: Stuck after 20 minutes → escalate to Opus
|
|
2205
|
+
* - Opus: Stuck after 30 minutes → alert user (no auto-escalation)
|
|
2206
|
+
*
|
|
2207
|
+
* @param health - Agent health state
|
|
2208
|
+
* @param currentModel - Current model
|
|
2209
|
+
* @param config - Cloister configuration
|
|
2210
|
+
* @returns Trigger detection result
|
|
2211
|
+
*/
|
|
2212
|
+
function checkStuckEscalation(health, currentModel, config) {
|
|
2213
|
+
const stuckConfig = (config || loadCloisterConfigSync()).handoffs?.auto_triggers?.stuck_escalation;
|
|
2214
|
+
if (!stuckConfig?.enabled) return {
|
|
2215
|
+
triggered: false,
|
|
2216
|
+
type: "stuck_escalation",
|
|
2217
|
+
reason: "Stuck escalation disabled in config",
|
|
2218
|
+
confidence: "high"
|
|
2219
|
+
};
|
|
2220
|
+
if (health.state !== "stuck") return {
|
|
2221
|
+
triggered: false,
|
|
2222
|
+
type: "stuck_escalation",
|
|
2223
|
+
reason: `Agent is ${health.state}, not stuck`,
|
|
2224
|
+
confidence: "high"
|
|
2225
|
+
};
|
|
2226
|
+
if (!health.lastActivity) return {
|
|
2227
|
+
triggered: false,
|
|
2228
|
+
type: "stuck_escalation",
|
|
2229
|
+
reason: "No last activity timestamp available",
|
|
2230
|
+
confidence: "low"
|
|
2231
|
+
};
|
|
2232
|
+
const minutesSinceActivity = (Date.now() - health.lastActivity.getTime()) / (1e3 * 60);
|
|
2233
|
+
if (currentModel === "haiku" && minutesSinceActivity >= stuckConfig.haiku_to_sonnet_minutes) return {
|
|
2234
|
+
triggered: true,
|
|
2235
|
+
type: "stuck_escalation",
|
|
2236
|
+
reason: `Haiku agent stuck for ${Math.round(minutesSinceActivity)} minutes`,
|
|
2237
|
+
suggestedModel: "sonnet",
|
|
2238
|
+
confidence: "high"
|
|
2239
|
+
};
|
|
2240
|
+
if (currentModel === "sonnet" && minutesSinceActivity >= stuckConfig.sonnet_to_opus_minutes) return {
|
|
2241
|
+
triggered: true,
|
|
2242
|
+
type: "stuck_escalation",
|
|
2243
|
+
reason: `Sonnet agent stuck for ${Math.round(minutesSinceActivity)} minutes`,
|
|
2244
|
+
suggestedModel: "opus",
|
|
2245
|
+
confidence: "high"
|
|
2246
|
+
};
|
|
2247
|
+
if (currentModel === "opus") return {
|
|
2248
|
+
triggered: false,
|
|
2249
|
+
type: "stuck_escalation",
|
|
2250
|
+
reason: "Opus agent stuck - no higher model available, manual intervention needed",
|
|
2251
|
+
confidence: "high"
|
|
2252
|
+
};
|
|
2253
|
+
return {
|
|
2254
|
+
triggered: false,
|
|
2255
|
+
type: "stuck_escalation",
|
|
2256
|
+
reason: `Agent stuck but threshold not reached (${Math.round(minutesSinceActivity)} minutes)`,
|
|
2257
|
+
confidence: "medium"
|
|
2258
|
+
};
|
|
2052
2259
|
}
|
|
2053
|
-
/**
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2260
|
+
/**
|
|
2261
|
+
* Check if test failures should trigger escalation
|
|
2262
|
+
*
|
|
2263
|
+
* Aggressive escalation: Any test failure from Haiku escalates to Sonnet
|
|
2264
|
+
* Reasoning: Haiku is for simple tasks - if tests fail, the task isn't simple
|
|
2265
|
+
*
|
|
2266
|
+
* @param workspace - Workspace path
|
|
2267
|
+
* @param currentModel - Current model
|
|
2268
|
+
* @param config - Cloister configuration
|
|
2269
|
+
* @returns Trigger detection result
|
|
2270
|
+
*/
|
|
2271
|
+
function checkTestFailure(workspace, currentModel, config) {
|
|
2272
|
+
const testConfig = (config || loadCloisterConfigSync()).handoffs?.auto_triggers?.test_failure;
|
|
2273
|
+
if (!testConfig?.enabled) return {
|
|
2274
|
+
triggered: false,
|
|
2275
|
+
type: "test_failure",
|
|
2276
|
+
reason: "Test failure escalation disabled in config",
|
|
2277
|
+
confidence: "high"
|
|
2278
|
+
};
|
|
2279
|
+
if (currentModel !== testConfig.from_model) return {
|
|
2280
|
+
triggered: false,
|
|
2281
|
+
type: "test_failure",
|
|
2282
|
+
reason: `Test failure escalation only applies to ${testConfig.from_model} model`,
|
|
2283
|
+
confidence: "high"
|
|
2284
|
+
};
|
|
2285
|
+
const testFailure = detectTestFailure(workspace);
|
|
2286
|
+
if (testFailure.failed) return {
|
|
2287
|
+
triggered: true,
|
|
2288
|
+
type: "test_failure",
|
|
2289
|
+
reason: `Test failures detected: ${testFailure.reason}`,
|
|
2290
|
+
suggestedModel: testConfig.to_model,
|
|
2291
|
+
confidence: testFailure.confidence
|
|
2292
|
+
};
|
|
2293
|
+
return {
|
|
2294
|
+
triggered: false,
|
|
2295
|
+
type: "test_failure",
|
|
2296
|
+
reason: "No test failures detected",
|
|
2297
|
+
confidence: "medium"
|
|
2298
|
+
};
|
|
2063
2299
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2300
|
+
/**
|
|
2301
|
+
* Detect test failures in workspace
|
|
2302
|
+
*
|
|
2303
|
+
* Checks for:
|
|
2304
|
+
* - npm test output
|
|
2305
|
+
* - Jest results
|
|
2306
|
+
* - pytest results
|
|
2307
|
+
* - cargo test results
|
|
2308
|
+
*
|
|
2309
|
+
* @param workspace - Workspace path
|
|
2310
|
+
* @returns Test failure detection
|
|
2311
|
+
*/
|
|
2312
|
+
function detectTestFailure(workspace) {
|
|
2313
|
+
return {
|
|
2314
|
+
failed: false,
|
|
2315
|
+
reason: "Test result detection not yet implemented",
|
|
2316
|
+
confidence: "low"
|
|
2070
2317
|
};
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2318
|
+
}
|
|
2319
|
+
async function checkTaskCompletionPromise(issueId, config, workspace) {
|
|
2320
|
+
const completionConfig = (config || loadCloisterConfigSync()).handoffs?.auto_triggers?.implementation_complete;
|
|
2321
|
+
if (!completionConfig?.enabled) return {
|
|
2322
|
+
triggered: false,
|
|
2323
|
+
type: "task_complete",
|
|
2324
|
+
reason: "Task completion detection disabled in config",
|
|
2325
|
+
confidence: "high"
|
|
2075
2326
|
};
|
|
2076
|
-
|
|
2327
|
+
const cacheKey = `${workspace ?? ""}::${issueId.toLowerCase()}`;
|
|
2328
|
+
const inflight = taskCompletionInflight.get(cacheKey);
|
|
2329
|
+
if (inflight) return inflight;
|
|
2330
|
+
const computePromise = (async () => {
|
|
2331
|
+
if (!workspace) return {
|
|
2332
|
+
triggered: false,
|
|
2333
|
+
type: "task_complete",
|
|
2334
|
+
reason: "No workspace available to check task completion",
|
|
2335
|
+
confidence: "high"
|
|
2336
|
+
};
|
|
2337
|
+
try {
|
|
2338
|
+
const result = await createBeadsResolver(workspace).getBeadsForIssue(issueId);
|
|
2339
|
+
if (!result.ok) throw result.error;
|
|
2340
|
+
if (result.value.filter((task) => task.status === "closed").find((t) => t.title.toLowerCase().includes("implement") || t.labels?.includes("implementation"))) {
|
|
2341
|
+
const openTasks = result.value.filter((task) => task.status === "open");
|
|
2342
|
+
if (openTasks.length === 0) return {
|
|
2343
|
+
triggered: true,
|
|
2344
|
+
type: "task_complete",
|
|
2345
|
+
reason: "Implementation task closed, no remaining tasks",
|
|
2346
|
+
suggestedModel: completionConfig.to_specialist,
|
|
2347
|
+
confidence: "high"
|
|
2348
|
+
};
|
|
2349
|
+
return {
|
|
2350
|
+
triggered: false,
|
|
2351
|
+
type: "task_complete",
|
|
2352
|
+
reason: `Implementation task closed but ${openTasks.length} tasks remain`,
|
|
2353
|
+
confidence: "medium"
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
} catch {}
|
|
2357
|
+
return {
|
|
2358
|
+
triggered: false,
|
|
2359
|
+
type: "task_complete",
|
|
2360
|
+
reason: "No implementation completion signals detected",
|
|
2361
|
+
confidence: "high"
|
|
2362
|
+
};
|
|
2363
|
+
})();
|
|
2364
|
+
taskCompletionInflight.set(cacheKey, computePromise);
|
|
2365
|
+
try {
|
|
2366
|
+
return await computePromise;
|
|
2367
|
+
} finally {
|
|
2368
|
+
taskCompletionInflight.delete(cacheKey);
|
|
2369
|
+
}
|
|
2077
2370
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2371
|
+
async function checkAllTriggersPromise(agentId, workspace, issueId, currentModel, health, config) {
|
|
2372
|
+
const triggers = [];
|
|
2373
|
+
const stuckCheck = checkStuckEscalation(health, currentModel, config);
|
|
2374
|
+
if (stuckCheck.triggered) triggers.push(stuckCheck);
|
|
2375
|
+
const testCheck = checkTestFailure(workspace, currentModel, config);
|
|
2376
|
+
if (testCheck.triggered) triggers.push(testCheck);
|
|
2377
|
+
const completionCheck = await Effect.runPromise(checkTaskCompletion(issueId, config, workspace));
|
|
2378
|
+
if (completionCheck.triggered) triggers.push(completionCheck);
|
|
2379
|
+
return triggers;
|
|
2081
2380
|
}
|
|
2082
|
-
const ROLE_RUN_STATES = {
|
|
2083
|
-
todo: null,
|
|
2084
|
-
open: null,
|
|
2085
|
-
in_planning: "plan",
|
|
2086
|
-
in_progress: "work",
|
|
2087
|
-
in_review: "review",
|
|
2088
|
-
testing: "test",
|
|
2089
|
-
shipping: null,
|
|
2090
|
-
closed: null,
|
|
2091
|
-
canceled: null
|
|
2092
|
-
};
|
|
2093
2381
|
/**
|
|
2094
|
-
*
|
|
2382
|
+
* Effect-typed variant of {@link checkTaskCompletion}. Wraps the Promise-based
|
|
2383
|
+
* implementation; never fails (the underlying function swallows errors and
|
|
2384
|
+
* returns a "not triggered" detection on failure).
|
|
2095
2385
|
*/
|
|
2096
|
-
function
|
|
2097
|
-
return
|
|
2098
|
-
}
|
|
2099
|
-
function normalizeIssueId(issueId) {
|
|
2100
|
-
return issueId.trim().toUpperCase();
|
|
2101
|
-
}
|
|
2102
|
-
function roleFromAgentId(agentId, issueId) {
|
|
2103
|
-
const base = `agent-${issueId.toLowerCase()}`;
|
|
2104
|
-
if (agentId === base) return "work";
|
|
2105
|
-
const role = agentId.slice(base.length + 1);
|
|
2106
|
-
return [
|
|
2107
|
-
"plan",
|
|
2108
|
-
"review",
|
|
2109
|
-
"test"
|
|
2110
|
-
].includes(role) ? role : null;
|
|
2386
|
+
function checkTaskCompletion(issueId, config, workspace) {
|
|
2387
|
+
return Effect.promise(() => checkTaskCompletionPromise(issueId, config, workspace));
|
|
2111
2388
|
}
|
|
2112
2389
|
/**
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
* transition (PAN-1048) so this guard can see the planner coming — but the
|
|
2116
|
-
* tmux session is only created after the transition. Treating any
|
|
2117
|
-
* starting-without-session as dead (the original S1 unstick heuristic)
|
|
2118
|
-
* re-opened the PAN-1048 race and spawned a duplicate `agent-<issue>-plan`
|
|
2119
|
-
* twin on EVERY `pan plan` (PAN-2159, 100% repro). A fresh `starting` state
|
|
2120
|
-
* is a startup in progress; only a stale one is a crashed spawn to unstick.
|
|
2390
|
+
* Effect-typed variant of {@link checkAllTriggers}. Never fails — uses
|
|
2391
|
+
* `Effect.promise` because the underlying async path absorbs bd / fs errors.
|
|
2121
2392
|
*/
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
const startedAt = Date.parse(state.startedAt ?? "");
|
|
2125
|
-
return Number.isFinite(startedAt) && Date.now() - startedAt < STARTING_WITHOUT_SESSION_GRACE_MS;
|
|
2126
|
-
}
|
|
2127
|
-
async function activeRoleRunExists(issueId, role, workspacePath) {
|
|
2128
|
-
const issueLower = issueId.toLowerCase();
|
|
2129
|
-
if (role === "plan") {
|
|
2130
|
-
const legacyId = `planning-${issueLower}`;
|
|
2131
|
-
const legacyState = await Effect.runPromise(getAgentState(legacyId));
|
|
2132
|
-
if (legacyState?.role === "plan" && legacyState.status !== "stopped" && legacyState.status !== "error") {
|
|
2133
|
-
if (legacyState.status === "starting" && !await Effect.runPromise(sessionExists(legacyId))) return isFreshStarting(legacyState);
|
|
2134
|
-
return true;
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
const candidateId = role === "work" ? `agent-${issueLower}` : `agent-${issueLower}-${role}`;
|
|
2138
|
-
const state = await Effect.runPromise(getAgentState(candidateId));
|
|
2139
|
-
if (!state) return false;
|
|
2140
|
-
const stateRole = state.role ?? roleFromAgentId(candidateId, issueId);
|
|
2141
|
-
if (stateRole === role && state.status === "starting" && !await Effect.runPromise(sessionExists(candidateId))) return isFreshStarting(state);
|
|
2142
|
-
if (!(stateRole === role && state.status !== "stopped" && state.status !== "error")) return false;
|
|
2143
|
-
if (workspacePath && state.roleRunHead) try {
|
|
2144
|
-
const { stdout } = await execAsync("git rev-parse --short=8 HEAD", { cwd: workspacePath });
|
|
2145
|
-
const currentHead = stdout.trim();
|
|
2146
|
-
if (currentHead && currentHead !== state.roleRunHead) {
|
|
2147
|
-
console.log(`[cloister] ${issueId}: ${role} session ${candidateId} is stale (ran against ${state.roleRunHead}, HEAD is now ${currentHead}) — not active`);
|
|
2148
|
-
return false;
|
|
2149
|
-
}
|
|
2150
|
-
} catch {}
|
|
2151
|
-
return true;
|
|
2152
|
-
}
|
|
2153
|
-
function buildReactiveRolePrompt(issueId, state, role) {
|
|
2154
|
-
return `${role.toUpperCase()} TASK for ${issueId}:
|
|
2155
|
-
|
|
2156
|
-
The issue lifecycle transitioned to ${state}. Run the ${role} role for this issue.
|
|
2157
|
-
|
|
2158
|
-
Required steps:
|
|
2159
|
-
1. Work only in the workspace configured for ${issueId}.
|
|
2160
|
-
2. Read .pan/continue.json, .pan/spec.vbrief.json, project instructions, and issue context.
|
|
2161
|
-
3. Follow the boundaries and success criteria in roles/${role}.md exactly.
|
|
2162
|
-
4. Report the role-specific terminal status when done.`;
|
|
2393
|
+
function checkAllTriggers(agentId, workspace, issueId, currentModel, health, config) {
|
|
2394
|
+
return Effect.promise(() => checkAllTriggersPromise(agentId, workspace, issueId, currentModel, health, config));
|
|
2163
2395
|
}
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region ../../lib/cloister/service-health.ts
|
|
2398
|
+
/** Cloister health monitoring seam. */
|
|
2399
|
+
init_agents();
|
|
2164
2400
|
/**
|
|
2165
|
-
*
|
|
2166
|
-
* to the canonical `<projectPath>/workspaces/feature-<issueLower>` layout.
|
|
2167
|
-
* Mirrors the resolution used by startup recovery (service.ts:583-609) so
|
|
2168
|
-
* the reactive scheduler dispatches review/test wrappers with the same
|
|
2169
|
-
* workspace contract those wrappers receive on the manual code path.
|
|
2401
|
+
* Perform a health check on all running agents
|
|
2170
2402
|
*/
|
|
2171
|
-
async function
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
const message = `${normalizedIssueId}: skipping ${role} dispatch — merge already landed (merge_status='merged' is terminal)`;
|
|
2200
|
-
console.log(`[cloister] ${message}`);
|
|
2201
|
-
emitActivityEntrySync({
|
|
2202
|
-
source: "cloister",
|
|
2203
|
-
level: "info",
|
|
2204
|
-
message,
|
|
2205
|
-
issueId: normalizedIssueId
|
|
2403
|
+
async function performHealthCheck(host) {
|
|
2404
|
+
try {
|
|
2405
|
+
const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);
|
|
2406
|
+
const agentIds = runningAgents.map((a) => a.id);
|
|
2407
|
+
const currentRunningSet = new Set(agentIds);
|
|
2408
|
+
if (host.previousRunningAgents.size > 0 && host.config.auto_restart?.enabled) {
|
|
2409
|
+
for (const previousAgentId of host.previousRunningAgents) if (!currentRunningSet.has(previousAgentId)) await host.handleAgentCrash(previousAgentId);
|
|
2410
|
+
}
|
|
2411
|
+
host.previousRunningAgents = currentRunningSet;
|
|
2412
|
+
host.healthCheckCount++;
|
|
2413
|
+
if (host.healthCheckCount % 4 === 0) host.checkCompletionMarkers();
|
|
2414
|
+
if (agentIds.length === 0) {
|
|
2415
|
+
host.lastCheck = /* @__PURE__ */ new Date();
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
const agentHealths = [];
|
|
2419
|
+
for (const agentId of agentIds) {
|
|
2420
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
2421
|
+
if (runtime) {
|
|
2422
|
+
const health = getAgentHealth(agentId, runtime);
|
|
2423
|
+
agentHealths.push(health);
|
|
2424
|
+
host.recordHealthEvent(health);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
host.lastCheck = /* @__PURE__ */ new Date();
|
|
2428
|
+
host.emit({
|
|
2429
|
+
type: "health_check",
|
|
2430
|
+
agentHealths
|
|
2206
2431
|
});
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2432
|
+
const needsAttention = getAgentsNeedingAttention(agentHealths);
|
|
2433
|
+
const pokeCooldownMs = host.config.auto_actions.poke_cooldown_ms ?? 1800 * 1e3;
|
|
2434
|
+
const now = Date.now();
|
|
2435
|
+
for (const health of needsAttention) {
|
|
2436
|
+
const idleAgentState = getAgentStateSync(health.agentId);
|
|
2437
|
+
if (idleAgentState?.role === "sequencer") continue;
|
|
2438
|
+
const { shouldSkipIdlePokeForAgent } = await import("./stuck-remediation-nTIvhGOR.js");
|
|
2439
|
+
if (shouldSkipIdlePokeForAgent(idleAgentState)) {
|
|
2440
|
+
host.pokeProgress.delete(health.agentId);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
const cooledDown = now - (host.lastPokeTimestamps.get(health.agentId) ?? 0) >= pokeCooldownMs;
|
|
2444
|
+
if (health.state === "warning") {
|
|
2445
|
+
host.emit({
|
|
2446
|
+
type: "agent_warning",
|
|
2447
|
+
agentId: health.agentId,
|
|
2448
|
+
health
|
|
2449
|
+
});
|
|
2450
|
+
if (host.config.auto_actions.poke_on_warning && cooledDown) {
|
|
2451
|
+
host.pokeAgent(health.agentId);
|
|
2452
|
+
host.lastPokeTimestamps.set(health.agentId, now);
|
|
2453
|
+
}
|
|
2454
|
+
} else if (health.state === "stuck") {
|
|
2455
|
+
host.emit({
|
|
2456
|
+
type: "agent_stuck",
|
|
2457
|
+
agentId: health.agentId,
|
|
2458
|
+
health
|
|
2459
|
+
});
|
|
2460
|
+
if ((host.config.auto_actions.poke_on_stuck ?? true) && cooledDown) {
|
|
2461
|
+
host.pokeAgent(health.agentId);
|
|
2462
|
+
host.lastPokeTimestamps.set(health.agentId, now);
|
|
2463
|
+
}
|
|
2464
|
+
if (host.config.auto_actions.kill_on_stuck) host.killAgent(health.agentId);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
host.checkHandoffTriggers(agentHealths);
|
|
2468
|
+
host.checkFPPViolations(agentIds);
|
|
2469
|
+
await reconcilePiCostEventsForRunningAgents(runningAgents);
|
|
2470
|
+
host.checkCostAlerts(agentIds);
|
|
2471
|
+
if (Math.random() < .1) host.checkSpecialistRotations();
|
|
2472
|
+
if (Math.random() < .01) clearOldViolationsSync(24);
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
console.error("Cloister health check failed:", error);
|
|
2475
|
+
host.emit({
|
|
2476
|
+
type: "error",
|
|
2477
|
+
error
|
|
2218
2478
|
});
|
|
2219
|
-
return;
|
|
2220
2479
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Check if any specialists need session rotation
|
|
2483
|
+
*/
|
|
2484
|
+
async function checkSpecialistRotations(host) {
|
|
2485
|
+
const mergeAgentResult = await Effect.runPromise(checkAndRotateIfNeeded("merge-agent", process.cwd()));
|
|
2486
|
+
if (mergeAgentResult) {
|
|
2487
|
+
host.emit({
|
|
2488
|
+
type: "session_rotated",
|
|
2489
|
+
specialistName: "merge-agent",
|
|
2490
|
+
result: mergeAgentResult
|
|
2230
2491
|
});
|
|
2231
|
-
|
|
2492
|
+
if (mergeAgentResult.success) console.log(`🔔 Rotated merge-agent session: ${mergeAgentResult.oldSessionId.substring(0, 8)} → ${mergeAgentResult.newSessionId?.substring(0, 8)}`);
|
|
2493
|
+
else console.error(`🔔 Failed to rotate merge-agent: ${mergeAgentResult.error}`);
|
|
2232
2494
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Record health event to database
|
|
2498
|
+
*
|
|
2499
|
+
* Only writes events when state changes or on first check.
|
|
2500
|
+
*/
|
|
2501
|
+
function recordHealthEvent(host, health) {
|
|
2502
|
+
try {
|
|
2503
|
+
const currentState = health.state;
|
|
2504
|
+
const previousState = host.previousStates.get(health.agentId);
|
|
2505
|
+
if (previousState === void 0 || previousState !== currentState) {
|
|
2506
|
+
const source = health.heartbeat?.source ? host.mapHeartbeatSource(health.heartbeat.source) : "unknown";
|
|
2507
|
+
writeHealthEvent({
|
|
2508
|
+
agentId: health.agentId,
|
|
2509
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2510
|
+
state: currentState,
|
|
2511
|
+
source,
|
|
2512
|
+
metadata: health.heartbeat ? JSON.stringify({
|
|
2513
|
+
confidence: health.heartbeat.confidence,
|
|
2514
|
+
lastAction: health.heartbeat.lastAction,
|
|
2515
|
+
toolName: health.heartbeat.toolName,
|
|
2516
|
+
timeSinceActivity: health.timeSinceActivity
|
|
2517
|
+
}) : void 0
|
|
2518
|
+
});
|
|
2519
|
+
host.previousStates.set(health.agentId, currentState);
|
|
2248
2520
|
}
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
console.error(`Failed to record health event for ${health.agentId}:`, error);
|
|
2249
2523
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Check for handoff triggers and execute handoffs (Phase 4)
|
|
2527
|
+
*
|
|
2528
|
+
* Checks all triggers for each agent and performs handoffs when triggered.
|
|
2529
|
+
*/
|
|
2530
|
+
async function checkHandoffTriggers(host, agentHealths) {
|
|
2531
|
+
for (const health of agentHealths) try {
|
|
2532
|
+
const agentState = getAgentStateSync(health.agentId);
|
|
2533
|
+
if (!agentState) continue;
|
|
2534
|
+
if (!agentState.workspace) continue;
|
|
2535
|
+
const triggers = await Effect.runPromise(checkAllTriggers(health.agentId, agentState.workspace, agentState.issueId, agentState.model, health, host.config));
|
|
2536
|
+
if (triggers.length > 0) {
|
|
2537
|
+
const trigger = triggers[0];
|
|
2538
|
+
if (trigger.type === "task_complete" && [
|
|
2539
|
+
"review-agent",
|
|
2540
|
+
"test-agent",
|
|
2541
|
+
"merge-agent",
|
|
2542
|
+
"inspect-agent",
|
|
2543
|
+
"uat-agent"
|
|
2544
|
+
].includes(trigger.suggestedModel || "")) {
|
|
2545
|
+
console.log(`[cloister] Skipping handoff for ${health.agentId}: task_complete triggers specialist dispatch via completion marker, not model swap`);
|
|
2546
|
+
continue;
|
|
2262
2547
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
workspace,
|
|
2268
|
-
branch
|
|
2269
|
-
}));
|
|
2270
|
-
const message = `${normalizedIssueId}: review role dispatched from lifecycle state '${newState}' (${result.message})`;
|
|
2271
|
-
console.log(`[cloister] ${message}`);
|
|
2272
|
-
emitActivityEntrySync({
|
|
2273
|
-
source: "cloister",
|
|
2274
|
-
level: result.success ? "info" : "error",
|
|
2275
|
-
message,
|
|
2276
|
-
issueId: normalizedIssueId
|
|
2548
|
+
host.emit({
|
|
2549
|
+
type: "handoff_triggered",
|
|
2550
|
+
agentId: health.agentId,
|
|
2551
|
+
trigger
|
|
2277
2552
|
});
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
source: "cloister",
|
|
2288
|
-
level: "info",
|
|
2289
|
-
message,
|
|
2290
|
-
issueId: normalizedIssueId
|
|
2553
|
+
console.log(`🔔 Handoff triggered for ${health.agentId}: ${trigger.reason}`);
|
|
2554
|
+
const result = await Effect.runPromise(performHandoff(health.agentId, {
|
|
2555
|
+
targetModel: trigger.suggestedModel || "sonnet",
|
|
2556
|
+
reason: trigger.reason
|
|
2557
|
+
}));
|
|
2558
|
+
host.emit({
|
|
2559
|
+
type: "handoff_completed",
|
|
2560
|
+
agentId: health.agentId,
|
|
2561
|
+
result
|
|
2291
2562
|
});
|
|
2292
|
-
|
|
2563
|
+
if (result.context) logHandoffEventSync(createHandoffEvent(health.agentId, agentState.issueId, result.context, trigger.type, result.success, result.error));
|
|
2564
|
+
if (result.success) console.log(`✓ Handoff completed: ${health.agentId} → ${result.newAgentId} (${trigger.suggestedModel})`);
|
|
2565
|
+
else console.error(`✗ Handoff failed: ${result.error}`);
|
|
2293
2566
|
}
|
|
2294
|
-
const { spawnRun } = await import("./agents-CxbuBjID.js");
|
|
2295
|
-
const message = `${normalizedIssueId}: ${role} role started from lifecycle state '${newState}' as ${(await spawnRun(normalizedIssueId, role, { prompt: buildReactiveRolePrompt(normalizedIssueId, newState, role) })).id}`;
|
|
2296
|
-
console.log(`[cloister] ${message}`);
|
|
2297
|
-
emitActivityEntrySync({
|
|
2298
|
-
source: "cloister",
|
|
2299
|
-
level: "info",
|
|
2300
|
-
message,
|
|
2301
|
-
issueId: normalizedIssueId
|
|
2302
|
-
});
|
|
2303
2567
|
} catch (error) {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2568
|
+
console.error(`Failed to check handoff triggers for ${health.agentId}:`, error);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Check for FPP violations and send nudges
|
|
2573
|
+
*/
|
|
2574
|
+
function checkFPPViolations(host, agentIds) {
|
|
2575
|
+
for (const agentId of agentIds) {
|
|
2576
|
+
const violation = checkAgentForViolations(agentId);
|
|
2577
|
+
if (!violation) continue;
|
|
2578
|
+
if (violation.nudgeCount === 0) host.emit({
|
|
2579
|
+
type: "fpp_violation_detected",
|
|
2580
|
+
agentId,
|
|
2581
|
+
violation
|
|
2582
|
+
});
|
|
2583
|
+
if ((violation.lastNudgeAt ? Date.now() - new Date(violation.lastNudgeAt).getTime() : Infinity) >= 300 * 1e3 || violation.nudgeCount === 0) {
|
|
2584
|
+
if (hasExceededMaxNudges(violation)) {
|
|
2585
|
+
host.emit({
|
|
2586
|
+
type: "fpp_max_nudges_exceeded",
|
|
2587
|
+
agentId,
|
|
2588
|
+
violation
|
|
2589
|
+
});
|
|
2590
|
+
console.error(`🔔 Agent ${agentId} exceeded max nudges for ${violation.type} - manual intervention required`);
|
|
2591
|
+
} else if (sendNudge(violation)) host.emit({
|
|
2592
|
+
type: "fpp_nudge_sent",
|
|
2593
|
+
agentId,
|
|
2594
|
+
nudgeCount: violation.nudgeCount
|
|
2313
2595
|
});
|
|
2314
|
-
return;
|
|
2315
2596
|
}
|
|
2316
|
-
console.error(`[cloister] Failed to start ${role} role for ${normalizedIssueId}:`, error);
|
|
2317
|
-
emitActivityEntrySync({
|
|
2318
|
-
source: "cloister",
|
|
2319
|
-
level: "error",
|
|
2320
|
-
message: `${normalizedIssueId}: failed to start ${role} role: ${message}`,
|
|
2321
|
-
issueId: normalizedIssueId
|
|
2322
|
-
});
|
|
2323
2597
|
}
|
|
2324
2598
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
function
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
issueId
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
case "issue.closed": return {
|
|
2342
|
-
issueId,
|
|
2343
|
-
state: "closed"
|
|
2344
|
-
};
|
|
2345
|
-
case "agent.completed": {
|
|
2346
|
-
const role = typeof payload.role === "string" ? payload.role : void 0;
|
|
2347
|
-
if (role === void 0 || role === "work") return {
|
|
2348
|
-
issueId,
|
|
2349
|
-
state: "in_review"
|
|
2350
|
-
};
|
|
2351
|
-
return null;
|
|
2599
|
+
/**
|
|
2600
|
+
* Check for cost limit alerts
|
|
2601
|
+
*/
|
|
2602
|
+
function checkCostAlerts(host, agentIds) {
|
|
2603
|
+
const config = host.config.cost_limits;
|
|
2604
|
+
if (!config) return;
|
|
2605
|
+
for (const agentId of agentIds) {
|
|
2606
|
+
const alerts = checkCostLimits(agentId, agentId.startsWith("agent-") ? agentId.replace(/^agent-/, "") : agentId, config);
|
|
2607
|
+
for (const alert of alerts) {
|
|
2608
|
+
host.emit({
|
|
2609
|
+
type: "cost_alert",
|
|
2610
|
+
alert
|
|
2611
|
+
});
|
|
2612
|
+
const entityLabel = alert.agentId || alert.issueId || "(unattributed)";
|
|
2613
|
+
if (alert.level === "limit_reached") console.error(`🔔 COST LIMIT REACHED: ${alert.type} for ${entityLabel} - $${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)}`);
|
|
2614
|
+
else console.warn(`🔔 Cost warning: ${alert.type} for ${entityLabel} at ${alert.percentUsed.toFixed(0)}% ($${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)})`);
|
|
2352
2615
|
}
|
|
2353
|
-
case "work.completed": return {
|
|
2354
|
-
issueId,
|
|
2355
|
-
state: "in_review"
|
|
2356
|
-
};
|
|
2357
|
-
case "review.approved": return {
|
|
2358
|
-
issueId,
|
|
2359
|
-
state: "testing"
|
|
2360
|
-
};
|
|
2361
|
-
case "test.passed": return {
|
|
2362
|
-
issueId,
|
|
2363
|
-
state: "shipping"
|
|
2364
|
-
};
|
|
2365
|
-
default: return null;
|
|
2366
2616
|
}
|
|
2367
2617
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2618
|
+
/**
|
|
2619
|
+
* Map heartbeat source to database source string
|
|
2620
|
+
*/
|
|
2621
|
+
function mapHeartbeatSource(_host, source) {
|
|
2622
|
+
switch (source) {
|
|
2623
|
+
case "jsonl": return "jsonl_mtime";
|
|
2624
|
+
case "tmux": return "tmux_activity";
|
|
2625
|
+
case "git": return "git_activity";
|
|
2626
|
+
case "active-heartbeat": return "active_heartbeat";
|
|
2627
|
+
default: return source;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
//#endregion
|
|
2631
|
+
//#region ../../lib/cloister/service-completion.ts
|
|
2632
|
+
/** Cloister completion marker fallback seam. */
|
|
2633
|
+
init_paths();
|
|
2634
|
+
/** Fallback scan for completion markers when `pan done` did not reach the dashboard. */
|
|
2635
|
+
async function checkCompletionMarkers(host) {
|
|
2636
|
+
try {
|
|
2637
|
+
if (!existsSync(AGENTS_DIR)) return;
|
|
2638
|
+
const agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-"));
|
|
2639
|
+
for (const dir of agentDirs) {
|
|
2640
|
+
const completedFile = join$1(AGENTS_DIR, dir.name, "completed");
|
|
2641
|
+
const processedFile = join$1(AGENTS_DIR, dir.name, "completed.processed");
|
|
2642
|
+
if (!existsSync(completedFile)) continue;
|
|
2643
|
+
if (existsSync(processedFile)) try {
|
|
2644
|
+
if (statSync(completedFile).mtimeMs > statSync(processedFile).mtimeMs) {
|
|
2645
|
+
try {
|
|
2646
|
+
unlinkSync(processedFile);
|
|
2647
|
+
} catch {}
|
|
2648
|
+
host.processedCompletions.delete(dir.name);
|
|
2649
|
+
console.log(`🔔 Cloister: Detected re-completion for ${dir.name} (completed newer than .processed) — clearing stale marker`);
|
|
2650
|
+
} else continue;
|
|
2651
|
+
} catch {
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
const content = JSON.parse(readFileSync(completedFile, "utf-8"));
|
|
2656
|
+
const ageMs = Date.now() - new Date(content.timestamp).getTime();
|
|
2657
|
+
if (ageMs > 1440 * 60 * 1e3) {
|
|
2658
|
+
console.log(`🔔 Cloister: Skipping stale completion marker for ${dir.name} (${Math.floor(ageMs / 36e5)}h old)`);
|
|
2659
|
+
host.processedCompletions.set(dir.name, Infinity);
|
|
2660
|
+
try {
|
|
2661
|
+
renameSync(completedFile, processedFile);
|
|
2662
|
+
} catch {}
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
} catch (parseErr) {
|
|
2666
|
+
console.warn(` ⚠ Cloister: Could not parse completion marker for ${dir.name}, skipping`);
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const retryCount = host.processedCompletions.get(dir.name) || 0;
|
|
2670
|
+
if (retryCount === Infinity) host.processedCompletions.delete(dir.name);
|
|
2671
|
+
else if (retryCount >= 3) continue;
|
|
2672
|
+
const issueId = dir.name.replace("agent-", "").toUpperCase();
|
|
2673
|
+
const { getReviewStatusSync } = await import("./review-status-CG0iDuWa.js");
|
|
2674
|
+
const existingReview = getReviewStatusSync(issueId);
|
|
2675
|
+
if (existingReview && ["reviewing", "passed"].includes(existingReview.reviewStatus || "")) {
|
|
2676
|
+
console.log(`🔔 Cloister: Completion marker for ${issueId} — review already ${existingReview.reviewStatus}, marking processed`);
|
|
2677
|
+
try {
|
|
2678
|
+
renameSync(completedFile, processedFile);
|
|
2679
|
+
} catch {}
|
|
2680
|
+
host.processedCompletions.set(dir.name, Infinity);
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
console.log(`🔔 Cloister: Found completion marker for ${issueId}, triggering review...${retryCount > 0 ? ` (retry ${retryCount}/3)` : ""}`);
|
|
2684
|
+
try {
|
|
2685
|
+
const result = await (async () => {
|
|
2686
|
+
const controller = new AbortController();
|
|
2687
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
2688
|
+
try {
|
|
2689
|
+
const res = await fetch(`${host.getDashboardApiUrl()}/api/review/${issueId}/trigger`, {
|
|
2690
|
+
method: "POST",
|
|
2691
|
+
headers: { "Content-Type": "application/json" },
|
|
2692
|
+
body: JSON.stringify({}),
|
|
2693
|
+
signal: controller.signal
|
|
2694
|
+
});
|
|
2695
|
+
clearTimeout(timer);
|
|
2696
|
+
try {
|
|
2697
|
+
return await res.json();
|
|
2698
|
+
} catch {
|
|
2699
|
+
return {
|
|
2700
|
+
success: false,
|
|
2701
|
+
error: `Invalid response (HTTP ${res.status})`
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
} catch (e) {
|
|
2705
|
+
clearTimeout(timer);
|
|
2706
|
+
if (e instanceof Error && e.name === "AbortError") return {
|
|
2707
|
+
success: false,
|
|
2708
|
+
error: "Timeout (5s)"
|
|
2709
|
+
};
|
|
2710
|
+
return {
|
|
2711
|
+
success: false,
|
|
2712
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
})();
|
|
2716
|
+
if (result.success) {
|
|
2717
|
+
console.log(` ✓ Review triggered for ${issueId}`);
|
|
2718
|
+
renameSync(completedFile, processedFile);
|
|
2719
|
+
host.processedCompletions.set(dir.name, Infinity);
|
|
2720
|
+
} else if (result.alreadyReviewed || result.alreadyMerged) {
|
|
2721
|
+
console.log(` ✓ ${issueId} already ${result.alreadyMerged ? "merged" : "reviewed"}, marking processed`);
|
|
2722
|
+
renameSync(completedFile, processedFile);
|
|
2723
|
+
host.processedCompletions.set(dir.name, Infinity);
|
|
2724
|
+
} else {
|
|
2725
|
+
host.processedCompletions.set(dir.name, retryCount + 1);
|
|
2726
|
+
console.log(` ⚠ Review trigger failed for ${issueId}: ${result.error || "unknown"} (will retry, ${2 - retryCount} attempts left)`);
|
|
2727
|
+
}
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
host.processedCompletions.set(dir.name, retryCount + 1);
|
|
2730
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2731
|
+
console.error(` ✗ Failed to trigger review for ${issueId}: ${message} (will retry, ${2 - retryCount} attempts left)`);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
} catch (error) {}
|
|
2735
|
+
}
|
|
2736
|
+
//#endregion
|
|
2737
|
+
//#region ../../lib/cloister/service-crash.ts
|
|
2738
|
+
/** Cloister crash recovery and poke escalation seam. */
|
|
2739
|
+
init_context_overflow();
|
|
2740
|
+
init_agents();
|
|
2741
|
+
init_tmux();
|
|
2742
|
+
init_reap_terminal_sessions();
|
|
2743
|
+
const execAsync = promisify(exec);
|
|
2744
|
+
const execFileAsync = promisify(execFile);
|
|
2745
|
+
/**
|
|
2746
|
+
* One-shot convoy roles (PAN-1742). The review lenses + synthesis and the test
|
|
2747
|
+
* role are spawned fresh via `spawnRun` each cycle; they run once, write their
|
|
2748
|
+
* artifact, and exit. They never save a sessionId, so they cannot be resumed.
|
|
2749
|
+
*/
|
|
2750
|
+
const ONE_SHOT_ROLES = new Set(["review", "test"]);
|
|
2751
|
+
/**
|
|
2752
|
+
* Decide whether a vanished tmux session should be treated as a crash worth
|
|
2753
|
+
* auto-restarting (PAN-1742). Returns a human-readable reason to SKIP the
|
|
2754
|
+
* crash/restart machinery, or `null` when the agent is a genuine restart
|
|
2755
|
+
* candidate.
|
|
2756
|
+
*
|
|
2757
|
+
* Two cases are not crashes:
|
|
2758
|
+
* - one-shot convoy roles, whose session-end is a normal completion; and
|
|
2759
|
+
* - any agent without a saved session, which `restartAgent` cannot resume
|
|
2760
|
+
* (it throws "No session ID found"), so counting a crash and scheduling a
|
|
2761
|
+
* restart is pure noise.
|
|
2762
|
+
*
|
|
2763
|
+
* Pure and exported so the rule is unit-tested independent of the service's
|
|
2764
|
+
* private health-check plumbing.
|
|
2765
|
+
*/
|
|
2766
|
+
function nonRestartableReason(role, sessionId) {
|
|
2767
|
+
if (ONE_SHOT_ROLES.has(role)) return `(${role}) finished its one-shot run — completion, not a crash`;
|
|
2768
|
+
if (!sessionId) return "ended with no resumable session — not auto-restartable";
|
|
2769
|
+
return null;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Poke an agent (send "are you stuck?" message).
|
|
2773
|
+
*
|
|
2774
|
+
* NOTE: runtime.sendMessage() is async — both ClaudeCodeRuntime and PiRuntime
|
|
2775
|
+
* are declared `async sendMessage(): Promise<void>`. A `throw` inside an
|
|
2776
|
+
* async function before any await still returns a rejected Promise, so the
|
|
2777
|
+
* surrounding try/catch CANNOT catch it. Without explicit `.catch()`, the
|
|
2778
|
+
* rejection becomes an UnhandledPromiseRejection and crashes the dashboard
|
|
2779
|
+
* server. We hit this in production when the deacon health-check polled a
|
|
2780
|
+
* dead agent (PAN-1189 wedge sweep #12-13).
|
|
2781
|
+
*/
|
|
2782
|
+
function pokeAgent(host, agentId) {
|
|
2783
|
+
host.pokeAgentWithEscalation(agentId).catch((error) => {
|
|
2784
|
+
console.error(`Failed to poke ${agentId}:`, error);
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
/** PAN-2452: fingerprint of observable progress — workspace HEAD + pane tail.
|
|
2788
|
+
* Unchanged fingerprint across pokes = the poke did nothing. */
|
|
2789
|
+
async function progressFingerprint(_host, agentId) {
|
|
2790
|
+
const state = getAgentStateSync(agentId);
|
|
2791
|
+
let head = "";
|
|
2792
|
+
if (state?.workspace) try {
|
|
2793
|
+
const { stdout } = await execAsync("git rev-parse HEAD", {
|
|
2794
|
+
cwd: state.workspace,
|
|
2795
|
+
encoding: "utf-8"
|
|
2796
|
+
});
|
|
2797
|
+
head = stdout.trim();
|
|
2798
|
+
} catch {}
|
|
2799
|
+
let pane = "";
|
|
2800
|
+
try {
|
|
2801
|
+
const { stdout } = await execFileAsync("tmux", [
|
|
2802
|
+
"-L",
|
|
2803
|
+
"overdeck",
|
|
2804
|
+
"capture-pane",
|
|
2805
|
+
"-t",
|
|
2806
|
+
exactPaneTarget(agentId),
|
|
2807
|
+
"-p",
|
|
2808
|
+
"-S",
|
|
2809
|
+
`-40`
|
|
2810
|
+
], { encoding: "utf-8" });
|
|
2811
|
+
pane = stdout;
|
|
2812
|
+
} catch {}
|
|
2813
|
+
const paneHash = createHash("sha1").update(pane).digest("hex").slice(0, 12);
|
|
2814
|
+
return `${head}:${paneHash}`;
|
|
2815
|
+
}
|
|
2816
|
+
async function pokeAgentWithEscalation(host, agentId) {
|
|
2817
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
2818
|
+
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
2819
|
+
const fingerprint = await host.progressFingerprint(agentId);
|
|
2820
|
+
const prior = host.pokeProgress.get(agentId);
|
|
2821
|
+
const ineffective = prior && prior.fingerprint === fingerprint ? prior.ineffective + 1 : 0;
|
|
2822
|
+
host.pokeProgress.set(agentId, {
|
|
2823
|
+
fingerprint,
|
|
2824
|
+
ineffective
|
|
2825
|
+
});
|
|
2826
|
+
if (ineffective >= 4) {
|
|
2827
|
+
const { setAgentPausedSync } = await import("./agent-state-lGvgPP4m.js");
|
|
2828
|
+
try {
|
|
2829
|
+
setAgentPausedSync(agentId, `needs-you: idle-alive — no observable progress across ${ineffective + 1} pokes (idle-alive escalation)`);
|
|
2830
|
+
host.emit({
|
|
2831
|
+
type: "agent_stuck",
|
|
2832
|
+
agentId,
|
|
2833
|
+
health: void 0
|
|
2834
|
+
});
|
|
2835
|
+
console.log(`🛑 ${agentId} paused: idle-alive across ${ineffective + 1} pokes`);
|
|
2836
|
+
} catch (pauseErr) {
|
|
2837
|
+
console.error(`Failed to pause idle-alive ${agentId}:`, pauseErr);
|
|
2381
2838
|
}
|
|
2382
2839
|
return;
|
|
2383
2840
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2841
|
+
let pokeMessage = "Hey, I noticed you haven't made progress in a while. Are you stuck? If you need help or clarification, please ask. Otherwise, please continue with your work.";
|
|
2842
|
+
if (ineffective >= 2) {
|
|
2843
|
+
let tail = "";
|
|
2844
|
+
try {
|
|
2845
|
+
const { stdout } = await execFileAsync("tmux", [
|
|
2846
|
+
"-L",
|
|
2847
|
+
"overdeck",
|
|
2848
|
+
"capture-pane",
|
|
2849
|
+
"-t",
|
|
2850
|
+
exactPaneTarget(agentId),
|
|
2851
|
+
"-p",
|
|
2852
|
+
"-S",
|
|
2853
|
+
`-40`
|
|
2854
|
+
], { encoding: "utf-8" });
|
|
2855
|
+
tail = stdout;
|
|
2856
|
+
} catch {}
|
|
2857
|
+
if (isContextOverflowTail(tail)) {
|
|
2858
|
+
pokeMessage = "/compact";
|
|
2859
|
+
console.log(`🔔 ${agentId}: overflow tail detected — delivering /compact instead of a poke`);
|
|
2860
|
+
} else pokeMessage = "You appear active but have made no observable progress (no new commits, unchanged output) across multiple checks. Do exactly one of: (1) work is done — commit, push, and run pan done; (2) blocked — state the blocker in one sentence and commit what you have as WIP; (3) lost context — re-read your bead (bd show), your latest .pan/feedback/ file, and git status, then resume. Act now.";
|
|
2861
|
+
}
|
|
2862
|
+
await Promise.resolve(runtime.sendMessage(agentId, pokeMessage)).catch((sendErr) => {
|
|
2863
|
+
console.error(`Failed to send poke to ${agentId}:`, sendErr);
|
|
2864
|
+
});
|
|
2865
|
+
host.emit({
|
|
2866
|
+
type: "poked_agent",
|
|
2867
|
+
agentId
|
|
2868
|
+
});
|
|
2869
|
+
console.log(`🔔 Poked ${agentId}${ineffective > 0 ? ` (ineffective streak: ${ineffective})` : ""}`);
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Kill an agent
|
|
2873
|
+
*
|
|
2874
|
+
* runtime.killAgent() is also async in some runtime implementations — apply
|
|
2875
|
+
* the same fire-and-forget guard as pokeAgent so async rejection cannot
|
|
2876
|
+
* crash the dashboard from a deacon health-check timer callback.
|
|
2877
|
+
*/
|
|
2878
|
+
function killAgent(host, agentId) {
|
|
2879
|
+
try {
|
|
2880
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
2881
|
+
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
2882
|
+
Promise.resolve(runtime.killAgent(agentId)).catch((killErr) => {
|
|
2883
|
+
console.error(`Failed to kill ${agentId}:`, killErr);
|
|
2884
|
+
});
|
|
2885
|
+
host.emit({
|
|
2886
|
+
type: "killed_agent",
|
|
2887
|
+
agentId
|
|
2888
|
+
});
|
|
2889
|
+
console.log(`🔔 Killed ${agentId}`);
|
|
2890
|
+
} catch (error) {
|
|
2891
|
+
console.error(`Failed to kill ${agentId}:`, error);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Handle agent crash with auto-restart logic
|
|
2896
|
+
*/
|
|
2897
|
+
async function handleAgentCrash(host, agentId) {
|
|
2898
|
+
const config = host.config.auto_restart;
|
|
2899
|
+
if (!config?.enabled) return;
|
|
2900
|
+
const agentState = getAgentStateSync(agentId);
|
|
2901
|
+
if (!agentState || agentState.status === "stopped") {
|
|
2902
|
+
console.log(`🔔 Agent ${agentId} was intentionally stopped, skipping restart`);
|
|
2387
2903
|
return;
|
|
2388
2904
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
const { handleAgentHeartbeatDeadEvent } = await import("./deacon-Bz-6Wjmn.js");
|
|
2393
|
-
await handleAgentHeartbeatDeadEvent(agentId, "event");
|
|
2394
|
-
}
|
|
2905
|
+
const runtimeState = getAgentRuntimeStateSync(agentId);
|
|
2906
|
+
if (runtimeState?.state === "suspended") {
|
|
2907
|
+
console.log(`🔔 Agent ${agentId} is suspended, skipping restart`);
|
|
2395
2908
|
return;
|
|
2396
2909
|
}
|
|
2397
|
-
if (
|
|
2398
|
-
|
|
2399
|
-
const issueId = payload?.issueId;
|
|
2400
|
-
if (issueId) {
|
|
2401
|
-
const { handleReviewCoordinatorDied } = await import("./deacon-Bz-6Wjmn.js");
|
|
2402
|
-
await handleReviewCoordinatorDied(issueId, payload?.sessionName ?? "", payload?.reason ?? "");
|
|
2403
|
-
}
|
|
2910
|
+
if (runtimeState?.state === "stopped") {
|
|
2911
|
+
console.log(`🔔 Agent ${agentId} runtime is stopped, skipping restart`);
|
|
2404
2912
|
return;
|
|
2405
2913
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
await
|
|
2914
|
+
const skipReason = nonRestartableReason(agentState.role, agentState.sessionId);
|
|
2915
|
+
if (skipReason) {
|
|
2916
|
+
let recoverUnfinishedOneShot = false;
|
|
2917
|
+
if (ONE_SHOT_ROLES.has(agentState.role) && agentState.sessionId && agentState.issueId) try {
|
|
2918
|
+
const { getReviewStatusSync } = await import("./review-status-CG0iDuWa.js");
|
|
2919
|
+
const status = getReviewStatusSync(agentState.issueId);
|
|
2920
|
+
recoverUnfinishedOneShot = !(status ? isRoleTerminal(agentState.role, {
|
|
2921
|
+
reviewStatus: status.reviewStatus,
|
|
2922
|
+
testStatus: status.testStatus,
|
|
2923
|
+
readyForMerge: status.readyForMerge,
|
|
2924
|
+
mergeStatus: status.mergeStatus
|
|
2925
|
+
}) : false);
|
|
2926
|
+
} catch {
|
|
2927
|
+
recoverUnfinishedOneShot = false;
|
|
2928
|
+
}
|
|
2929
|
+
if (!recoverUnfinishedOneShot) {
|
|
2930
|
+
console.log(`🔔 Agent ${agentId} ${skipReason}; skipping restart`);
|
|
2931
|
+
return;
|
|
2411
2932
|
}
|
|
2933
|
+
console.log(`🔔 Agent ${agentId} (${agentState.role}) vanished before a terminal verdict — recovering instead of treating as a one-shot completion (PAN-2007)`);
|
|
2412
2934
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2935
|
+
const now = /* @__PURE__ */ new Date();
|
|
2936
|
+
host.deathTimestamps.push(now);
|
|
2937
|
+
host.checkForMassDeaths();
|
|
2938
|
+
let tracker = host.crashTrackers.get(agentId);
|
|
2939
|
+
if (!tracker) {
|
|
2940
|
+
tracker = {
|
|
2941
|
+
agentId,
|
|
2942
|
+
crashCount: 0,
|
|
2943
|
+
lastCrash: now,
|
|
2944
|
+
gaveUp: false
|
|
2945
|
+
};
|
|
2946
|
+
host.crashTrackers.set(agentId, tracker);
|
|
2947
|
+
}
|
|
2948
|
+
if (tracker.gaveUp) return;
|
|
2949
|
+
tracker.crashCount++;
|
|
2950
|
+
tracker.lastCrash = now;
|
|
2951
|
+
host.emit({
|
|
2952
|
+
type: "agent_crashed",
|
|
2953
|
+
agentId,
|
|
2954
|
+
crashCount: tracker.crashCount
|
|
2955
|
+
});
|
|
2956
|
+
console.log(`🔔 Agent ${agentId} crashed (crash #${tracker.crashCount})`);
|
|
2957
|
+
if (host.eventStore) try {
|
|
2958
|
+
host.eventStore.append({
|
|
2959
|
+
type: "agent.heartbeat_dead",
|
|
2960
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2961
|
+
payload: {
|
|
2962
|
+
agentId,
|
|
2963
|
+
issueId: agentState.issueId,
|
|
2964
|
+
sessionId: agentState.sessionId
|
|
2427
2965
|
}
|
|
2966
|
+
});
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
console.error(`[cloister] Failed to emit heartbeat_dead for ${agentId}:`, err);
|
|
2969
|
+
}
|
|
2970
|
+
if (tracker.crashCount > config.max_retries) {
|
|
2971
|
+
tracker.gaveUp = true;
|
|
2972
|
+
host.emit({
|
|
2973
|
+
type: "agent_gave_up",
|
|
2974
|
+
agentId,
|
|
2975
|
+
maxRetries: config.max_retries
|
|
2976
|
+
});
|
|
2977
|
+
console.error(`🔔 Gave up on restarting ${agentId} after ${config.max_retries} attempts`);
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
const backoffIndex = Math.min(tracker.crashCount - 1, config.backoff_seconds.length - 1);
|
|
2981
|
+
const backoffSeconds = config.backoff_seconds[backoffIndex];
|
|
2982
|
+
const nextRetryAt = new Date(Date.now() + backoffSeconds * 1e3);
|
|
2983
|
+
tracker.nextRetryAt = nextRetryAt;
|
|
2984
|
+
host.emit({
|
|
2985
|
+
type: "agent_restarting",
|
|
2986
|
+
agentId,
|
|
2987
|
+
crashCount: tracker.crashCount,
|
|
2988
|
+
backoffSeconds
|
|
2989
|
+
});
|
|
2990
|
+
console.log(`🔔 Deacon will recover ${agentId} via heartbeat_dead (attempt ${tracker.crashCount}/${config.max_retries}, backoff ${backoffSeconds}s)`);
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Restart an agent using its saved session
|
|
2994
|
+
*/
|
|
2995
|
+
async function restartAgent(_host, agentId) {
|
|
2996
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
2997
|
+
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
2998
|
+
const agentState = getAgentStateSync(agentId);
|
|
2999
|
+
if (!agentState?.sessionId) throw new Error(`No session ID found for agent ${agentId}`);
|
|
3000
|
+
if (!agentState.workspace) throw new Error(`No workspace found for agent ${agentId}`);
|
|
3001
|
+
console.log(`🔔 Restarting ${agentId} with session ${agentState.sessionId.substring(0, 8)}...`);
|
|
3002
|
+
runtime.spawnAgent({
|
|
3003
|
+
agentId,
|
|
3004
|
+
workspace: agentState.workspace,
|
|
3005
|
+
sessionId: agentState.sessionId,
|
|
3006
|
+
runtime: runtime.name
|
|
3007
|
+
});
|
|
3008
|
+
console.log(`🔔 Successfully restarted ${agentId}`);
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* Check for mass death events
|
|
3012
|
+
*
|
|
3013
|
+
* Detects when 3+ agents die within 30 seconds and pauses spawns.
|
|
3014
|
+
*/
|
|
3015
|
+
function checkForMassDeaths(host) {
|
|
3016
|
+
const MASS_DEATH_THRESHOLD = 3;
|
|
3017
|
+
const WINDOW_SECONDS = 30;
|
|
3018
|
+
const windowStart = Date.now() - WINDOW_SECONDS * 1e3;
|
|
3019
|
+
host.deathTimestamps = host.deathTimestamps.filter((timestamp) => timestamp.getTime() >= windowStart);
|
|
3020
|
+
if (host.deathTimestamps.length >= MASS_DEATH_THRESHOLD) {
|
|
3021
|
+
host.emit({
|
|
3022
|
+
type: "mass_death_detected",
|
|
3023
|
+
deathCount: host.deathTimestamps.length,
|
|
3024
|
+
windowSeconds: WINDOW_SECONDS
|
|
3025
|
+
});
|
|
3026
|
+
if (!host.spawnsPaused) {
|
|
3027
|
+
host.pauseSpawns("Mass death detected - system stability concern");
|
|
3028
|
+
console.error(`🔔 MASS DEATH DETECTED: ${host.deathTimestamps.length} agents died in ${WINDOW_SECONDS}s - spawns paused`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Pause new agent spawns
|
|
3034
|
+
*/
|
|
3035
|
+
function pauseSpawns(host, reason) {
|
|
3036
|
+
host.spawnsPaused = true;
|
|
3037
|
+
setCloisterSpawnsPausedSync(true);
|
|
3038
|
+
host.emit({
|
|
3039
|
+
type: "spawn_paused",
|
|
3040
|
+
reason
|
|
3041
|
+
});
|
|
3042
|
+
console.log(`🔔 Agent spawns paused: ${reason}`);
|
|
3043
|
+
}
|
|
3044
|
+
//#endregion
|
|
3045
|
+
//#region ../../lib/cloister/service-status.ts
|
|
3046
|
+
init_agents();
|
|
3047
|
+
/**
|
|
3048
|
+
* Get current status
|
|
3049
|
+
*
|
|
3050
|
+
* Uses a 3-second TTL cache to avoid blocking the event loop on repeated
|
|
3051
|
+
* dashboard polls. The underlying computation does sync file I/O and tmux
|
|
3052
|
+
* calls for every agent, which scales poorly with agent count.
|
|
3053
|
+
*/
|
|
3054
|
+
function getStatus(host) {
|
|
3055
|
+
const now = Date.now();
|
|
3056
|
+
if (host.statusCache && now - host.statusCacheAt < host.statusCacheTtlMs) return host.statusCache;
|
|
3057
|
+
const agentIds = listRunningAgentsSync().filter((a) => a.tmuxActive).map((a) => a.id);
|
|
3058
|
+
const agentHealths = [];
|
|
3059
|
+
for (const agentId of agentIds) {
|
|
3060
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
3061
|
+
if (runtime) {
|
|
3062
|
+
const health = getAgentHealth(agentId, runtime);
|
|
3063
|
+
agentHealths.push(health);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const summary = generateHealthSummary(agentHealths);
|
|
3067
|
+
const needsAttention = getAgentsNeedingAttention(agentHealths).map((h) => h.agentId);
|
|
3068
|
+
const deaconStatus = getDeaconStatus();
|
|
3069
|
+
const patrol = assessDeaconPatrolFreshness({
|
|
3070
|
+
isRunning: deaconStatus.isRunning,
|
|
3071
|
+
lastPatrol: deaconStatus.state.lastPatrol,
|
|
3072
|
+
patrolIntervalMs: deaconStatus.config.patrolIntervalMs
|
|
3073
|
+
});
|
|
3074
|
+
const status = {
|
|
3075
|
+
running: host.isRunning(),
|
|
3076
|
+
lastCheck: host.lastCheck,
|
|
3077
|
+
config: host.config,
|
|
3078
|
+
summary,
|
|
3079
|
+
agentsNeedingAttention: needsAttention,
|
|
3080
|
+
patrol: {
|
|
3081
|
+
...patrol,
|
|
3082
|
+
loopRunning: deaconStatus.isRunning,
|
|
3083
|
+
patrolIntervalMs: deaconStatus.config.patrolIntervalMs
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
host.statusCache = status;
|
|
3087
|
+
host.statusCacheAt = now;
|
|
3088
|
+
return status;
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Get health for a specific agent
|
|
3092
|
+
*/
|
|
3093
|
+
function getServiceAgentHealth(_host, agentId) {
|
|
3094
|
+
const runtime = getRuntimeForAgent(agentId);
|
|
3095
|
+
if (!runtime) return null;
|
|
3096
|
+
return getAgentHealth(agentId, runtime);
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Get health for all running agents
|
|
3100
|
+
*/
|
|
3101
|
+
function getAllAgentHealth(_host) {
|
|
3102
|
+
const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);
|
|
3103
|
+
const agentHealths = [];
|
|
3104
|
+
for (const agent of runningAgents) {
|
|
3105
|
+
const runtime = getRuntimeForAgent(agent.id);
|
|
3106
|
+
if (runtime) {
|
|
3107
|
+
const health = getAgentHealth(agent.id, runtime);
|
|
3108
|
+
agentHealths.push(health);
|
|
2428
3109
|
}
|
|
2429
3110
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
3111
|
+
return agentHealths;
|
|
3112
|
+
}
|
|
3113
|
+
//#endregion
|
|
3114
|
+
//#region ../../lib/cloister/service.ts
|
|
3115
|
+
init_config();
|
|
3116
|
+
init_agents();
|
|
3117
|
+
init_control_settings();
|
|
3118
|
+
init_paths();
|
|
3119
|
+
init_tmux();
|
|
3120
|
+
init_activity_logger();
|
|
3121
|
+
const CLOISTER_STATE_FILE = join$1(OVERDECK_HOME, "cloister.state");
|
|
3122
|
+
const LEGACY_SPECIALISTS_DIR = join$1(OVERDECK_HOME, "specialists");
|
|
3123
|
+
async function cleanupLegacySpecialistsDirectory() {
|
|
3124
|
+
await rm(LEGACY_SPECIALISTS_DIR, {
|
|
3125
|
+
recursive: true,
|
|
3126
|
+
force: true
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
let cloisterEventStoreProvider = null;
|
|
3130
|
+
function setCloisterEventStoreProvider(provider) {
|
|
3131
|
+
cloisterEventStoreProvider = provider;
|
|
2433
3132
|
}
|
|
2434
3133
|
/**
|
|
2435
3134
|
* Write Cloister running state to file for cross-process visibility
|
|
@@ -2466,32 +3165,6 @@ function readCloisterStateFile() {
|
|
|
2466
3165
|
return { running: false };
|
|
2467
3166
|
}
|
|
2468
3167
|
/**
|
|
2469
|
-
* One-shot convoy roles (PAN-1742). The review lenses + synthesis and the test
|
|
2470
|
-
* role are spawned fresh via `spawnRun` each cycle; they run once, write their
|
|
2471
|
-
* artifact, and exit. They never save a sessionId, so they cannot be resumed.
|
|
2472
|
-
*/
|
|
2473
|
-
const ONE_SHOT_ROLES = new Set(["review", "test"]);
|
|
2474
|
-
/**
|
|
2475
|
-
* Decide whether a vanished tmux session should be treated as a crash worth
|
|
2476
|
-
* auto-restarting (PAN-1742). Returns a human-readable reason to SKIP the
|
|
2477
|
-
* crash/restart machinery, or `null` when the agent is a genuine restart
|
|
2478
|
-
* candidate.
|
|
2479
|
-
*
|
|
2480
|
-
* Two cases are not crashes:
|
|
2481
|
-
* - one-shot convoy roles, whose session-end is a normal completion; and
|
|
2482
|
-
* - any agent without a saved session, which `restartAgent` cannot resume
|
|
2483
|
-
* (it throws "No session ID found"), so counting a crash and scheduling a
|
|
2484
|
-
* restart is pure noise.
|
|
2485
|
-
*
|
|
2486
|
-
* Pure and exported so the rule is unit-tested independent of the service's
|
|
2487
|
-
* private health-check plumbing.
|
|
2488
|
-
*/
|
|
2489
|
-
function nonRestartableReason(role, sessionId) {
|
|
2490
|
-
if (ONE_SHOT_ROLES.has(role)) return `(${role}) finished its one-shot run — completion, not a crash`;
|
|
2491
|
-
if (!sessionId) return "ended with no resumable session — not auto-restartable";
|
|
2492
|
-
return null;
|
|
2493
|
-
}
|
|
2494
|
-
/**
|
|
2495
3168
|
* Cloister Service
|
|
2496
3169
|
*
|
|
2497
3170
|
* Monitors agent health and performs auto-actions.
|
|
@@ -2520,6 +3193,122 @@ var CloisterService = class {
|
|
|
2520
3193
|
constructor(config) {
|
|
2521
3194
|
this.config = config || loadCloisterConfigSync();
|
|
2522
3195
|
}
|
|
3196
|
+
healthHost() {
|
|
3197
|
+
const service = this;
|
|
3198
|
+
return {
|
|
3199
|
+
get previousRunningAgents() {
|
|
3200
|
+
return service.previousRunningAgents;
|
|
3201
|
+
},
|
|
3202
|
+
set previousRunningAgents(value) {
|
|
3203
|
+
service.previousRunningAgents = value;
|
|
3204
|
+
},
|
|
3205
|
+
get config() {
|
|
3206
|
+
return service.config;
|
|
3207
|
+
},
|
|
3208
|
+
get healthCheckCount() {
|
|
3209
|
+
return service.healthCheckCount;
|
|
3210
|
+
},
|
|
3211
|
+
set healthCheckCount(value) {
|
|
3212
|
+
service.healthCheckCount = value;
|
|
3213
|
+
},
|
|
3214
|
+
get lastCheck() {
|
|
3215
|
+
return service.lastCheck;
|
|
3216
|
+
},
|
|
3217
|
+
set lastCheck(value) {
|
|
3218
|
+
service.lastCheck = value;
|
|
3219
|
+
},
|
|
3220
|
+
get lastPokeTimestamps() {
|
|
3221
|
+
return service.lastPokeTimestamps;
|
|
3222
|
+
},
|
|
3223
|
+
get pokeProgress() {
|
|
3224
|
+
return service.pokeProgress;
|
|
3225
|
+
},
|
|
3226
|
+
get previousStates() {
|
|
3227
|
+
return service.previousStates;
|
|
3228
|
+
},
|
|
3229
|
+
handleAgentCrash: (agentId) => service.handleAgentCrash(agentId),
|
|
3230
|
+
checkCompletionMarkers: () => service.checkCompletionMarkers(),
|
|
3231
|
+
recordHealthEvent: (health) => service.recordHealthEvent(health),
|
|
3232
|
+
emit: (event) => service.emit(event),
|
|
3233
|
+
pokeAgent: (agentId) => service.pokeAgent(agentId),
|
|
3234
|
+
killAgent: (agentId) => service.killAgent(agentId),
|
|
3235
|
+
checkHandoffTriggers: (agentHealths) => service.checkHandoffTriggers(agentHealths),
|
|
3236
|
+
checkFPPViolations: (agentIds) => service.checkFPPViolations(agentIds),
|
|
3237
|
+
checkCostAlerts: (agentIds) => service.checkCostAlerts(agentIds),
|
|
3238
|
+
checkSpecialistRotations: () => service.checkSpecialistRotations(),
|
|
3239
|
+
mapHeartbeatSource: (source) => service.mapHeartbeatSource(source)
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
completionHost() {
|
|
3243
|
+
const service = this;
|
|
3244
|
+
return {
|
|
3245
|
+
get processedCompletions() {
|
|
3246
|
+
return service.processedCompletions;
|
|
3247
|
+
},
|
|
3248
|
+
getDashboardApiUrl: () => service.getDashboardApiUrl()
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
crashHost() {
|
|
3252
|
+
const service = this;
|
|
3253
|
+
return {
|
|
3254
|
+
get config() {
|
|
3255
|
+
return service.config;
|
|
3256
|
+
},
|
|
3257
|
+
get crashTrackers() {
|
|
3258
|
+
return service.crashTrackers;
|
|
3259
|
+
},
|
|
3260
|
+
get deathTimestamps() {
|
|
3261
|
+
return service.deathTimestamps;
|
|
3262
|
+
},
|
|
3263
|
+
set deathTimestamps(value) {
|
|
3264
|
+
service.deathTimestamps = value;
|
|
3265
|
+
},
|
|
3266
|
+
get spawnsPaused() {
|
|
3267
|
+
return service.spawnsPaused;
|
|
3268
|
+
},
|
|
3269
|
+
set spawnsPaused(value) {
|
|
3270
|
+
service.spawnsPaused = value;
|
|
3271
|
+
},
|
|
3272
|
+
get pokeProgress() {
|
|
3273
|
+
return service.pokeProgress;
|
|
3274
|
+
},
|
|
3275
|
+
get eventStore() {
|
|
3276
|
+
return service.eventStore;
|
|
3277
|
+
},
|
|
3278
|
+
progressFingerprint: (agentId) => service.progressFingerprint(agentId),
|
|
3279
|
+
pokeAgentWithEscalation: (agentId) => service.pokeAgentWithEscalation(agentId),
|
|
3280
|
+
checkForMassDeaths: () => service.checkForMassDeaths(),
|
|
3281
|
+
pauseSpawns: (reason) => service.pauseSpawns(reason),
|
|
3282
|
+
emit: (event) => service.emit(event)
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
statusHost() {
|
|
3286
|
+
const service = this;
|
|
3287
|
+
return {
|
|
3288
|
+
get statusCache() {
|
|
3289
|
+
return service._statusCache;
|
|
3290
|
+
},
|
|
3291
|
+
set statusCache(value) {
|
|
3292
|
+
service._statusCache = value;
|
|
3293
|
+
},
|
|
3294
|
+
get statusCacheAt() {
|
|
3295
|
+
return service._statusCacheAt;
|
|
3296
|
+
},
|
|
3297
|
+
set statusCacheAt(value) {
|
|
3298
|
+
service._statusCacheAt = value;
|
|
3299
|
+
},
|
|
3300
|
+
get statusCacheTtlMs() {
|
|
3301
|
+
return service.STATUS_CACHE_TTL_MS;
|
|
3302
|
+
},
|
|
3303
|
+
get lastCheck() {
|
|
3304
|
+
return service.lastCheck;
|
|
3305
|
+
},
|
|
3306
|
+
get config() {
|
|
3307
|
+
return service.config;
|
|
3308
|
+
},
|
|
3309
|
+
isRunning: () => service.isRunning()
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
2523
3312
|
getDashboardApiUrl() {
|
|
2524
3313
|
return `http://127.0.0.1:${process.env.API_PORT || process.env.PORT || "3011"}`;
|
|
2525
3314
|
}
|
|
@@ -2558,7 +3347,7 @@ var CloisterService = class {
|
|
|
2558
3347
|
let clearedSpecialistCount = 0;
|
|
2559
3348
|
try {
|
|
2560
3349
|
if (existsSync(AGENTS_DIR)) {
|
|
2561
|
-
const { isRunning: isSpecialistRunning } = await import("./specialists-
|
|
3350
|
+
const { isRunning: isSpecialistRunning } = await import("./specialists-k22_Btaq.js");
|
|
2562
3351
|
const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });
|
|
2563
3352
|
for (const entry of entries) {
|
|
2564
3353
|
if (!entry.isDirectory()) continue;
|
|
@@ -2593,8 +3382,8 @@ var CloisterService = class {
|
|
|
2593
3382
|
}
|
|
2594
3383
|
try {
|
|
2595
3384
|
const reviewStatuses = loadReviewStatuses();
|
|
2596
|
-
const { resolveProjectFromIssueSync } = await import("./projects-
|
|
2597
|
-
const { getTmuxSessionName, getAllProjectSpecialistStatuses } = await import("./specialists-
|
|
3385
|
+
const { resolveProjectFromIssueSync } = await import("./projects-CrBE6IhN.js");
|
|
3386
|
+
const { getTmuxSessionName, getAllProjectSpecialistStatuses } = await import("./specialists-k22_Btaq.js");
|
|
2598
3387
|
const activeReviewIssues = /* @__PURE__ */ new Set();
|
|
2599
3388
|
try {
|
|
2600
3389
|
const projSpecs = await getAllProjectSpecialistStatuses();
|
|
@@ -2605,7 +3394,7 @@ var CloisterService = class {
|
|
|
2605
3394
|
}
|
|
2606
3395
|
const globalRs = getAgentRuntimeStateSync(getTmuxSessionName("review-agent"));
|
|
2607
3396
|
if (globalRs?.state === "active" && globalRs.currentIssue) activeReviewIssues.add(globalRs.currentIssue.toUpperCase());
|
|
2608
|
-
const { listRunningAgents } = await import("./agents-
|
|
3397
|
+
const { listRunningAgents } = await import("./agents-CEcaxSkR.js");
|
|
2609
3398
|
const agents = await Effect.runPromise(listRunningAgents());
|
|
2610
3399
|
for (const agent of agents) {
|
|
2611
3400
|
if (agent.status === "stopped" || agent.status === "error") continue;
|
|
@@ -2648,7 +3437,7 @@ var CloisterService = class {
|
|
|
2648
3437
|
continue;
|
|
2649
3438
|
}
|
|
2650
3439
|
const branch = `feature/${issueId.toLowerCase()}`;
|
|
2651
|
-
const { spawnReviewRoleForIssue } = await import("./review-agent-
|
|
3440
|
+
const { spawnReviewRoleForIssue } = await import("./review-agent-u5q3ip7H.js");
|
|
2652
3441
|
const dispatchResult = await Effect.runPromise(spawnReviewRoleForIssue({
|
|
2653
3442
|
issueId,
|
|
2654
3443
|
workspace,
|
|
@@ -2736,7 +3525,7 @@ var CloisterService = class {
|
|
|
2736
3525
|
console.log(" ✓ Cloister event store provider installed");
|
|
2737
3526
|
return;
|
|
2738
3527
|
}
|
|
2739
|
-
const { initEventStore } = await import("./event-store-
|
|
3528
|
+
const { initEventStore } = await import("./event-store-CqDgfbAs.js");
|
|
2740
3529
|
const store = await initEventStore();
|
|
2741
3530
|
this.eventStore = store;
|
|
2742
3531
|
this.domainEventUnsubscribe = store.subscribe((event) => {
|
|
@@ -2837,177 +3626,11 @@ var CloisterService = class {
|
|
|
2837
3626
|
* Perform a health check on all running agents
|
|
2838
3627
|
*/
|
|
2839
3628
|
async performHealthCheck() {
|
|
2840
|
-
|
|
2841
|
-
const runningAgents = listRunningAgentsSync().filter((a) => a.tmuxActive);
|
|
2842
|
-
const agentIds = runningAgents.map((a) => a.id);
|
|
2843
|
-
const currentRunningSet = new Set(agentIds);
|
|
2844
|
-
if (this.previousRunningAgents.size > 0 && this.config.auto_restart?.enabled) {
|
|
2845
|
-
for (const previousAgentId of this.previousRunningAgents) if (!currentRunningSet.has(previousAgentId)) await this.handleAgentCrash(previousAgentId);
|
|
2846
|
-
}
|
|
2847
|
-
this.previousRunningAgents = currentRunningSet;
|
|
2848
|
-
this.healthCheckCount++;
|
|
2849
|
-
if (this.healthCheckCount % 4 === 0) this.checkCompletionMarkers();
|
|
2850
|
-
if (agentIds.length === 0) {
|
|
2851
|
-
this.lastCheck = /* @__PURE__ */ new Date();
|
|
2852
|
-
return;
|
|
2853
|
-
}
|
|
2854
|
-
const agentHealths = [];
|
|
2855
|
-
for (const agentId of agentIds) {
|
|
2856
|
-
const runtime = getRuntimeForAgent(agentId);
|
|
2857
|
-
if (runtime) {
|
|
2858
|
-
const health = getAgentHealth(agentId, runtime);
|
|
2859
|
-
agentHealths.push(health);
|
|
2860
|
-
this.recordHealthEvent(health);
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
this.lastCheck = /* @__PURE__ */ new Date();
|
|
2864
|
-
this.emit({
|
|
2865
|
-
type: "health_check",
|
|
2866
|
-
agentHealths
|
|
2867
|
-
});
|
|
2868
|
-
const needsAttention = getAgentsNeedingAttention(agentHealths);
|
|
2869
|
-
const pokeCooldownMs = this.config.auto_actions.poke_cooldown_ms ?? 1800 * 1e3;
|
|
2870
|
-
const now = Date.now();
|
|
2871
|
-
for (const health of needsAttention) {
|
|
2872
|
-
if (getAgentStateSync(health.agentId)?.role === "sequencer") continue;
|
|
2873
|
-
const cooledDown = now - (this.lastPokeTimestamps.get(health.agentId) ?? 0) >= pokeCooldownMs;
|
|
2874
|
-
if (health.state === "warning") {
|
|
2875
|
-
this.emit({
|
|
2876
|
-
type: "agent_warning",
|
|
2877
|
-
agentId: health.agentId,
|
|
2878
|
-
health
|
|
2879
|
-
});
|
|
2880
|
-
if (this.config.auto_actions.poke_on_warning && cooledDown) {
|
|
2881
|
-
this.pokeAgent(health.agentId);
|
|
2882
|
-
this.lastPokeTimestamps.set(health.agentId, now);
|
|
2883
|
-
}
|
|
2884
|
-
} else if (health.state === "stuck") {
|
|
2885
|
-
this.emit({
|
|
2886
|
-
type: "agent_stuck",
|
|
2887
|
-
agentId: health.agentId,
|
|
2888
|
-
health
|
|
2889
|
-
});
|
|
2890
|
-
if ((this.config.auto_actions.poke_on_stuck ?? true) && cooledDown) {
|
|
2891
|
-
this.pokeAgent(health.agentId);
|
|
2892
|
-
this.lastPokeTimestamps.set(health.agentId, now);
|
|
2893
|
-
}
|
|
2894
|
-
if (this.config.auto_actions.kill_on_stuck) this.killAgent(health.agentId);
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
this.checkHandoffTriggers(agentHealths);
|
|
2898
|
-
this.checkFPPViolations(agentIds);
|
|
2899
|
-
await reconcilePiCostEventsForRunningAgents(runningAgents);
|
|
2900
|
-
this.checkCostAlerts(agentIds);
|
|
2901
|
-
if (Math.random() < .1) this.checkSpecialistRotations();
|
|
2902
|
-
if (Math.random() < .01) clearOldViolationsSync(24);
|
|
2903
|
-
} catch (error) {
|
|
2904
|
-
console.error("Cloister health check failed:", error);
|
|
2905
|
-
this.emit({
|
|
2906
|
-
type: "error",
|
|
2907
|
-
error
|
|
2908
|
-
});
|
|
2909
|
-
}
|
|
3629
|
+
return performHealthCheck(this.healthHost());
|
|
2910
3630
|
}
|
|
2911
3631
|
/** Fallback scan for completion markers when `pan done` did not reach the dashboard. */
|
|
2912
3632
|
async checkCompletionMarkers() {
|
|
2913
|
-
|
|
2914
|
-
if (!existsSync(AGENTS_DIR)) return;
|
|
2915
|
-
const agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-"));
|
|
2916
|
-
for (const dir of agentDirs) {
|
|
2917
|
-
const completedFile = join$1(AGENTS_DIR, dir.name, "completed");
|
|
2918
|
-
const processedFile = join$1(AGENTS_DIR, dir.name, "completed.processed");
|
|
2919
|
-
if (!existsSync(completedFile)) continue;
|
|
2920
|
-
if (existsSync(processedFile)) try {
|
|
2921
|
-
if (statSync(completedFile).mtimeMs > statSync(processedFile).mtimeMs) {
|
|
2922
|
-
try {
|
|
2923
|
-
unlinkSync(processedFile);
|
|
2924
|
-
} catch {}
|
|
2925
|
-
this.processedCompletions.delete(dir.name);
|
|
2926
|
-
console.log(`🔔 Cloister: Detected re-completion for ${dir.name} (completed newer than .processed) — clearing stale marker`);
|
|
2927
|
-
} else continue;
|
|
2928
|
-
} catch {
|
|
2929
|
-
continue;
|
|
2930
|
-
}
|
|
2931
|
-
try {
|
|
2932
|
-
const content = JSON.parse(readFileSync(completedFile, "utf-8"));
|
|
2933
|
-
const ageMs = Date.now() - new Date(content.timestamp).getTime();
|
|
2934
|
-
if (ageMs > 1440 * 60 * 1e3) {
|
|
2935
|
-
console.log(`🔔 Cloister: Skipping stale completion marker for ${dir.name} (${Math.floor(ageMs / 36e5)}h old)`);
|
|
2936
|
-
this.processedCompletions.set(dir.name, Infinity);
|
|
2937
|
-
try {
|
|
2938
|
-
renameSync(completedFile, processedFile);
|
|
2939
|
-
} catch {}
|
|
2940
|
-
continue;
|
|
2941
|
-
}
|
|
2942
|
-
} catch (parseErr) {
|
|
2943
|
-
console.warn(` ⚠ Cloister: Could not parse completion marker for ${dir.name}, skipping`);
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
const retryCount = this.processedCompletions.get(dir.name) || 0;
|
|
2947
|
-
if (retryCount === Infinity) this.processedCompletions.delete(dir.name);
|
|
2948
|
-
else if (retryCount >= 3) continue;
|
|
2949
|
-
const issueId = dir.name.replace("agent-", "").toUpperCase();
|
|
2950
|
-
const { getReviewStatusSync } = await import("./review-status-B0Axo-Ma.js");
|
|
2951
|
-
const existingReview = getReviewStatusSync(issueId);
|
|
2952
|
-
if (existingReview && ["reviewing", "passed"].includes(existingReview.reviewStatus || "")) {
|
|
2953
|
-
console.log(`🔔 Cloister: Completion marker for ${issueId} — review already ${existingReview.reviewStatus}, marking processed`);
|
|
2954
|
-
try {
|
|
2955
|
-
renameSync(completedFile, processedFile);
|
|
2956
|
-
} catch {}
|
|
2957
|
-
this.processedCompletions.set(dir.name, Infinity);
|
|
2958
|
-
continue;
|
|
2959
|
-
}
|
|
2960
|
-
console.log(`🔔 Cloister: Found completion marker for ${issueId}, triggering review...${retryCount > 0 ? ` (retry ${retryCount}/3)` : ""}`);
|
|
2961
|
-
try {
|
|
2962
|
-
const result = await (async () => {
|
|
2963
|
-
const controller = new AbortController();
|
|
2964
|
-
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
2965
|
-
try {
|
|
2966
|
-
const res = await fetch(`${this.getDashboardApiUrl()}/api/review/${issueId}/trigger`, {
|
|
2967
|
-
method: "POST",
|
|
2968
|
-
headers: { "Content-Type": "application/json" },
|
|
2969
|
-
body: JSON.stringify({}),
|
|
2970
|
-
signal: controller.signal
|
|
2971
|
-
});
|
|
2972
|
-
clearTimeout(timer);
|
|
2973
|
-
try {
|
|
2974
|
-
return await res.json();
|
|
2975
|
-
} catch {
|
|
2976
|
-
return {
|
|
2977
|
-
success: false,
|
|
2978
|
-
error: `Invalid response (HTTP ${res.status})`
|
|
2979
|
-
};
|
|
2980
|
-
}
|
|
2981
|
-
} catch (e) {
|
|
2982
|
-
clearTimeout(timer);
|
|
2983
|
-
if (e?.name === "AbortError") return {
|
|
2984
|
-
success: false,
|
|
2985
|
-
error: "Timeout (5s)"
|
|
2986
|
-
};
|
|
2987
|
-
return {
|
|
2988
|
-
success: false,
|
|
2989
|
-
error: e?.message || String(e)
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
})();
|
|
2993
|
-
if (result.success) {
|
|
2994
|
-
console.log(` ✓ Review triggered for ${issueId}`);
|
|
2995
|
-
renameSync(completedFile, processedFile);
|
|
2996
|
-
this.processedCompletions.set(dir.name, Infinity);
|
|
2997
|
-
} else if (result.alreadyReviewed || result.alreadyMerged) {
|
|
2998
|
-
console.log(` ✓ ${issueId} already ${result.alreadyMerged ? "merged" : "reviewed"}, marking processed`);
|
|
2999
|
-
renameSync(completedFile, processedFile);
|
|
3000
|
-
this.processedCompletions.set(dir.name, Infinity);
|
|
3001
|
-
} else {
|
|
3002
|
-
this.processedCompletions.set(dir.name, retryCount + 1);
|
|
3003
|
-
console.log(` ⚠ Review trigger failed for ${issueId}: ${result.error || "unknown"} (will retry, ${2 - retryCount} attempts left)`);
|
|
3004
|
-
}
|
|
3005
|
-
} catch (err) {
|
|
3006
|
-
this.processedCompletions.set(dir.name, retryCount + 1);
|
|
3007
|
-
console.error(` ✗ Failed to trigger review for ${issueId}: ${err.message} (will retry, ${2 - retryCount} attempts left)`);
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
} catch (error) {}
|
|
3633
|
+
return checkCompletionMarkers(this.completionHost());
|
|
3011
3634
|
}
|
|
3012
3635
|
/**
|
|
3013
3636
|
* Poke an agent (send "are you stuck?" message).
|
|
@@ -3021,75 +3644,15 @@ var CloisterService = class {
|
|
|
3021
3644
|
* dead agent (PAN-1189 wedge sweep #12-13).
|
|
3022
3645
|
*/
|
|
3023
3646
|
pokeAgent(agentId) {
|
|
3024
|
-
this.
|
|
3025
|
-
console.error(`Failed to poke ${agentId}:`, error);
|
|
3026
|
-
});
|
|
3647
|
+
return pokeAgent(this.crashHost(), agentId);
|
|
3027
3648
|
}
|
|
3028
3649
|
/** PAN-2452: fingerprint of observable progress — workspace HEAD + pane tail.
|
|
3029
3650
|
* Unchanged fingerprint across pokes = the poke did nothing. */
|
|
3030
3651
|
async progressFingerprint(agentId) {
|
|
3031
|
-
|
|
3032
|
-
let head = "";
|
|
3033
|
-
if (state?.workspace) try {
|
|
3034
|
-
const { stdout } = await execAsync("git rev-parse HEAD", {
|
|
3035
|
-
cwd: state.workspace,
|
|
3036
|
-
encoding: "utf-8"
|
|
3037
|
-
});
|
|
3038
|
-
head = stdout.trim();
|
|
3039
|
-
} catch {}
|
|
3040
|
-
let pane = "";
|
|
3041
|
-
try {
|
|
3042
|
-
const { stdout } = await execAsync(`tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -40`, { encoding: "utf-8" });
|
|
3043
|
-
pane = stdout;
|
|
3044
|
-
} catch {}
|
|
3045
|
-
const paneHash = createHash("sha1").update(pane).digest("hex").slice(0, 12);
|
|
3046
|
-
return `${head}:${paneHash}`;
|
|
3652
|
+
return progressFingerprint(this.crashHost(), agentId);
|
|
3047
3653
|
}
|
|
3048
3654
|
async pokeAgentWithEscalation(agentId) {
|
|
3049
|
-
|
|
3050
|
-
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
3051
|
-
const fingerprint = await this.progressFingerprint(agentId);
|
|
3052
|
-
const prior = this.pokeProgress.get(agentId);
|
|
3053
|
-
const ineffective = prior && prior.fingerprint === fingerprint ? prior.ineffective + 1 : 0;
|
|
3054
|
-
this.pokeProgress.set(agentId, {
|
|
3055
|
-
fingerprint,
|
|
3056
|
-
ineffective
|
|
3057
|
-
});
|
|
3058
|
-
if (ineffective >= 4) {
|
|
3059
|
-
const { setAgentPausedSync } = await import("./agent-state-C6Rm6FPR.js");
|
|
3060
|
-
try {
|
|
3061
|
-
setAgentPausedSync(agentId, `needs-you: idle-alive — no observable progress across ${ineffective + 1} pokes (idle-alive escalation)`);
|
|
3062
|
-
this.emit({
|
|
3063
|
-
type: "agent_stuck",
|
|
3064
|
-
agentId,
|
|
3065
|
-
health: void 0
|
|
3066
|
-
});
|
|
3067
|
-
console.log(`🛑 ${agentId} paused: idle-alive across ${ineffective + 1} pokes`);
|
|
3068
|
-
} catch (pauseErr) {
|
|
3069
|
-
console.error(`Failed to pause idle-alive ${agentId}:`, pauseErr);
|
|
3070
|
-
}
|
|
3071
|
-
return;
|
|
3072
|
-
}
|
|
3073
|
-
let pokeMessage = "Hey, I noticed you haven't made progress in a while. Are you stuck? If you need help or clarification, please ask. Otherwise, please continue with your work.";
|
|
3074
|
-
if (ineffective >= 2) {
|
|
3075
|
-
let tail = "";
|
|
3076
|
-
try {
|
|
3077
|
-
const { stdout } = await execAsync(`tmux -L overdeck capture-pane -t ${JSON.stringify(agentId)} -p -S -40`, { encoding: "utf-8" });
|
|
3078
|
-
tail = stdout;
|
|
3079
|
-
} catch {}
|
|
3080
|
-
if (isContextOverflowTail(tail)) {
|
|
3081
|
-
pokeMessage = "/compact";
|
|
3082
|
-
console.log(`🔔 ${agentId}: overflow tail detected — delivering /compact instead of a poke`);
|
|
3083
|
-
} else pokeMessage = "You appear active but have made no observable progress (no new commits, unchanged output) across multiple checks. Do exactly one of: (1) work is done — commit, push, and run pan done; (2) blocked — state the blocker in one sentence and commit what you have as WIP; (3) lost context — re-read your bead (bd show), your latest .pan/feedback/ file, and git status, then resume. Act now.";
|
|
3084
|
-
}
|
|
3085
|
-
await Promise.resolve(runtime.sendMessage(agentId, pokeMessage)).catch((sendErr) => {
|
|
3086
|
-
console.error(`Failed to send poke to ${agentId}:`, sendErr);
|
|
3087
|
-
});
|
|
3088
|
-
this.emit({
|
|
3089
|
-
type: "poked_agent",
|
|
3090
|
-
agentId
|
|
3091
|
-
});
|
|
3092
|
-
console.log(`🔔 Poked ${agentId}${ineffective > 0 ? ` (ineffective streak: ${ineffective})` : ""}`);
|
|
3655
|
+
return pokeAgentWithEscalation(this.crashHost(), agentId);
|
|
3093
3656
|
}
|
|
3094
3657
|
/**
|
|
3095
3658
|
* Kill an agent
|
|
@@ -3099,136 +3662,19 @@ var CloisterService = class {
|
|
|
3099
3662
|
* crash the dashboard from a deacon health-check timer callback.
|
|
3100
3663
|
*/
|
|
3101
3664
|
killAgent(agentId) {
|
|
3102
|
-
|
|
3103
|
-
const runtime = getRuntimeForAgent(agentId);
|
|
3104
|
-
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
3105
|
-
Promise.resolve(runtime.killAgent(agentId)).catch((killErr) => {
|
|
3106
|
-
console.error(`Failed to kill ${agentId}:`, killErr);
|
|
3107
|
-
});
|
|
3108
|
-
this.emit({
|
|
3109
|
-
type: "killed_agent",
|
|
3110
|
-
agentId
|
|
3111
|
-
});
|
|
3112
|
-
console.log(`🔔 Killed ${agentId}`);
|
|
3113
|
-
} catch (error) {
|
|
3114
|
-
console.error(`Failed to kill ${agentId}:`, error);
|
|
3115
|
-
}
|
|
3665
|
+
return killAgent(this.crashHost(), agentId);
|
|
3116
3666
|
}
|
|
3117
3667
|
/**
|
|
3118
3668
|
* Handle agent crash with auto-restart logic
|
|
3119
3669
|
*/
|
|
3120
3670
|
async handleAgentCrash(agentId) {
|
|
3121
|
-
|
|
3122
|
-
if (!config?.enabled) return;
|
|
3123
|
-
const agentState = getAgentStateSync(agentId);
|
|
3124
|
-
if (!agentState || agentState.status === "stopped") {
|
|
3125
|
-
console.log(`🔔 Agent ${agentId} was intentionally stopped, skipping restart`);
|
|
3126
|
-
return;
|
|
3127
|
-
}
|
|
3128
|
-
const runtimeState = getAgentRuntimeStateSync(agentId);
|
|
3129
|
-
if (runtimeState?.state === "suspended") {
|
|
3130
|
-
console.log(`🔔 Agent ${agentId} is suspended, skipping restart`);
|
|
3131
|
-
return;
|
|
3132
|
-
}
|
|
3133
|
-
if (runtimeState?.state === "stopped") {
|
|
3134
|
-
console.log(`🔔 Agent ${agentId} runtime is stopped, skipping restart`);
|
|
3135
|
-
return;
|
|
3136
|
-
}
|
|
3137
|
-
const skipReason = nonRestartableReason(agentState.role, agentState.sessionId);
|
|
3138
|
-
if (skipReason) {
|
|
3139
|
-
let recoverUnfinishedOneShot = false;
|
|
3140
|
-
if (ONE_SHOT_ROLES.has(agentState.role) && agentState.sessionId && agentState.issueId) try {
|
|
3141
|
-
const { getReviewStatusSync } = await import("./review-status-B0Axo-Ma.js");
|
|
3142
|
-
const status = getReviewStatusSync(agentState.issueId);
|
|
3143
|
-
recoverUnfinishedOneShot = !(status ? isRoleTerminal(agentState.role, {
|
|
3144
|
-
reviewStatus: status.reviewStatus,
|
|
3145
|
-
testStatus: status.testStatus,
|
|
3146
|
-
readyForMerge: status.readyForMerge,
|
|
3147
|
-
mergeStatus: status.mergeStatus
|
|
3148
|
-
}) : false);
|
|
3149
|
-
} catch {
|
|
3150
|
-
recoverUnfinishedOneShot = false;
|
|
3151
|
-
}
|
|
3152
|
-
if (!recoverUnfinishedOneShot) {
|
|
3153
|
-
console.log(`🔔 Agent ${agentId} ${skipReason}; skipping restart`);
|
|
3154
|
-
return;
|
|
3155
|
-
}
|
|
3156
|
-
console.log(`🔔 Agent ${agentId} (${agentState.role}) vanished before a terminal verdict — recovering instead of treating as a one-shot completion (PAN-2007)`);
|
|
3157
|
-
}
|
|
3158
|
-
const now = /* @__PURE__ */ new Date();
|
|
3159
|
-
this.deathTimestamps.push(now);
|
|
3160
|
-
this.checkForMassDeaths();
|
|
3161
|
-
let tracker = this.crashTrackers.get(agentId);
|
|
3162
|
-
if (!tracker) {
|
|
3163
|
-
tracker = {
|
|
3164
|
-
agentId,
|
|
3165
|
-
crashCount: 0,
|
|
3166
|
-
lastCrash: now,
|
|
3167
|
-
gaveUp: false
|
|
3168
|
-
};
|
|
3169
|
-
this.crashTrackers.set(agentId, tracker);
|
|
3170
|
-
}
|
|
3171
|
-
if (tracker.gaveUp) return;
|
|
3172
|
-
tracker.crashCount++;
|
|
3173
|
-
tracker.lastCrash = now;
|
|
3174
|
-
this.emit({
|
|
3175
|
-
type: "agent_crashed",
|
|
3176
|
-
agentId,
|
|
3177
|
-
crashCount: tracker.crashCount
|
|
3178
|
-
});
|
|
3179
|
-
console.log(`🔔 Agent ${agentId} crashed (crash #${tracker.crashCount})`);
|
|
3180
|
-
if (this.eventStore) try {
|
|
3181
|
-
this.eventStore.append({
|
|
3182
|
-
type: "agent.heartbeat_dead",
|
|
3183
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3184
|
-
payload: {
|
|
3185
|
-
agentId,
|
|
3186
|
-
issueId: agentState.issueId,
|
|
3187
|
-
sessionId: agentState.sessionId
|
|
3188
|
-
}
|
|
3189
|
-
});
|
|
3190
|
-
} catch (err) {
|
|
3191
|
-
console.error(`[cloister] Failed to emit heartbeat_dead for ${agentId}:`, err);
|
|
3192
|
-
}
|
|
3193
|
-
if (tracker.crashCount > config.max_retries) {
|
|
3194
|
-
tracker.gaveUp = true;
|
|
3195
|
-
this.emit({
|
|
3196
|
-
type: "agent_gave_up",
|
|
3197
|
-
agentId,
|
|
3198
|
-
maxRetries: config.max_retries
|
|
3199
|
-
});
|
|
3200
|
-
console.error(`🔔 Gave up on restarting ${agentId} after ${config.max_retries} attempts`);
|
|
3201
|
-
return;
|
|
3202
|
-
}
|
|
3203
|
-
const backoffIndex = Math.min(tracker.crashCount - 1, config.backoff_seconds.length - 1);
|
|
3204
|
-
const backoffSeconds = config.backoff_seconds[backoffIndex];
|
|
3205
|
-
const nextRetryAt = new Date(Date.now() + backoffSeconds * 1e3);
|
|
3206
|
-
tracker.nextRetryAt = nextRetryAt;
|
|
3207
|
-
this.emit({
|
|
3208
|
-
type: "agent_restarting",
|
|
3209
|
-
agentId,
|
|
3210
|
-
crashCount: tracker.crashCount,
|
|
3211
|
-
backoffSeconds
|
|
3212
|
-
});
|
|
3213
|
-
console.log(`🔔 Deacon will recover ${agentId} via heartbeat_dead (attempt ${tracker.crashCount}/${config.max_retries}, backoff ${backoffSeconds}s)`);
|
|
3671
|
+
return handleAgentCrash(this.crashHost(), agentId);
|
|
3214
3672
|
}
|
|
3215
3673
|
/**
|
|
3216
3674
|
* Restart an agent using its saved session
|
|
3217
3675
|
*/
|
|
3218
3676
|
async restartAgent(agentId) {
|
|
3219
|
-
|
|
3220
|
-
if (!runtime) throw new Error(`No runtime found for agent ${agentId}`);
|
|
3221
|
-
const agentState = getAgentStateSync(agentId);
|
|
3222
|
-
if (!agentState?.sessionId) throw new Error(`No session ID found for agent ${agentId}`);
|
|
3223
|
-
if (!agentState.workspace) throw new Error(`No workspace found for agent ${agentId}`);
|
|
3224
|
-
console.log(`🔔 Restarting ${agentId} with session ${agentState.sessionId.substring(0, 8)}...`);
|
|
3225
|
-
runtime.spawnAgent({
|
|
3226
|
-
agentId,
|
|
3227
|
-
workspace: agentState.workspace,
|
|
3228
|
-
sessionId: agentState.sessionId,
|
|
3229
|
-
runtime: runtime.name
|
|
3230
|
-
});
|
|
3231
|
-
console.log(`🔔 Successfully restarted ${agentId}`);
|
|
3677
|
+
return restartAgent(this.crashHost(), agentId);
|
|
3232
3678
|
}
|
|
3233
3679
|
/**
|
|
3234
3680
|
* Check for mass death events
|
|
@@ -3236,33 +3682,13 @@ var CloisterService = class {
|
|
|
3236
3682
|
* Detects when 3+ agents die within 30 seconds and pauses spawns.
|
|
3237
3683
|
*/
|
|
3238
3684
|
checkForMassDeaths() {
|
|
3239
|
-
|
|
3240
|
-
const WINDOW_SECONDS = 30;
|
|
3241
|
-
const windowStart = Date.now() - WINDOW_SECONDS * 1e3;
|
|
3242
|
-
this.deathTimestamps = this.deathTimestamps.filter((timestamp) => timestamp.getTime() >= windowStart);
|
|
3243
|
-
if (this.deathTimestamps.length >= MASS_DEATH_THRESHOLD) {
|
|
3244
|
-
this.emit({
|
|
3245
|
-
type: "mass_death_detected",
|
|
3246
|
-
deathCount: this.deathTimestamps.length,
|
|
3247
|
-
windowSeconds: WINDOW_SECONDS
|
|
3248
|
-
});
|
|
3249
|
-
if (!this.spawnsPaused) {
|
|
3250
|
-
this.pauseSpawns("Mass death detected - system stability concern");
|
|
3251
|
-
console.error(`🔔 MASS DEATH DETECTED: ${this.deathTimestamps.length} agents died in ${WINDOW_SECONDS}s - spawns paused`);
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3685
|
+
return checkForMassDeaths(this.crashHost());
|
|
3254
3686
|
}
|
|
3255
3687
|
/**
|
|
3256
3688
|
* Pause new agent spawns
|
|
3257
3689
|
*/
|
|
3258
3690
|
pauseSpawns(reason) {
|
|
3259
|
-
this.
|
|
3260
|
-
setCloisterSpawnsPausedSync(true);
|
|
3261
|
-
this.emit({
|
|
3262
|
-
type: "spawn_paused",
|
|
3263
|
-
reason
|
|
3264
|
-
});
|
|
3265
|
-
console.log(`🔔 Agent spawns paused: ${reason}`);
|
|
3691
|
+
return pauseSpawns(this.crashHost(), reason);
|
|
3266
3692
|
}
|
|
3267
3693
|
/**
|
|
3268
3694
|
* Resume agent spawns
|
|
@@ -3286,48 +3712,13 @@ var CloisterService = class {
|
|
|
3286
3712
|
* Check for FPP violations and send nudges
|
|
3287
3713
|
*/
|
|
3288
3714
|
checkFPPViolations(agentIds) {
|
|
3289
|
-
|
|
3290
|
-
const violation = checkAgentForViolations(agentId);
|
|
3291
|
-
if (!violation) continue;
|
|
3292
|
-
if (violation.nudgeCount === 0) this.emit({
|
|
3293
|
-
type: "fpp_violation_detected",
|
|
3294
|
-
agentId,
|
|
3295
|
-
violation
|
|
3296
|
-
});
|
|
3297
|
-
if ((violation.lastNudgeAt ? Date.now() - new Date(violation.lastNudgeAt).getTime() : Infinity) >= 300 * 1e3 || violation.nudgeCount === 0) {
|
|
3298
|
-
if (hasExceededMaxNudges(violation)) {
|
|
3299
|
-
this.emit({
|
|
3300
|
-
type: "fpp_max_nudges_exceeded",
|
|
3301
|
-
agentId,
|
|
3302
|
-
violation
|
|
3303
|
-
});
|
|
3304
|
-
console.error(`🔔 Agent ${agentId} exceeded max nudges for ${violation.type} - manual intervention required`);
|
|
3305
|
-
} else if (sendNudge(violation)) this.emit({
|
|
3306
|
-
type: "fpp_nudge_sent",
|
|
3307
|
-
agentId,
|
|
3308
|
-
nudgeCount: violation.nudgeCount
|
|
3309
|
-
});
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3715
|
+
return checkFPPViolations(this.healthHost(), agentIds);
|
|
3312
3716
|
}
|
|
3313
3717
|
/**
|
|
3314
3718
|
* Check for cost limit alerts
|
|
3315
3719
|
*/
|
|
3316
3720
|
checkCostAlerts(agentIds) {
|
|
3317
|
-
|
|
3318
|
-
if (!config) return;
|
|
3319
|
-
for (const agentId of agentIds) {
|
|
3320
|
-
const alerts = checkCostLimits(agentId, agentId.startsWith("agent-") ? agentId.replace(/^agent-/, "") : agentId, config);
|
|
3321
|
-
for (const alert of alerts) {
|
|
3322
|
-
this.emit({
|
|
3323
|
-
type: "cost_alert",
|
|
3324
|
-
alert
|
|
3325
|
-
});
|
|
3326
|
-
const entityLabel = alert.agentId || alert.issueId || "(unattributed)";
|
|
3327
|
-
if (alert.level === "limit_reached") console.error(`🔔 COST LIMIT REACHED: ${alert.type} for ${entityLabel} - $${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)}`);
|
|
3328
|
-
else console.warn(`🔔 Cost warning: ${alert.type} for ${entityLabel} at ${alert.percentUsed.toFixed(0)}% ($${alert.currentCost.toFixed(2)} / $${alert.limit.toFixed(2)})`);
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3721
|
+
return checkCostAlerts(this.healthHost(), agentIds);
|
|
3331
3722
|
}
|
|
3332
3723
|
/**
|
|
3333
3724
|
* Get cost summary
|
|
@@ -3339,16 +3730,7 @@ var CloisterService = class {
|
|
|
3339
3730
|
* Check if any specialists need session rotation
|
|
3340
3731
|
*/
|
|
3341
3732
|
async checkSpecialistRotations() {
|
|
3342
|
-
|
|
3343
|
-
if (mergeAgentResult) {
|
|
3344
|
-
this.emit({
|
|
3345
|
-
type: "session_rotated",
|
|
3346
|
-
specialistName: "merge-agent",
|
|
3347
|
-
result: mergeAgentResult
|
|
3348
|
-
});
|
|
3349
|
-
if (mergeAgentResult.success) console.log(`🔔 Rotated merge-agent session: ${mergeAgentResult.oldSessionId.substring(0, 8)} → ${mergeAgentResult.newSessionId?.substring(0, 8)}`);
|
|
3350
|
-
else console.error(`🔔 Failed to rotate merge-agent: ${mergeAgentResult.error}`);
|
|
3351
|
-
}
|
|
3733
|
+
return checkSpecialistRotations(this.healthHost());
|
|
3352
3734
|
}
|
|
3353
3735
|
/**
|
|
3354
3736
|
* Record health event to database
|
|
@@ -3356,28 +3738,7 @@ var CloisterService = class {
|
|
|
3356
3738
|
* Only writes events when state changes or on first check.
|
|
3357
3739
|
*/
|
|
3358
3740
|
recordHealthEvent(health) {
|
|
3359
|
-
|
|
3360
|
-
const currentState = health.state;
|
|
3361
|
-
const previousState = this.previousStates.get(health.agentId);
|
|
3362
|
-
if (previousState === void 0 || previousState !== currentState) {
|
|
3363
|
-
const source = health.heartbeat?.source ? this.mapHeartbeatSource(health.heartbeat.source) : "unknown";
|
|
3364
|
-
writeHealthEvent({
|
|
3365
|
-
agentId: health.agentId,
|
|
3366
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3367
|
-
state: currentState,
|
|
3368
|
-
source,
|
|
3369
|
-
metadata: health.heartbeat ? JSON.stringify({
|
|
3370
|
-
confidence: health.heartbeat.confidence,
|
|
3371
|
-
lastAction: health.heartbeat.lastAction,
|
|
3372
|
-
toolName: health.heartbeat.toolName,
|
|
3373
|
-
timeSinceActivity: health.timeSinceActivity
|
|
3374
|
-
}) : void 0
|
|
3375
|
-
});
|
|
3376
|
-
this.previousStates.set(health.agentId, currentState);
|
|
3377
|
-
}
|
|
3378
|
-
} catch (error) {
|
|
3379
|
-
console.error(`Failed to record health event for ${health.agentId}:`, error);
|
|
3380
|
-
}
|
|
3741
|
+
return recordHealthEvent(this.healthHost(), health);
|
|
3381
3742
|
}
|
|
3382
3743
|
/**
|
|
3383
3744
|
* Check for handoff triggers and execute handoffs (Phase 4)
|
|
@@ -3385,57 +3746,13 @@ var CloisterService = class {
|
|
|
3385
3746
|
* Checks all triggers for each agent and performs handoffs when triggered.
|
|
3386
3747
|
*/
|
|
3387
3748
|
async checkHandoffTriggers(agentHealths) {
|
|
3388
|
-
|
|
3389
|
-
const agentState = getAgentStateSync(health.agentId);
|
|
3390
|
-
if (!agentState) continue;
|
|
3391
|
-
if (!agentState.workspace) continue;
|
|
3392
|
-
const triggers = await Effect.runPromise(checkAllTriggers(health.agentId, agentState.workspace, agentState.issueId, agentState.model, health, this.config));
|
|
3393
|
-
if (triggers.length > 0) {
|
|
3394
|
-
const trigger = triggers[0];
|
|
3395
|
-
if (trigger.type === "task_complete" && [
|
|
3396
|
-
"review-agent",
|
|
3397
|
-
"test-agent",
|
|
3398
|
-
"merge-agent",
|
|
3399
|
-
"inspect-agent",
|
|
3400
|
-
"uat-agent"
|
|
3401
|
-
].includes(trigger.suggestedModel || "")) {
|
|
3402
|
-
console.log(`[cloister] Skipping handoff for ${health.agentId}: task_complete triggers specialist dispatch via completion marker, not model swap`);
|
|
3403
|
-
continue;
|
|
3404
|
-
}
|
|
3405
|
-
this.emit({
|
|
3406
|
-
type: "handoff_triggered",
|
|
3407
|
-
agentId: health.agentId,
|
|
3408
|
-
trigger
|
|
3409
|
-
});
|
|
3410
|
-
console.log(`🔔 Handoff triggered for ${health.agentId}: ${trigger.reason}`);
|
|
3411
|
-
const result = await Effect.runPromise(performHandoff(health.agentId, {
|
|
3412
|
-
targetModel: trigger.suggestedModel || "sonnet",
|
|
3413
|
-
reason: trigger.reason
|
|
3414
|
-
}));
|
|
3415
|
-
this.emit({
|
|
3416
|
-
type: "handoff_completed",
|
|
3417
|
-
agentId: health.agentId,
|
|
3418
|
-
result
|
|
3419
|
-
});
|
|
3420
|
-
if (result.context) logHandoffEventSync(createHandoffEvent(health.agentId, agentState.issueId, result.context, trigger.type, result.success, result.error));
|
|
3421
|
-
if (result.success) console.log(`✓ Handoff completed: ${health.agentId} → ${result.newAgentId} (${trigger.suggestedModel})`);
|
|
3422
|
-
else console.error(`✗ Handoff failed: ${result.error}`);
|
|
3423
|
-
}
|
|
3424
|
-
} catch (error) {
|
|
3425
|
-
console.error(`Failed to check handoff triggers for ${health.agentId}:`, error);
|
|
3426
|
-
}
|
|
3749
|
+
return checkHandoffTriggers(this.healthHost(), agentHealths);
|
|
3427
3750
|
}
|
|
3428
3751
|
/**
|
|
3429
3752
|
* Map heartbeat source to database source string
|
|
3430
3753
|
*/
|
|
3431
3754
|
mapHeartbeatSource(source) {
|
|
3432
|
-
|
|
3433
|
-
case "jsonl": return "jsonl_mtime";
|
|
3434
|
-
case "tmux": return "tmux_activity";
|
|
3435
|
-
case "git": return "git_activity";
|
|
3436
|
-
case "active-heartbeat": return "active_heartbeat";
|
|
3437
|
-
default: return source;
|
|
3438
|
-
}
|
|
3755
|
+
return mapHeartbeatSource(this.healthHost(), source);
|
|
3439
3756
|
}
|
|
3440
3757
|
/**
|
|
3441
3758
|
* Get current status
|
|
@@ -3445,63 +3762,19 @@ var CloisterService = class {
|
|
|
3445
3762
|
* calls for every agent, which scales poorly with agent count.
|
|
3446
3763
|
*/
|
|
3447
3764
|
getStatus() {
|
|
3448
|
-
|
|
3449
|
-
if (this._statusCache && now - this._statusCacheAt < this.STATUS_CACHE_TTL_MS) return this._statusCache;
|
|
3450
|
-
const agentIds = listRunningAgentsSync().filter((a) => a.tmuxActive).map((a) => a.id);
|
|
3451
|
-
const agentHealths = [];
|
|
3452
|
-
for (const agentId of agentIds) {
|
|
3453
|
-
const runtime = getRuntimeForAgent(agentId);
|
|
3454
|
-
if (runtime) {
|
|
3455
|
-
const health = getAgentHealth(agentId, runtime);
|
|
3456
|
-
agentHealths.push(health);
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
const summary = generateHealthSummary(agentHealths);
|
|
3460
|
-
const needsAttention = getAgentsNeedingAttention(agentHealths).map((h) => h.agentId);
|
|
3461
|
-
const deaconStatus = getDeaconStatus();
|
|
3462
|
-
const patrol = assessDeaconPatrolFreshness({
|
|
3463
|
-
isRunning: deaconStatus.isRunning,
|
|
3464
|
-
lastPatrol: deaconStatus.state.lastPatrol,
|
|
3465
|
-
patrolIntervalMs: deaconStatus.config.patrolIntervalMs
|
|
3466
|
-
});
|
|
3467
|
-
const status = {
|
|
3468
|
-
running: this.isRunning(),
|
|
3469
|
-
lastCheck: this.lastCheck,
|
|
3470
|
-
config: this.config,
|
|
3471
|
-
summary,
|
|
3472
|
-
agentsNeedingAttention: needsAttention,
|
|
3473
|
-
patrol: {
|
|
3474
|
-
...patrol,
|
|
3475
|
-
loopRunning: deaconStatus.isRunning,
|
|
3476
|
-
patrolIntervalMs: deaconStatus.config.patrolIntervalMs
|
|
3477
|
-
}
|
|
3478
|
-
};
|
|
3479
|
-
this._statusCache = status;
|
|
3480
|
-
this._statusCacheAt = now;
|
|
3481
|
-
return status;
|
|
3765
|
+
return getStatus(this.statusHost());
|
|
3482
3766
|
}
|
|
3483
3767
|
/**
|
|
3484
3768
|
* Get health for a specific agent
|
|
3485
3769
|
*/
|
|
3486
3770
|
getAgentHealth(agentId) {
|
|
3487
|
-
|
|
3488
|
-
if (!runtime) return null;
|
|
3489
|
-
return getAgentHealth(agentId, runtime);
|
|
3771
|
+
return getServiceAgentHealth(this.statusHost(), agentId);
|
|
3490
3772
|
}
|
|
3491
3773
|
/**
|
|
3492
3774
|
* Get health for all running agents
|
|
3493
3775
|
*/
|
|
3494
3776
|
getAllAgentHealth() {
|
|
3495
|
-
|
|
3496
|
-
const agentHealths = [];
|
|
3497
|
-
for (const agent of runningAgents) {
|
|
3498
|
-
const runtime = getRuntimeForAgent(agent.id);
|
|
3499
|
-
if (runtime) {
|
|
3500
|
-
const health = getAgentHealth(agent.id, runtime);
|
|
3501
|
-
agentHealths.push(health);
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
return agentHealths;
|
|
3777
|
+
return getAllAgentHealth(this.statusHost());
|
|
3505
3778
|
}
|
|
3506
3779
|
/**
|
|
3507
3780
|
* Get deacon (specialist health monitor) status
|
|
@@ -3599,20 +3872,7 @@ function getCloisterService() {
|
|
|
3599
3872
|
if (!globalService) globalService = new CloisterService();
|
|
3600
3873
|
return globalService;
|
|
3601
3874
|
}
|
|
3602
|
-
/**
|
|
3603
|
-
* Effect-typed variant of {@link onIssueStateChange}. Never fails — failures
|
|
3604
|
-
* surface through `emitActivityEntry` inside the legacy implementation.
|
|
3605
|
-
*/
|
|
3606
|
-
function onIssueStateChange(issueId, newState) {
|
|
3607
|
-
return Effect.promise(() => onIssueStateChangePromise(issueId, newState));
|
|
3608
|
-
}
|
|
3609
|
-
/**
|
|
3610
|
-
* Effect-typed variant of {@link handleCloisterDomainEvent}. Never fails.
|
|
3611
|
-
*/
|
|
3612
|
-
function handleCloisterDomainEvent(event) {
|
|
3613
|
-
return Effect.promise(() => handleCloisterDomainEventPromise(event));
|
|
3614
|
-
}
|
|
3615
3875
|
//#endregion
|
|
3616
3876
|
export { performHandoff as a, getAgentsNeedingAttention as c, CostResolverLive as d, CostWriter as f, checkAllTriggers as i, CostDoorLive as l, readCloisterStateFile as n, generateHealthSummary as o, setCloisterEventStoreProvider as r, getAgentHealth as s, getCloisterService as t, CostResolver as u };
|
|
3617
3877
|
|
|
3618
|
-
//# sourceMappingURL=service-
|
|
3878
|
+
//# sourceMappingURL=service-DfTVoqJg.js.map
|