@overdeck/desktop 0.45.12 → 0.45.15
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/drizzle/overdeck/0000_overdeck_init.sql +33 -0
- package/package.json +7 -2
- package/server/{acceptance-criteria-1LHgD9vg.js → acceptance-criteria-DHZk2ceV.js} +2 -2
- package/server/{advancing-selfheal-BqsLWqEM.js → advancing-selfheal-CSuZ31bB.js} +5 -5
- package/server/{agent-gc-CUb52n7s.js → agent-gc-D_DxfBHR.js} +3 -3
- package/server/{agent-state-D-moNwhA.js → agent-state-EP2QFV9n.js} +1 -1
- package/server/{agent-state-service-BzESvLOf.js → agent-state-service-BZkvgQf4.js} +5 -5
- package/server/{agent-state-service-BaZH7sXJ.js → agent-state-service-C_2ZAQRP.js} +1 -1
- package/server/{agents-B5LfpLPv.js → agents-CsOzAmRg.js} +145 -78
- package/server/{agents-C4kZ8a2r.js → agents-DCvyTXNU.js} +1 -1
- package/server/{archive-planning-BbZe-8IK.js → archive-planning-CTILQRnV.js} +1 -1
- package/server/{archive-planning-oX1pj6nY.js → archive-planning-DkivCk1A.js} +4 -4
- package/server/{artifact-index-jobs-DGr6bxev.js → artifact-index-jobs-CmsvQ1ey.js} +2 -2
- package/server/auto-commit-QAfsu3Sb.js +3 -0
- package/server/{backlog-auto-trigger-BlJHikOm.js → backlog-auto-trigger-ueRaT8Dy.js} +2 -2
- package/server/{bd-process-lock-i3CYMS-B.js → bd-process-lock-D3S4T4o-.js} +32 -11
- package/server/{beads-DD7fYCl5.js → beads-BGGxrkZ5.js} +96 -196
- package/server/{beads-BNU-fb48.js → beads-CYsLiyIM.js} +1 -1
- package/server/{beads-query-e6oku4P5.js → beads-query-B1DlJVp9.js} +24 -99
- package/server/beads-restore-CyygSmmn.js +23 -0
- package/server/{behavior-CPf0pbOt.js → behavior-Cpj03N4v.js} +3 -3
- package/server/{briefing-freshness-B2FdLwIv.js → briefing-freshness-Kh5V8RTh.js} +4 -4
- package/server/{checkpoint-manager-eFgaMMu4.js → checkpoint-manager-CAFos8e1.js} +1 -1
- package/server/{checkpoint-manager-JryW6MpM.js → checkpoint-manager-fu9oVsR-.js} +2 -2
- package/server/checkpoint-worker.js +1 -1
- package/server/{checkpoints-qvOqvMuC.js → checkpoints-1BqOlS4q.js} +2 -2
- package/server/{checkpoints-BK17oicz.js → checkpoints-DvhGi8P_.js} +1 -1
- package/server/claude-hooks-provision-s_wdpeVj.js +360 -0
- package/server/{claude-permissions-B6bWGh9d.js → claude-permissions-CaWqysff.js} +2 -2
- package/server/{claude-settings-overlay-BPdhTPjo.js → claude-settings-overlay-BbA4FbVI.js} +1 -1
- package/server/{claude-settings-overlay-CRyyKKXQ.js → claude-settings-overlay-CcTf0xzG.js} +2 -2
- package/server/{cli-B75AieAM.js → cli-BwCvCUBK.js} +9 -9
- package/server/cli-ClIQwgMI.js +3 -0
- package/server/{cliproxy-CCzwuSNe.js → cliproxy-BsoNNDAj.js} +2 -2
- package/server/{cliproxy-DOQf66G9.js → cliproxy-C2xC-3vH.js} +1 -1
- package/server/{close-issue-_X3MXJyG.js → close-issue-BleIwP3f.js} +5 -5
- package/server/{closed-issue-reaper-CLR7ClsO.js → closed-issue-reaper-BwUUvjEW.js} +1 -1
- package/server/{closed-issue-reaper-BsUfBSjM.js → closed-issue-reaper-mC5pujeb.js} +102 -30
- package/server/{codex-CY-QRuU9.js → codex-B7DTpwEw.js} +1 -1
- package/server/{codex-B4KEpX5V.js → codex-DW60_5j2.js} +5 -5
- package/server/{compact-beads-cZds2MTr.js → compact-beads-CK_hHEnB.js} +1 -1
- package/server/{compact-beads-uEDccC76.js → compact-beads-DKL54y6j.js} +15 -8
- package/server/{config-DARrpYWA.js → config-BRNQJXhA.js} +1 -1
- package/server/{config-8sjaU6jZ.js → config-ZNnMpIlh.js} +2 -2
- package/server/{config-yaml-ClpGgrf6.js → config-yaml-Dcz-8uit.js} +29 -3
- package/server/{conflict-gate-Ds9_2yjd.js → conflict-gate-DGT8jCLp.js} +1 -1
- package/server/{conflict-gate-BtB_o-Vi.js → conflict-gate-DTje2Ll7.js} +3 -3
- package/server/{context-layers-r2ywnQYA.js → context-layers-BdUCQ-cl.js} +3 -3
- package/server/{context-layers-CRCk1QG7.js → context-layers-ByOWUjUg.js} +2 -2
- package/server/{control-settings-D1B3u6HP.js → control-settings-DwkAeooY.js} +4 -4
- package/server/{conversation-compaction-D86Zo2Ci.js → conversation-compaction-OTYgd6eg.js} +8 -8
- package/server/{conversation-compaction-CZRVk4hg.js → conversation-compaction-WYQZUVDm.js} +1 -1
- package/server/{conversation-service-CuTVU8dV.js → conversation-service-BR1OgvxO.js} +5 -5
- package/server/{conversation-service-CkL-6MuG.js → conversation-service-C_kNZPLx.js} +1 -1
- package/server/{conversations-DfJKBDwB.js → conversations-DG2M8WnT.js} +4 -4
- package/server/{conversations-BmWEluPL.js → conversations-izHvSoAR.js} +1 -1
- package/server/{cost-DAPYN6iA.js → cost-B2mj17iG.js} +3 -3
- package/server/{cost-CCnqa5xK.js → cost-DQ2ajr_m.js} +2 -2
- package/server/{cost-sync-BDL10Nwu.js → cost-sync-gTayEuWP.js} +3 -3
- package/server/{dag-Dv2r5bVD.js → dag-CqTehWpY.js} +3 -3
- package/server/{dangerous-git-ops-Dcw7PbYo.js → dangerous-git-ops-DDWQlPC3.js} +1 -1
- package/server/dashboard-db-worker.js +6 -6
- package/server/{deacon-DYnZH7kC.js → deacon-BqW9HMew.js} +3 -3
- package/server/{deacon-CM0MxbS8.js → deacon-DrKDBeJL.js} +161 -1594
- package/server/{deacon-nudge-log-B-naf89q.js → deacon-nudge-log-D1wEK6zF.js} +1 -1
- package/server/deacon-swarm-BY5hKNaU.js +2 -0
- package/server/{deacon-swarm-BstYoz8Q.js → deacon-swarm-CO_A7xyd.js} +12 -105
- package/server/deacon.js +7 -7
- package/server/{devcontainer-renderer-CaYOG9JN.js → devcontainer-renderer-BQolp_qZ.js} +3 -3
- package/server/{dist-BIC8spJ0.js → dist-Bc32rWJB.js} +1 -1
- package/server/dist-DGTwcZhI.js +3 -0
- package/server/{docker-DDI1shu3.js → docker-s-esqZ__.js} +67 -28
- package/server/{ensure-devcontainer-DCAg3ksz.js → ensure-devcontainer-DA4-jF6R.js} +4 -4
- package/server/{ensure-devcontainer-CbSXvN9W.js → ensure-devcontainer-pUVaT0h3.js} +1 -1
- package/server/{esm-shims-DiV9jHa_.js → esm-shims-CJHy8CZY.js} +1 -1
- package/server/{event-store-DU3HkZlN.js → event-store-CcJAhKCl.js} +1 -1
- package/server/{event-store-DCMqyI07.js → event-store-DRte2Rwt.js} +3 -3
- package/server/{events-BfiosVAn.js → events-Bej1w_k8.js} +3 -3
- package/server/{events-DLRxpSuV.js → events-CtsLScn8.js} +1 -1
- package/server/export-Dx_qJ3h8.js +87 -0
- package/server/{factory-BMtWV9iv.js → factory-FN727tWr.js} +4 -4
- package/server/{feature-registry-population-DA7bO6Zn.js → feature-registry-population-ie-yBcQC.js} +4 -4
- package/server/{feature-registry-storage-nAY1fbBt.js → feature-registry-storage-CkZTFSsn.js} +2 -2
- package/server/{features-DMu7eGmU.js → features-Dtm9ABuz.js} +2 -2
- package/server/{feedback-target-DHqcw3cN.js → feedback-target-BYtpyt1i.js} +6 -6
- package/server/{feedback-target-B2SEai8-.js → feedback-target-DVJQDiE4.js} +1 -1
- package/server/{feedback-writer-DMBdS_jL.js → feedback-writer-DOUhyIW2.js} +1 -1
- package/server/{feedback-writer-qvm7UON7.js → feedback-writer-fdE1BzeL.js} +4 -4
- package/server/{flywheel-k4FPtCQ_.js → flywheel-Bi82iiNl.js} +1 -1
- package/server/{flywheel-Dfpx4bIV.js → flywheel-Bv3wv2S6.js} +23 -15
- package/server/{flywheel-D2LdPSic.js → flywheel-CUErLAyN.js} +13 -13
- package/server/{flywheel-merge-order-CB4lbXeJ.js → flywheel-merge-order-BaEN3iyN.js} +7 -7
- package/server/{flywheel-merge-order-CttTazRY.js → flywheel-merge-order-DXjeBnJ1.js} +1 -1
- package/server/{forge-CoPFXFyl.js → forge-ButEdJ31.js} +2 -2
- package/server/forge-XlDgPQWs.js +2 -0
- package/server/{from-B0YoJl9-.js → from-CbtTxftl.js} +1 -1
- package/server/{fts-db-DAHW1yA_.js → fts-db-CBgBLZcL.js} +2 -2
- package/server/{fts-operations-DDXDBi0d.js → fts-operations-CI9Xuo_1.js} +2 -2
- package/server/{git-utils-DbfC0zls.js → git-utils-BCLRWQ-y.js} +1 -1
- package/server/{git-utils-B90Zjzfl.js → git-utils-ClfpP47N.js} +2 -2
- package/server/{github-app-C7lftMNj.js → github-app-DRD2woO0.js} +1 -1
- package/server/{github-app-BecAnXRX.js → github-app-DscZ0_B6.js} +2 -2
- package/server/{github-client-D21rCIXk.js → github-client-DpiZnxGH.js} +2 -2
- package/server/{github-client-BwtKZtM0.js → github-client-iFRw8f7e.js} +1 -1
- package/server/{github-pr-lookup-CzVWuysl.js → github-pr-lookup-D-EBexxC.js} +2 -2
- package/server/{harness-resolve-SkRJm4a7.js → harness-resolve-iMjyNUJ2.js} +3 -3
- package/server/{health-events-DA_JLpgZ.js → health-events-B_WsQyYm.js} +1 -1
- package/server/{health-events-BT0aAfeA.js → health-events-CRdxDFN2.js} +2 -2
- package/server/{hooks-BQxRXtRb.js → hooks-C1ScyVxP.js} +1 -1
- package/server/{hooks-DxEn2BoY.js → hooks-C9b7Vqpw.js} +2 -2
- package/server/hume-K1pi8jC4.js +3 -0
- package/server/{hume-DkVOs1Qi.js → hume-TQ5pvOZ_.js} +2 -2
- package/server/{idle-stack-reaper-DGy7tvC9.js → idle-stack-reaper-B1kG7B_X.js} +2 -2
- package/server/{idle-stack-reaper-C7fJL8jH.js → idle-stack-reaper-iFVOAoJy.js} +1 -1
- package/server/{infra-DE3C4PMr.js → infra-bpq1MHKU.js} +50 -3
- package/server/{injection-Dt22tGvS.js → injection-BLWfwTVZ.js} +1 -1
- package/server/{injection-Cc4Qt8LZ.js → injection-DJqWld1v.js} +7 -7
- package/server/inspect-agent-477VxQim.js +2 -0
- package/server/{inspect-agent-DOaCDkCF.js → inspect-agent-DytCsltn.js} +9 -9
- package/server/{internal-token-nobT6FhA.js → internal-token-CCaWaTwE.js} +1 -1
- package/server/{internal-token-B_1YLWLI.js → internal-token-CjlhdufS.js} +2 -2
- package/server/{io-A7ZuOBkz.js → io-BiQ2Cs3k.js} +2 -2
- package/server/{issue-closed-Da3DeomA.js → issue-closed-FApNiOOs.js} +4 -4
- package/server/{issue-lifecycle-Bd9XBJAQ.js → issue-lifecycle-BkFWNjlD.js} +1 -1
- package/server/{issue-lifecycle-543pQX9D.js → issue-lifecycle-C-K53ASY.js} +21 -21
- package/server/{issue-service-singleton-Brc3t0SV.js → issue-service-singleton-CuHGpJJP.js} +8 -8
- package/server/{issue-service-singleton-BC2JLZi7.js → issue-service-singleton-bG1WdSdQ.js} +1 -1
- package/server/{issues-D3kt9UWE.js → issues-CwQMphDH.js} +2 -2
- package/server/jsonl-parser-dNIjHgym.js +245 -0
- package/server/{label-cleanup-w9X_T3AQ.js → label-cleanup-DYvAZknd.js} +3 -3
- package/server/{layers-Cwe1J-9o.js → layers-CD6FysRU.js} +2 -2
- package/server/{lifecycle-f1GbDK33.js → lifecycle-C_73gi-C.js} +7 -7
- package/server/{lifecycle-io-D56q8zlO.js → lifecycle-io-C_Wu0_i-.js} +1 -1
- package/server/{lifecycle-io-Bqvj0m3i.js → lifecycle-io-CeYxP3cE.js} +4 -4
- package/server/{linear-client-Bh4T4S-c.js → linear-client-4qoKnuQu.js} +1 -1
- package/server/{linear-client-CfJMTAYd.js → linear-client-EtVyblVz.js} +2 -2
- package/server/memory-fts-worker.js +1 -1
- package/server/{merge-ZrXFM3jv.js → merge-7rqkTUm2.js} +3 -3
- package/server/merge-agent-36wfYm2u.js +3 -0
- package/server/{merge-agent-CHHs5S_0.js → merge-agent-4Zhlgvqp.js} +62 -27
- package/server/{merge-batch-BwhGdlh4.js → merge-batch-itfnwdsT.js} +1 -1
- package/server/{merge-blockers-BMhDpMl_.js → merge-blockers-Bto_Oe_o.js} +2 -2
- package/server/{merge-DRMPa8CQ.js → merge-brm721Es.js} +1 -1
- package/server/merge-ops-PzqsW69V.js +2 -0
- package/server/{merge-rebase-R7MfqpiE.js → merge-rebase-Czfqfe4q.js} +2 -2
- package/server/{merge-set-B3Breacq.js → merge-set-BIkGQL9j.js} +9 -5
- package/server/{merge-set-D-iXNP0I.js → merge-set-G1RHcu3D.js} +1 -1
- package/server/{merge-sync-DHWbGtjR.js → merge-sync-B43Dybea.js} +2 -2
- package/server/{merge-sync-bOOtfeXR.js → merge-sync-DIrmlZUr.js} +1 -1
- package/server/{merge-train-98ptZrPT.js → merge-train-DGqVuNZV.js} +3 -3
- package/server/{merge-train-deps-B5FWJTE2.js → merge-train-deps-q15XLnAS.js} +4 -4
- package/server/{merge-verification-BGiuiXAK.js → merge-verification-Cqfxx6bE.js} +4 -4
- package/server/{multipart-parser-D7mzNNDm.js → multipart-parser-CiPxxfUH.js} +2 -2
- package/server/node_modules/.package-lock.json +1 -1
- package/server/{orphan-proposed-reconciler-BSGCNJ6X.js → orphan-proposed-reconciler-BWWHn4Lr.js} +15 -39
- package/server/{orphan-proposed-reconciler-CSuxivr9.js → orphan-proposed-reconciler-DAD9FJtQ.js} +1 -1
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
- package/server/{pan-dir-DoPtdHrp.js → pan-dir-Bj7Hy8iu.js} +3 -3
- package/server/{paths-CVDLPxXp.js → paths-C9DkhIOT.js} +2 -2
- package/server/{pickup-KLRUhThD.js → pickup-CKRuGDsB.js} +1 -1
- package/server/{pipeline-notifier-BtYHbBPl.js → pipeline-notifier-BkKbQ8ji.js} +1 -1
- package/server/{pipeline-notifier-WD2dnOsR.js → pipeline-notifier-CAurUste.js} +2 -2
- package/server/project-pipelines-BRWaGmI4.js +162 -0
- package/server/projects-KJ94e50I.js +3 -0
- package/server/{prompts-Cg7e9FPb.js → prompts-DfI5qbTW.js} +2 -2
- package/server/{providers-fLduChnX.js → providers-CWDXoaMA.js} +7 -7
- package/server/providers-D-5txBRc.js +3 -0
- package/server/public/assets/{chunk-BO2N2NFS-aNlsGDCu.js → chunk-BO2N2NFS-ClukPsZQ.js} +4 -4
- package/server/public/assets/{diagram-OG6HWLK6-CnjkrvT8.js → diagram-OG6HWLK6-Avtvq2qI.js} +1 -1
- package/server/public/assets/{dist-Bg3Sbsx4.js → dist-BEHQuasD.js} +3 -3
- package/server/public/assets/{dist-CGKBCieA.js → dist-BvmY-qb1.js} +1 -1
- package/server/public/assets/{ganttDiagram-6RSMTGT7-C8LLKj23.js → ganttDiagram-6RSMTGT7-7KhCd0fH.js} +1 -1
- package/server/public/assets/{highlighted-body-OFNGDK62-BSDEUvhW.js → highlighted-body-OFNGDK62-iQoBMTf0.js} +1 -1
- package/server/public/assets/index-DUFDmvmH.css +1 -0
- package/server/public/assets/index-DnTZ2QeN.js +796 -0
- package/server/public/assets/{linear-DEbYZoxg.js → linear-CiJHW9g5.js} +1 -1
- package/server/public/assets/mermaid-GHXKKRXX-DZJOwgBG.js +1 -0
- package/server/public/assets/{pieDiagram-4H26LBE5-BtZL5pXE.js → pieDiagram-4H26LBE5-BOhmvUhW.js} +1 -1
- package/server/public/assets/{prod-DYVvIyuh.js → prod-DmLBBqat.js} +4 -4
- package/server/public/assets/{quadrantDiagram-W4KKPZXB-a7vS8Jc8.js → quadrantDiagram-W4KKPZXB-Cjku660G.js} +1 -1
- package/server/public/assets/{sankeyDiagram-5OEKKPKP-Ckbq2SWH.js → sankeyDiagram-5OEKKPKP-D52w5oPh.js} +1 -1
- package/server/public/assets/{xychartDiagram-2RQKCTM6-DJyeNa99.js → xychartDiagram-2RQKCTM6-BTO07_Iv.js} +1 -1
- package/server/public/index.html +2 -2
- package/server/{pull-requests-1WXmTBlo.js → pull-requests-Bmre-Xlz.js} +3 -3
- package/server/{pull-requests-D2SjJIwY.js → pull-requests-CKZPWhjD.js} +1 -1
- package/server/{rally-8aQlLLVj.js → rally-BBd9tQ__.js} +3 -3
- package/server/rally-Dc2Wnxw2.js +3 -0
- package/server/{rally-api-BtvwdX6d.js → rally-api-DDc85HIz.js} +1 -1
- package/server/{rally-api-H5I2CBO0.js → rally-api-DxZFObI_.js} +2 -2
- package/server/{rally-client-Bt-qHiOh.js → rally-client-BADHn0m_.js} +1 -1
- package/server/{rally-client-33CYZYYu.js → rally-client-DvvLiAQP.js} +4 -4
- package/server/{reap-terminal-sessions-CAmURC2h.js → reap-terminal-sessions-D6JDJsqF.js} +1 -1
- package/server/{reap-terminal-sessions-BwJb2DLJ.js → reap-terminal-sessions-D_xvy-Mk.js} +1 -1
- package/server/{rebuild-stack-KHkkrGjx.js → rebuild-stack-D19i080j.js} +6 -6
- package/server/{rebuild-stack-CXSgAGPR.js → rebuild-stack-GOsWqXi3.js} +1 -1
- package/server/{reconstruct-cache-D_Ml65_6.js → reconstruct-cache-lQrG4xGg.js} +6 -6
- package/server/record-pyB6VA32.js +3 -0
- package/server/recovery-trip-Dfh1tNHn.js +98 -0
- package/server/release-engine-DxcMWRuK.js +306 -0
- package/server/release-set-BYsgX7o6.js +125 -0
- package/server/{remote-CGuognN3.js → remote-8W2Yhygw.js} +3 -3
- package/server/{remote-D0Old0Cv.js → remote-DmeL21dF.js} +3 -3
- package/server/{remote-agents-DfhMq6kQ.js → remote-agents-BfQBE6MN.js} +5 -5
- package/server/{remote-agents-UHoi4oGr.js → remote-agents-DlvbzO7m.js} +1 -1
- package/server/{remote-completion-RZYePR_0.js → remote-completion-BgkiSQmx.js} +12 -12
- package/server/resolver-dTpSSzaH.js +158 -0
- package/server/resume-DhcIqsTm.js +3 -0
- package/server/{review-agent-sZBCpEVv.js → review-agent-D025Wt7w.js} +22 -22
- package/server/{review-agent-uwVftH98.js → review-agent-Rub0OpZc.js} +1 -1
- package/server/{review-artifacts-C7a45b1_.js → review-artifacts-CX951jTU.js} +6 -6
- package/server/{review-context-CErbQpKL.js → review-context-DGGGR_w5.js} +7 -7
- package/server/{review-monitor-DHD9m_kh.js → review-monitor-0_qqMcnS.js} +2 -2
- package/server/{review-status-gPndPUBc.js → review-status-4qqLkRMu.js} +24 -17
- package/server/{review-status-JhQ3qlDS.js → review-status-Br6lAFGO.js} +1 -1
- package/server/{review-status-enrichment-qWvWckYN.js → review-status-enrichment-Cr8m3uVL.js} +3 -3
- package/server/{review-status-sync-B_GrEke8.js → review-status-sync-D3KUhsdA.js} +1 -1
- package/server/{review-status-sync-D3A9KX95.js → review-status-sync-OC-l1lNt.js} +9 -5
- package/server/{review-verdict-feedback-4WZ5ZdbC.js → review-verdict-feedback-DrWMuRaj.js} +6 -6
- package/server/{review-verdict-feedback-Zo0r_z4m.js → review-verdict-feedback-YOzmsq9G.js} +1 -1
- package/server/{rollup-BrUIKMv_.js → rollup-CnxbwaLH.js} +8 -8
- package/server/{rollup-Bmhn80tB.js → rollup-DmazRY0j.js} +1 -1
- package/server/{scanner-BIGumel_.js → scanner-DE1MqAaF.js} +7 -7
- package/server/{sequence-io-DA0akEQN.js → sequence-io-C0uoqfGx.js} +4 -4
- package/server/{sequencer-agent-DeflKafB.js → sequencer-agent-i-b2u4SJ.js} +10 -6
- package/server/server.js +1115 -297
- package/server/{service-V0sUK-OR.js → service-D0_gh2tl.js} +1668 -1408
- package/server/{session-fork-Yx3UcAQf.js → session-fork-DZ12vipL.js} +2 -2
- package/server/{settings-CFEeQRj2.js → settings-ByqjPtFO.js} +2 -2
- package/server/{settings-CxVate0h.js → settings-DCC4BAqj.js} +3 -3
- package/server/settings-yydgaqhv.js +2 -0
- package/server/{shadow-engineering-BRxnVSDk.js → shadow-engineering-BhnmtW5m.js} +2 -2
- package/server/{shadow-state-1aS8FijU.js → shadow-state-ByXZehsy.js} +1 -1
- package/server/{shadow-state-BiMwdyl1.js → shadow-state-CKm8xWX9.js} +2 -2
- package/server/{ship-log-CMWH3H5-.js → ship-log-DE_0Cv_i.js} +1 -1
- package/server/{smart-compaction-Bw1K_Sbl.js → smart-compaction-D5Zen3AA.js} +7 -7
- package/server/{smart-compaction-xblMyVpT.js → smart-compaction-h4_ca6Uk.js} +1 -1
- package/server/{spawn-planning-session-CzDFSKAQ.js → spawn-planning-session-Bgi7Ub02.js} +1 -1
- package/server/{spawn-planning-session-DxLxcM7m.js → spawn-planning-session-Dueit4WX.js} +15 -15
- package/server/{specialist-completion-BiEXUHfD.js → specialist-completion-BZ8M-WZ0.js} +1 -1
- package/server/{specialist-context-BfkTA81h.js → specialist-context-BMq83C58.js} +5 -5
- package/server/{specialist-handoff-logger-BiKuLtNm.js → specialist-handoff-logger-BxX1YefH.js} +2 -2
- package/server/{specialist-logs-CFJjGsLC.js → specialist-logs-BWZYcJ6P.js} +3 -3
- package/server/{specialists-DdvpFNt1.js → specialists-8ZAguXef.js} +1 -1
- package/server/{specialists-B4byf18k.js → specialists-BudKrjOu.js} +76 -76
- package/server/{specialists-DjHbwQ_p.js → specialists-DrPrC9Y9.js} +20 -256
- package/server/{specialists-BLIreoE7.js → specialists-ZkW4Mb7U.js} +1 -1
- package/server/specs-DwoAEGrW.js +3 -0
- package/server/{src-fgbU-jJ-.js → src-DHn4Rd2F.js} +25 -3
- package/server/{src-mK_M_ZIj.js → src-DNUUqkec.js} +6 -6
- package/server/{src-CALBEUMX.js → src-Dv6L03vW.js} +3 -3
- package/server/{stack-health-BFM8YlHw.js → stack-health-Dclm7_tu.js} +1 -1
- package/server/{stack-health-Qd7rPiVe.js → stack-health-DxsTx7II.js} +2 -2
- package/server/{state-home-C2Bicw9N.js → state-home-CRowbXqU.js} +6 -6
- package/server/stuck-remediation-CFWUP8_I.js +1491 -0
- package/server/stuck-remediation-CLHd-lrF.js +2 -0
- package/server/{summary-fork-EWWBvzgE.js → summary-fork-CKc01Our.js} +2 -2
- package/server/{summary-fork-DezPkJSm.js → summary-fork-CtCwBJ2Z.js} +8 -8
- package/server/{supervisor-D5GC5jUM.js → supervisor-BA5D72Qa.js} +2 -2
- package/server/supervisor-DHjumXlL.js +2 -0
- package/server/{swarm-readiness-Cf9mommA.js → swarm-readiness-CL5sM9B1.js} +3 -3
- package/server/{system-prerequisites-BsI9hxh9.js → system-prerequisites-DmFSXIE5.js} +13 -1
- package/server/{teardown-workspace-CWz1pbEK.js → teardown-workspace-DWmFJzTw.js} +37 -68
- package/server/{test-agent-queue-TiVXe3ZT.js → test-agent-queue-DjsQoV5X.js} +6 -6
- package/server/{test-verdict-CCDd25uN.js → test-verdict-BSqsQp3G.js} +1 -1
- package/server/{test-verdict-Bm4uv_AA.js → test-verdict-D1AmZ2c0.js} +1 -1
- package/server/{tldr-daemon-C_1frSDj.js → tldr-daemon-C-MGomL8.js} +2 -2
- package/server/{tldr-daemon-Big5IZ_M.js → tldr-daemon-DD9rI5gA.js} +1 -1
- package/server/{tmux-D57f-XNv.js → tmux-C_d-lSIq.js} +2 -2
- package/server/{tmux-DClqWwb8.js → tmux-x291Gjjb.js} +1 -1
- package/server/{token-_qujO3JZ.js → token-DfBf1bsd.js} +1 -1
- package/server/{tracker-config-BmdJU_w6.js → tracker-config-CNODpd11.js} +2 -2
- package/server/{tracker-config-B_Ubts1Z.js → tracker-config-DItsdelv.js} +1 -1
- package/server/{tracker-utils-Bg-Sevcv.js → tracker-utils-B9ZoVA9b.js} +1 -1
- package/server/{tracker-utils-CWFt6jjV.js → tracker-utils-Ch2_5GGf.js} +2 -2
- package/server/{traefik-connect-5EP1WS_Z.js → traefik-connect-BF4x_LUQ.js} +1 -1
- package/server/{tunnel-hOBNQbmF.js → tunnel-YD80_IHr.js} +1 -1
- package/server/{tunnel-UBf2vyAL.js → tunnel-r_5nHAI6.js} +3 -3
- package/server/{types-B5u3NxMl.js → types-33JEP4LZ.js} +1 -1
- package/server/{uat-train-DGkIkLxu.js → uat-train-DDI00aqD.js} +1 -1
- package/server/{uat-train-qNanv5hN.js → uat-train-DjkFWXge.js} +25 -25
- package/server/{validation-C4wegCvH.js → validation-DUTiJnyz.js} +4 -4
- package/server/{validation-B0QaLIHP.js → validation-ict8hnXk.js} +1 -1
- package/server/{verification-runner-DdleMNLq.js → verification-runner-ATaLbpr5.js} +1 -1
- package/server/{verification-runner-CntudBes.js → verification-runner-inLgeQux.js} +12 -12
- package/server/{vtt-parser-CKc-smWM.js → vtt-parser-QnRfcjiU.js} +1 -1
- package/server/{work-agent-prompt-CG5jFV9A.js → work-agent-prompt-fZljjA1_.js} +12 -12
- package/server/{workflows-BK2xs_a8.js → workflows-oBnJ7Tm0.js} +14 -14
- package/server/workflows-tU7KKxQi.js +2 -0
- package/server/{workspace-C9lC5o3E.js → workspace-B2WOW9Hw.js} +1 -1
- package/server/{workspace-CLrSIRCx.js → workspace-CNuW2j41.js} +3 -3
- package/server/{workspace-config-DA8U97k9.js → workspace-config-CBgMGhWs.js} +1 -1
- package/server/{workspace-manager-BJhW2sky.js → workspace-manager-CB-R07R3.js} +12 -12
- package/server/{workspace-manager-Cb2a7bg9.js → workspace-manager-DjUAJCSs.js} +1 -1
- package/server/{workspace-metadata-QQmo_Z0l.js → workspace-metadata-9Nn5kgRl.js} +2 -2
- package/server/{workspace-metadata-CTRnr323.js → workspace-metadata-Fd58uUQG.js} +1 -1
- package/server/{workspaces-Ca4zZs12.js → workspaces-BjWXntty.js} +106 -120
- package/server/writer-CQMGzEkw.js +3 -0
- package/server/writer-fe1-8iGc.js +251 -0
- package/sync-sources/hooks/ask-user-question-hook +87 -0
- package/sync-sources/hooks/auto-approve-hook +96 -0
- package/sync-sources/hooks/codex-notify-hook +62 -0
- package/sync-sources/hooks/gh-issue-trailer-hook +4 -0
- package/sync-sources/hooks/gh-issue-trailer-hook.d.ts +7 -0
- package/sync-sources/hooks/gh-issue-trailer-hook.d.ts.map +1 -0
- package/sync-sources/hooks/gh-issue-trailer-hook.js +239 -0
- package/sync-sources/hooks/gh-issue-trailer-hook.js.map +1 -0
- package/sync-sources/hooks/gh-issue-trailer-hook.ts +296 -0
- package/sync-sources/hooks/git-hooks/post-checkout +109 -0
- package/sync-sources/hooks/heartbeat-hook +113 -0
- package/sync-sources/hooks/notification-hook +70 -0
- package/sync-sources/hooks/notify-complete +79 -0
- package/sync-sources/hooks/pan-hook-lib.sh +153 -0
- package/sync-sources/hooks/permission-event-hook +24 -0
- package/sync-sources/hooks/post-compact-hook +25 -0
- package/sync-sources/hooks/pre-compact-hook +26 -0
- package/sync-sources/hooks/pre-tool-hook +35 -0
- package/sync-sources/hooks/record-cost-event.d.ts +1 -0
- package/sync-sources/hooks/record-cost-event.js +31859 -0
- package/sync-sources/hooks/record-cost-event.js.map +1 -0
- package/sync-sources/hooks/record-cost-event.ts +267 -0
- package/sync-sources/hooks/rtk-bash-filter +152 -0
- package/sync-sources/hooks/session-start-hook +80 -0
- package/sync-sources/hooks/specialist-stop-hook +106 -0
- package/sync-sources/hooks/statusline.sh +169 -0
- package/sync-sources/hooks/stop-hook +217 -0
- package/sync-sources/hooks/tldr-post-edit +72 -0
- package/sync-sources/hooks/tldr-read-enforcer +340 -0
- package/sync-sources/hooks/tmux-send-keys-guard +87 -0
- package/sync-sources/hooks/tsdown.config.ts +14 -0
- package/sync-sources/hooks/user-prompt-submit-hook +57 -0
- package/sync-sources/hooks/work-agent-stop-hook +424 -0
- package/server/auto-commit-CGQlb4rV.js +0 -3
- package/server/bd-mutex-BItGvLQ8.js +0 -35
- package/server/beads-restore-CMbDSB5R.js +0 -60
- package/server/cli-Ch_BWJB2.js +0 -3
- package/server/deacon-swarm-CSWGRfzr.js +0 -2
- package/server/dist-BUplB3Od.js +0 -3
- package/server/forge-IgLR0Ist.js +0 -2
- package/server/hume-x9voULey.js +0 -3
- package/server/inspect-agent-CYoWAeeB.js +0 -2
- package/server/merge-agent-CfohxFWM.js +0 -3
- package/server/merge-ops-Czke6UAp.js +0 -2
- package/server/projects-Bqfwuqet.js +0 -3
- package/server/providers-JjK6izqX.js +0 -3
- package/server/public/assets/index-CZiADHcL.css +0 -1
- package/server/public/assets/index-LQIfhmgI.js +0 -796
- package/server/public/assets/mermaid-GHXKKRXX-CZOsRYoT.js +0 -1
- package/server/rally-Ba9QoNYc.js +0 -3
- package/server/record-De2syxNB.js +0 -3
- package/server/resume-CmxF3Kjz.js +0 -3
- package/server/settings-DEhYpW0k.js +0 -2
- package/server/specs-CVH1kvz3.js +0 -3
- package/server/supervisor-DQMK2e3I.js +0 -2
- package/server/workflows-pKw9yRSV.js +0 -2
|
@@ -0,0 +1,1491 @@
|
|
|
1
|
+
import { Kn as resolveProjectFromIssueSync, Kr as getOverdeckHome, Pn as init_projects, _r as TmuxError, br as init_errors, er as extractPrefixSync, qr as init_paths, tr as init_issue_id, ur as FsError, zr as encodeClaudeProjectDir } from "./config-yaml-Dcz-8uit.js";
|
|
2
|
+
import { D as listPaneValuesSync, P as sendKeys, R as sessionExistsSync, T as killSessionSync, i as capturePane, x as init_tmux } from "./tmux-C_d-lSIq.js";
|
|
3
|
+
import { n as CLAUDE_CODE_BEHAVIOR } from "./src-DHn4Rd2F.js";
|
|
4
|
+
import { c as init_review_status, s as getReviewStatusSync } from "./review-status-4qqLkRMu.js";
|
|
5
|
+
import { An as getAgentDir, B as createOhmypiRuntimeSync, E as resumeAgent, Hn as markAgentTroubled, Jn as saveAgentStateSync, Pn as getAgentStateSync, Q as listRunningAgentsSync, U as spawnAgent, V as init_ohmypi, _t as init_config, cn as determineModel, en as init_pi_fifo, er as init_persistent_logger, gr as getAgentRuntimeStateSync, hr as getAgentRuntimeState, ht as DEFAULT_CLOISTER_CONFIG, j as messageAgent, nr as logDeaconEventSync, t as init_agents, vr as saveAgentRuntimeState, yt as loadCloisterConfigSync } from "./agents-CsOzAmRg.js";
|
|
6
|
+
import { t as init_behavior } from "./behavior-Cpj03N4v.js";
|
|
7
|
+
import { a as init_tracker_config, t as getGitHubConfig } from "./tracker-config-CNODpd11.js";
|
|
8
|
+
import { a as init_launcher_generator } from "./state-home-CRowbXqU.js";
|
|
9
|
+
import { i as createCodexRuntimeSync, l as init_codex } from "./codex-DW60_5j2.js";
|
|
10
|
+
import { f as isFlywheelGloballyPaused, i as getFlywheelActiveRunId, s as init_control_settings } from "./control-settings-DwkAeooY.js";
|
|
11
|
+
import { o as queryReadyBeadsByIssueLabels, r as init_beads_query, s as resolveBeadsQueryRoot } from "./beads-query-B1DlJVp9.js";
|
|
12
|
+
import { a as init_jsonl_parser, i as getSessionFilesSync, r as getProjectDirsSync, s as parseClaudeSessionSync } from "./jsonl-parser-dNIjHgym.js";
|
|
13
|
+
import { t as recordRecoveryFailure } from "./recovery-trip-Dfh1tNHn.js";
|
|
14
|
+
import "node:path";
|
|
15
|
+
import { Effect } from "effect";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
17
|
+
import { join as join$1 } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import "node:os";
|
|
20
|
+
import "node:fs";
|
|
21
|
+
import { readFile, readdir, stat as stat$1 } from "fs/promises";
|
|
22
|
+
import { exec } from "node:child_process";
|
|
23
|
+
import { promisify } from "node:util";
|
|
24
|
+
import { exec as exec$1 } from "child_process";
|
|
25
|
+
import { promisify as promisify$1 } from "util";
|
|
26
|
+
import "node:crypto";
|
|
27
|
+
//#region ../../lib/agent-input-detection.ts
|
|
28
|
+
init_paths();
|
|
29
|
+
init_issue_id();
|
|
30
|
+
init_projects();
|
|
31
|
+
init_review_status();
|
|
32
|
+
init_behavior();
|
|
33
|
+
init_persistent_logger();
|
|
34
|
+
init_codex();
|
|
35
|
+
init_launcher_generator();
|
|
36
|
+
init_beads_query();
|
|
37
|
+
init_agents();
|
|
38
|
+
init_tmux();
|
|
39
|
+
init_errors();
|
|
40
|
+
const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
41
|
+
const MAX_PROMPT_CHARS = 2e3;
|
|
42
|
+
const PANE_DETECTION_CACHE_TTL_MS = 1e4;
|
|
43
|
+
const MAX_PANE_DETECTION_CACHE_ENTRIES = 256;
|
|
44
|
+
const MAX_CONCURRENT_PANE_DETECTIONS = 4;
|
|
45
|
+
const paneDetectionCache = /* @__PURE__ */ new Map();
|
|
46
|
+
const paneDetectionInFlight = /* @__PURE__ */ new Map();
|
|
47
|
+
const paneDetectionWaiters = [];
|
|
48
|
+
let activePaneDetections = 0;
|
|
49
|
+
function getPaneDetectionCacheKey(agentId, options) {
|
|
50
|
+
return `${agentId}:${options.isPlanning === true ? "planning" : "agent"}:${options.lines ?? 90}`;
|
|
51
|
+
}
|
|
52
|
+
function sweepPaneDetectionCache(now = Date.now()) {
|
|
53
|
+
for (const [key, entry] of paneDetectionCache) if (entry.expiresAt <= now) paneDetectionCache.delete(key);
|
|
54
|
+
while (paneDetectionCache.size > MAX_PANE_DETECTION_CACHE_ENTRIES) {
|
|
55
|
+
const oldestKey = paneDetectionCache.keys().next().value;
|
|
56
|
+
if (!oldestKey) break;
|
|
57
|
+
paneDetectionCache.delete(oldestKey);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function withPaneDetectionSlot(fn) {
|
|
61
|
+
if (activePaneDetections >= MAX_CONCURRENT_PANE_DETECTIONS) await new Promise((resolve) => paneDetectionWaiters.push(resolve));
|
|
62
|
+
activePaneDetections += 1;
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
} finally {
|
|
66
|
+
activePaneDetections = Math.max(0, activePaneDetections - 1);
|
|
67
|
+
paneDetectionWaiters.shift()?.();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function cleanPaneLine(line) {
|
|
71
|
+
return line.replace(ANSI_PATTERN, "").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+$/g, "");
|
|
72
|
+
}
|
|
73
|
+
function compactPrompt(prompt) {
|
|
74
|
+
const compact = prompt.split("\n").map((line) => line.trim()).filter(Boolean).join("\n").trim();
|
|
75
|
+
if (compact.length <= MAX_PROMPT_CHARS) return compact;
|
|
76
|
+
return `${compact.slice(0, MAX_PROMPT_CHARS - 1)}…`;
|
|
77
|
+
}
|
|
78
|
+
function normalizeAwaitingInputPrompt(prompt) {
|
|
79
|
+
return compactPrompt(prompt);
|
|
80
|
+
}
|
|
81
|
+
function snippetAround(lines, index, before = 8, after = 4) {
|
|
82
|
+
const start = Math.max(0, index - before);
|
|
83
|
+
const end = Math.min(lines.length, index + after + 1);
|
|
84
|
+
return compactPrompt(lines.slice(start, end).join("\n"));
|
|
85
|
+
}
|
|
86
|
+
function lastIndexMatching(lines, predicate) {
|
|
87
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) if (predicate(lines[i], i)) return i;
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
90
|
+
function isRecentPromptIndex(lines, index) {
|
|
91
|
+
if (index < 0) return false;
|
|
92
|
+
return lines.length - index <= 25;
|
|
93
|
+
}
|
|
94
|
+
function isCurrentPromptIndex(lines, index, maxTrailingLines = 0) {
|
|
95
|
+
if (index < 0) return false;
|
|
96
|
+
return lines.length - index <= maxTrailingLines + 1;
|
|
97
|
+
}
|
|
98
|
+
function findPermissionMenuEndIndex(lines, menuStartIndex) {
|
|
99
|
+
if (menuStartIndex < 0) return -1;
|
|
100
|
+
const end = Math.min(lines.length, menuStartIndex + 8);
|
|
101
|
+
for (let i = menuStartIndex; i < end; i += 1) if (/\s*3\.\s*No\b/i.test(lines[i])) return i;
|
|
102
|
+
return menuStartIndex;
|
|
103
|
+
}
|
|
104
|
+
function looksLikeClaudePermissionMenu(text) {
|
|
105
|
+
return /(?:^|\n)\s*❯?\s*1\.\s*Yes\b/i.test(text) && /(?:^|\n)\s*2\.\s*Yes,?\s*(?:and\s*)?(?:allow|don't ask|always)/i.test(text) && /(?:^|\n)\s*3\.\s*No\b/i.test(text);
|
|
106
|
+
}
|
|
107
|
+
function looksLikePermissionText(text) {
|
|
108
|
+
return /permission|allow(?:\s+this)?|tool use|bash command|mcp tool|do you want to proceed|do you want to continue/i.test(text);
|
|
109
|
+
}
|
|
110
|
+
function looksLikeGenericConfirmation(line) {
|
|
111
|
+
return /(?:\?|:)\s*(?:\[[Yy]\/[Nn]\]|\[[Nn]\/[Yy]\]|\([Yy]\/[Nn]\)|\([Nn]\/[Yy]\)|\[y\/N\]|\[Y\/n\])\s*$/i.test(line) || /\b(?:continue|proceed|confirm|approve|allow|run|execute|overwrite|delete|remove)\b.*(?:\[[Yy]\/[Nn]\]|\([Yy]\/[Nn]\)|\[y\/N\]|\[Y\/n\])/i.test(line);
|
|
112
|
+
}
|
|
113
|
+
function looksLikePiPermission(text) {
|
|
114
|
+
return /(?:permission|approval|approve|allow).{0,80}(?:required|request|prompt|action|command)/i.test(text) || /(?:allow|approve|run|execute)\s+(?:this\s+)?(?:command|tool|action)/i.test(text);
|
|
115
|
+
}
|
|
116
|
+
function looksLikePlanningDone(text) {
|
|
117
|
+
return /planning finalized\s*[—-]\s*click done/i.test(text) || /click Done in the dashboard/i.test(text) || /planning (?:is )?complete.{0,120}(?:click|press|select) Done/i.test(text);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* PAN-1520 (covers #1197) — Claude Code session-resume dialog. Fires when an
|
|
121
|
+
* agent is resumed via `claude --resume <session>` and a prior session is
|
|
122
|
+
* still considered active or interrupted. The dialog blocks tool execution
|
|
123
|
+
* until acknowledged. Conservative patterns — we want false-negatives over
|
|
124
|
+
* false-positives, so we only match strings that clearly indicate a resume
|
|
125
|
+
* choice point.
|
|
126
|
+
*/
|
|
127
|
+
function looksLikeSessionResumeDialog(text) {
|
|
128
|
+
return /this session (?:is|was)\s+(?:still\s+)?(?:active|interrupted|running)/i.test(text) || /resume (?:the\s+)?previous session/i.test(text) || /(?:continue|resume) (?:this\s+)?session\??.{0,40}(?:\[[Yy]\/[Nn]\]|\([Yy]\/[Nn]\)|press\s+enter)/i.test(text) || /press enter to (?:continue|resume)/i.test(text);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* PAN-1834 — Codex/gpt-5.5 rate-limit / model-switch modal. The harness shows a
|
|
132
|
+
* choice between keeping the current model and switching to an alternative.
|
|
133
|
+
* To avoid matching prose that merely mentions rate limits, require BOTH option
|
|
134
|
+
* lines to be present near the bottom of the pane.
|
|
135
|
+
*/
|
|
136
|
+
function looksLikeRateLimitModelSwitchModal(text) {
|
|
137
|
+
return /keep current model/i.test(text) && /switch to\s+\S+/i.test(text);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* PAN-1690 — Codex TUI approval prompt header. Codex asks for approval with a
|
|
141
|
+
* distinctive "Would you like to …?" prompt box (command run, edits, network/
|
|
142
|
+
* host grants) followed by a numbered Yes/…/No menu. Unlike Claude's menu,
|
|
143
|
+
* Codex renders option descriptions and a footer hint *below* "3. No", so the
|
|
144
|
+
* trailing-line heuristics the Claude path relies on never fire — we key on the
|
|
145
|
+
* header instead. Verb list pulled from the codex binary; see PAN-1690.
|
|
146
|
+
*/
|
|
147
|
+
function looksLikeCodexApprovalHeader(line) {
|
|
148
|
+
return /Would you like to (?:run the following command|grant these permissions?|make the following edits|allow|apply)\b/i.test(line);
|
|
149
|
+
}
|
|
150
|
+
function detectAwaitingInputFromPaneSync(pane, options = {}) {
|
|
151
|
+
const lines = pane.split("\n").map(cleanPaneLine).filter((line) => line.trim().length > 0);
|
|
152
|
+
if (lines.length === 0) return null;
|
|
153
|
+
const recentText = lines.slice(-45).join("\n");
|
|
154
|
+
if (looksLikeClaudePermissionMenu(recentText) && looksLikePermissionText(recentText)) {
|
|
155
|
+
const menuIndex = lastIndexMatching(lines, (line) => /❯?\s*1\.\s*Yes\b/i.test(line));
|
|
156
|
+
const menuEndIndex = findPermissionMenuEndIndex(lines, menuIndex);
|
|
157
|
+
if (isRecentPromptIndex(lines, menuIndex) && isCurrentPromptIndex(lines, menuEndIndex)) return {
|
|
158
|
+
reason: "tool_permission",
|
|
159
|
+
prompt: snippetAround(lines, menuIndex, 10, 3)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const codexHeaderIndex = lastIndexMatching(lines, looksLikeCodexApprovalHeader);
|
|
163
|
+
if (codexHeaderIndex >= 0 && isRecentPromptIndex(lines, codexHeaderIndex)) {
|
|
164
|
+
const optionIndex = lastIndexMatching(lines, (line) => /^[^\w]*1\.\s*Yes\b/i.test(line));
|
|
165
|
+
if (optionIndex > codexHeaderIndex && lines.length - optionIndex <= 12) return {
|
|
166
|
+
reason: "tool_permission",
|
|
167
|
+
prompt: snippetAround(lines, codexHeaderIndex, 1, 14)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const piIndex = lastIndexMatching(lines, (line) => looksLikePiPermission(line) || looksLikeGenericConfirmation(line));
|
|
171
|
+
if (isRecentPromptIndex(lines, piIndex) && isCurrentPromptIndex(lines, piIndex)) {
|
|
172
|
+
const snippet = snippetAround(lines, piIndex, 6, 2);
|
|
173
|
+
return {
|
|
174
|
+
reason: looksLikeGenericConfirmation(lines[piIndex]) ? "confirmation" : "tool_permission",
|
|
175
|
+
prompt: snippet
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (options.isPlanning && looksLikePlanningDone(recentText)) {
|
|
179
|
+
const doneIndex = lastIndexMatching(lines, (line) => /done|planning/i.test(line));
|
|
180
|
+
return {
|
|
181
|
+
reason: "planning_done",
|
|
182
|
+
prompt: snippetAround(lines, doneIndex >= 0 ? doneIndex : lines.length - 1, 8, 2)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (looksLikeSessionResumeDialog(recentText)) {
|
|
186
|
+
const resumeIndex = lastIndexMatching(lines, (line) => looksLikeSessionResumeDialog(line));
|
|
187
|
+
return {
|
|
188
|
+
reason: "session_resume",
|
|
189
|
+
prompt: snippetAround(lines, resumeIndex >= 0 ? resumeIndex : lines.length - 1, 8, 2)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (looksLikeRateLimitModelSwitchModal(recentText)) {
|
|
193
|
+
const rateLimitIndex = lastIndexMatching(lines, (line) => /keep current model|switch to\s+\S+/i.test(line));
|
|
194
|
+
if (isRecentPromptIndex(lines, rateLimitIndex)) return {
|
|
195
|
+
reason: "rate_limit",
|
|
196
|
+
prompt: snippetAround(lines, rateLimitIndex >= 0 ? rateLimitIndex : lines.length - 1, 8, 2)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
async function detectAwaitingInputForAgentPromise(agentId, options = {}) {
|
|
202
|
+
const cacheEnabled = options.cache !== false;
|
|
203
|
+
const cacheKey = getPaneDetectionCacheKey(agentId, options);
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
if (cacheEnabled) {
|
|
206
|
+
sweepPaneDetectionCache(now);
|
|
207
|
+
const cached = paneDetectionCache.get(cacheKey);
|
|
208
|
+
if (cached && cached.expiresAt > now) {
|
|
209
|
+
paneDetectionCache.delete(cacheKey);
|
|
210
|
+
paneDetectionCache.set(cacheKey, cached);
|
|
211
|
+
return cached.detection;
|
|
212
|
+
}
|
|
213
|
+
const inFlight = paneDetectionInFlight.get(cacheKey);
|
|
214
|
+
if (inFlight) return inFlight;
|
|
215
|
+
}
|
|
216
|
+
const detectionPromise = withPaneDetectionSlot(async () => {
|
|
217
|
+
const detection = detectAwaitingInputFromPaneSync(await Effect.runPromise(capturePane(agentId, options.lines ?? 90)), options);
|
|
218
|
+
if (cacheEnabled) {
|
|
219
|
+
paneDetectionCache.set(cacheKey, {
|
|
220
|
+
expiresAt: Date.now() + PANE_DETECTION_CACHE_TTL_MS,
|
|
221
|
+
detection
|
|
222
|
+
});
|
|
223
|
+
sweepPaneDetectionCache();
|
|
224
|
+
}
|
|
225
|
+
return detection;
|
|
226
|
+
});
|
|
227
|
+
if (!cacheEnabled) return detectionPromise;
|
|
228
|
+
paneDetectionInFlight.set(cacheKey, detectionPromise);
|
|
229
|
+
try {
|
|
230
|
+
return await detectionPromise;
|
|
231
|
+
} finally {
|
|
232
|
+
paneDetectionInFlight.delete(cacheKey);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Effect-native variant of detectAwaitingInputForAgent. Fails with TmuxError if
|
|
237
|
+
* the pane capture fails outside the in-cache fast path.
|
|
238
|
+
*/
|
|
239
|
+
const detectAwaitingInputForAgent = (agentId, options = {}) => Effect.tryPromise({
|
|
240
|
+
try: () => detectAwaitingInputForAgentPromise(agentId, options),
|
|
241
|
+
catch: (cause) => new TmuxError({
|
|
242
|
+
command: "capture-pane",
|
|
243
|
+
message: `failed to detect awaiting input for ${agentId}`,
|
|
244
|
+
cause
|
|
245
|
+
})
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* PAN-1690 — parse a Codex approval prompt (as captured by the codex-aware
|
|
249
|
+
* branch of detectAwaitingInputFromPaneSync) into its header, detail, and
|
|
250
|
+
* numbered options, so the dashboard can render the menu and answer it without
|
|
251
|
+
* the terminal. Returns null when the text doesn't contain a usable menu
|
|
252
|
+
* (fewer than two numbered options). Option lines tolerate a leading selection
|
|
253
|
+
* cursor (❯ › > etc.); the footer hint and other non-numbered lines are
|
|
254
|
+
* naturally excluded.
|
|
255
|
+
*/
|
|
256
|
+
function parseCodexApprovalPrompt(text) {
|
|
257
|
+
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
258
|
+
if (lines.length === 0) return null;
|
|
259
|
+
const headerIndex = lines.findIndex((l) => looksLikeCodexApprovalHeader(l));
|
|
260
|
+
if (headerIndex < 0) return null;
|
|
261
|
+
const optionRe = /^[^\w]*(\d+)\.\s*(.+?)\s*$/;
|
|
262
|
+
const options = [];
|
|
263
|
+
const detailLines = [];
|
|
264
|
+
let firstOptionSeen = false;
|
|
265
|
+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
|
|
266
|
+
const m = optionRe.exec(lines[i]);
|
|
267
|
+
if (m) {
|
|
268
|
+
firstOptionSeen = true;
|
|
269
|
+
options.push({
|
|
270
|
+
number: Number(m[1]),
|
|
271
|
+
label: m[2].trim()
|
|
272
|
+
});
|
|
273
|
+
} else if (!firstOptionSeen) detailLines.push(lines[i]);
|
|
274
|
+
}
|
|
275
|
+
if (options.length < 2) return null;
|
|
276
|
+
return {
|
|
277
|
+
header: lines[headerIndex],
|
|
278
|
+
detail: detailLines.join("\n"),
|
|
279
|
+
options
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region ../../lib/agent-enrichment.ts
|
|
284
|
+
/**
|
|
285
|
+
* Agent enrichment utilities (PAN-440 / PAN-1048)
|
|
286
|
+
*
|
|
287
|
+
* Shared functions for computing enrichment fields:
|
|
288
|
+
* role, hasPendingQuestion, pendingQuestionCount, resolution, resolutionCount
|
|
289
|
+
*
|
|
290
|
+
* Used by both the legacy REST /api/agents endpoint and the new
|
|
291
|
+
* AgentEnrichmentService background poller.
|
|
292
|
+
*
|
|
293
|
+
* PAN-1048: replaced the legacy `agentPhase` string with the role primitive —
|
|
294
|
+
* the dashboard derives label/status from `role` + lifecycle state.
|
|
295
|
+
*/
|
|
296
|
+
init_errors();
|
|
297
|
+
init_tracker_config();
|
|
298
|
+
const execAsync$1 = promisify$1(exec$1);
|
|
299
|
+
function appendPaneDetectionKind(detection, kinds) {
|
|
300
|
+
if (detection?.reason === "session_resume" && !kinds.includes("sessionResume")) kinds.push("sessionResume");
|
|
301
|
+
if (detection?.reason === "rate_limit" && !kinds.includes("rateLimit")) kinds.push("rateLimit");
|
|
302
|
+
}
|
|
303
|
+
function isOwnActiveSpecialist(role) {
|
|
304
|
+
return role === "review" || role === "test" || role === "ship";
|
|
305
|
+
}
|
|
306
|
+
function getClaudeProjectDir(workspacePath) {
|
|
307
|
+
return join$1(homedir(), ".claude", "projects", encodeClaudeProjectDir(workspacePath));
|
|
308
|
+
}
|
|
309
|
+
async function getActiveSessionPath(projectDir) {
|
|
310
|
+
if (!existsSync(projectDir)) return null;
|
|
311
|
+
try {
|
|
312
|
+
const jsonlFiles = (await readdir(projectDir)).filter((f) => f.endsWith(".jsonl"));
|
|
313
|
+
if (jsonlFiles.length === 0) return null;
|
|
314
|
+
const withMtime = (await Promise.all(jsonlFiles.map(async (f) => {
|
|
315
|
+
try {
|
|
316
|
+
return {
|
|
317
|
+
name: f,
|
|
318
|
+
path: join$1(projectDir, f),
|
|
319
|
+
mtime: (await stat$1(join$1(projectDir, f))).mtime.getTime()
|
|
320
|
+
};
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}))).filter((x) => x !== null);
|
|
325
|
+
if (withMtime.length === 0) return null;
|
|
326
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
327
|
+
return withMtime[0].path;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function getProjectPathByPrefix(issuePrefix) {
|
|
333
|
+
const resolved = resolveProjectFromIssueSync(`${issuePrefix}-1`);
|
|
334
|
+
if (resolved) return resolved.projectPath;
|
|
335
|
+
const config = getGitHubConfig();
|
|
336
|
+
if (config) {
|
|
337
|
+
for (const { owner, repo, prefix } of config.repos) if ((prefix || repo.toUpperCase().replace(/-CLI$/, "").replace(/-/g, "")).toUpperCase() === issuePrefix.toUpperCase()) {
|
|
338
|
+
const possiblePaths = [
|
|
339
|
+
join$1(homedir(), "Projects", repo),
|
|
340
|
+
join$1(homedir(), "Projects", repo.replace(/-cli$/, "")),
|
|
341
|
+
join$1(homedir(), "Projects", owner, repo)
|
|
342
|
+
];
|
|
343
|
+
for (const path of possiblePaths) if (existsSync(path)) return path;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return join$1(homedir(), "Projects");
|
|
347
|
+
}
|
|
348
|
+
async function getAgentWorkspacePromise(agentId) {
|
|
349
|
+
const workspace = getAgentStateSync(agentId)?.workspace;
|
|
350
|
+
if (workspace) return workspace;
|
|
351
|
+
try {
|
|
352
|
+
const { stdout: paneCwd } = await execAsync$1(`tmux display-message -t ${agentId} -p '#{pane_current_path}' 2>/dev/null`, { encoding: "utf-8" });
|
|
353
|
+
const trimmed = paneCwd.trim();
|
|
354
|
+
if (trimmed && existsSync(trimmed)) return trimmed;
|
|
355
|
+
} catch {}
|
|
356
|
+
const issueId = agentId.replace(/^(agent-|planning-)/, "").toUpperCase();
|
|
357
|
+
const prefix = extractPrefixSync(issueId);
|
|
358
|
+
if (!prefix) return null;
|
|
359
|
+
try {
|
|
360
|
+
const projectPath = getProjectPathByPrefix(prefix);
|
|
361
|
+
const workspacePath = join$1(projectPath, "workspaces", `feature-${issueId.toLowerCase()}`);
|
|
362
|
+
if (existsSync(workspacePath)) return workspacePath;
|
|
363
|
+
return projectPath;
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function getAgentJsonlPathPromise(agentId) {
|
|
369
|
+
const workspace = await Effect.runPromise(getAgentWorkspace(agentId));
|
|
370
|
+
if (!workspace) return null;
|
|
371
|
+
return await getActiveSessionPath(getClaudeProjectDir(workspace));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Count outstanding AskUserQuestions across ALL of an agent's workspace JSONL
|
|
375
|
+
* session files — not just the newest one. Claude Code rotates session files,
|
|
376
|
+
* so an open AskUserQuestion can sit in a file that is not the most-recently-
|
|
377
|
+
* modified. The complete-planning guard MUST be robust to that: scanning only
|
|
378
|
+
* the active file let TIN-1 complete planning while the operator's question was
|
|
379
|
+
* still open. Returns 0 only when no file has an unanswered AUQ.
|
|
380
|
+
*/
|
|
381
|
+
async function countPendingAskUserQuestionsForAgentPromise(agentId) {
|
|
382
|
+
const workspace = await Effect.runPromise(getAgentWorkspace(agentId));
|
|
383
|
+
if (!workspace) return 0;
|
|
384
|
+
const projectDir = getClaudeProjectDir(workspace);
|
|
385
|
+
if (!existsSync(projectDir)) return 0;
|
|
386
|
+
let files;
|
|
387
|
+
try {
|
|
388
|
+
files = (await readdir(projectDir)).filter((f) => f.endsWith(".jsonl"));
|
|
389
|
+
} catch {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
let total = 0;
|
|
393
|
+
for (const f of files) try {
|
|
394
|
+
const scan = await scanPendingInputsPromise(join$1(projectDir, f));
|
|
395
|
+
total += scan.askUserQuestions.length;
|
|
396
|
+
} catch {}
|
|
397
|
+
return total;
|
|
398
|
+
}
|
|
399
|
+
/** Effect-native: count pending AskUserQuestions across all of an agent's JSONL files. */
|
|
400
|
+
const countPendingAskUserQuestionsForAgent = (agentId) => Effect.promise(() => countPendingAskUserQuestionsForAgentPromise(agentId));
|
|
401
|
+
/**
|
|
402
|
+
* Read the last `maxBytes` from a file. Efficient for large JSONL files where
|
|
403
|
+
* only recent entries (at the end) are relevant.
|
|
404
|
+
*/
|
|
405
|
+
async function readFileTail(filePath, maxBytes) {
|
|
406
|
+
try {
|
|
407
|
+
const fileStat = await stat$1(filePath);
|
|
408
|
+
const start = Math.max(0, fileStat.size - maxBytes);
|
|
409
|
+
if (start === 0) return readFile(filePath, "utf-8");
|
|
410
|
+
const { createReadStream } = await import("node:fs");
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const stream = createReadStream(filePath, {
|
|
413
|
+
start,
|
|
414
|
+
encoding: "utf-8"
|
|
415
|
+
});
|
|
416
|
+
let data = "";
|
|
417
|
+
stream.on("data", (chunk) => {
|
|
418
|
+
data += chunk;
|
|
419
|
+
});
|
|
420
|
+
stream.on("end", () => resolve(data));
|
|
421
|
+
stream.on("error", reject);
|
|
422
|
+
});
|
|
423
|
+
} catch {
|
|
424
|
+
return "";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const ASK_USER_QUESTION_HOOK_DENY_MARKER = "PAN-1520";
|
|
428
|
+
function isAskUserQuestionHookDenyToolResult(item) {
|
|
429
|
+
if (item.is_error !== true) return false;
|
|
430
|
+
const content = item.content;
|
|
431
|
+
if (typeof content === "string") return content.includes(ASK_USER_QUESTION_HOOK_DENY_MARKER);
|
|
432
|
+
if (Array.isArray(content)) return content.some((part) => {
|
|
433
|
+
if (typeof part === "string") return part.includes(ASK_USER_QUESTION_HOOK_DENY_MARKER);
|
|
434
|
+
if (part && typeof part === "object" && "text" in part) {
|
|
435
|
+
const text = part.text;
|
|
436
|
+
return typeof text === "string" && text.includes(ASK_USER_QUESTION_HOOK_DENY_MARKER);
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
});
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
async function getPendingQuestionsPromise(jsonlPath) {
|
|
443
|
+
return (await scanPendingInputsPromise(jsonlPath)).askUserQuestions;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* PAN-1520 — does this user-message entry carry operator text (i.e. an answer)
|
|
447
|
+
* rather than just being a wrapper for a tool_result? When the hook denies an
|
|
448
|
+
* AskUserQuestion the agent restates the question as plain text; the operator's
|
|
449
|
+
* next user message — whether typed in the terminal, sent from the conversation
|
|
450
|
+
* composer, or delivered by the answer-question route — is the actual answer.
|
|
451
|
+
* The presence of that user-text turn after the deny is what resolves the AUQ.
|
|
452
|
+
*/
|
|
453
|
+
function isUserTextEntry(entry) {
|
|
454
|
+
if (!entry || typeof entry !== "object") return false;
|
|
455
|
+
const e = entry;
|
|
456
|
+
if (e.type !== "user") return false;
|
|
457
|
+
const content = e.message?.content;
|
|
458
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
459
|
+
if (Array.isArray(content)) return content.some((item) => {
|
|
460
|
+
if (!item || typeof item !== "object") return false;
|
|
461
|
+
return item.type !== "tool_result";
|
|
462
|
+
});
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
async function scanPendingInputsPromise(jsonlPath) {
|
|
466
|
+
if (!existsSync(jsonlPath)) return {
|
|
467
|
+
askUserQuestions: [],
|
|
468
|
+
enterPlanModeOpen: false,
|
|
469
|
+
exitPlanModePending: false
|
|
470
|
+
};
|
|
471
|
+
try {
|
|
472
|
+
const lines = (await readFileTail(jsonlPath, 512e3)).split("\n").filter((line) => line.trim());
|
|
473
|
+
const askToolCalls = /* @__PURE__ */ new Map();
|
|
474
|
+
const askAnswered = /* @__PURE__ */ new Set();
|
|
475
|
+
const askDeniedAwaitingUser = /* @__PURE__ */ new Set();
|
|
476
|
+
const exitPlanModeIds = /* @__PURE__ */ new Set();
|
|
477
|
+
const exitPlanModeAnswered = /* @__PURE__ */ new Set();
|
|
478
|
+
const enterPlanModeIds = /* @__PURE__ */ new Set();
|
|
479
|
+
const exitPlanModeFiredAfterEnter = /* @__PURE__ */ new Set();
|
|
480
|
+
for (const line of lines) try {
|
|
481
|
+
const entry = JSON.parse(line);
|
|
482
|
+
if (isUserTextEntry(entry)) {
|
|
483
|
+
for (const id of askDeniedAwaitingUser) askAnswered.add(id);
|
|
484
|
+
askDeniedAwaitingUser.clear();
|
|
485
|
+
}
|
|
486
|
+
const messageContent = entry.message?.content;
|
|
487
|
+
if (!Array.isArray(messageContent)) continue;
|
|
488
|
+
for (const item of messageContent) {
|
|
489
|
+
if (item.type === "tool_use") {
|
|
490
|
+
if (item.name === "AskUserQuestion" && typeof item.id === "string") askToolCalls.set(item.id, {
|
|
491
|
+
toolId: item.id,
|
|
492
|
+
timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
493
|
+
questions: item.input?.questions || []
|
|
494
|
+
});
|
|
495
|
+
else if (item.name === "ExitPlanMode" && typeof item.id === "string") {
|
|
496
|
+
exitPlanModeIds.add(item.id);
|
|
497
|
+
exitPlanModeFiredAfterEnter.add(item.id);
|
|
498
|
+
} else if (item.name === "EnterPlanMode" && typeof item.id === "string") enterPlanModeIds.add(item.id);
|
|
499
|
+
}
|
|
500
|
+
if (item.type === "tool_result" && typeof item.tool_use_id === "string") {
|
|
501
|
+
if (askToolCalls.has(item.tool_use_id)) if (isAskUserQuestionHookDenyToolResult(item)) askDeniedAwaitingUser.add(item.tool_use_id);
|
|
502
|
+
else {
|
|
503
|
+
askAnswered.add(item.tool_use_id);
|
|
504
|
+
askDeniedAwaitingUser.delete(item.tool_use_id);
|
|
505
|
+
}
|
|
506
|
+
if (exitPlanModeIds.has(item.tool_use_id)) exitPlanModeAnswered.add(item.tool_use_id);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch {}
|
|
510
|
+
const askUserQuestions = Array.from(askToolCalls.entries()).filter(([id]) => !askAnswered.has(id)).map(([, question]) => question);
|
|
511
|
+
const exitPlanModePending = Array.from(exitPlanModeIds).some((id) => !exitPlanModeAnswered.has(id));
|
|
512
|
+
return {
|
|
513
|
+
askUserQuestions,
|
|
514
|
+
enterPlanModeOpen: enterPlanModeIds.size > 0 && exitPlanModeFiredAfterEnter.size === 0,
|
|
515
|
+
exitPlanModePending
|
|
516
|
+
};
|
|
517
|
+
} catch {
|
|
518
|
+
return {
|
|
519
|
+
askUserQuestions: [],
|
|
520
|
+
enterPlanModeOpen: false,
|
|
521
|
+
exitPlanModePending: false
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function getAgentPendingQuestionsPromise(agentId) {
|
|
526
|
+
const jsonlPath = await Effect.runPromise(getAgentJsonlPath(agentId));
|
|
527
|
+
if (!jsonlPath) return [];
|
|
528
|
+
return [...await Effect.runPromise(getPendingQuestions(jsonlPath))];
|
|
529
|
+
}
|
|
530
|
+
async function getAgentJsonlMtimePromise(agentId) {
|
|
531
|
+
const jsonlPath = await Effect.runPromise(getAgentJsonlPath(agentId));
|
|
532
|
+
if (!jsonlPath || !existsSync(jsonlPath)) return null;
|
|
533
|
+
try {
|
|
534
|
+
return (await stat$1(jsonlPath)).mtime.getTime();
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function computeAgentEnrichmentPromise(agentId, startedAt, hasActiveSpecialist, skipJsonlScan) {
|
|
540
|
+
const isPlanning = agentId.startsWith("planning-");
|
|
541
|
+
const stateRole = getAgentStateSync(agentId)?.role;
|
|
542
|
+
const role = stateRole === "plan" || stateRole === "work" || stateRole === "review" || stateRole === "test" || stateRole === "ship" || stateRole === "flywheel" ? stateRole : isPlanning ? "plan" : void 0;
|
|
543
|
+
const runtimeState = await Effect.runPromise(getAgentRuntimeState(agentId));
|
|
544
|
+
let pendingQuestions = [];
|
|
545
|
+
let enterPlanModeOpen = false;
|
|
546
|
+
let exitPlanModePending = false;
|
|
547
|
+
if (!skipJsonlScan) {
|
|
548
|
+
const jsonlPath = await Effect.runPromise(getAgentJsonlPath(agentId));
|
|
549
|
+
if (jsonlPath) {
|
|
550
|
+
const scan = await scanPendingInputsPromise(jsonlPath);
|
|
551
|
+
pendingQuestions = [...scan.askUserQuestions];
|
|
552
|
+
enterPlanModeOpen = scan.enterPlanModeOpen;
|
|
553
|
+
exitPlanModePending = scan.exitPlanModePending;
|
|
554
|
+
}
|
|
555
|
+
if (pendingQuestions.length > 0 && startedAt) {
|
|
556
|
+
const agentStartTime = new Date(startedAt).getTime();
|
|
557
|
+
pendingQuestions = pendingQuestions.filter((q) => {
|
|
558
|
+
const qTime = new Date(q.timestamp).getTime();
|
|
559
|
+
return !isNaN(qTime) && qTime >= agentStartTime;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const questionDetection = pendingQuestions.length > 0 ? {
|
|
564
|
+
reason: "user_question",
|
|
565
|
+
prompt: normalizeAwaitingInputPrompt(pendingQuestions[0]?.questions.map((q) => q.question).filter(Boolean).join("\n") || "Agent is waiting for a user answer")
|
|
566
|
+
} : null;
|
|
567
|
+
const runtimeDetection = runtimeState?.state === "waiting-on-human" ? {
|
|
568
|
+
reason: runtimeState.waitingReason || "other",
|
|
569
|
+
prompt: normalizeAwaitingInputPrompt(runtimeState.waitingNotification || "Agent is waiting for human input")
|
|
570
|
+
} : null;
|
|
571
|
+
const paneDetection = !questionDetection && !runtimeDetection ? await Effect.runPromise(detectAwaitingInputForAgent(agentId, { isPlanning })) : null;
|
|
572
|
+
const fallbackDetection = runtimeState?.resolution === "needs_input" ? {
|
|
573
|
+
reason: "other",
|
|
574
|
+
prompt: normalizeAwaitingInputPrompt("Agent stopped because it needs human input or hit a blocker")
|
|
575
|
+
} : null;
|
|
576
|
+
const detection = questionDetection ?? runtimeDetection ?? paneDetection ?? fallbackDetection;
|
|
577
|
+
const shouldSuppressPendingInput = hasActiveSpecialist === true && !isOwnActiveSpecialist(role);
|
|
578
|
+
const hasPendingQuestion = !shouldSuppressPendingInput && detection !== null;
|
|
579
|
+
const pendingInputKinds = [];
|
|
580
|
+
let pendingAskUserQuestion;
|
|
581
|
+
if (!shouldSuppressPendingInput) {
|
|
582
|
+
if (pendingQuestions.length > 0) {
|
|
583
|
+
pendingInputKinds.push("askUserQuestion");
|
|
584
|
+
const first = pendingQuestions[0];
|
|
585
|
+
pendingAskUserQuestion = {
|
|
586
|
+
toolUseId: first.toolId,
|
|
587
|
+
askedAt: first.timestamp,
|
|
588
|
+
questions: first.questions.map((q) => ({
|
|
589
|
+
question: q.question,
|
|
590
|
+
header: q.header,
|
|
591
|
+
multiSelect: q.multiSelect,
|
|
592
|
+
options: q.options.map((o) => ({
|
|
593
|
+
label: o.label,
|
|
594
|
+
description: o.description
|
|
595
|
+
}))
|
|
596
|
+
}))
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (exitPlanModePending) pendingInputKinds.push("exitPlanMode");
|
|
600
|
+
if (enterPlanModeOpen && !exitPlanModePending) pendingInputKinds.push("enterPlanMode");
|
|
601
|
+
appendPaneDetectionKind(detection, pendingInputKinds);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
role,
|
|
605
|
+
hasPendingQuestion,
|
|
606
|
+
pendingQuestionCount: pendingQuestions.length,
|
|
607
|
+
pendingQuestionPrompt: detection?.prompt,
|
|
608
|
+
pendingQuestionReason: detection?.reason,
|
|
609
|
+
pendingInputCount: pendingInputKinds.length,
|
|
610
|
+
pendingInputKinds,
|
|
611
|
+
pendingAskUserQuestion,
|
|
612
|
+
resolution: runtimeState?.resolution || "working",
|
|
613
|
+
resolutionCount: runtimeState?.resolutionCount || 0
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Effect-native variant of computeAgentEnrichment. Fails with FsError if any
|
|
618
|
+
* underlying filesystem read fails outside the swallowed branches.
|
|
619
|
+
*/
|
|
620
|
+
const computeAgentEnrichment = (agentId, startedAt, hasActiveSpecialist, skipJsonlScan) => Effect.tryPromise({
|
|
621
|
+
try: () => computeAgentEnrichmentPromise(agentId, startedAt, hasActiveSpecialist, skipJsonlScan),
|
|
622
|
+
catch: (cause) => new FsError({
|
|
623
|
+
path: getAgentDir(agentId),
|
|
624
|
+
operation: "computeAgentEnrichment",
|
|
625
|
+
cause
|
|
626
|
+
})
|
|
627
|
+
});
|
|
628
|
+
/** Effect-native: resolve the workspace path for an agent (null on failure). */
|
|
629
|
+
const getAgentWorkspace = (agentId) => Effect.promise(() => getAgentWorkspacePromise(agentId));
|
|
630
|
+
/** Effect-native: resolve the active JSONL session path for an agent (null on failure). */
|
|
631
|
+
const getAgentJsonlPath = (agentId) => Effect.promise(() => getAgentJsonlPathPromise(agentId));
|
|
632
|
+
/** Effect-native: get the mtime of the agent's active JSONL session file. */
|
|
633
|
+
const getAgentJsonlMtime = (agentId) => Effect.promise(() => getAgentJsonlMtimePromise(agentId));
|
|
634
|
+
/** Effect-native: parse pending questions from a JSONL file. */
|
|
635
|
+
const getPendingQuestions = (jsonlPath) => Effect.promise(() => getPendingQuestionsPromise(jsonlPath));
|
|
636
|
+
/** Effect-native: get pending questions for an agent by id. */
|
|
637
|
+
const getAgentPendingQuestions = (agentId) => Effect.promise(() => getAgentPendingQuestionsPromise(agentId));
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region ../../lib/runtimes/claude-code.ts
|
|
640
|
+
init_config();
|
|
641
|
+
/**
|
|
642
|
+
* Claude Code Runtime Implementation
|
|
643
|
+
*
|
|
644
|
+
* Implements AgentRuntime for Claude Code CLI.
|
|
645
|
+
*
|
|
646
|
+
* Session storage: ~/.claude/projects/<workspace-hash>/<session-id>.jsonl
|
|
647
|
+
* Session index: ~/.claude/projects/<workspace-hash>/sessions-index.json
|
|
648
|
+
*/
|
|
649
|
+
init_agents();
|
|
650
|
+
init_tmux();
|
|
651
|
+
init_jsonl_parser();
|
|
652
|
+
init_errors();
|
|
653
|
+
const CLAUDE_PROJECTS_DIR = join$1(homedir(), ".claude", "projects");
|
|
654
|
+
/**
|
|
655
|
+
* Claude Code Runtime implementation
|
|
656
|
+
*/
|
|
657
|
+
var ClaudeCodeRuntimeSync = class {
|
|
658
|
+
name = "claude-code";
|
|
659
|
+
getHarnessBehavior() {
|
|
660
|
+
return CLAUDE_CODE_BEHAVIOR;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get the project directory for a workspace
|
|
664
|
+
*
|
|
665
|
+
* Claude Code hashes the workspace path to create project directories.
|
|
666
|
+
* We need to find the project directory that contains sessions for this workspace.
|
|
667
|
+
*/
|
|
668
|
+
getProjectDirForWorkspace(workspace) {
|
|
669
|
+
if (!existsSync(CLAUDE_PROJECTS_DIR)) return null;
|
|
670
|
+
const projectDirs = getProjectDirsSync();
|
|
671
|
+
for (const projectDir of projectDirs) {
|
|
672
|
+
const indexPath = join$1(projectDir, "sessions-index.json");
|
|
673
|
+
if (existsSync(indexPath)) try {
|
|
674
|
+
if (readFileSync(indexPath, "utf-8").includes(workspace)) return projectDir;
|
|
675
|
+
} catch {}
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get the active session ID for an agent from the sessions index
|
|
681
|
+
*/
|
|
682
|
+
getActiveSessionId(projectDir) {
|
|
683
|
+
const indexPath = join$1(projectDir, "sessions-index.json");
|
|
684
|
+
if (!existsSync(indexPath)) return null;
|
|
685
|
+
try {
|
|
686
|
+
const indexContent = readFileSync(indexPath, "utf-8");
|
|
687
|
+
const index = JSON.parse(indexContent);
|
|
688
|
+
if (index.sessions && Array.isArray(index.sessions)) {
|
|
689
|
+
const sessions = index.sessions;
|
|
690
|
+
if (sessions.length === 0) return null;
|
|
691
|
+
return sessions.sort((a, b) => {
|
|
692
|
+
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
693
|
+
})[0].sessionId;
|
|
694
|
+
}
|
|
695
|
+
} catch {}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Get the most recent JSONL file for a project
|
|
700
|
+
*/
|
|
701
|
+
getMostRecentJSONL(projectDir) {
|
|
702
|
+
const files = getSessionFilesSync(projectDir);
|
|
703
|
+
return files.length > 0 ? files[0] : null;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Get the session path for an agent
|
|
707
|
+
*/
|
|
708
|
+
getSessionPath(agentId) {
|
|
709
|
+
const state = getAgentStateSync(agentId);
|
|
710
|
+
if (!state) return null;
|
|
711
|
+
const projectDir = this.getProjectDirForWorkspace(state.workspace);
|
|
712
|
+
if (!projectDir) return null;
|
|
713
|
+
const sessionId = this.getActiveSessionId(projectDir);
|
|
714
|
+
if (sessionId) {
|
|
715
|
+
const sessionPath = join$1(projectDir, `${sessionId}.jsonl`);
|
|
716
|
+
if (existsSync(sessionPath)) return sessionPath;
|
|
717
|
+
}
|
|
718
|
+
return this.getMostRecentJSONL(projectDir);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Get last activity timestamp for an agent
|
|
722
|
+
*
|
|
723
|
+
* Uses passive detection via JSONL file modification time.
|
|
724
|
+
*/
|
|
725
|
+
getLastActivity(agentId) {
|
|
726
|
+
const sessionPath = this.getSessionPath(agentId);
|
|
727
|
+
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
728
|
+
try {
|
|
729
|
+
return statSync(sessionPath).mtime;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Read active heartbeat file if it exists
|
|
736
|
+
*/
|
|
737
|
+
getActiveHeartbeat(agentId) {
|
|
738
|
+
const heartbeatPath = join$1(homedir(), ".overdeck", "heartbeats", `${agentId}.json`);
|
|
739
|
+
if (!existsSync(heartbeatPath)) return null;
|
|
740
|
+
try {
|
|
741
|
+
const content = readFileSync(heartbeatPath, "utf-8");
|
|
742
|
+
const data = JSON.parse(content);
|
|
743
|
+
const timestamp = new Date(data.timestamp);
|
|
744
|
+
if ((/* @__PURE__ */ new Date()).getTime() - timestamp.getTime() > 300 * 1e3) return null;
|
|
745
|
+
return {
|
|
746
|
+
timestamp,
|
|
747
|
+
agentId: data.agent_id || agentId,
|
|
748
|
+
source: "active-heartbeat",
|
|
749
|
+
confidence: "high",
|
|
750
|
+
toolName: data.tool_name,
|
|
751
|
+
lastAction: data.last_action,
|
|
752
|
+
currentTask: data.current_task,
|
|
753
|
+
gitBranch: data.git_branch,
|
|
754
|
+
workspace: data.workspace,
|
|
755
|
+
pid: data.pid,
|
|
756
|
+
sessionId: data.session_id
|
|
757
|
+
};
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Get passive heartbeat from file timestamps
|
|
764
|
+
*/
|
|
765
|
+
getPassiveHeartbeat(agentId) {
|
|
766
|
+
const lastActivity = this.getLastActivity(agentId);
|
|
767
|
+
if (!lastActivity) return null;
|
|
768
|
+
return {
|
|
769
|
+
timestamp: lastActivity,
|
|
770
|
+
agentId,
|
|
771
|
+
source: "jsonl",
|
|
772
|
+
confidence: "medium"
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get heartbeat for an agent
|
|
777
|
+
*
|
|
778
|
+
* Tries active heartbeat first (if hooks configured), falls back to passive.
|
|
779
|
+
*/
|
|
780
|
+
getHeartbeat(agentId) {
|
|
781
|
+
const activeHeartbeat = this.getActiveHeartbeat(agentId);
|
|
782
|
+
if (activeHeartbeat) return activeHeartbeat;
|
|
783
|
+
return this.getPassiveHeartbeat(agentId);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Get token usage for an agent's current session
|
|
787
|
+
*/
|
|
788
|
+
getTokenUsage(agentId) {
|
|
789
|
+
const sessionPath = this.getSessionPath(agentId);
|
|
790
|
+
if (!sessionPath) return null;
|
|
791
|
+
const sessionUsage = parseClaudeSessionSync(sessionPath);
|
|
792
|
+
if (!sessionUsage) return null;
|
|
793
|
+
return sessionUsage.usage;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Get cost breakdown for an agent's current session
|
|
797
|
+
*/
|
|
798
|
+
getSessionCost(agentId) {
|
|
799
|
+
const sessionPath = this.getSessionPath(agentId);
|
|
800
|
+
if (!sessionPath) return null;
|
|
801
|
+
const sessionUsage = parseClaudeSessionSync(sessionPath);
|
|
802
|
+
if (!sessionUsage) return null;
|
|
803
|
+
const inputPrice = 3 / 1e6;
|
|
804
|
+
const outputPrice = 15 / 1e6;
|
|
805
|
+
const cacheReadPrice = .3 / 1e6;
|
|
806
|
+
const cacheWritePrice = 3.75 / 1e6;
|
|
807
|
+
const usage = sessionUsage.usage;
|
|
808
|
+
const inputCost = usage.inputTokens * inputPrice;
|
|
809
|
+
const outputCost = usage.outputTokens * outputPrice;
|
|
810
|
+
const cacheReadCost = (usage.cacheReadTokens || 0) * cacheReadPrice;
|
|
811
|
+
const cacheWriteCost = (usage.cacheWriteTokens || 0) * cacheWritePrice;
|
|
812
|
+
return {
|
|
813
|
+
inputCost,
|
|
814
|
+
outputCost,
|
|
815
|
+
cacheReadCost,
|
|
816
|
+
cacheWriteCost,
|
|
817
|
+
totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
|
|
818
|
+
currency: "USD"
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Send a message to a running agent
|
|
823
|
+
*/
|
|
824
|
+
async sendMessage(agentId, message) {
|
|
825
|
+
if (!sessionExistsSync(agentId)) throw new Error(`Agent ${agentId} is not running`);
|
|
826
|
+
await Effect.runPromise(sendKeys(agentId, message));
|
|
827
|
+
const mailDir = join$1(getAgentDir(agentId), "mail");
|
|
828
|
+
mkdirSync(mailDir, { recursive: true });
|
|
829
|
+
writeFileSync(join$1(mailDir, `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.md`), `# Message\n\n${message}\n`);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Kill an agent
|
|
833
|
+
*/
|
|
834
|
+
killAgent(agentId) {
|
|
835
|
+
if (!sessionExistsSync(agentId)) throw new Error(`Agent ${agentId} is not running`);
|
|
836
|
+
killSessionSync(agentId);
|
|
837
|
+
saveAgentRuntimeState(agentId, {
|
|
838
|
+
state: "idle",
|
|
839
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
840
|
+
});
|
|
841
|
+
const state = getAgentStateSync(agentId);
|
|
842
|
+
if (state) {
|
|
843
|
+
state.status = "stopped";
|
|
844
|
+
saveAgentStateSync(state);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Spawn a new agent
|
|
849
|
+
*/
|
|
850
|
+
async spawnAgent(config) {
|
|
851
|
+
const state = await spawnAgent({
|
|
852
|
+
issueId: config.agentId.replace(/^agent-/, ""),
|
|
853
|
+
workspace: config.workspace,
|
|
854
|
+
harness: "claude-code",
|
|
855
|
+
model: determineModel({
|
|
856
|
+
model: config.model,
|
|
857
|
+
role: "work",
|
|
858
|
+
spawnKey: `work:${config.agentId.replace(/^agent-/, "")}`
|
|
859
|
+
}),
|
|
860
|
+
role: "work",
|
|
861
|
+
prompt: config.prompt
|
|
862
|
+
});
|
|
863
|
+
const projectDir = this.getProjectDirForWorkspace(config.workspace);
|
|
864
|
+
const sessionId = projectDir ? this.getActiveSessionId(projectDir) : void 0;
|
|
865
|
+
return {
|
|
866
|
+
id: state.id,
|
|
867
|
+
sessionId: sessionId || "unknown",
|
|
868
|
+
runtime: "claude-code",
|
|
869
|
+
model: state.model,
|
|
870
|
+
workspace: state.workspace,
|
|
871
|
+
startedAt: new Date(state.startedAt)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* List all sessions for this runtime
|
|
876
|
+
*/
|
|
877
|
+
listSessions(workspace) {
|
|
878
|
+
const sessions = [];
|
|
879
|
+
if (workspace) {
|
|
880
|
+
const projectDir = this.getProjectDirForWorkspace(workspace);
|
|
881
|
+
if (projectDir) {
|
|
882
|
+
const files = getSessionFilesSync(projectDir);
|
|
883
|
+
for (const file of files) {
|
|
884
|
+
const session = this.parseSessionFile(file, workspace);
|
|
885
|
+
if (session) sessions.push(session);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
const projectDirs = getProjectDirsSync();
|
|
890
|
+
for (const projectDir of projectDirs) {
|
|
891
|
+
const files = getSessionFilesSync(projectDir);
|
|
892
|
+
for (const file of files) {
|
|
893
|
+
const session = this.parseSessionFile(file);
|
|
894
|
+
if (session) sessions.push(session);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return sessions;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Parse a session file into a Session object
|
|
902
|
+
*/
|
|
903
|
+
parseSessionFile(file, workspace) {
|
|
904
|
+
const sessionUsage = parseClaudeSessionSync(file);
|
|
905
|
+
if (!sessionUsage) return null;
|
|
906
|
+
const stat = statSync(file);
|
|
907
|
+
return {
|
|
908
|
+
id: sessionUsage.sessionId,
|
|
909
|
+
agentId: "unknown",
|
|
910
|
+
workspace: workspace || "unknown",
|
|
911
|
+
model: sessionUsage.model,
|
|
912
|
+
startedAt: new Date(sessionUsage.startTime),
|
|
913
|
+
lastActivity: stat.mtime,
|
|
914
|
+
tokenUsage: sessionUsage.usage
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Check if an agent is running
|
|
919
|
+
*/
|
|
920
|
+
isRunning(agentId) {
|
|
921
|
+
return sessionExistsSync(agentId);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
/**
|
|
925
|
+
* Create a Claude Code runtime instance
|
|
926
|
+
*/
|
|
927
|
+
function createClaudeCodeRuntimeSync() {
|
|
928
|
+
return new ClaudeCodeRuntimeSync();
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region ../../lib/cost-parsers/pi-parser.ts
|
|
932
|
+
init_errors();
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region ../../lib/runtimes/pi.ts
|
|
935
|
+
init_behavior();
|
|
936
|
+
init_tmux();
|
|
937
|
+
init_pi_fifo();
|
|
938
|
+
init_errors();
|
|
939
|
+
init_paths();
|
|
940
|
+
promisify(exec);
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region ../../lib/runtimes/index.ts
|
|
943
|
+
init_behavior();
|
|
944
|
+
init_ohmypi();
|
|
945
|
+
init_agents();
|
|
946
|
+
/**
|
|
947
|
+
* Runtime registry implementation
|
|
948
|
+
*
|
|
949
|
+
* Manages multiple runtime adapters and provides lookup by agent ID.
|
|
950
|
+
*/
|
|
951
|
+
var RuntimeRegistry = class {
|
|
952
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
953
|
+
/**
|
|
954
|
+
* Register a runtime adapter
|
|
955
|
+
*/
|
|
956
|
+
register(runtime) {
|
|
957
|
+
this.runtimes.set(runtime.name, runtime);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Get a runtime by name
|
|
961
|
+
*/
|
|
962
|
+
get(name) {
|
|
963
|
+
return this.runtimes.get(name);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get all registered runtimes
|
|
967
|
+
*/
|
|
968
|
+
getAll() {
|
|
969
|
+
return Array.from(this.runtimes.values());
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get the runtime for a specific agent.
|
|
973
|
+
*
|
|
974
|
+
* Reads the agent's state file and dispatches by `state.harness`. When
|
|
975
|
+
* the harness field is missing or carries a legacy value (e.g. 'claude'
|
|
976
|
+
* from pre-PAN-636 wire format), we fall back to the claude-code runtime
|
|
977
|
+
* to preserve back-compat (PAN-636 ac2).
|
|
978
|
+
*/
|
|
979
|
+
getRuntimeForAgent(agentId) {
|
|
980
|
+
const state = getAgentStateSync(agentId);
|
|
981
|
+
if (!state) return null;
|
|
982
|
+
const harness = state.harness;
|
|
983
|
+
if (harness === "ohmypi" || harness === "pi") return this.get("ohmypi") ?? null;
|
|
984
|
+
if (harness === "codex") return this.get("codex") ?? null;
|
|
985
|
+
return this.get("claude-code") ?? null;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
/**
|
|
989
|
+
* Global runtime registry instance
|
|
990
|
+
*/
|
|
991
|
+
let globalRegistry = null;
|
|
992
|
+
/**
|
|
993
|
+
* Get the global runtime registry
|
|
994
|
+
*
|
|
995
|
+
* Creates a new registry if one doesn't exist.
|
|
996
|
+
* Registers Claude Code runtime by default.
|
|
997
|
+
*/
|
|
998
|
+
function getGlobalRegistry() {
|
|
999
|
+
if (!globalRegistry) {
|
|
1000
|
+
globalRegistry = new RuntimeRegistry();
|
|
1001
|
+
globalRegistry.register(createClaudeCodeRuntimeSync());
|
|
1002
|
+
globalRegistry.register(createOhmypiRuntimeSync());
|
|
1003
|
+
globalRegistry.register(createCodexRuntimeSync());
|
|
1004
|
+
}
|
|
1005
|
+
return globalRegistry;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Helper to get the runtime for an agent from the global registry
|
|
1009
|
+
*/
|
|
1010
|
+
function getRuntimeForAgent(agentId) {
|
|
1011
|
+
return getGlobalRegistry().getRuntimeForAgent(agentId);
|
|
1012
|
+
}
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region ../../lib/cloister/agent-idle.ts
|
|
1015
|
+
init_agents();
|
|
1016
|
+
init_tmux();
|
|
1017
|
+
function getAgentEffectiveLastActivityMs(agentId) {
|
|
1018
|
+
const candidates = [];
|
|
1019
|
+
const runtimeState = getAgentRuntimeStateSync(agentId);
|
|
1020
|
+
const runtimeMs = runtimeState?.lastActivity ? new Date(runtimeState.lastActivity).getTime() : NaN;
|
|
1021
|
+
if (Number.isFinite(runtimeMs)) candidates.push(runtimeMs);
|
|
1022
|
+
try {
|
|
1023
|
+
const tmuxActivity = listPaneValuesSync(agentId, "#{window_activity}").map((value) => Number.parseInt(value.trim(), 10) * 1e3).filter(Number.isFinite);
|
|
1024
|
+
candidates.push(...tmuxActivity);
|
|
1025
|
+
} catch {}
|
|
1026
|
+
try {
|
|
1027
|
+
const heartbeat = getRuntimeForAgent(agentId)?.getHeartbeat(agentId);
|
|
1028
|
+
if (heartbeat?.timestamp) {
|
|
1029
|
+
const heartbeatMs = heartbeat.timestamp.getTime();
|
|
1030
|
+
if (Number.isFinite(heartbeatMs)) candidates.push(heartbeatMs);
|
|
1031
|
+
}
|
|
1032
|
+
} catch {}
|
|
1033
|
+
return candidates.length > 0 ? Math.max(...candidates) : null;
|
|
1034
|
+
}
|
|
1035
|
+
function isAgentIdleForNudge(agentId, staleActiveThresholdMs = 300 * 1e3, now = Date.now()) {
|
|
1036
|
+
const runtimeState = getAgentRuntimeStateSync(agentId);
|
|
1037
|
+
const effectiveLastActivityMs = getAgentEffectiveLastActivityMs(agentId);
|
|
1038
|
+
if (!runtimeState) {
|
|
1039
|
+
if (effectiveLastActivityMs === null) {
|
|
1040
|
+
console.log(`[deacon] ${agentId}: no runtime.json — skipping (hook not yet fired)`);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
return now - effectiveLastActivityMs > staleActiveThresholdMs;
|
|
1044
|
+
}
|
|
1045
|
+
if (runtimeState.state === "suspended" || runtimeState.state === "stopped") return false;
|
|
1046
|
+
if (runtimeState.state === "waiting-on-human") return false;
|
|
1047
|
+
if (runtimeState.state === "idle") return true;
|
|
1048
|
+
return now - (effectiveLastActivityMs ?? new Date(runtimeState.lastActivity).getTime()) > staleActiveThresholdMs;
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region ../../lib/cloister/agent-death.ts
|
|
1052
|
+
/**
|
|
1053
|
+
* Agent death diagnostics (PAN-2108).
|
|
1054
|
+
*
|
|
1055
|
+
* When an ohmypi/pi agent process dies, the launcher records its exit code +
|
|
1056
|
+
* timestamp to `<agentDir>/exit-status` (see buildOhmypiCommand in
|
|
1057
|
+
* launcher-generator.ts), and omp's stdout/stderr is captured to
|
|
1058
|
+
* `<agentDir>/output.log`. This module turns those into a one-line death reason
|
|
1059
|
+
* the deacon can log, replacing the opaque "session is dead" with an actual
|
|
1060
|
+
* cause — the gap that made the flywheel orchestrator's silent death
|
|
1061
|
+
* undiagnosable in RUN-30.
|
|
1062
|
+
*/
|
|
1063
|
+
init_paths();
|
|
1064
|
+
init_tmux();
|
|
1065
|
+
function agentDir(agentId) {
|
|
1066
|
+
return join$1(getOverdeckHome(), "agents", agentId);
|
|
1067
|
+
}
|
|
1068
|
+
/** Read the launcher-recorded exit code + UTC timestamp, or null if absent. */
|
|
1069
|
+
function readAgentExitStatus(agentId) {
|
|
1070
|
+
try {
|
|
1071
|
+
const p = join$1(agentDir(agentId), "exit-status");
|
|
1072
|
+
if (!existsSync(p)) return null;
|
|
1073
|
+
const raw = readFileSync(p, "utf-8").trim();
|
|
1074
|
+
if (!raw) return null;
|
|
1075
|
+
const [code, at = ""] = raw.split(/\s+/);
|
|
1076
|
+
return {
|
|
1077
|
+
code: code ?? "?",
|
|
1078
|
+
at
|
|
1079
|
+
};
|
|
1080
|
+
} catch {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Best-effort one-line description of why an agent process died: the
|
|
1086
|
+
* launcher-recorded exit status (preferred), the tmux pane exit status (fallback
|
|
1087
|
+
* when the dead pane corpse still exists), and the tail of output.log.
|
|
1088
|
+
*/
|
|
1089
|
+
function describeAgentDeath(agentId) {
|
|
1090
|
+
const parts = [];
|
|
1091
|
+
const exit = readAgentExitStatus(agentId);
|
|
1092
|
+
if (exit) parts.push(`exit=${exit.code}${exit.at ? ` at ${exit.at}` : ""}`);
|
|
1093
|
+
else if (sessionExistsSync(agentId)) {
|
|
1094
|
+
const paneExit = listPaneValuesSync(agentId, "#{pane_exit_status}").find((v) => v !== "");
|
|
1095
|
+
if (paneExit) parts.push(`pane_exit=${paneExit}`);
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const logPath = join$1(agentDir(agentId), "output.log");
|
|
1099
|
+
if (existsSync(logPath)) {
|
|
1100
|
+
const tail = readFileSync(logPath, "utf-8").slice(-1500).trim().split("\n").slice(-6).join(" | ").trim();
|
|
1101
|
+
if (tail) parts.push(`output.log tail: ${tail}`);
|
|
1102
|
+
}
|
|
1103
|
+
} catch {}
|
|
1104
|
+
return parts.length ? parts.join("; ") : "no exit trace (exit-status + output.log empty)";
|
|
1105
|
+
}
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region ../../lib/cloister/stuck-remediation-state.ts
|
|
1108
|
+
init_control_settings();
|
|
1109
|
+
init_paths();
|
|
1110
|
+
function agentStateDir(agentId) {
|
|
1111
|
+
return join$1(getOverdeckHome(), "agents", agentId);
|
|
1112
|
+
}
|
|
1113
|
+
function statePath(agentId) {
|
|
1114
|
+
return join$1(agentStateDir(agentId), "stuck-remediation.json");
|
|
1115
|
+
}
|
|
1116
|
+
function readStuckRemediationState(agentId) {
|
|
1117
|
+
const filePath = statePath(agentId);
|
|
1118
|
+
if (!existsSync(filePath)) return null;
|
|
1119
|
+
try {
|
|
1120
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
console.warn(`Failed to read stuck-remediation state for ${agentId}:`, error);
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function writeStuckRemediationState(agentId, state) {
|
|
1127
|
+
mkdirSync(agentStateDir(agentId), { recursive: true });
|
|
1128
|
+
writeFileSync(statePath(agentId), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
1129
|
+
}
|
|
1130
|
+
function clearStuckRemediationState(agentId) {
|
|
1131
|
+
const filePath = statePath(agentId);
|
|
1132
|
+
if (!existsSync(filePath)) return;
|
|
1133
|
+
unlinkSync(filePath);
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region ../../lib/cloister/stuck-remediation.ts
|
|
1137
|
+
init_agents();
|
|
1138
|
+
init_tmux();
|
|
1139
|
+
function issueIdForAgent(agent) {
|
|
1140
|
+
return agent.issueId || agent.id.replace(/^agent-/, "").toUpperCase();
|
|
1141
|
+
}
|
|
1142
|
+
function shouldSkipReviewStatus(status) {
|
|
1143
|
+
if (!status) return false;
|
|
1144
|
+
if (status.stuck || status.deaconIgnored || status.mergeStatus === "merged" || status.readyForMerge) return true;
|
|
1145
|
+
if (status.reviewStatus === "blocked" || status.reviewStatus === "failed") return true;
|
|
1146
|
+
if (status.verificationStatus === "failed" || status.testStatus === "failed") return true;
|
|
1147
|
+
if (status.reviewStatus === "reviewing" || status.reviewStatus === "passed") return true;
|
|
1148
|
+
return status.reviewStatus === "pending" && !!status.reviewRequestedAt;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* PAN-2581 (FR-2, residual): phase gate for the health-check poke loop
|
|
1152
|
+
* (service.ts pokeAgent → pokeAgentWithEscalation → idle-alive pause), the
|
|
1153
|
+
* SECOND independent idleness consumer besides stuck-remediation. A warm-idle
|
|
1154
|
+
* work agent on a pipeline-owned issue — and a warm-idle review/test agent
|
|
1155
|
+
* after its verdict — must never be poked ("are you stuck?") or idle-alive
|
|
1156
|
+
* paused: the poke spends tokens re-explaining the wait, and the pause
|
|
1157
|
+
* manufactures the paused-delivery-target deadlock PAN-2461 exists to undo.
|
|
1158
|
+
* A genuinely stalled mid-review parent is covered by the PAN-2584 liveness
|
|
1159
|
+
* deadline, not pokes. Owed-rework agents (review blocked/failed, tests
|
|
1160
|
+
* failed) are ALSO skipped here by design: shouldSkipReviewStatus returns
|
|
1161
|
+
* true for those because the PAN-2519 wedged-rework path owns their
|
|
1162
|
+
* escalation — the health-check poke loop must not double-drive them.
|
|
1163
|
+
* Roles outside work/review/test (plan, strike, flywheel, sequencer) keep
|
|
1164
|
+
* their existing idleness semantics.
|
|
1165
|
+
*/
|
|
1166
|
+
function shouldSkipIdlePokeForAgent(agent, readStatus = getReviewStatusSync) {
|
|
1167
|
+
if (!agent) return false;
|
|
1168
|
+
if (agent.role !== "work" && agent.role !== "review" && agent.role !== "test") return false;
|
|
1169
|
+
return shouldSkipReviewStatus(readStatus((agent.issueId || agent.id.replace(/^agent-/, "").replace(/-(review|test|ship)(-.*)?$/, "").replace(/-slot-\d+$/, "")).toUpperCase()));
|
|
1170
|
+
}
|
|
1171
|
+
function hasPendingRework(status) {
|
|
1172
|
+
if (!status) return false;
|
|
1173
|
+
if (status.stuck || status.deaconIgnored || status.mergeStatus === "merged" || status.readyForMerge) return false;
|
|
1174
|
+
return status.reviewStatus === "blocked" || status.reviewStatus === "failed" || status.verificationStatus === "failed" || status.testStatus === "failed";
|
|
1175
|
+
}
|
|
1176
|
+
const STUCK_KILL_REQUEUE_CEILING = 25;
|
|
1177
|
+
async function evaluateWedgedReworkAgent(agent, config, now, actions) {
|
|
1178
|
+
const agentId = agent.id;
|
|
1179
|
+
if (agent.role !== "work" || !agent.workspace) return false;
|
|
1180
|
+
const issueId = issueIdForAgent(agent);
|
|
1181
|
+
const status = getReviewStatusSync(issueId);
|
|
1182
|
+
if (!hasPendingRework(status)) return false;
|
|
1183
|
+
const lastActivityMs = getAgentEffectiveLastActivityMs(agentId);
|
|
1184
|
+
if (lastActivityMs === null || !Number.isFinite(lastActivityMs)) return false;
|
|
1185
|
+
const stuckState = readStuckRemediationState(agentId);
|
|
1186
|
+
if (stuckState) {
|
|
1187
|
+
const firstStuckMs = new Date(stuckState.firstStuckAt).getTime();
|
|
1188
|
+
if (Number.isFinite(firstStuckMs) && lastActivityMs > firstStuckMs) {
|
|
1189
|
+
clearStuckRemediationState(agentId);
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const idleMinutes = Math.floor((now - lastActivityMs) / 6e4);
|
|
1194
|
+
const lastStage = stuckState?.lastStage ?? 0;
|
|
1195
|
+
if (idleMinutes < config.stage3_minutes || lastStage >= 3) return false;
|
|
1196
|
+
const firstStuck = firstStuckAt(new Date(lastActivityMs).toISOString(), stuckState);
|
|
1197
|
+
if ((status?.autoRequeueCount ?? 0) >= STUCK_KILL_REQUEUE_CEILING) {
|
|
1198
|
+
markAgentTroubled(agentId);
|
|
1199
|
+
writeStuckRemediationState(agentId, stageState(3, now, firstStuck));
|
|
1200
|
+
logAction(actions, transitionAction(3, issueId, idleMinutes, "rework-wedge-troubled"));
|
|
1201
|
+
surfaceStuckNeedsYou(agent, issueId, firstStuck, actions);
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
killSessionSync(agentId);
|
|
1205
|
+
writeStuckRemediationState(agentId, stageState(3, now, firstStuck));
|
|
1206
|
+
logAction(actions, transitionAction(3, issueId, idleMinutes, "killed-for-respawn"));
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
function hasReadyBeads(readyByQueryRoot, agent, issueLabel) {
|
|
1210
|
+
return (readyByQueryRoot.get(resolveBeadsQueryRoot(agent.workspace))?.[issueLabel] ?? []).length > 0;
|
|
1211
|
+
}
|
|
1212
|
+
function shouldCheckReadyBeadsForAgent(agent, now) {
|
|
1213
|
+
const agentId = agent.id;
|
|
1214
|
+
if (!agentId) return false;
|
|
1215
|
+
if (agent.status !== "running") return false;
|
|
1216
|
+
if (agent.role !== "work") return false;
|
|
1217
|
+
if (!agent.workspace) return false;
|
|
1218
|
+
const completedAt = agent.completedAt;
|
|
1219
|
+
if (agent.paused || agent.troubled || completedAt) return false;
|
|
1220
|
+
if (!sessionExistsSync(agentId)) return false;
|
|
1221
|
+
if (shouldSkipReviewStatus(getReviewStatusSync(issueIdForAgent(agent)))) return false;
|
|
1222
|
+
return isAgentIdleForNudge(agentId, 300 * 1e3, now);
|
|
1223
|
+
}
|
|
1224
|
+
function firstStuckAt(runtimeLastActivity, stuckState) {
|
|
1225
|
+
return stuckState?.firstStuckAt ?? runtimeLastActivity;
|
|
1226
|
+
}
|
|
1227
|
+
function stageState(stage, now, firstStuck, prev) {
|
|
1228
|
+
return {
|
|
1229
|
+
lastStage: stage,
|
|
1230
|
+
lastStageAt: new Date(now).toISOString(),
|
|
1231
|
+
firstStuckAt: firstStuck,
|
|
1232
|
+
...prev?.respawnCount !== void 0 ? { respawnCount: prev.respawnCount } : {},
|
|
1233
|
+
...prev?.lastRespawnAt !== void 0 ? { lastRespawnAt: prev.lastRespawnAt } : {}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function transitionAction(stage, issueId, idleMinutes, action) {
|
|
1237
|
+
return `[deacon] stuck-remediation stage=${stage} issue=${issueId} idleMin=${idleMinutes} action=${action}`;
|
|
1238
|
+
}
|
|
1239
|
+
function logAction(actions, action) {
|
|
1240
|
+
actions.push(action);
|
|
1241
|
+
console.log(action);
|
|
1242
|
+
logDeaconEventSync(action);
|
|
1243
|
+
}
|
|
1244
|
+
function surfaceStuckNeedsYou(agent, issueId, generation, actions) {
|
|
1245
|
+
if (!agent.workspace) return;
|
|
1246
|
+
if (recordRecoveryFailure(agent.workspace, issueId, "stuck-remediation", generation, 1).emitNeedsYou) logAction(actions, `[deacon] needs-you ${issueId}: stuck remediation exhausted`);
|
|
1247
|
+
}
|
|
1248
|
+
async function evaluateAgent(agent, config, now, actions, readyByQueryRoot) {
|
|
1249
|
+
const agentId = agent.id;
|
|
1250
|
+
if (!agentId) return;
|
|
1251
|
+
if (agent.status !== "running") return;
|
|
1252
|
+
const completedAt = agent.completedAt;
|
|
1253
|
+
if (agent.paused || agent.troubled || completedAt) return;
|
|
1254
|
+
if (agent.role === "flywheel") {
|
|
1255
|
+
await evaluateFlywheelOrchestrator(agent, config, now, actions);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (!sessionExistsSync(agentId)) return;
|
|
1259
|
+
if (agent.role === "plan" && agent.auto === true) {
|
|
1260
|
+
await evaluateAutoPlanningAgent(agent, config, now, actions);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (await evaluateWedgedReworkAgent(agent, config, now, actions)) return;
|
|
1264
|
+
if (!shouldCheckReadyBeadsForAgent(agent, now)) return;
|
|
1265
|
+
if (!agent.workspace) return;
|
|
1266
|
+
const issueId = issueIdForAgent(agent);
|
|
1267
|
+
if (hasReadyBeads(readyByQueryRoot, agent, issueId.toLowerCase())) return;
|
|
1268
|
+
const lastActivityMs = getAgentEffectiveLastActivityMs(agentId);
|
|
1269
|
+
if (lastActivityMs === null) return;
|
|
1270
|
+
if (!Number.isFinite(lastActivityMs)) return;
|
|
1271
|
+
const lastActivity = new Date(lastActivityMs).toISOString();
|
|
1272
|
+
const stuckState = readStuckRemediationState(agentId);
|
|
1273
|
+
if (stuckState) {
|
|
1274
|
+
const firstStuckMs = new Date(stuckState.firstStuckAt).getTime();
|
|
1275
|
+
if (Number.isFinite(firstStuckMs) && lastActivityMs > firstStuckMs) {
|
|
1276
|
+
clearStuckRemediationState(agentId);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const idleMinutes = Math.floor((now - lastActivityMs) / 6e4);
|
|
1281
|
+
const lastStage = stuckState?.lastStage ?? 0;
|
|
1282
|
+
const firstStuck = firstStuckAt(lastActivity, stuckState);
|
|
1283
|
+
if (idleMinutes >= config.stage3_minutes && lastStage < 3) {
|
|
1284
|
+
markAgentTroubled(agentId);
|
|
1285
|
+
writeStuckRemediationState(agentId, stageState(3, now, firstStuck));
|
|
1286
|
+
logAction(actions, transitionAction(3, issueId, idleMinutes, "marked-troubled"));
|
|
1287
|
+
surfaceStuckNeedsYou(agent, issueId, firstStuck, actions);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (idleMinutes >= config.stage2_minutes && lastStage < 2) {
|
|
1291
|
+
if ((await resumeAgent(agentId, `Resuming after auto-detected stall (${idleMinutes} min idle). Review your last work and decide whether to continue or signal done with \`pan done ${issueId}\`.`)).success) {
|
|
1292
|
+
writeStuckRemediationState(agentId, stageState(2, now, firstStuck));
|
|
1293
|
+
logAction(actions, transitionAction(2, issueId, idleMinutes, "resumed"));
|
|
1294
|
+
} else {
|
|
1295
|
+
const action = `${transitionAction(2, issueId, idleMinutes, "resume-failed")} — death: ${describeAgentDeath(agentId)}`;
|
|
1296
|
+
console.warn(action);
|
|
1297
|
+
logDeaconEventSync(action);
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (idleMinutes >= config.stage1_minutes && lastStage < 1) {
|
|
1302
|
+
await messageAgent(agentId, `You appear stuck — no tool calls for ${idleMinutes} min. If your implementation is complete, run \`pan done ${issueId}\`. Otherwise reply with a one-line summary of what you're waiting on, then continue.`);
|
|
1303
|
+
writeStuckRemediationState(agentId, stageState(1, now, firstStuck));
|
|
1304
|
+
logAction(actions, transitionAction(1, issueId, idleMinutes, "poked"));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async function evaluateAutoPlanningAgent(agent, config, now, actions) {
|
|
1308
|
+
const agentId = agent.id;
|
|
1309
|
+
if (!isAgentIdleForNudge(agentId, 300 * 1e3, now)) return;
|
|
1310
|
+
if (await Effect.runPromise(countPendingAskUserQuestionsForAgent(agentId)) === 0) return;
|
|
1311
|
+
const lastActivityMs = getAgentEffectiveLastActivityMs(agentId);
|
|
1312
|
+
if (lastActivityMs === null || !Number.isFinite(lastActivityMs)) return;
|
|
1313
|
+
const lastActivity = new Date(lastActivityMs).toISOString();
|
|
1314
|
+
const stuckState = readStuckRemediationState(agentId);
|
|
1315
|
+
if (stuckState) {
|
|
1316
|
+
const firstStuckMs = new Date(stuckState.firstStuckAt).getTime();
|
|
1317
|
+
if (Number.isFinite(firstStuckMs) && lastActivityMs > firstStuckMs) {
|
|
1318
|
+
clearStuckRemediationState(agentId);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const idleMinutes = Math.floor((now - lastActivityMs) / 6e4);
|
|
1323
|
+
const lastStage = stuckState?.lastStage ?? 0;
|
|
1324
|
+
if (idleMinutes < config.stage1_minutes || lastStage >= 1) return;
|
|
1325
|
+
const issueId = issueIdForAgent(agent);
|
|
1326
|
+
await messageAgent(agentId, `This planning session was launched with \`pan plan ${issueId} --auto\`, so do not wait for operator input. Proceed with the most defensible default from the issue/PRD/comments, record the choice in \`plan.autoDecisions[]\` with rationale, and continue to \`pan plan finalize\`. Only halt for a genuine contradiction between authoritative inputs.`);
|
|
1327
|
+
writeStuckRemediationState(agentId, stageState(1, now, firstStuckAt(lastActivity, stuckState)));
|
|
1328
|
+
logAction(actions, transitionAction(1, issueId, idleMinutes, "auto-planning-default"));
|
|
1329
|
+
}
|
|
1330
|
+
const FLYWHEEL_RESPAWN_WINDOW_MS = 1800 * 1e3;
|
|
1331
|
+
const FLYWHEEL_MAX_RESPAWNS = 3;
|
|
1332
|
+
const FLYWHEEL_ORCHESTRATOR_AGENT_ID = "flywheel-orchestrator";
|
|
1333
|
+
/**
|
|
1334
|
+
* Pure decision for recovering a dead or wedged flywheel orchestrator (no I/O —
|
|
1335
|
+
* side effects live in remediateFlywheelOrchestrator). The respawn cap
|
|
1336
|
+
* (FLYWHEEL_MAX_RESPAWNS within FLYWHEEL_RESPAWN_WINDOW_MS) stops an infinite
|
|
1337
|
+
* relaunch loop: past the cap, escalate to paused+troubled. `hasActiveRun` false
|
|
1338
|
+
* ⇒ the operator stopped the flywheel deliberately ⇒ do not resurrect.
|
|
1339
|
+
*/
|
|
1340
|
+
function decideFlywheelRemediation(opts) {
|
|
1341
|
+
if (!opts.hasActiveRun) return { kind: "noop" };
|
|
1342
|
+
const lastRespawnMs = opts.prev?.lastRespawnAt ? new Date(opts.prev.lastRespawnAt).getTime() : 0;
|
|
1343
|
+
const respawnCount = Number.isFinite(lastRespawnMs) && opts.now - lastRespawnMs < FLYWHEEL_RESPAWN_WINDOW_MS ? opts.prev?.respawnCount ?? 0 : 0;
|
|
1344
|
+
if (respawnCount >= FLYWHEEL_MAX_RESPAWNS) return {
|
|
1345
|
+
kind: "escalate",
|
|
1346
|
+
respawnCount
|
|
1347
|
+
};
|
|
1348
|
+
return {
|
|
1349
|
+
kind: "relaunch",
|
|
1350
|
+
respawnCount: respawnCount + 1
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
/** True when the orchestrator's process is actually gone (session missing or dead pane). */
|
|
1354
|
+
function isFlywheelOrchestratorDead(agentId) {
|
|
1355
|
+
if (!sessionExistsSync(agentId)) return true;
|
|
1356
|
+
return listPaneValuesSync(agentId, "#{pane_dead}").some((v) => v === "1");
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Recover a dead or wedged flywheel orchestrator: relaunch (capped) or, past the
|
|
1360
|
+
* cap, pause+trouble. `reason` describes why recovery fired (death vs wedge) for
|
|
1361
|
+
* the deacon log. NO_RESUME is intentionally NOT consulted (see header comment) —
|
|
1362
|
+
* the operator gate is the active run (`pan flywheel stop`) / patrol freeze.
|
|
1363
|
+
*/
|
|
1364
|
+
async function remediateFlywheelOrchestrator(agentId, now, actions, reason) {
|
|
1365
|
+
const decision = decideFlywheelRemediation({
|
|
1366
|
+
hasActiveRun: Boolean(getFlywheelActiveRunId()),
|
|
1367
|
+
prev: readStuckRemediationState(agentId),
|
|
1368
|
+
now
|
|
1369
|
+
});
|
|
1370
|
+
if (decision.kind === "noop") return;
|
|
1371
|
+
const { resumeFlywheel, pauseFlywheel } = await import("./flywheel-Bi82iiNl.js");
|
|
1372
|
+
const prev = readStuckRemediationState(agentId);
|
|
1373
|
+
const nowIso = new Date(now).toISOString();
|
|
1374
|
+
if (decision.kind === "escalate") {
|
|
1375
|
+
await pauseFlywheel();
|
|
1376
|
+
markAgentTroubled(agentId);
|
|
1377
|
+
writeStuckRemediationState(agentId, {
|
|
1378
|
+
lastStage: 3,
|
|
1379
|
+
lastStageAt: nowIso,
|
|
1380
|
+
firstStuckAt: prev?.firstStuckAt ?? nowIso,
|
|
1381
|
+
respawnCount: decision.respawnCount,
|
|
1382
|
+
lastRespawnAt: prev?.lastRespawnAt
|
|
1383
|
+
});
|
|
1384
|
+
logAction(actions, `[deacon] FLYWHEEL orchestrator ${reason} and exceeded ${FLYWHEEL_MAX_RESPAWNS} relaunches in ${FLYWHEEL_RESPAWN_WINDOW_MS / 6e4}min — paused + troubled; operator needed`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
killSessionSync(agentId);
|
|
1389
|
+
} catch {}
|
|
1390
|
+
try {
|
|
1391
|
+
await resumeFlywheel();
|
|
1392
|
+
writeStuckRemediationState(agentId, {
|
|
1393
|
+
lastStage: 0,
|
|
1394
|
+
lastStageAt: nowIso,
|
|
1395
|
+
firstStuckAt: nowIso,
|
|
1396
|
+
respawnCount: decision.respawnCount,
|
|
1397
|
+
lastRespawnAt: nowIso
|
|
1398
|
+
});
|
|
1399
|
+
logAction(actions, `[deacon] FLYWHEEL orchestrator ${reason} — fresh-launched (relaunch ${decision.respawnCount}/${FLYWHEEL_MAX_RESPAWNS})`);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
logAction(actions, `[deacon] FLYWHEEL orchestrator ${reason} — relaunch FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function evaluateFlywheelOrchestrator(agent, config, now, actions) {
|
|
1405
|
+
const agentId = agent.id;
|
|
1406
|
+
if (isFlywheelOrchestratorDead(agentId)) {
|
|
1407
|
+
await remediateFlywheelOrchestrator(agentId, now, actions, `DIED (${describeAgentDeath(agentId)})`);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (!isAgentIdleForNudge(agentId, 300 * 1e3, now)) return;
|
|
1411
|
+
const runtime = getAgentRuntimeStateSync(agentId);
|
|
1412
|
+
if (!runtime?.lastActivity) return;
|
|
1413
|
+
const lastActivityMs = new Date(runtime.lastActivity).getTime();
|
|
1414
|
+
if (!Number.isFinite(lastActivityMs)) return;
|
|
1415
|
+
const stuckState = readStuckRemediationState(agentId);
|
|
1416
|
+
if (stuckState) {
|
|
1417
|
+
const firstStuckMs = new Date(stuckState.firstStuckAt).getTime();
|
|
1418
|
+
if (Number.isFinite(firstStuckMs) && lastActivityMs > firstStuckMs) {
|
|
1419
|
+
clearStuckRemediationState(agentId);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const idleMinutes = Math.floor((now - lastActivityMs) / 6e4);
|
|
1424
|
+
const lastStage = stuckState?.lastStage ?? 0;
|
|
1425
|
+
const firstStuck = firstStuckAt(runtime.lastActivity, stuckState);
|
|
1426
|
+
if (idleMinutes >= config.flywheel_stage3_minutes && lastStage < 3) {
|
|
1427
|
+
await remediateFlywheelOrchestrator(agentId, now, actions, `wedged (idle ${idleMinutes}min)`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (idleMinutes >= config.flywheel_stage2_minutes && lastStage < 2) {
|
|
1431
|
+
await messageAgent(agentId, `Stage 2: idle ${idleMinutes} min — run a FULL flywheel tick NOW: inventory -> diagnose -> suggest -> launch ready work -> \`pan flywheel emit-status\`. Then call ScheduleWakeup(delaySeconds:1000) to arm the next tick. Do NOT ask the operator a question, do NOT wait, and do NOT just emit a stale status or pause.`);
|
|
1432
|
+
writeStuckRemediationState(agentId, stageState(2, now, firstStuck, stuckState));
|
|
1433
|
+
logAction(actions, transitionAction(2, "FLYWHEEL", idleMinutes, "escalated-nudge"));
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (idleMinutes >= config.flywheel_stage1_minutes && lastStage < 1) {
|
|
1437
|
+
await messageAgent(agentId, `You appear stuck — ${idleMinutes} min since your last tick. Run a FULL flywheel tick NOW: inventory -> diagnose -> suggest -> launch ready work -> \`pan flywheel emit-status\`. Then call ScheduleWakeup(delaySeconds:1000) to arm the next tick. Do NOT ask the operator a question, do NOT wait, and do NOT just emit a stale status or pause.`);
|
|
1438
|
+
writeStuckRemediationState(agentId, stageState(1, now, firstStuck, stuckState));
|
|
1439
|
+
logAction(actions, transitionAction(1, "FLYWHEEL", idleMinutes, "poked"));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function reconcileActiveFlywheelWithoutRunningAgent(now, actions) {
|
|
1443
|
+
if (!getFlywheelActiveRunId()) return;
|
|
1444
|
+
if (isFlywheelGloballyPaused()) return;
|
|
1445
|
+
if (sessionExistsSync(FLYWHEEL_ORCHESTRATOR_AGENT_ID)) return;
|
|
1446
|
+
await remediateFlywheelOrchestrator(FLYWHEEL_ORCHESTRATOR_AGENT_ID, now, actions, `DIED (${describeAgentDeath(FLYWHEEL_ORCHESTRATOR_AGENT_ID)})`);
|
|
1447
|
+
}
|
|
1448
|
+
async function checkStuckAgentRemediation(opts = {}) {
|
|
1449
|
+
const config = loadCloisterConfigSync().stuck_remediation ?? DEFAULT_CLOISTER_CONFIG.stuck_remediation;
|
|
1450
|
+
if (!config.enabled) return [];
|
|
1451
|
+
const actions = [];
|
|
1452
|
+
const now = opts.now ?? Date.now();
|
|
1453
|
+
const runningAgents = listRunningAgentsSync();
|
|
1454
|
+
const readyByQueryRoot = /* @__PURE__ */ new Map();
|
|
1455
|
+
const workAgentsByQueryRoot = /* @__PURE__ */ new Map();
|
|
1456
|
+
let sawFlywheelOrchestrator = false;
|
|
1457
|
+
for (const agent of runningAgents) {
|
|
1458
|
+
if (!shouldCheckReadyBeadsForAgent(agent, now)) continue;
|
|
1459
|
+
const queryRoot = resolveBeadsQueryRoot(agent.workspace);
|
|
1460
|
+
const workspaceAgents = workAgentsByQueryRoot.get(queryRoot) ?? [];
|
|
1461
|
+
workspaceAgents.push(agent);
|
|
1462
|
+
workAgentsByQueryRoot.set(queryRoot, workspaceAgents);
|
|
1463
|
+
}
|
|
1464
|
+
for (const [queryRoot, workspaceAgents] of workAgentsByQueryRoot) {
|
|
1465
|
+
const issueIds = workspaceAgents.map(issueIdForAgent);
|
|
1466
|
+
const ready = await Effect.runPromise(queryReadyBeadsByIssueLabels(queryRoot, issueIds, { acquisitionTimeoutMs: 500 }));
|
|
1467
|
+
readyByQueryRoot.set(queryRoot, ready.byIssue);
|
|
1468
|
+
}
|
|
1469
|
+
for (const agent of runningAgents) {
|
|
1470
|
+
if (agent.id === FLYWHEEL_ORCHESTRATOR_AGENT_ID || agent.role === "flywheel") sawFlywheelOrchestrator = true;
|
|
1471
|
+
try {
|
|
1472
|
+
await evaluateAgent(agent, config, now, actions, readyByQueryRoot);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
const message = `[deacon] stuck-remediation agent=${agent.id || "(unknown)"} error=${error instanceof Error ? error.message : String(error)}`;
|
|
1475
|
+
console.error(message, error);
|
|
1476
|
+
logDeaconEventSync(message);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (!sawFlywheelOrchestrator) try {
|
|
1480
|
+
await reconcileActiveFlywheelWithoutRunningAgent(now, actions);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
const message = `[deacon] stuck-remediation agent=${FLYWHEEL_ORCHESTRATOR_AGENT_ID} error=${error instanceof Error ? error.message : String(error)}`;
|
|
1483
|
+
console.error(message, error);
|
|
1484
|
+
logDeaconEventSync(message);
|
|
1485
|
+
}
|
|
1486
|
+
return actions;
|
|
1487
|
+
}
|
|
1488
|
+
//#endregion
|
|
1489
|
+
export { parseCodexApprovalPrompt as _, isAgentIdleForNudge as a, countPendingAskUserQuestionsForAgent as c, getAgentPendingQuestions as d, getAgentWorkspace as f, normalizeAwaitingInputPrompt as g, detectAwaitingInputFromPaneSync as h, shouldSkipReviewStatus as i, getAgentJsonlMtime as l, detectAwaitingInputForAgent as m, decideFlywheelRemediation as n, getRuntimeForAgent as o, scanPendingInputsPromise as p, shouldSkipIdlePokeForAgent as r, computeAgentEnrichment as s, checkStuckAgentRemediation as t, getAgentJsonlPath as u };
|
|
1490
|
+
|
|
1491
|
+
//# sourceMappingURL=stuck-remediation-CFWUP8_I.js.map
|